@godot-scene-web/hb-gpu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// HarfBuzz's Slug (GPU glyph) encoder, compiled to wasm — outlines in, one texel blob per glyph out.\n//\n// WHAT THIS IS. `hb-atlas` and `hb-run` bake PIXELS: they rasterize once, at one size and one\n// rotation, and thereafter blit. This bakes the OUTLINE instead. `hb_gpu_draw_encode` turns a\n// glyph's curves into a banded, quantized RGBA16I texel stream, and a fragment shader evaluates\n// exact coverage from it at whatever size, rotation and sub-pixel phase the frame asks for. There\n// is no atlas resolution to pick, no phase grid, and no rotation baked into anything — which is\n// the whole reason it is worth measuring against arms whose VRAM grows 12x with a phone's device\n// pixel ratio.\n//\n// IT SHAPES NOW, AND THAT IS WHY `heapBytes` GOT SMALLER BY GETTING BIGGER. This module used to\n// export no `hb_shape`: the perf-harness shaped with npm `harfbuzzjs` and this binary only read\n// outlines, so an `hb-gpu` arm beside a `harfbuzz` shaper held the same face bytes in TWO wasm\n// heaps — 4.19 MiB across the pair on the phone, every face resident twice. `hb-gpu.symbols` now\n// names the shaper (the engine was always compiled; only the symbol list kept `--gc-sections` from\n// throwing it away), which costs +190 KB of binary once and saves a whole second HarfBuzz and a\n// second copy of every face at runtime. `heapBytes` is still published so the caller can count\n// what is left, and a caller that shapes here should be counting ONE module, not two.\n//\n// THE SHADER TEXT COMES OUT OF THE WASM. `hb_gpu_shader_source` / `hb_gpu_draw_shader_source`\n// return the GLSL that reads the blob format this same binary writes, so the two cannot drift.\n// None of HarfBuzz's GLSL is copied into this repo.\n//\n// THE ONE EXCEPTION, STATED PLAINLY: HarfBuzz supplies the two shader LIBRARY halves, not an entry\n// point. There is no `main()` in either, because HarfBuzz cannot know what a consumer's attributes\n// are called or what its fragment writes to. So a ~30-line `main()` per stage is unavoidably ours,\n// and it lives in `./webgl.ts` beside the attribute layout it names. Upstream's own\n// `util/gpu/demo-{vertex,fragment}.glsl` is exactly the same thing for its GLFW demo. Ours is\n// written against `hb-gpu-vertex.glsl`'s documented `hb_gpu_dilate` contract and\n// `hb-gpu-draw-fragment.glsl`'s `hb_gpu_draw`, not copied from the demo.\n//\n// LOADED WITH `{ wasmBinary }`, never by URL. `dist/hb-gpu.mjs` is emscripten ES6 glue that would\n// otherwise `fetch` its sibling `.wasm` relative to `import.meta.url` — which is wrong under every\n// bundler this repo drives a page with, and silently so.\n\n/**\n * Every way this package can decline, as a stable string.\n *\n * A REASON CHANNEL EXISTS BECAUSE THE FUNCTIONS RETURN `null`. Construct-or-null is the repo's\n * idiom (`createCanvasStage`) and the right one for a shipped renderer — a consumer that cannot\n * have the GPU glyph path falls back to its DOM one. But a component that declined silently\n * reports as a CHEAP one, in a perf table literally so, so every `null` is paired with one of\n * these plus a sentence naming the failure mode.\n *\n * String union rather than an enum so a report can carry it verbatim and a `switch` over it is\n * exhaustively checked.\n */\nexport type HbGpuFailureReason =\n /** `_malloc` returned 0. The heap could not grow; the write would have gone to the null page. */\n | \"out-of-memory\"\n /** Zero-length face bytes — usually a detached ArrayBuffer that was transferred elsewhere. */\n | \"empty-face\"\n /** HarfBuzz would not take the bytes as a face, or the face's `upem` is unusable. */\n | \"face-rejected\"\n /** `hb_gpu_draw_create_or_fail` returned null. */\n | \"encoder-unavailable\"\n /** A `upem` that would make every glyph scale `Infinity` or `NaN`. */\n | \"degenerate-upem\"\n /**\n * A `contrast.gamma` that is not a finite positive number. Not fatal — the exponent falls back\n * to 1 and the renderer builds.\n *\n * Refused rather than passed through because `pow (cov, gamma)` with a NaN or an infinity is a\n * NaN alpha, and a NaN alpha through a premultiplied MIX blend is an undefined framebuffer over\n * the glyph's whole quad on some drivers and a black box on others. Neither reads as \"somebody\n * typed a bad number into a renderer option\".\n */\n | \"degenerate-contrast\"\n /** The WebGL2 context was lost, or was already lost when handed over. */\n | \"context-lost\"\n /** `gl.create*` returned null. */\n | \"gl-object\"\n | \"shader-compile\"\n | \"program-link\"\n /** `MAX_TEXTURE_SIZE` is too small for an atlas at all. */\n | \"texture-size\"\n /** `MAX_TEXTURE_SIZE` forced a narrower or shorter atlas than asked for. Not fatal. */\n | \"atlas-clamped\"\n /** A blob whose length is not a whole number of texels. */\n | \"blob-malformed\"\n /**\n * `hb_feature_from_string` would not parse one of the feature strings handed to `shape`.\n *\n * Its own failure mode is the reason this is a refusal and not a warning: HarfBuzz zeroes the\n * struct and returns false, and a zeroed `hb_feature_t` is tag 0 with value 0 over the whole\n * run — which shapes, and shapes WITHOUT the feature the caller asked for. A typo in `\"-liga\"`\n * would otherwise be a ligature quietly still applied.\n */\n | \"feature-malformed\"\n /** One glyph larger than the entire atlas. */\n | \"blob-too-large\"\n /** A face handle that this renderer never issued. */\n | \"face-unregistered\"\n /** A context restore that could not put every resident glyph back. Not fatal. */\n | \"rebuild-incomplete\";\n\n/** A refusal, with the reason machine-readable and the sentence written for a human. */\nexport interface HbGpuFailure {\n reason: HbGpuFailureReason;\n /** Always prefixed `hb-gpu: `, and always says what the failure LOOKS like, not just its name. */\n message: string;\n}\n\nexport interface HbGpuOptions {\n /** Where a refusal goes. See {@link HbGpuFailureReason}. */\n onError?(failure: HbGpuFailure): void;\n}\n\n/** Values of `hb_gpu_shader_stage_t`. */\nexport const HB_GPU_SHADER_STAGE_VERTEX = 0;\nexport const HB_GPU_SHADER_STAGE_FRAGMENT = 1;\n\n/** `HB_GPU_SHADER_LANG_GLSL`. WebGL2 is the only backend this package has a pipeline for. */\nexport const HB_GPU_SHADER_LANG_GLSL = 1;\n\n/** `HB_MEMORY_MODE_READONLY`: the blob points at our allocation and never writes to it. */\nconst HB_MEMORY_MODE_READONLY = 1;\n\n/** Bytes per encoded texel: RGBA16I, and the unit the atlas allocator counts in. */\nexport const HB_GPU_TEXEL_BYTES = 8;\n\n/**\n * A glyph's ink box, in FONT UNITS at the font's scale, y-UP — HarfBuzz's convention, unaltered.\n *\n * `height` IS NEGATIVE and `yBearing` is the ink's TOP. Restated here because it is the single most\n * common way to get a glyph quad upside down, and because `hb_gpu_draw_encode` floors/ceils these\n * to whole font units, so the box is always at least the ink and never less.\n */\nexport interface HbGpuGlyphExtents {\n xBearing: number;\n yBearing: number;\n width: number;\n height: number;\n}\n\n/** One encoded glyph: the texel stream to upload, and the box to draw it in. */\nexport interface EncodedGlyph {\n /** RGBA16I texels, little-endian, exactly as `hb_gpu_draw_encode` produced them. */\n texels: Uint8Array;\n extents: HbGpuGlyphExtents;\n}\n\n/**\n * `hb_direction_t`, as the four names rather than the four integers.\n *\n * A STRING UNION AND NOT A NUMBER, because `hb_buffer_set_direction` has no error channel: hand it\n * anything outside 4..7 and the buffer's direction is INVALID, `guess_segment_properties` then\n * fills in whatever it likes, and the run comes out laid the wrong way with nothing said. The\n * mapping is an ABI constant, kept here the way `HB_MEMORY_MODE_READONLY` is.\n */\nexport type HbGpuDirection = \"ltr\" | \"rtl\" | \"ttb\" | \"btt\";\n\n/** `hb_direction_t`: HB_DIRECTION_LTR is 4 and the rest follow. */\nconst HB_DIRECTION: Record<HbGpuDirection, number> = {\n ltr: 4,\n rtl: 5,\n ttb: 6,\n btt: 7,\n};\n\n/**\n * What a caller can tell the shaper about a run.\n *\n * EVERY FIELD IS OPTIONAL AND THE DEFAULT IS `hb_buffer_guess_segment_properties`, which infers\n * script from the code points and direction from the script. That is the right default and a poor\n * guarantee: it cannot know that a Latin quotation inside an Arabic paragraph is still RTL, and it\n * has no opinion at all about language. Anything set here is set BEFORE the guess, and the guess\n * only fills what is still unset — so an explicit value always wins.\n */\nexport interface HbGpuShapeOptions {\n /** Overrides the direction the script implies. */\n direction?: HbGpuDirection;\n /** An ISO 15924 tag — `\"Hans\"`, `\"Latn\"`, `\"Arab\"`. Case is canonicalised by HarfBuzz. */\n script?: string;\n /**\n * A BCP 47 tag — `\"zh-Hans\"`, `\"en\"`, `\"tr\"`.\n *\n * Left unset it stays unset: this build has `HB_NO_SETLOCALE`, so there is no ambient locale to\n * fall back to and the font's `dflt` language system is used. Set it when the face has a\n * language-specific feature the run needs (Turkish dotless i, Serbian Cyrillic italics).\n */\n language?: string;\n /**\n * OpenType features in `hb-shape`'s own syntax: `\"kern\"`, `\"-liga\"`, `\"ss01\"`, `\"aalt[3:5]=2\"`.\n *\n * Parsed by `hb_feature_from_string`, and a string it refuses fails the whole call with\n * `\"feature-malformed\"` rather than being dropped — see that reason.\n */\n features?: readonly string[];\n}\n\n/**\n * One glyph of a shaped run, in the font's own units, y-UP — HarfBuzz's output, unconverted.\n *\n * See {@link HbGpuFont.shape} for the pen arithmetic these five numbers go into; getting the\n * offset/advance split or the y sign wrong produces text that looks plausible and is mis-spaced.\n */\nexport interface HbGpuShapedGlyph {\n /** Glyph id in this face. The same id `encode` takes. */\n glyphId: number;\n /**\n * Where this glyph came from in `text`, as a UTF-16 code-unit index — i.e. an index you can\n * hand straight to `String.prototype.slice`, because the text goes in through\n * `hb_buffer_add_utf16` and a JS string already is UTF-16.\n *\n * Not one per glyph and not monotonic in general: several glyphs share a cluster when one\n * character became many, and many characters share one when a ligature ate them.\n */\n cluster: number;\n /** How far the pen moves AFTER this glyph. */\n xAdvance: number;\n yAdvance: number;\n /** Added to the pen for THIS glyph only, and never accumulated. */\n xOffset: number;\n yOffset: number;\n}\n\nexport interface HbGpuFont {\n /** Units per em of the face — the scale outlines and extents are expressed in. */\n readonly upem: number;\n /** Glyph id for a code point, or 0 (`.notdef`) when the face has no cmap entry. */\n glyphFor(codepoint: number): number;\n /**\n * Shape a run: text in, positioned glyph ids out.\n *\n * UNITS ARE THE FONT'S OWN AND NOT PIXELS, which is the sentence to read twice. `createFont`\n * calls `hb_font_set_scale(font, upem, upem)` — see there for why — so every number that comes\n * back is in font units at that scale, y is UP, and nothing has been converted. Same contract as\n * {@link HbGpuGlyphExtents}, for the same reason: this package cannot know the caller's pixel\n * size, and a half-applied convention is worse than none.\n *\n * THE PEN ARITHMETIC, WHICH IS THE PART THAT GOES SUBTLY WRONG. With `toPx = fontSizePx / upem`,\n * a horizontal run is laid out:\n *\n * ```ts\n * let pen = 0;\n * for (const glyph of run) {\n * const xPx = (pen + glyph.xOffset) * toPx; // offset positions THIS glyph...\n * const yPx = -glyph.yOffset * toPx; // ...and y flips, because canvases are y-DOWN\n * pen += glyph.xAdvance; // ...and only the advance moves the pen\n * }\n * ```\n *\n * The offset does not accumulate and the advance is applied AFTER the glyph. That is\n * byte-for-byte what `perf-harness/src/scenarios/text-hb.ts` runs over npm `harfbuzzjs`'s\n * output, whose pen positions are in turn proven identical to the independently written\n * `hb-atlas` arm's — so matching it is what makes a run shaped here land exactly where every\n * other arm in that round puts it. `test/shape.test.ts` asserts the two agree, glyph for glyph.\n *\n * `[]` FOR EMPTY TEXT AND `null` FOR A FAILURE, which are different answers to different\n * questions. An empty run is legal; a run that could not be shaped is not, and it would\n * otherwise render as a page with some of its text quietly missing.\n *\n * An array of objects rather than a flat typed array: shaping is a startup cost in every\n * consumer here (each run is shaped once and baked), each field is read exactly once on the way\n * into a pen position, and a per-glyph object is field-for-field what npm `harfbuzzjs` hands\n * back — which is what makes the cross-check, and any port off it, a direct comparison rather\n * than a re-derivation.\n */\n shape(text: string, options?: HbGpuShapeOptions): HbGpuShapedGlyph[] | null;\n /**\n * Encode one glyph. `null` when the encoder failed; a zero-length `texels` when the glyph has no\n * ink (a space), which is a different thing and must not be uploaded.\n */\n encode(glyphId: number): EncodedGlyph | null;\n destroy(): void;\n}\n\nexport interface HbGpu {\n /**\n * Bytes of wasm heap this module currently holds.\n *\n * `HEAPU8.byteLength` rather than an instrumented allocator: it reports what the heap has\n * actually grown to, font copy and encoder scratch included, which is the number a consumer\n * weighs against the arm's other costs.\n *\n * IT IS A CEILING, NOT A HIGH-WATER MARK, AND `build.sh` HAD TO BE CHANGED TO MAKE IT USEFUL.\n * A wasm heap is only ever as small as its initial reservation, so this reads whatever\n * `-sINITIAL_MEMORY` asked for until something exceeds it. At emscripten's default it read\n * 16.19 MiB for an 826 KB face — about 8x the truth. The build asks for 2 MiB, re-measured when\n * shaping landed: both S9 fixture faces, 2080 shaped runs and 340 encoded outlines high-water at\n * 1.125 MiB and never reach the reservation. See `build.sh` for the whole ladder.\n *\n * AND IT IS NOW ONE NUMBER RATHER THAN TWO. A caller that shapes here has no second wasm heap to\n * add to it; the arrangement this replaces held every face in both.\n */\n readonly heapBytes: number;\n /**\n * HarfBuzz's own GLSL for one stage: the shared library half plus the draw-renderer half,\n * concatenated in the order `demo-shader.cc` uses. No `main()` — see this file's header.\n */\n shaderLibrary(stage: number): string;\n /**\n * Copy a face into the wasm heap and open an encoder over it, or `null`.\n *\n * `null` RATHER THAN A THROW, and the throw it replaces leaked: the old code allocated the face\n * copy, created a blob, a face and a font, and then threw when the encoder came back null —\n * losing all four for the life of the module. Every failure path below unwinds in the reverse\n * order `destroy` uses. The reason reaches {@link HbGpuOptions.onError}.\n */\n createFont(bytes: Uint8Array): HbGpuFont | null;\n destroy(): void;\n}\n\n/**\n * The subset of emscripten's module object this package uses.\n *\n * Hand-written rather than generated: `-sEXPORTED_FUNCTIONS=@hb-gpu.symbols` already fixes the\n * list, and a type that restates it is a second place the two can disagree — loudly, at the call\n * site, which is where a missing export should be noticed.\n */\nexport interface HbGpuWasmExports {\n HEAPU8: Uint8Array;\n HEAP32: Int32Array;\n /** The view `hb_buffer_add_utf16`'s text is written through. See `build.sh`. */\n HEAPU16: Uint16Array;\n UTF8ToString(pointer: number): string;\n _malloc(bytes: number): number;\n _free(pointer: number): void;\n _hb_blob_create(\n data: number,\n length: number,\n mode: number,\n userData: number,\n destroy: number,\n ): number;\n _hb_blob_get_data(blob: number, lengthOut: number): number;\n _hb_blob_get_length(blob: number): number;\n _hb_blob_destroy(blob: number): void;\n _hb_face_create(blob: number, index: number): number;\n _hb_face_destroy(face: number): void;\n _hb_face_get_upem(face: number): number;\n _hb_font_create(face: number): number;\n _hb_font_destroy(font: number): void;\n _hb_font_set_scale(font: number, xScale: number, yScale: number): void;\n _hb_font_get_nominal_glyph(\n font: number,\n codepoint: number,\n glyphOut: number,\n ): number;\n _hb_shape(\n font: number,\n buffer: number,\n features: number,\n featureCount: number,\n ): void;\n _hb_buffer_create(): number;\n _hb_buffer_destroy(buffer: number): void;\n _hb_buffer_allocation_successful(buffer: number): number;\n _hb_buffer_clear_contents(buffer: number): void;\n _hb_buffer_add_utf16(\n buffer: number,\n text: number,\n textLength: number,\n itemOffset: number,\n itemLength: number,\n ): void;\n _hb_buffer_guess_segment_properties(buffer: number): void;\n _hb_buffer_set_direction(buffer: number, direction: number): void;\n _hb_buffer_set_script(buffer: number, script: number): void;\n _hb_buffer_set_language(buffer: number, language: number): void;\n _hb_language_from_string(text: number, length: number): number;\n _hb_script_from_string(text: number, length: number): number;\n _hb_feature_from_string(\n text: number,\n length: number,\n featureOut: number,\n ): number;\n _hb_buffer_get_length(buffer: number): number;\n _hb_buffer_get_glyph_infos(buffer: number, lengthOut: number): number;\n _hb_buffer_get_glyph_positions(buffer: number, lengthOut: number): number;\n _hb_gpu_draw_create_or_fail(): number;\n _hb_gpu_draw_destroy(draw: number): void;\n _hb_gpu_draw_set_scale(draw: number, xScale: number, yScale: number): void;\n _hb_gpu_draw_glyph_or_fail(draw: number, font: number, glyph: number): number;\n _hb_gpu_draw_encode(draw: number, extentsOut: number): number;\n _hb_gpu_draw_clear(draw: number): void;\n _hb_gpu_draw_reset(draw: number): void;\n _hb_gpu_draw_recycle_blob(draw: number, blob: number): void;\n _hb_gpu_shader_source(stage: number, lang: number): number;\n _hb_gpu_draw_shader_source(stage: number, lang: number): number;\n}\n\n/** The default export of `dist/hb-gpu.mjs`, under `-sMODULARIZE=1 -sEXPORT_ES6=1`. */\nexport type HbGpuModuleFactory = (options: {\n wasmBinary: ArrayBuffer | Uint8Array;\n}) => Promise<HbGpuWasmExports>;\n\n/** `sizeof (hb_glyph_extents_t)`: four `hb_position_t`, which is `int32_t`. */\nconst EXTENTS_BYTES = 16;\n\n/**\n * `sizeof (hb_glyph_info_t)` and `sizeof (hb_glyph_position_t)` on wasm32. Both 20, and the fact\n * that they are EQUAL is a coincidence of layout, not a rule — they are named separately so that a\n * future HarfBuzz that grows one of them cannot silently shift the other's reads.\n *\n * `hb_glyph_info_t` = codepoint, mask, cluster, var1, var2 (5 x uint32)\n * `hb_glyph_position_t` = x_advance, y_advance, x_offset, y_offset, var (5 x int32)\n */\nconst GLYPH_INFO_BYTES = 20;\nconst GLYPH_POSITION_BYTES = 20;\n\n/** `sizeof (hb_feature_t)`: tag, value, start, end. */\nconst FEATURE_BYTES = 16;\n\n/** UTF-8 for the tag and feature strings. ASCII in practice; this is the correct encoder anyway. */\nconst utf8 = new TextEncoder();\n\n/**\n * Instantiate the module.\n *\n * The FACTORY is a parameter rather than an import, and deliberately: `dist/hb-gpu.mjs` is a build\n * output of `build.sh` (docker, emscripten, minutes) that `dist/` gitignores, so a static import\n * here would make this package fail to typecheck on a fresh checkout and fail to build in CI. The\n * caller imports the glue — it is the caller that knows whether the build has been run.\n */\nexport async function createHbGpu(\n factory: HbGpuModuleFactory,\n wasmBinary: ArrayBuffer | Uint8Array,\n options: HbGpuOptions = {},\n): Promise<HbGpu> {\n const wasm = await factory({ wasmBinary });\n const fonts = new Set<HbGpuFont>();\n const fail = (reason: HbGpuFailureReason, message: string): null => {\n options.onError?.({ reason, message: `hb-gpu: ${message}` });\n return null;\n };\n\n return {\n get heapBytes() {\n return wasm.HEAPU8.byteLength;\n },\n\n shaderLibrary(stage) {\n const shared = wasm._hb_gpu_shader_source(stage, HB_GPU_SHADER_LANG_GLSL);\n const draw = wasm._hb_gpu_draw_shader_source(\n stage,\n HB_GPU_SHADER_LANG_GLSL,\n );\n // Both halves, shared first, exactly as `util/gpu/demo-shader.cc` orders them: the draw half\n // calls `_hb_gpu_slug` out of the shared one, and GLSL has no forward declarations for it.\n // A null pointer is a stage with no source (the vertex draw half is empty), not an error.\n return (\n (shared ? wasm.UTF8ToString(shared) : \"\") +\n (draw ? wasm.UTF8ToString(draw) : \"\")\n );\n },\n\n createFont(bytes) {\n // A DETACHED BUFFER READS AS ZERO BYTES, WHICH IS THE COMMON WAY TO GET HERE. Handing the\n // same ArrayBuffer to two wasm modules and letting one transfer it leaves the other with an\n // empty face — which HarfBuzz accepts, encodes to nothing, and renders as a blank page.\n if (bytes.byteLength === 0) {\n return fail(\n \"empty-face\",\n \"createFont was given zero face bytes — a transferred or detached ArrayBuffer looks exactly like this, and the face would encode every glyph to nothing\",\n );\n }\n // COPIED INTO THE WASM HEAP AND KEPT THERE. `HB_MEMORY_MODE_READONLY` means HarfBuzz reads\n // our allocation in place rather than duplicating it, so this is ONE copy of the face inside\n // the module — which is what `heapBytes` should be reporting — but it also means the\n // allocation has to outlive the face, so it is freed in `destroy` and nowhere else.\n const dataPointer = wasm._malloc(bytes.byteLength);\n // 0 IS emscripten's OOM, AND IT IS NOT AN EXCEPTION. `_malloc` returning 0 was unchecked, so\n // `HEAPU8.set(bytes, 0)` wrote the whole face over address 0 — the null page, where\n // emscripten keeps nothing but where every null pointer in the module points. That corrupts\n // the heap silently and the first symptom is somewhere else entirely.\n if (!dataPointer) {\n return fail(\n \"out-of-memory\",\n `_malloc(${bytes.byteLength}) returned 0 — the wasm heap could not grow, and writing the face at address 0 would have corrupted the null page`,\n );\n }\n wasm.HEAPU8.set(bytes, dataPointer);\n\n // Everything from here unwinds through `unwind`, in the reverse order `destroy` uses. The\n // version this replaces threw at the encoder check and leaked the face copy plus the blob,\n // the face and the font with it — once per corrupt face, for the life of the module.\n let blob = 0;\n let face = 0;\n let font = 0;\n let draw = 0;\n let scratch = 0;\n let buffer = 0;\n const unwind = (reason: HbGpuFailureReason, message: string): null => {\n if (buffer) wasm._hb_buffer_destroy(buffer);\n if (draw) wasm._hb_gpu_draw_destroy(draw);\n if (scratch) wasm._free(scratch);\n if (font) wasm._hb_font_destroy(font);\n if (face) wasm._hb_face_destroy(face);\n if (blob) wasm._hb_blob_destroy(blob);\n // LAST, for the reason `destroy` gives: the blob was created READONLY over this pointer.\n wasm._free(dataPointer);\n return fail(reason, message);\n };\n\n blob = wasm._hb_blob_create(\n dataPointer,\n bytes.byteLength,\n HB_MEMORY_MODE_READONLY,\n 0,\n 0,\n );\n // `hb_blob_create` never returns null — it returns the immortal EMPTY blob when it will not\n // take the allocation — so the length round trip is the only way to tell the two apart.\n if (!blob || wasm._hb_blob_get_length(blob) !== bytes.byteLength) {\n return unwind(\n \"face-rejected\",\n `hb_blob_create returned a ${blob ? wasm._hb_blob_get_length(blob) : 0}-byte blob for ${bytes.byteLength} bytes of face — HarfBuzz would not take the allocation`,\n );\n }\n face = wasm._hb_face_create(blob, 0);\n if (!face) {\n return unwind(\"face-rejected\", \"hb_face_create returned null\");\n }\n const upem = wasm._hb_face_get_upem(face);\n // THE `Infinity` THIS EXISTS TO PREVENT is one layer up: `webgl.ts` scales every glyph by\n // `pixelsPerEm / upem`, so a upem of 0 makes every instance record NaN and the draw a silent\n // no-op — a blank page with no error anywhere.\n //\n // HONEST ABOUT WHAT THIS CANNOT DO: HarfBuzz substitutes 1000 for a face with no readable\n // `head` table (`head::get_upem` returns 1000 for anything below 16), so a upem in range does\n // NOT prove the bytes are a font. It proves only that the arithmetic downstream is finite,\n // which is the specific failure this rejects. A truly corrupt face is caught per glyph, by\n // `encode` returning null.\n if (!Number.isInteger(upem) || upem <= 0 || upem > 16384) {\n return unwind(\n \"face-rejected\",\n `the face reports upem ${upem} — every glyph scaled by that is Infinity or NaN, which draws nothing and reports no error`,\n );\n }\n font = wasm._hb_font_create(face);\n if (!font) {\n return unwind(\"face-rejected\", \"hb_font_create returned null\");\n }\n // FONT UNITS, not pixels. HarfBuzz scales outline coordinates by `scale / upem` in integer\n // arithmetic, so asking for a pixel size here would round every curve control point to a\n // whole pixel inside the encoder. It also matters more here than it does for a rasterizer:\n // the blob quantizes to 4 units per step over a +/-8192 range, so a 1000-unit em lands in the\n // middle of the format's precision while a 14-unit one would collapse to nothing.\n // `hb-gpu.h` says as much in its coordinate-system note.\n wasm._hb_font_set_scale(font, upem, upem);\n\n draw = wasm._hb_gpu_draw_create_or_fail();\n if (!draw) {\n return unwind(\n \"encoder-unavailable\",\n \"hb_gpu_draw_create_or_fail returned null — this face has no encoder, and every glyph of it would be missing from the frame\",\n );\n }\n // Redundant with `hb_gpu_draw_glyph_or_fail`, which sets the scale from the font on every\n // call, and set anyway: the scale is written into every blob's header (`buf[1].b/.a`) and\n // the fragment shader divides by it to get ppem. An encoder used for a non-glyph outline —\n // which the public API explicitly supports — would otherwise emit a header saying scale 0.\n wasm._hb_gpu_draw_set_scale(draw, upem, upem);\n\n scratch = wasm._malloc(EXTENTS_BYTES);\n // Same null-page hazard as the face copy, and a nastier one: `glyphFor` and `encode` both\n // read `HEAP32[scratch >> 2]`, so a scratch of 0 would return whatever sits at address 0 as\n // a glyph id and as an extents box.\n if (!scratch) {\n return unwind(\n \"out-of-memory\",\n `_malloc(${EXTENTS_BYTES}) for the extents scratch returned 0 — reading glyph ids out of address 0 would hand back whatever the null page holds`,\n );\n }\n\n // ONE SHAPING BUFFER PER FONT, CREATED HERE AND REUSED. A buffer is where HarfBuzz keeps the\n // run's code points, its glyph array and its positions, and those arrays are what make\n // shaping allocate at all — so creating one per `shape` call would hand the allocator a\n // fresh growth curve on every run. It is cleared, not recreated, between runs.\n buffer = wasm._hb_buffer_create();\n // `hb_buffer_create` NEVER RETURNS NULL — it hands back the immortal EMPTY buffer, the same\n // trap `hb_blob_create` sets above. The empty buffer is the one object whose `successful`\n // flag is false out of the box, so this is the only way to tell it apart, and shaping into\n // it silently produces zero glyphs for every run forever.\n if (!buffer || !wasm._hb_buffer_allocation_successful(buffer)) {\n return unwind(\n \"out-of-memory\",\n \"hb_buffer_create handed back the immortal empty buffer — the heap could not allocate one, and every run shaped into it would come back with no glyphs at all\",\n );\n }\n let alive = true;\n\n const self: HbGpuFont = {\n upem,\n\n glyphFor(codepoint) {\n const ok = wasm._hb_font_get_nominal_glyph(font, codepoint, scratch);\n // Re-read `HEAP32` through the module every time, never through a cached local. Under\n // `ALLOW_MEMORY_GROWTH` emscripten's `updateMemoryViews` REPLACES the typed arrays on\n // growth — except when the engine gave it a resizable `ArrayBuffer`, in which case it\n // returns early and the old views keep working. So a cached view is correct on some\n // engines and a detached zero-length array on others, which is worse than being simply\n // wrong: it would pass here and fail on a phone.\n return ok ? wasm.HEAP32[scratch >> 2] >>> 0 : 0;\n },\n\n shape(text, options = {}) {\n // CLEARED FIRST, NOT LAST, and `clear_contents` rather than `reset`: it drops the\n // previous run's glyphs AND puts the segment properties back to invalid, so a run that\n // asked for `direction: \"rtl\"` cannot leak its direction into the next one.\n wasm._hb_buffer_clear_contents(buffer);\n // Nothing to shape and nothing to allocate. `[]` and not `null`, because an empty run is\n // an answer and only a failure is a refusal.\n if (text.length === 0) return [];\n\n // ONE `_malloc` FOR THE WHOLE CALL — one null check, one `_free` on every path out,\n // which is the shape the rest of this file already uses. Laid out:\n //\n // [0, textBytes) the run, as UTF-16 code units\n // [featuresAt, +16 * n) hb_feature_t[]\n // [stringsAt, ...) script tag, language tag, feature strings\n //\n // THE TEXT IS AT OFFSET 0 AND HAS TO BE. `hb_buffer_add_utf16` reads a `const uint16_t\n // *`, and the base pointer is the only part of the block `_malloc` guarantees is even.\n const featureStrings = options.features ?? [];\n const textBytes = text.length * 2;\n // `hb_feature_t` is four uint32s and wants 4-byte alignment; `textBytes` is even, not\n // necessarily a multiple of four.\n const featuresAt = (textBytes + 3) & ~3;\n const strings: { bytes: Uint8Array; at: number }[] = [];\n let end = featuresAt + featureStrings.length * FEATURE_BYTES;\n const place = (value: string): { at: number; length: number } => {\n const bytes = utf8.encode(value);\n strings.push({ bytes, at: end });\n const placed = { at: end, length: bytes.length };\n end += bytes.length;\n return placed;\n };\n const script = options.script ? place(options.script) : null;\n const language = options.language ? place(options.language) : null;\n const features = featureStrings.map(place);\n\n const block = wasm._malloc(end);\n // Same null-page hazard as the face copy: at 0, `HEAPU16.set` would write the run over\n // the null page and `hb_buffer_add_utf16` would read whatever is there as text.\n if (!block) {\n return fail(\n \"out-of-memory\",\n `_malloc(${end}) for a ${text.length}-code-unit run returned 0 — the wasm heap could not grow, and shaping out of address 0 would read the null page as text`,\n );\n }\n\n // `charCodeAt` and not a code-point iteration: `hb_buffer_add_utf16` wants the raw code\n // units, surrogate pairs included, which is exactly what a JS string already holds.\n const units = new Uint16Array(text.length);\n for (let i = 0; i < text.length; i += 1)\n units[i] = text.charCodeAt(i);\n wasm.HEAPU16.set(units, block >> 1);\n for (const string of strings) {\n wasm.HEAPU8.set(string.bytes, block + string.at);\n }\n\n for (let i = 0; i < features.length; i += 1) {\n const at = block + featuresAt + i * FEATURE_BYTES;\n if (\n !wasm._hb_feature_from_string(\n block + features[i].at,\n features[i].length,\n at,\n )\n ) {\n wasm._free(block);\n // REFUSED, NOT SKIPPED. HarfBuzz zeroes the struct and returns false, and a zeroed\n // `hb_feature_t` is a tag of 0 applied over the whole run — so shaping on would\n // produce a run laid out WITHOUT the feature that was asked for, and say nothing.\n return fail(\n \"feature-malformed\",\n `hb_feature_from_string refused \"${featureStrings[i]}\" — shaping on would silently lay the run out without the feature, so the run is refused instead`,\n );\n }\n }\n\n wasm._hb_buffer_add_utf16(buffer, block, text.length, 0, text.length);\n // Explicit properties BEFORE the guess, because `guess_segment_properties` only fills in\n // what is still invalid — so anything set here wins and the heuristic covers the rest.\n if (options.direction) {\n wasm._hb_buffer_set_direction(\n buffer,\n HB_DIRECTION[options.direction],\n );\n }\n if (script) {\n wasm._hb_buffer_set_script(\n buffer,\n wasm._hb_script_from_string(block + script.at, script.length),\n );\n }\n if (language) {\n wasm._hb_buffer_set_language(\n buffer,\n wasm._hb_language_from_string(\n block + language.at,\n language.length,\n ),\n );\n }\n wasm._hb_buffer_guess_segment_properties(buffer);\n\n wasm._hb_shape(\n font,\n buffer,\n features.length ? block + featuresAt : 0,\n features.length,\n );\n // FREED HERE AND NOT LATER. The buffer copied the code points in, and the features were\n // read during `hb_shape`; the glyph arrays below live in the buffer, not in this block.\n wasm._free(block);\n\n // The SILENT failure this catches: an allocation that failed anywhere inside\n // `add_utf16` or `hb_shape` leaves the buffer un-`successful` and EMPTY, which is\n // indistinguishable from \"this run had no glyphs\" at the call site.\n if (!wasm._hb_buffer_allocation_successful(buffer)) {\n return fail(\n \"out-of-memory\",\n `shaping a ${text.length}-code-unit run exhausted the wasm heap — the buffer came back empty, which is indistinguishable from a run with no glyphs and would render as missing text`,\n );\n }\n\n const count = wasm._hb_buffer_get_length(buffer);\n const infos = wasm._hb_buffer_get_glyph_infos(buffer, 0);\n const positions = wasm._hb_buffer_get_glyph_positions(buffer, 0);\n // CACHED ONLY HERE, AND ONLY BECAUSE NOTHING BELOW ALLOCATES. `glyphFor` re-reads\n // `wasm.HEAP32` on every access for the reason stated there — growth replaces the view\n // on some engines and not others. Everything that could grow the heap has already\n // happened by this line, and the loop only reads, so one lookup is safe here and a\n // per-glyph property read on a few hundred glyphs is not free.\n const heap = wasm.HEAP32;\n const run: HbGpuShapedGlyph[] = [];\n for (let i = 0; i < count; i += 1) {\n const info = (infos + i * GLYPH_INFO_BYTES) >> 2;\n const position = (positions + i * GLYPH_POSITION_BYTES) >> 2;\n run.push({\n // `>>> 0`: both are `uint32_t` in C and `HEAP32` is signed.\n glyphId: heap[info] >>> 0,\n cluster: heap[info + 2] >>> 0,\n // `hb_position_t` IS signed, so these are read as-is — a negative x_offset is how a\n // mark gets placed to the left of the glyph it hangs off.\n xAdvance: heap[position],\n yAdvance: heap[position + 1],\n xOffset: heap[position + 2],\n yOffset: heap[position + 3],\n });\n }\n return run;\n },\n\n encode(glyphId) {\n // Clear first. `hb_gpu_draw_encode` auto-clears on the way out, but a FAILED\n // `glyph_or_fail` leaves partial curves behind, and the next glyph would then encode\n // itself plus somebody else's strokes — which renders as a plausible glyph with a stray\n // mark, the hardest kind of error to trace.\n wasm._hb_gpu_draw_clear(draw);\n if (!wasm._hb_gpu_draw_glyph_or_fail(draw, font, glyphId)) {\n return null;\n }\n const blobPointer = wasm._hb_gpu_draw_encode(draw, scratch);\n if (!blobPointer) {\n return null;\n }\n const extents: HbGpuGlyphExtents = {\n xBearing: wasm.HEAP32[scratch >> 2],\n yBearing: wasm.HEAP32[(scratch >> 2) + 1],\n width: wasm.HEAP32[(scratch >> 2) + 2],\n height: wasm.HEAP32[(scratch >> 2) + 3],\n };\n const length = wasm._hb_blob_get_length(blobPointer);\n const data = wasm._hb_blob_get_data(blobPointer, 0);\n // `.slice`, not `.subarray`: the bytes are recycled on the very next line, and a view\n // onto a recycled allocation is a texel stream that changes under the caller.\n const texels = wasm.HEAPU8.slice(data, data + length);\n // RECYCLED, not destroyed. The encoder keeps one blob's allocation for reuse, so a run\n // of a few hundred Han glyphs makes a few hundred encodes out of one buffer.\n wasm._hb_gpu_draw_recycle_blob(draw, blobPointer);\n return { texels, extents };\n },\n\n destroy() {\n if (!alive) return;\n alive = false;\n fonts.delete(self);\n wasm._hb_buffer_destroy(buffer);\n wasm._hb_gpu_draw_destroy(draw);\n wasm._free(scratch);\n wasm._hb_font_destroy(font);\n wasm._hb_face_destroy(face);\n wasm._hb_blob_destroy(blob);\n // LAST. The blob was created READONLY over this pointer, so freeing it before the blob\n // is destroyed hands HarfBuzz a dangling face for the length of one more statement.\n wasm._free(dataPointer);\n },\n };\n fonts.add(self);\n return self;\n },\n\n destroy() {\n for (const font of [...fonts]) font.destroy();\n },\n };\n}\n\n/**\n * Total and per-glyph encoded bytes for a set of glyphs.\n *\n * THE PREDICTION UNDER TEST, made directly measurable. `docs/text-rendering.md` records it as\n * \"~5.4 KB per Han glyph against ~1.4 KB for a 38x38 R8 atlas cell\", and that comparison decides\n * the VRAM half of whether this arm is worth shipping — so it should be one call, on the real\n * fixture face, rather than something reconstructed from a frame counter.\n *\n * DISTINCT glyph ids, because that is what the atlas pays for too: a Han run draws 12 glyphs and an\n * atlas stores as many outlines as the pool has distinct members.\n */\nexport function measureBlobBytes(\n font: HbGpuFont,\n glyphIds: Iterable<number>,\n): {\n glyphs: number;\n totalBytes: number;\n bytesPerGlyph: number;\n minBytes: number;\n maxBytes: number;\n} {\n let glyphs = 0;\n let totalBytes = 0;\n let minBytes = Number.POSITIVE_INFINITY;\n let maxBytes = 0;\n for (const id of new Set(glyphIds)) {\n const encoded = font.encode(id);\n if (!encoded || encoded.texels.length === 0) continue;\n glyphs += 1;\n totalBytes += encoded.texels.length;\n minBytes = Math.min(minBytes, encoded.texels.length);\n maxBytes = Math.max(maxBytes, encoded.texels.length);\n }\n // MIN AND MAX, not just the mean, because the spread is the interesting part: measured on the\n // fixture's Han pool the mean is 4.26 KiB and the range is 376 B to 9568 B, a factor of 25. A\n // single average would suggest an atlas can be sized by multiplying it by a glyph count.\n return {\n glyphs,\n totalBytes,\n bytesPerGlyph: glyphs > 0 ? totalBytes / glyphs : 0,\n minBytes: glyphs > 0 ? minBytes : 0,\n maxBytes,\n };\n}\n"],"mappings":"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"}