A poem of forty-seven kana, and the one raylib call a Flan string cannot make
This commit is contained in:
parent
fdf7531c37
commit
4c184e68ce
257
examples/text-codepoints-loading.flan
Normal file
257
examples/text-codepoints-loading.flan
Normal file
@ -0,0 +1,257 @@
|
||||
;;;; raylib [text] example - codepoints loading
|
||||
;;;;
|
||||
;;;; examples/text/text_codepoints_loading.c. The example that scans a piece of
|
||||
;;;; UTF-8 for every codepoint it contains, throws the duplicates away, and
|
||||
;;;; builds a font atlas holding exactly those glyphs and no others — which is
|
||||
;;;; how a game that displays Japanese ships a font without shipping all
|
||||
;;;; twenty thousand kanji.
|
||||
;;;;
|
||||
;;;; It is here because it is the first thing in this tree to put **text** under
|
||||
;;;; load rather than geometry. Everything else in examples/ that draws words
|
||||
;;;; draws ASCII literals through the default font. This one carries a
|
||||
;;;; non-ASCII string through the reader, into the object file, out to raylib,
|
||||
;;;; back as an array of codepoints, and into a glyph set — and each of those
|
||||
;;;; steps is somewhere a byte could be lost with no diagnostic anywhere.
|
||||
;;;;
|
||||
;;;; **What worked with nothing added, and is worth saying because it was the
|
||||
;;;; open question.** Flan's reader takes UTF-8 in a string literal and the
|
||||
;;;; bytes survive to the binary unexamined: the 54 codepoints in `text` below
|
||||
;;;; come back out of LoadCodepoints as the right 54, and as 49 distinct ones.
|
||||
;;;; Nothing in the
|
||||
;;;; language claims to know what a character is — a `string` is bytes and a
|
||||
;;;; `[u8]` is the same bytes, which `(string ...)` and `(bytes ...)` say in
|
||||
;;;; both directions — and that turns out to be exactly the right amount of
|
||||
;;;; opinion for this. The count below is a count of codepoints because raylib
|
||||
;;;; decoded them, not because Flan did.
|
||||
;;;;
|
||||
;;;; And `load-font-ex` needed nothing either. It already takes a `[i32]` slice
|
||||
;;;; of codepoints and takes the pointer-and-count apart itself, with a
|
||||
;;;; zeroed global standing in for the null pointer that means "the default
|
||||
;;;; ASCII set" — written for exactly this call, before anything called it.
|
||||
;;;;
|
||||
;;;; **Two things Flan would not do, both written up in PORTING.md.**
|
||||
;;;;
|
||||
;;;; 1. `GetCodepointPrevious` cannot be called at all. It reads BACKWARDS
|
||||
;;;; from the pointer it is handed, and a Flan `string` crosses to C as a
|
||||
;;;; NUL-terminated *copy* (lib/shim.ml) — so the bytes in front of that
|
||||
;;;; pointer belong to the allocator and not to the text. It does not
|
||||
;;;; crash; it reads rubbish and answers 0, which is the worst of the
|
||||
;;;; available outcomes. `step-back` below is the replacement, and it is
|
||||
;;;; four lines: a UTF-8 continuation byte is 10xxxxxx, so walking back
|
||||
;;;; over them lands on the lead byte of the previous codepoint, and
|
||||
;;;; get-codepoint-next from there says what it is.
|
||||
;;;;
|
||||
;;;; 2. The C's duplicate removal shifts the tail of the array down over
|
||||
;;;; each duplicate it finds, and its inner loop reads one element past
|
||||
;;;; the end on the first one it removes. C does not notice; Flan's
|
||||
;;;; bounds check signals BoundsError and the frame stops. It is rewritten
|
||||
;;;; here as a build-up — scan the output for the codepoint, append it if
|
||||
;;;; it is not there — which is shorter, has no shifting in it at all, and
|
||||
;;;; keeps first-seen order exactly as the C's version does.
|
||||
;;;;
|
||||
;;;; **The font is not in this repository, and the example says so on screen.**
|
||||
;;;; raylib's own resources/DotGothic16-Regular.ttf is 2 MB, and whether a
|
||||
;;;; compiler repository with one 1 KB PNG in it should grow a 2 MB font is a
|
||||
;;;; decision about the repository rather than about this port — so it is left
|
||||
;;;; to the author, and `font-path` below is where it goes. Without it raylib
|
||||
;;;; hands back the default font, whose 224 glyphs are ASCII, and the kana draw
|
||||
;;;; as boxes. Everything else in the program — the scan, the deduplication,
|
||||
;;;; the codepoint walk, the atlas request — runs either way, and
|
||||
;;;; test/programs/raylib-codepoints.flan asserts the arithmetic half of it
|
||||
;;;; with no window and no font at all.
|
||||
|
||||
(import rl "vendor:raylib")
|
||||
(import d "digits.flan")
|
||||
|
||||
(defconst screen-width 800)
|
||||
(defconst screen-height 450)
|
||||
|
||||
;; The text to display, which must be UTF-8 — and, as the C's comment says,
|
||||
;; can be all the text a game will ever show: it is scanned to find the glyphs
|
||||
;; the atlas needs. This is the Iroha, a poem that uses each kana exactly once,
|
||||
;; which is why the duplicate count below is as small as it is: the only
|
||||
;; repeats are the ideographic space and the newline.
|
||||
(defconst text
|
||||
"いろはにほへと ちりぬるを\nわかよたれそ つねならむ\nうゐのおくやま けふこえて\nあさきゆめみし ゑひもせす")
|
||||
|
||||
;; Where the font goes if somebody puts it there. Relative to the working
|
||||
;; directory, as raylib resolves every path — the C says `resources/...` and
|
||||
;; is run from its own directory; this says the same thing from the root of
|
||||
;; this repository.
|
||||
(defconst font-path "examples/resources/DotGothic16-Regular.ttf")
|
||||
|
||||
;; The Iroha below is 47 kana, 4 ideographic spaces and 3 newlines — 54
|
||||
;; codepoints of which 49 are distinct, the kana being famously each used once,
|
||||
;; so the only repeats are the space and the newline. 128 is the round number
|
||||
;; with room above that for the
|
||||
;; text being edited, and it is a constant because a fixed array's length must
|
||||
;; be a literal and because an atlas's glyph count is a thing worth having a
|
||||
;; ceiling on: this is the number that decides how big the font texture gets.
|
||||
(defconst max-codepoints 128)
|
||||
|
||||
(defvar unique-codepoints [max-codepoints i32])
|
||||
(defvar unique-count i32)
|
||||
|
||||
;; Is `cp` already in the first `n` of the table?
|
||||
(defn seen? [cp i32 n i32] bool
|
||||
(let [found false]
|
||||
(dotimes [i n]
|
||||
(when (= (at unique-codepoints i) cp) (set found true)))
|
||||
found))
|
||||
|
||||
;; The C's CodepointRemoveDuplicates, turned inside out. Theirs copies the
|
||||
;; whole array and then compacts it in place, shifting the tail down over
|
||||
;; every duplicate; this appends each codepoint the table does not already
|
||||
;; have. Same answer, same order, and no shifting — see the header comment for
|
||||
;; why the shifting version is not a transcription this language accepts.
|
||||
;;
|
||||
;; Exported because test/programs/raylib-codepoints.flan runs it: which
|
||||
;; codepoints an atlas is asked for is arithmetic, and arithmetic is the half
|
||||
;; of a raylib example a headless case can assert.
|
||||
(defn collect-unique [codepoints [i32]] ()
|
||||
(set unique-count 0)
|
||||
(dotimes [i (len codepoints)]
|
||||
(let [cp (at codepoints i)]
|
||||
(when (and (not (seen? cp unique-count))
|
||||
(< unique-count max-codepoints))
|
||||
(set (at unique-codepoints unique-count) cp)
|
||||
(set unique-count (+ unique-count 1))))))
|
||||
|
||||
;; ── Walking the text one codepoint at a time ────────────────────────
|
||||
;;
|
||||
;; The C keeps a `char *ptr` and moves it by the size of each codepoint. Here
|
||||
;; the cursor is a byte offset into the text, because the pointer it would
|
||||
;; otherwise be cannot be handed to C: see `step-back`.
|
||||
|
||||
;; Is this byte a UTF-8 continuation — 10xxxxxx? Every byte of a multi-byte
|
||||
;; sequence after the first is, and no lead byte and no ASCII byte is, which
|
||||
;; is the property that makes the encoding walkable in both directions without
|
||||
;; a table.
|
||||
(defn continuation? [b u8] bool (= (bit-and b 0xc0) 0x80))
|
||||
|
||||
;; The codepoint starting at `off`, and its size in bytes through `size-out`.
|
||||
;;
|
||||
;; `(string (slice b off (len b)))` is the whole of what the C's `ptr` is: the
|
||||
;; tail of the text from here on. It costs nothing to say — a `string` and a
|
||||
;; `[u8]` are the same two words — and the shim NUL-terminates a copy of it
|
||||
;; for the duration of the call, which is all GetCodepointNext wants, because
|
||||
;; it only ever reads forwards.
|
||||
(defn codepoint-at [off i32 size-out (Ptr i32)] i32
|
||||
(let [b (bytes text)]
|
||||
(if (>= off (len b))
|
||||
0
|
||||
(rl/get-codepoint-next (string (slice b off (len b))) size-out))))
|
||||
|
||||
;; One codepoint forward, clamped at the end.
|
||||
;;
|
||||
;; The C clamps at neither end — `ptr` walks off the back of the string and
|
||||
;; off the front of it if you hold the key down, which is a real bug that C
|
||||
;; does not report and that this language will not perform. Stopping is the
|
||||
;; behaviour a reader of this example would have expected anyway.
|
||||
(defn step-forward [off i32] i32
|
||||
(let [size 0
|
||||
b (bytes text)]
|
||||
(if (>= off (len b))
|
||||
off
|
||||
(do (codepoint-at off (addr size))
|
||||
(if (>= (+ off size) (len b)) off (+ off size))))))
|
||||
|
||||
;; One codepoint back, and the replacement for GetCodepointPrevious.
|
||||
;;
|
||||
;; That function reads backwards from the pointer it is given, and a Flan
|
||||
;; string reaches C as a copy, so backwards from it is the allocator's
|
||||
;; business. What it was going to compute is computable here instead and in
|
||||
;; fewer instructions than the call would have cost: step back one byte, keep
|
||||
;; stepping while the byte is a continuation, and the codepoint that starts
|
||||
;; there is the previous one. codepoint-at then says what it is, from the
|
||||
;; forward direction, where the copy is not a problem.
|
||||
(defn step-back [off i32] i32
|
||||
(let [b (bytes text)
|
||||
i (- off 1)]
|
||||
(while (and (> i 0) (continuation? (at b i)))
|
||||
(set i (- i 1)))
|
||||
(if (< i 0) 0 i)))
|
||||
|
||||
(defvar font rl/Font)
|
||||
(defvar show-font-atlas bool)
|
||||
(defvar cursor i32)
|
||||
(defvar codepoint-count i32)
|
||||
|
||||
(defn main [] ()
|
||||
(rl/init-window screen-width screen-height
|
||||
"raylib [text] example - codepoints loading")
|
||||
(defer (rl/close-window))
|
||||
|
||||
;; Turn each UTF-8 character of the text into the codepoint the font file
|
||||
;; indexes its glyphs by. raylib owns the array; slice-from-ptr is the
|
||||
;; promise that there are `codepoint-count` of them behind the pointer, and
|
||||
;; raylib's own out-parameter is where that number came from.
|
||||
(let [codepoints (rl/load-codepoints text (addr codepoint-count))]
|
||||
(collect-unique (slice-from-ptr codepoints codepoint-count))
|
||||
(rl/unload-codepoints codepoints))
|
||||
|
||||
;; The atlas is generated here, from the deduplicated set — a smaller set is
|
||||
;; a smaller texture, which is the entire point of the deduplication.
|
||||
(set font (rl/load-font-ex font-path 36
|
||||
(slice unique-codepoints 0 unique-count)))
|
||||
(defer (rl/unload-font font))
|
||||
|
||||
;; Bilinear, so the 36-pixel atlas still reads when it is drawn at 48. The
|
||||
;; default is :point and the kana come out with stepped edges.
|
||||
(rl/set-texture-filter (.texture font) :bilinear)
|
||||
|
||||
;; Line spacing for the newlines the text contains. draw-text-ex honours it;
|
||||
;; nothing else in this program does.
|
||||
(rl/set-text-line-spacing 20)
|
||||
|
||||
(set show-font-atlas false)
|
||||
(set cursor 0)
|
||||
|
||||
(rl/set-target-fps 60)
|
||||
|
||||
(until (rl/window-should-close?)
|
||||
;; Update
|
||||
(when (rl/key-pressed? :space) (set show-font-atlas (not show-font-atlas)))
|
||||
|
||||
;; The C's "testing code": walk the text and throw the answer away. Kept
|
||||
;; because it is what exercises the codepoint walk, and because the two
|
||||
;; directions are not symmetric here the way they are in the C — one is a
|
||||
;; raylib call and the other is the four lines in step-back that stand in
|
||||
;; for the raylib call that cannot be made.
|
||||
(cond
|
||||
(rl/key-pressed? :right) (set cursor (step-forward cursor))
|
||||
(rl/key-pressed? :left) (set cursor (step-back cursor)))
|
||||
|
||||
;; Draw
|
||||
(rl/with-drawing
|
||||
(rl/clear-background rl/raywhite)
|
||||
|
||||
(rl/draw-rectangle 0 0 (rl/get-screen-width) 70 rl/black)
|
||||
|
||||
(let [x (+ 10 (d/draw-piece "Total codepoints contained in provided text: "
|
||||
10 10 20 rl/green))]
|
||||
(d/draw-int codepoint-count x 10 20 rl/green))
|
||||
(let [x (+ 10 (d/draw-piece
|
||||
"Total codepoints required for font atlas (duplicates excluded): "
|
||||
10 40 20 rl/green))]
|
||||
(d/draw-int unique-count x 40 20 rl/green))
|
||||
|
||||
(if show-font-atlas
|
||||
;; The generated atlas itself, which is the picture of what the
|
||||
;; deduplication bought: one cell per distinct codepoint and nothing
|
||||
;; else in it.
|
||||
(do (rl/draw-texture (.texture font) 150 100 rl/black)
|
||||
(rl/draw-rectangle-lines 150 100 (.width (.texture font))
|
||||
(.height (.texture font)) rl/black))
|
||||
(rl/draw-text-ex font text (rl/Vector2 {.x 160.0 .y 110.0})
|
||||
48.0 5.0 rl/black))
|
||||
|
||||
;; The line the C does not have, and the reason it is here is in the
|
||||
;; header comment: without the TTF this program draws boxes, and a
|
||||
;; program that draws boxes without saying why reads as broken.
|
||||
(when (not (rl/path-file? font-path))
|
||||
(d/draw-piece "No font at examples/resources/ — glyphs will be boxes."
|
||||
10 (- screen-height 55) 20 rl/maroon))
|
||||
|
||||
(d/draw-piece "Press SPACE to toggle font atlas view!"
|
||||
10 (- screen-height 30) 20 rl/gray))))
|
||||
119
test/programs/raylib-codepoints.flan
Normal file
119
test/programs/raylib-codepoints.flan
Normal file
@ -0,0 +1,119 @@
|
||||
;;;; examples/text-codepoints-loading.flan's other half: the scan, the
|
||||
;;;; deduplication and the codepoint walk, with no window and no font.
|
||||
;;;;
|
||||
;;;; The same split core-input-virtual-controls.flan already has. What makes it
|
||||
;;;; available here is that the *interesting* part of that example is not the
|
||||
;;;; drawing: it is which codepoints a piece of UTF-8 contains, which of them
|
||||
;;;; are distinct, and where each one starts in the bytes. All three are
|
||||
;;;; arithmetic, LoadCodepoints needs no GL context, and the example's `main`
|
||||
;;;; is not exported — so this runs the same code the window runs, without one.
|
||||
;;;;
|
||||
;;;; Three things are pinned here and they fail for three different reasons.
|
||||
;;;;
|
||||
;;;; **1. The text survived.** 49 distinct codepoints out of 54 is a
|
||||
;;;; property of the Iroha and of nothing else, so a literal that lost a byte
|
||||
;;;; between the reader and the object file changes both numbers. The first
|
||||
;;;; five distinct codepoints are printed as well, because a count alone would
|
||||
;;;; survive a re-ordering.
|
||||
;;;;
|
||||
;;;; **2. The walk agrees with itself.** The text is stepped from the first
|
||||
;;;; codepoint to the last, and then back from the last to the first, and the
|
||||
;;;; two sequences are compared. That is what pins `step-back` — the four lines
|
||||
;;;; that replace GetCodepointPrevious, which this language cannot call because
|
||||
;;;; a Flan string reaches C as a copy and that function reads backwards out of
|
||||
;;;; the pointer it is handed. If the continuation-byte test were wrong the
|
||||
;;;; backward walk would land mid-sequence and read a different codepoint, and
|
||||
;;;; the two sequences would stop being reverses of each other.
|
||||
;;;;
|
||||
;;;; **3. The walk agrees with raylib.** The sequence the walk produces is
|
||||
;;;; compared against the array LoadCodepoints returned, element for element.
|
||||
;;;; Item 2 on its own would pass if both walks were wrong in the same way;
|
||||
;;;; this is the outside opinion, and it is raylib's rather than ours.
|
||||
;;;;
|
||||
;;;; Nothing here draws, so nothing here needs the TTF the example looks for.
|
||||
|
||||
(import cp "../../examples/text-codepoints-loading.flan")
|
||||
(import rl "vendor:raylib")
|
||||
|
||||
(defconst max-walk 128)
|
||||
|
||||
(defvar forward [max-walk i32])
|
||||
(defvar forward-n i32)
|
||||
(defvar backward [max-walk i32])
|
||||
(defvar backward-n i32)
|
||||
|
||||
(defn yes-no [b bool] string (if b "yes" "no"))
|
||||
|
||||
(defn show [name string n i32] ()
|
||||
(print name) (print " ") (println n))
|
||||
|
||||
;; From the first codepoint to the last. step-forward clamps rather than
|
||||
;; running off the end — the C does not, which is a bug it gets away with —
|
||||
;; so the walk is over when the offset stops moving.
|
||||
(defn walk-forward [] ()
|
||||
(set forward-n 0)
|
||||
(let [off 0
|
||||
size 0
|
||||
going true]
|
||||
(while going
|
||||
(set (at forward forward-n) (cp/codepoint-at off (addr size)))
|
||||
(set forward-n (+ forward-n 1))
|
||||
(let [next (cp/step-forward off)]
|
||||
(if (= next off) (set going false) (set off next))))))
|
||||
|
||||
;; And back again, from wherever forward stopped. Written as a separate walk
|
||||
;; rather than as an index into the first one on purpose: the point is that
|
||||
;; step-back finds the lead byte of the previous codepoint out of the bytes
|
||||
;; alone, so it has to be asked, not remembered.
|
||||
(defn walk-backward [start i32] ()
|
||||
(set backward-n 0)
|
||||
(let [off start
|
||||
size 0
|
||||
going true]
|
||||
(while going
|
||||
(set (at backward backward-n) (cp/codepoint-at off (addr size)))
|
||||
(set backward-n (+ backward-n 1))
|
||||
(if (= off 0)
|
||||
(set going false)
|
||||
(set off (cp/step-back off))))))
|
||||
|
||||
(defn main [] ()
|
||||
(let [total 0
|
||||
raw (rl/load-codepoints cp/text (addr total))]
|
||||
(show "codepoints" total)
|
||||
(cp/collect-unique (slice-from-ptr raw total))
|
||||
(show "unique" cp/unique-count)
|
||||
(dotimes [i 5]
|
||||
(print "unique ") (print i) (print " ")
|
||||
(println (at cp/unique-codepoints i)))
|
||||
|
||||
(walk-forward)
|
||||
(show "forward" forward-n)
|
||||
|
||||
;; The last codepoint's offset, recomputed the same way walk-forward found
|
||||
;; it, because the walk deliberately keeps no offsets.
|
||||
(let [last-off 0
|
||||
going true]
|
||||
(while going
|
||||
(let [next (cp/step-forward last-off)]
|
||||
(if (= next last-off) (set going false) (set last-off next))))
|
||||
(walk-backward last-off))
|
||||
(show "backward" backward-n)
|
||||
|
||||
;; Item 2: the two walks are reverses of each other.
|
||||
(let [mirrored (= forward-n backward-n)]
|
||||
(dotimes [i forward-n]
|
||||
(when (and mirrored
|
||||
(not (= (at forward i) (at backward (- (- backward-n 1) i)))))
|
||||
(set mirrored false)))
|
||||
(print "walks mirror ") (println (yes-no mirrored)))
|
||||
|
||||
;; Item 3: and the forward walk is what raylib said the text contains.
|
||||
(let [agrees (= forward-n total)
|
||||
all (slice-from-ptr raw total)]
|
||||
(dotimes [i forward-n]
|
||||
(when (and agrees (not (= (at forward i) (at all i))))
|
||||
(set agrees false)))
|
||||
(print "walk matches raylib ") (println (yes-no agrees)))
|
||||
|
||||
(rl/unload-codepoints raw)))
|
||||
@ -1045,6 +1045,38 @@ let () =
|
||||
print_endline
|
||||
"acceptance: skipping the raylib image-processing case (no libraylib)";
|
||||
|
||||
(* The scan and the codepoint walk of examples/text-codepoints-loading.flan,
|
||||
headless. The example is imported, so the literal counted here is the
|
||||
literal that program draws — which is the point: a non-ASCII string
|
||||
literal is carried by the reader, the object file and the FFI without
|
||||
any of the three claiming to understand it, and 49 distinct codepoints
|
||||
out of 54 is a fact about the Iroha that a lost byte anywhere in that
|
||||
chain would change. The two walk rows are what stands in for
|
||||
GetCodepointPrevious, which cannot be called from Flan at all — see
|
||||
PORTING.md, and the file's own header for what each row fails on. *)
|
||||
let raylib_codepoints_out =
|
||||
"codepoints 54\n\
|
||||
unique 49\n\
|
||||
unique 0 12356\n\
|
||||
unique 1 12429\n\
|
||||
unique 2 12399\n\
|
||||
unique 3 12395\n\
|
||||
unique 4 12411\n\
|
||||
forward 54\n\
|
||||
backward 54\n\
|
||||
walks mirror yes\n\
|
||||
walk matches raylib yes\n"
|
||||
in
|
||||
if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin
|
||||
outputs "raylib codepoints, headless"
|
||||
"programs/raylib-codepoints.flan" raylib_codepoints_out;
|
||||
outputs ~opt:"-O0" "raylib codepoints, headless, -O0"
|
||||
"programs/raylib-codepoints.flan" raylib_codepoints_out
|
||||
end
|
||||
else
|
||||
print_endline
|
||||
"acceptance: skipping the raylib codepoints case (no libraylib)";
|
||||
|
||||
(* raylib's Wave family, headless — and the first claim to make about it
|
||||
is that it exists. The received wisdom in this repository was that
|
||||
audio needs a device and so cannot be in this table at all. That is
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user