@godot-scene-web/canvas 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":"glyph-pass-hbgpu.js","names":[],"sources":["../src/glyph-shape-cache.ts","../src/glyph-pass-hbgpu.ts"],"sourcesContent":["import type { HbGpuShapeOptions } from \"@godot-scene-web/hb-gpu\";\n\nexport interface GlyphShapeCacheStats {\n shapeHits: number;\n shapeMisses: number;\n shapeEntries: number;\n shapeGlyphs: number;\n shapeEvicted: number;\n}\n\nexport interface CachedShape {\n count: number;\n slots: Int32Array;\n /** `[penX + xOffset, penY + yOffset]`, in font units. */\n pen: Int32Array;\n advanceX: number;\n advanceY: number;\n usedAt: number;\n}\n\nexport type CachedShapeData = Omit<CachedShape, \"usedAt\">;\n\n/**\n * Small bounded cache for HarfBuzz's scale-free font-unit output. Unknown options deliberately\n * bypass it: forwarding a future shaping option must not accidentally reuse an older result.\n */\nexport class GlyphShapeCache {\n private readonly byFace = new Map<number, Map<string, CachedShape>>();\n private tick = 0;\n\n constructor(\n private readonly maxEntries: number,\n private readonly maxGlyphs: number,\n private readonly stats: GlyphShapeCacheStats,\n ) {}\n\n key(text: string, options: HbGpuShapeOptions | undefined): string | null {\n if (!options || Object.keys(options).length === 0) return text;\n const allowed = new Set([\"direction\", \"script\", \"language\", \"features\"]);\n if (Object.keys(options).some((name) => !allowed.has(name))) return null;\n // Features are an ordered OpenType program: sorting would change a caller's request.\n return JSON.stringify([\n options.direction,\n options.script,\n options.language,\n options.features,\n text,\n ]);\n }\n\n get(faceId: number, key: string): CachedShape | null {\n if (this.maxEntries === 0 || this.maxGlyphs === 0) return null;\n const entry = this.byFace.get(faceId)?.get(key);\n if (!entry) return null;\n entry.usedAt = ++this.tick;\n this.stats.shapeHits += 1;\n return entry;\n }\n\n put(faceId: number, key: string, shape: CachedShapeData): void {\n if (\n this.maxEntries === 0 ||\n this.maxGlyphs === 0 ||\n shape.count > this.maxGlyphs\n )\n return;\n while (\n this.stats.shapeEntries >= this.maxEntries ||\n this.stats.shapeGlyphs + shape.count > this.maxGlyphs\n ) {\n let oldestFace: Map<string, CachedShape> | undefined;\n let oldestKey: string | undefined;\n let oldest = Infinity;\n for (const entries of this.byFace.values()) {\n for (const [candidateKey, candidate] of entries) {\n if (candidate.usedAt < oldest) {\n oldest = candidate.usedAt;\n oldestFace = entries;\n oldestKey = candidateKey;\n }\n }\n }\n if (!oldestFace || oldestKey === undefined) break;\n const removed = oldestFace.get(oldestKey)!;\n oldestFace.delete(oldestKey);\n this.stats.shapeEntries -= 1;\n this.stats.shapeGlyphs -= removed.count;\n this.stats.shapeEvicted += 1;\n }\n let entries = this.byFace.get(faceId);\n if (!entries) {\n entries = new Map();\n this.byFace.set(faceId, entries);\n }\n entries.set(key, { ...shape, usedAt: ++this.tick });\n this.stats.shapeEntries += 1;\n this.stats.shapeGlyphs += shape.count;\n }\n\n miss(): void {\n this.stats.shapeMisses += 1;\n }\n\n clear(): void {\n this.byFace.clear();\n this.stats.shapeEntries = 0;\n this.stats.shapeGlyphs = 0;\n }\n}\n","// The glyph pass this package ships: `GlyphPass` over `@godot-scene-web/hb-gpu`.\n//\n// NOT ON THE MAIN BARREL, AND THAT IS THE FIRST THING TO KNOW ABOUT THIS FILE. It is reached\n// through the `./glyphs` subpath (`@godot-scene-web/canvas/glyphs`) for two reasons that both cost\n// something real: the main barrel's export list is mirrored by hand-maintained ambient `.d.ts`\n// files in `../sts2-couch-coop` and `../spirectl` (see `AGENTS.md`), so every name on it is work\n// downstream every time it moves; and a scene with no text should not pull a glyph renderer, its\n// wasm and a multi-MiB atlas into the bundle to draw rectangles.\n//\n// WHAT THIS FILE OWNS: THE ATLAS IDENTITY THE DRAW LIST'S SLOT IDS REFER TO. `GlyphsView.slots` is\n// a run of INTEGERS, not atlas offsets, because an atlas evicts and a retained draw list outlives\n// its evictions — recording an offset is how you get a different glyph's outline drawn at the right\n// size, in the right place, perfectly antialiased. The indirection has to be resolved by whoever\n// owns the atlas, which is this file: it hands out dense ids from `slotFor`, and at draw time turns\n// each one back into a live `GlyphSlot` through `HbGpuRenderer.resolve`, re-encoding and\n// re-uploading ONLY on a miss.\n//\n// SHAPING IS OPTIONAL HERE, AND WHEN IT HAPPENS IT HAPPENS IN THE SAME MODULE. The contract is a\n// glyph id and a pen position, so a caller that already has them from anywhere can fill a\n// `GlyphsView` itself. But {@link HbGpuGlyphPass.fillRun} shapes a string through the SAME\n// `HbGpuFont` this pass encodes outlines from, which is what lets a page hold one HarfBuzz instead\n// of adding npm `harfbuzzjs` as a second build with the same faces in a second heap — measured at\n// 4.50 MiB against 2.00 for the perf harness's own arm (`docs/text-rendering.md`). It also removes\n// a class of mismatch rather than making it unlikely: one `hb_font_t` cannot disagree with itself\n// about what gid 97 means.\n//\n// FILL AND OUTLINE, AND THE OUTLINE IS THE CALLER'S SECOND RUN. `GlyphsView.spreadPx` grows every\n// glyph of a run outward before filling it, so an outlined label is the same glyphs and pens\n// recorded TWICE — outline colour with a spread, then fill colour without, in that order. This pass\n// does not synthesise the pair: the kind carries one colour, and a command that quietly expanded to\n// two draws would hide the ordering, which is the half a caller has to get right. What the pass\n// does own is that the dilation happens INSIDE one fragment shader (`HbGpuRenderer.setSpread`)\n// rather than as N offset copies of the run, which is the only way a TRANSLUCENT outline\n// composites once instead of N times.\n//\n// THE FIDELITY CAVEAT SHIPS WITH THE API, because it is measured and it is a constraint rather\n// than a bug: HarfBuzz's coverage shader takes a five-tap MSAA branch below ppem 16\n// (`src/hb-gpu-fragment.glsl:321`, `if (ppem < 16.0)`). Graded against an 8x-downsampled\n// reference, this path's distortion is 0.196 on Han at ppem 14 — BLURRIER than the shipped DOM\n// path's 0.132 — and 0.017 at ppem 49 (a phone rendering 14 px at DPR 3.5). So: use it when\n// `pixelsPerEm * the scale in run.m * devicePixelRatio >= 16`, and below that a baked atlas or the\n// DOM path is crisper. THE MIDDLE FACTOR IS NOT DECORATION: a caller that scales a label through\n// its model matrix — which is where scale belongs, next to the rotation — is drawing at a ppem its\n// `pixelsPerEm` never mentions, and the shader gates on what it actually gets.\n// See {@link PPEM_FIDELITY_FLOOR} and `docs/text-rendering.md`.\n//\n// AND THE OTHER FIDELITY KNOB IS THE CONTRAST CURVE, WHICH IS NOT THE SAME QUESTION. The floor\n// above is about a size this path should not be asked to draw; {@link HbGpuGlyphPassOptions.contrast}\n// is about how it draws every size it does take. hb-gpu ships stem darkening ON, which is right\n// against the DOM text path and wrong against Godot — the A1 crossover sweep puts this path BEHIND\n// canvas2d at ppem 16 with the curve and ahead of every arm at every swept size without it. A\n// consumer mirroring a Godot scene passes `HB_GPU_CONTRAST_NONE`; omitting the option keeps the\n// shipped default, and therefore keeps every existing picture.\n\nimport type {\n EncodedGlyph,\n HbGpu,\n HbGpuFailure,\n HbGpuFont,\n HbGpuShapeOptions,\n} from \"@godot-scene-web/hb-gpu\";\nimport {\n createHbGpuRenderer,\n type GlyphSlot,\n type HbGpuContrast,\n type HbGpuFace,\n type HbGpuRenderer,\n} from \"@godot-scene-web/hb-gpu/webgl\";\nimport type { GlyphsView } from \"./draw-list\";\nimport type { GlyphPass } from \"./glyph-pass\";\nimport { type CachedShapeData, GlyphShapeCache } from \"./glyph-shape-cache\";\nimport type { StageProjection } from \"./present\";\n\n/**\n * Below this ppem the outline path is measurably blurrier than the alternatives.\n *\n * `ppem` here is `GlyphsView.pixelsPerEm` times the scale in `GlyphsView.m` times the device-pixel\n * ratio — the size in DEVICE pixels, which is the only size the shader can see, and it sees all\n * three factors (it derives ppem from `fwidth`). The number is HarfBuzz's own branch point, not a\n * threshold chosen here: `hb_gpu_draw` switches to a five-tap MSAA approximation under it.\n */\nexport const PPEM_FIDELITY_FLOOR = 16;\n\n/**\n * The id `slotFor` returns for a glyph with no ink, or one the atlas declined.\n *\n * Write it into `GlyphsView.slots` and the pass skips the glyph. It is a distinct value ON PURPOSE:\n * a caller that wrote a real-looking id for a space would get a degenerate quad reading texel 0,\n * which is some other glyph's header.\n */\nexport const GLYPH_SLOT_NONE = -1;\n\n/**\n * A face registered with the pass: the handle `slotFor` takes.\n *\n * It pairs the ENCODER (`HbGpuFont`, which turns a glyph id into an outline blob) with the atlas\n * FACE handle that namespaces that glyph id, because the two must not drift: glyph 42 of Noto Sans\n * SC and glyph 42 of Roboto are unrelated outlines, and an atlas that cannot tell them apart\n * renders fluent, crisp, wrong text.\n */\nexport interface GlyphFace {\n readonly font: HbGpuFont;\n readonly face: HbGpuFace;\n /** Units per em. The scale `GlyphsView.pixelsPerEm` is measured against. */\n readonly upem: number;\n readonly label: string;\n}\n\nexport interface HbGpuGlyphPassStats {\n /** Distinct `(face, glyph)` pairs that have ever been given an id. */\n slots: number;\n /** Runs drawn. */\n runs: number;\n /** Glyphs the renderer reported drawing, summed over runs. */\n glyphs: number;\n /** Glyphs skipped because their slot id was {@link GLYPH_SLOT_NONE} — spaces, mostly. */\n inkless: number;\n /**\n * Glyphs whose slot id resolved to nothing and had to be encoded and uploaded again.\n *\n * THE COUNTER THAT SAYS THE ATLAS IS TOO SMALL. Non-zero once, on a cold list, is the mechanism\n * working. Non-zero every frame means the working set does not fit and every frame is paying an\n * encode — raise `atlasTexels`.\n */\n reuploads: number;\n /**\n * Glyphs the pass could not draw at all: an id it never issued, an encode that failed, or a\n * re-upload the atlas refused. A hole in a word, so it is counted rather than left to a\n * screenshot.\n */\n dropped: number;\n /** Runs whose device ppem was under {@link PPEM_FIDELITY_FLOOR}. See this file's header. */\n runsBelowPpemFloor: number;\n /** Additive shaping-cache counters. */\n shapeHits: number;\n shapeMisses: number;\n shapeEntries: number;\n shapeGlyphs: number;\n shapeEvicted: number;\n}\n\nexport interface HbGpuGlyphPassOptions {\n /**\n * The context to draw in — the SAME one the executor was given.\n *\n * It must be `premultipliedAlpha: true`, which is what `createCanvasStage` asks for; hb-gpu's\n * fragment writes premultiplied coverage and a straight-alpha canvas composites it twice,\n * silently, as a merely darker picture.\n */\n gl: WebGL2RenderingContext;\n /** An instantiated hb-gpu module. The pass reads HarfBuzz's own GLSL and its encoders out of it. */\n module: HbGpu;\n /**\n * The scene's coordinate extent — `CanvasStage.designWidth`, the space a `GlyphsView` lives in.\n *\n * Only a starting value: `drawRun` takes both sizes from the frame's `StageProjection`, which is\n * the one authority on where design space lands.\n */\n designWidth: number;\n designHeight: number;\n /** The achieved drawing buffer. Defaults to the design pair, i.e. a device-pixel ratio of 1. */\n framebufferWidth?: number;\n framebufferHeight?: number;\n /** Atlas capacity in texels; see `HbGpuRendererOptions.atlasTexels`. */\n atlasTexels?: number;\n /**\n * The contrast curve hb-gpu applies to the coverage it computed. Omit for the renderer's own\n * default, `HB_GPU_CONTRAST_DEFAULT` — stem darkening ON.\n *\n * OMITTING IT IS EXACTLY THE PICTURE THIS PASS DREW BEFORE THE OPTION EXISTED. It is not\n * defaulted here: `undefined` is forwarded as `undefined` and `createHbGpuRenderer` decides, so\n * every existing consumer and every committed pixel golden is byte-identical to before.\n *\n * WHY A GODOT-PARITY CONSUMER PASSES `HB_GPU_CONTRAST_NONE`, AND IT IS MEASURED RATHER THAN\n * PREFERRED. Stem darkening is not a tie-breaker inside this path's quality — it IS the quality\n * crossover against the other text arms. Distortion against an 8x area-coverage reference, swept\n * over ppem 16-52 by `packages/perf-harness/probes/text-crossover.ts`\n * (`pnpm -w run text:crossover -- --hb-contrast default|none`):\n *\n * ppem 16 default 0.1091 none 0.0258 canvas2d 0.0925\n * ppem 24 default 0.0537 none 0.0138\n *\n * So the shipped curve LOSES to a plain `ctx.fillText` at 16 and costs this path ~4x its own\n * achievable distortion at 24, while with the curve off hb-gpu wins at every ppem swept, with\n * the tightest edges of any arm. AND GODOT APPLIES NO CURVE OF ITS OWN — its grayscale and MSDF\n * glyph interiors come out byte-uniform — so a consumer whose acceptance test is \"does this look\n * like the engine we are mirroring\" is holding its text up against a rasterizer that never\n * darkened a stem, and the correction reads as a weight mismatch rather than as contrast.\n *\n * THE DEFAULT STAYS ON, because the case it was measured for is real and is a different case:\n * against the DOM text path, uncorrected coverage is washed out (`HbGpuContrast`'s own doc — DOM\n * puts 66% more pixels in the deep-dark end of one fixed crop). Text that is not standing next\n * to Godot's own output still wants it.\n *\n * IT MOVES EDGES, NOT INK, WHICHEVER WAY YOU SET IT. `hb_gpu_stem_darken` is gated on partial\n * coverage (`cov > 0 && cov < 1`), so a glyph INTERIOR is byte-identical either way and only the\n * ramp at the boundary narrows or widens. The library also ramps the correction off by ppem 48,\n * so display-sized text barely moves and small UI text moves most.\n *\n * IT REACHES THE FILL PASS ONLY, and that is the renderer's rule rather than this pass's: a run\n * drawn with a non-zero `GlyphsView.spreadPx` emits raw coverage whatever this says. An outlined\n * label is therefore already half-uncorrected today — see `HbGpuContrast.stemDarkening`.\n */\n contrast?: HbGpuContrast;\n /** Opt-in expanded hb-gpu records used only by adjacent-run batching. */\n batchAdjacentRuns?: boolean;\n /** Every refusal, from this file and from the renderer under it. */\n onError?(failure: HbGpuFailure): void;\n /**\n * Warn on the console the first time a run is drawn below {@link PPEM_FIDELITY_FLOOR}. Default\n * on.\n *\n * ONCE, not per run: a page of small text would otherwise emit a line per label per frame, and a\n * warning nobody can read is a warning nobody reads. {@link HbGpuGlyphPassStats.runsBelowPpemFloor}\n * keeps the full tally. It is a `console.warn` rather than an `onError` because it is not a\n * refusal — the text IS drawn, and only a measurement says another path would draw it better.\n */\n warnBelowPpemFloor?: boolean;\n /** Maximum cached shaped runs. Zero disables memoisation. Defaults to 512. */\n shapeCacheEntries?: number;\n /** Maximum glyphs retained by shaped-run memoisation. Defaults to entries × 64. */\n shapeCacheGlyphs?: number;\n}\n\nexport interface HbGpuGlyphPass extends GlyphPass {\n /** The renderer underneath. Exposed for its `atlas` / `blobs` stats. */\n readonly renderer: HbGpuRenderer;\n readonly stats: HbGpuGlyphPassStats;\n /**\n * Register a face from its bytes, or `null` if hb-gpu will not take them.\n *\n * THE SAME BYTES THE SHAPER WAS GIVEN. Glyph ids come out of one HarfBuzz and outlines out of\n * another; two different files make gid 97 two different outlines, and the result is crisp,\n * fluent, wrong text that no counter downstream can see.\n */\n registerFace(bytes: Uint8Array, label?: string): GlyphFace | null;\n /**\n * The stable slot id for one glyph of one face — what a caller writes into `GlyphsView.slots`.\n *\n * Uploads the outline on first use and is a map lookup after that. The id is dense, permanent\n * and survives eviction: it names an entry in THIS pass's table, not a place in the atlas.\n *\n * {@link GLYPH_SLOT_NONE} for a glyph with no ink and for one the atlas refused.\n */\n slotFor(face: GlyphFace, glyphId: number): number;\n /** Glyph id for a code point in `face`, or 0 (`.notdef`). A convenience over `HbGpuFont`. */\n glyphFor(face: GlyphFace, codepoint: number): number;\n /**\n * Shape `text` and fill `run` with the result: slots, pen positions and count. `false` if the\n * shaper refused, leaving `run` untouched.\n *\n * ONE HARFBUZZ, WHICH IS THE WHOLE POINT OF HAVING THIS AT ALL. hb-gpu's wasm exports the\n * OpenType shaper as well as the Slug encoder, so a consumer that calls this loads one HarfBuzz\n * and holds each face once. Shaping elsewhere — npm `harfbuzzjs`, most likely — means a second\n * build, a second heap and the same font bytes resident twice, which measured 4.19 MiB across\n * the pair on a phone (`docs/text-rendering.md`). It is also the configuration in which glyph\n * ids and outlines can come from two DIFFERENT files, and gid 97 meaning two different things is\n * crisp, fluent, wrong text that no counter downstream can see.\n *\n * THE PEN ARITHMETIC LIVES HERE FOR THE SAME REASON IT IS EASY TO GET WRONG. HarfBuzz reports\n * advances and offsets in FONT units, y-UP; `GlyphsView.positions` are design units, y-DOWN,\n * measured from the run's origin. So each is scaled by `run.pixelsPerEm / face.upem` and y is\n * negated, an offset positions its own glyph only, and the advance accumulates after it. Getting\n * the scale or the y sign wrong produces text that looks entirely plausible and is subtly\n * mis-spaced — which is why there is one implementation rather than one per caller.\n *\n * `run.pixelsPerEm` must be set BEFORE the call. Exactly three fields are written — `slots`,\n * `positions` and `glyphCount` — and `run.slots` / `run.positions` are replaced by larger buffers\n * if the run does not fit. Inkless glyphs (a space) are skipped rather than given a slot, so\n * `run.glyphCount` can be smaller than the shaped length.\n *\n * `m`, the colour and `spreadPx` are LEFT ALONE, which is what makes an outlined label one shape\n * and two pushes: shape once, then `pushGlyphs` with the outline colour and a spread, then again\n * with the fill colour and `spreadPx = 0`. Both runs then carry pen positions that came from a\n * single shaping pass and cannot drift apart.\n */\n fillRun(\n run: GlyphsView,\n face: GlyphFace,\n text: string,\n options?: HbGpuShapeOptions,\n ): boolean;\n /** Drop shaped-run memoisation without disturbing faces or atlas residency. */\n clearShapeCache(): void;\n /** Re-state both sizes after the stage resized. See `HbGpuRenderer.setViewport`. */\n setViewport(\n designWidth: number,\n designHeight: number,\n framebufferWidth?: number,\n framebufferHeight?: number,\n ): void;\n /** Forward of `HbGpuRenderer.notifyContextLost` — wire it to the stage's `onContextLost`. */\n notifyContextLost(): void;\n /** Forward of `HbGpuRenderer.rebuild` — wire it to the stage's `onContextRestored`. */\n rebuild(): boolean;\n /** Delete the renderer's GL objects. Does NOT touch the context or the registered fonts. */\n dispose(): void;\n}\n\n/** One entry of the pass's slot table: everything needed to put the glyph back after an eviction. */\ninterface SlotEntry {\n face: GlyphFace;\n glyphId: number;\n}\n\n/**\n * Build a glyph pass over a borrowed context, or `null` when the renderer will not build.\n *\n * `null` rather than a throw, following `createCanvasStage` and `createHbGpuRenderer`: a consumer\n * that cannot have the GPU glyph path falls back to its DOM one, and only it knows whether that is\n * acceptable. Pass `onError` to be told why — a pass that declined silently reports as a page with\n * no words on it.\n */\nexport function createHbGpuGlyphPass(\n options: HbGpuGlyphPassOptions,\n): HbGpuGlyphPass | null {\n const report = (failure: HbGpuFailure): void => options.onError?.(failure);\n const rendererOptions = {\n gl: options.gl,\n designWidth: options.designWidth,\n designHeight: options.designHeight,\n framebufferWidth: options.framebufferWidth,\n framebufferHeight: options.framebufferHeight,\n atlasTexels: options.atlasTexels,\n // FORWARDED, NEVER DEFAULTED HERE. `undefined` has to reach `createHbGpuRenderer` as\n // `undefined` so the renderer's own `HB_GPU_CONTRAST_DEFAULT` is the one default in the chain;\n // writing `options.contrast ?? HB_GPU_CONTRAST_DEFAULT` would pin a copy of it that a change\n // on the hb-gpu side could no longer move. See {@link HbGpuGlyphPassOptions.contrast}.\n contrast: options.contrast,\n perInstanceRunState: options.batchAdjacentRuns === true,\n onError: report,\n } as Parameters<typeof createHbGpuRenderer>[1] & {\n perInstanceRunState?: boolean;\n };\n const renderer = createHbGpuRenderer(options.module, rendererOptions);\n if (!renderer) return null;\n // Aliased non-nullable, because TypeScript drops a narrowing across a hoisted function\n // declaration even when the binding is `const` — the same note `hb-gpu`'s own `webgl.ts` carries\n // about its stage.\n const live: HbGpuRenderer = renderer;\n const batchAdjacentRuns = options.batchAdjacentRuns === true;\n\n // DENSE IDS INTO A TABLE THIS FILE OWNS, not the atlas's offsets. An id is permanent: the entry\n // it names is never removed, only the glyph's atlas residency comes and goes.\n const entries: SlotEntry[] = [];\n const idByFace: Map<number, number>[] = [];\n const faces: GlyphFace[] = [];\n\n const stats: HbGpuGlyphPassStats = {\n slots: 0,\n runs: 0,\n glyphs: 0,\n inkless: 0,\n reuploads: 0,\n dropped: 0,\n runsBelowPpemFloor: 0,\n shapeHits: 0,\n shapeMisses: 0,\n shapeEntries: 0,\n shapeGlyphs: 0,\n shapeEvicted: 0,\n };\n const shapeCacheEntries = Math.max(0, options.shapeCacheEntries ?? 512);\n const shapeCache = new GlyphShapeCache(\n shapeCacheEntries,\n Math.max(0, options.shapeCacheGlyphs ?? shapeCacheEntries * 64),\n stats,\n );\n let warnedBelowPpemFloor = false;\n // `drawRuns` borrows the existing per-run preparation verbatim, but brackets the whole adjacent\n // sequence with one hb-gpu begin/end. State setters are captured per glyph by hb-gpu, so colour,\n // affine model and spread remain run-local inside the single instanced draw.\n let adjacentBatchOpen = false;\n\n let designWidth = Math.max(1, options.designWidth);\n let designHeight = Math.max(1, options.designHeight);\n let framebufferWidth = Math.max(\n 1,\n options.framebufferWidth ?? options.designWidth,\n );\n let framebufferHeight = Math.max(\n 1,\n options.framebufferHeight ?? options.designHeight,\n );\n\n /**\n * The slot table, as a plain function rather than only a method.\n *\n * `fillRun` needs it too, and reaching it through `this` would break the moment a caller\n * destructured the pass — a shape the rest of this package supports everywhere else. `live` and\n * not `renderer` because TypeScript will not carry the null narrowing above into a function\n * declaration, which could in principle be called before it.\n */\n function slotIdFor(face: GlyphFace, glyphId: number): number {\n let byGlyph = idByFace[face.face.id];\n if (!byGlyph) {\n byGlyph = new Map();\n idByFace[face.face.id] = byGlyph;\n }\n const cached = byGlyph.get(glyphId);\n if (cached !== undefined) return cached;\n const entry: SlotEntry = { face, glyphId };\n const glyph = encode(entry);\n if (!glyph) return GLYPH_SLOT_NONE;\n // A blank glyph — a space — encodes to a zero-length blob. A legitimate result and NOT an\n // allocation, so it gets no id at all: see {@link GLYPH_SLOT_NONE}.\n if (glyph.texels.length === 0) return GLYPH_SLOT_NONE;\n if (!live.upload(face.face, glyphId, glyph)) return GLYPH_SLOT_NONE;\n const id = entries.length;\n entries.push(entry);\n byGlyph.set(glyphId, id);\n stats.slots = entries.length;\n return id;\n }\n\n /** Encode one glyph, or say why not. A zero-length blob means \"no ink\", not \"failed\". */\n function encode(entry: SlotEntry): EncodedGlyph | null {\n const glyph = entry.face.font.encode(entry.glyphId);\n if (!glyph) {\n report({\n reason: \"encoder-unavailable\",\n message: `hb-gpu: hb_gpu_draw_encode refused glyph ${entry.glyphId} of face \"${entry.face.label}\" — the run will be drawn with a hole in it`,\n });\n return null;\n }\n return glyph;\n }\n\n const pass: HbGpuGlyphPass = {\n renderer,\n stats,\n\n registerFace(bytes, label) {\n const name = label ?? `face${faces.length}`;\n const font = options.module.createFont(bytes);\n // `createHbGpu`'s own `onError` has already said why, in more detail than this layer knows.\n if (!font) return null;\n const face = renderer.registerFace(font, name);\n if (!face) {\n // The font is this file's now — nothing else has a reference — so a refused registration\n // has to free it or the wasm heap keeps a whole face copy for the life of the module.\n font.destroy();\n return null;\n }\n const registered: GlyphFace = {\n font,\n face,\n upem: face.upem,\n label: name,\n };\n faces.push(registered);\n idByFace[face.id] = new Map();\n return registered;\n },\n\n glyphFor(face, codepoint) {\n return face.font.glyphFor(codepoint);\n },\n\n fillRun(run, face, text, shapeOptions) {\n const key = shapeCache.key(text, shapeOptions);\n let cached: CachedShapeData | null =\n key === null ? null : shapeCache.get(face.face.id, key);\n if (!cached) {\n shapeCache.miss();\n const shaped = face.font.shape(text, shapeOptions);\n // `null` is a refusal and `[]` is an empty run — a distinction hb-gpu is careful about.\n if (!shaped) return false;\n const slots = new Int32Array(shaped.length);\n const pen = new Int32Array(shaped.length * 2);\n let penX = 0;\n let penY = 0;\n let count = 0;\n for (const glyph of shaped) {\n const slot = slotIdFor(face, glyph.glyphId);\n if (slot !== GLYPH_SLOT_NONE) {\n slots[count] = slot;\n pen[count * 2] = penX + glyph.xOffset;\n pen[count * 2 + 1] = penY + glyph.yOffset;\n count += 1;\n }\n // Advance every glyph, including an inkless space.\n penX += glyph.xAdvance;\n penY += glyph.yAdvance;\n }\n cached = {\n count,\n slots: slots.slice(0, count),\n pen: pen.slice(0, count * 2),\n advanceX: penX,\n advanceY: penY,\n };\n if (key !== null) shapeCache.put(face.face.id, key, cached);\n }\n\n if (run.slots.length < cached.count)\n run.slots = new Int32Array(cached.count);\n if (run.positions.length < cached.count * 2)\n run.positions = new Float32Array(cached.count * 2);\n // The cache is font-unit data; scaling it here is the exact same expression as a cold shape.\n const scale = run.pixelsPerEm / face.upem;\n for (let i = 0; i < cached.count; i += 1) {\n run.slots[i] = cached.slots[i]!;\n run.positions[i * 2] = cached.pen[i * 2]! * scale;\n run.positions[i * 2 + 1] = -cached.pen[i * 2 + 1]! * scale;\n }\n run.glyphCount = cached.count;\n return true;\n },\n\n slotFor: slotIdFor,\n\n clearShapeCache() {\n shapeCache.clear();\n },\n\n drawRun(run: GlyphsView, projection: StageProjection) {\n // THE PROJECTION IS THE FRAME'S, NOT THIS PASS'S. The executor's `StageProjection` is the one\n // authority on where design space lands, so both halves are taken from it rather than from\n // whatever this pass was constructed with — a stage that resized between construction and\n // this frame would otherwise draw its text at the old scale, and only its text.\n //\n // Keep the exact design extent alongside the matrix. Recovering it from the\n // float32 clip scale introduces rounding and couples the adapter to its formula.\n const runDesignWidth = projection.designWidth;\n const runDesignHeight = projection.designHeight;\n if (\n runDesignWidth !== designWidth ||\n runDesignHeight !== designHeight ||\n projection.framebufferWidth !== framebufferWidth ||\n projection.framebufferHeight !== framebufferHeight\n ) {\n designWidth = runDesignWidth;\n designHeight = runDesignHeight;\n framebufferWidth = projection.framebufferWidth;\n framebufferHeight = projection.framebufferHeight;\n renderer.setViewport(\n designWidth,\n designHeight,\n framebufferWidth,\n framebufferHeight,\n );\n }\n\n // THE CPU'S COPY OF A NUMBER THE SHADER COMPUTES FOR ITSELF, AND IT HAS TO AGREE WITH IT.\n // `hb_gpu_draw` takes its five-tap branch on the ppem it derives from the fragment's own\n // `fwidth`, so it already sees the full chain: design ppem, the model matrix the caller\n // pushed, and the design->device scale. The number here exists only for GATING AND\n // REPORTING — `stats.runsBelowPpemFloor` and the warning — and a gate that disagrees with\n // the shader it is gating on is worse than no gate: it reports a comfortable size for text\n // the shader is approximating, so the counter reads zero and the warning never fires.\n //\n // THE CALLER OWES THE MODEL MATRIX, AND THE SCALE IS READ BACK OUT OF IT. `run.m` is where a\n // caller's own zoom, fit or hover scale lives (`GlyphsView.m`'s doc says rotation belongs\n // there and never in the pen positions, and a scale rides the same matrix), so the axis\n // lengths of its two basis vectors are the run's real magnification. Their MEAN is the\n // reduction: it is the rule a consumer's own raster-scale picks for a non-uniform matrix, it\n // is exactly 1 for the pure rotation this used to assume, and it is the right kind of wrong\n // for an anisotropic one — a gate, not a rasterisation parameter.\n //\n // `run.pixelsPerEm` ITSELF IS NOT TOUCHED. hb-gpu's push uses it as the run's geometric\n // scale as well as its size, so folding the matrix into it would apply the matrix twice.\n const axis =\n (Math.hypot(run.m[0], run.m[1]) + Math.hypot(run.m[2], run.m[3])) / 2;\n const ppem = run.pixelsPerEm * axis * (framebufferWidth / designWidth);\n if (ppem < PPEM_FIDELITY_FLOOR) {\n stats.runsBelowPpemFloor += 1;\n if (options.warnBelowPpemFloor !== false && !warnedBelowPpemFloor) {\n warnedBelowPpemFloor = true;\n console.warn(\n `[gsw canvas] glyph run drawn at ${ppem.toFixed(1)} device ppem (pixelsPerEm ${run.pixelsPerEm} x model scale ${axis.toFixed(3)} x ${(framebufferWidth / designWidth).toFixed(3)} device ratio), under the ${PPEM_FIDELITY_FLOOR} this path wants: HarfBuzz's coverage shader falls back to a five-tap approximation below it, measured blurrier than the DOM text path (0.196 vs 0.132 distortion on Han at ppem 14). A baked atlas or DOM text is crisper at this size. Reported once; see stats.runsBelowPpemFloor for the tally.`,\n );\n }\n }\n\n // ONE `begin`/`end` PER RUN, AND THAT IS THE DRAW CALL. hb-gpu carries the colour and the\n // model matrix as uniforms, so runs cannot merge — see `GlyphPass.drawRun`. Advancing the\n // renderer's frame counter per run also makes its in-use eviction guard per-run, which is\n // correct here rather than merely permissive: `end` has already ISSUED the previous run's\n // draw before the next one uploads anything, and GL orders a `texSubImage2D` behind the draws\n // that read the texture before it.\n if (!adjacentBatchOpen) renderer.begin();\n renderer.setModel(run.m);\n // PER RUN, UNCONDITIONALLY, INCLUDING THE 0. hb-gpu's spread is sticky like its colour and\n // its model — `begin` does not clear it — so a run recorded without a spread after one\n // recorded with one would otherwise inherit it and draw fat. Passing `run.spreadPx` every\n // time makes the draw list, not the call order, the thing that decides.\n renderer.setSpread(run.spreadPx);\n // STRAIGHT rgba out of a PREMULTIPLIED view. `GlyphsView` stores `rgb` already multiplied by\n // `a` (its own doc says so) and hb-gpu's fragment multiplies exactly once, so handing it the\n // premultiplied triple would apply alpha twice — silently, as text that is merely darker,\n // which is the same failure `./executor-webgl`'s alpha note names for every other stage.\n const inverse = run.a > 0 ? 1 / run.a : 0;\n renderer.setColor(\n run.r * inverse,\n run.g * inverse,\n run.b * inverse,\n run.a,\n );\n\n const count = run.glyphCount;\n for (let i = 0; i < count; i += 1) {\n const id = run.slots[i];\n if (id === GLYPH_SLOT_NONE) {\n stats.inkless += 1;\n continue;\n }\n const entry = entries[id];\n if (!entry) {\n // An id this pass never issued: a list built against a different pass, or a caller\n // writing raw numbers into `slots`. A hole in a word rather than a crash, and counted.\n stats.dropped += 1;\n continue;\n }\n // THE WHOLE REASON THE IR STORES IDS. `resolve` is a map lookup that answers `null` for a\n // glyph the ring has evicted since the list was recorded; the alternative — caching the\n // `GlyphSlot` here — draws a DIFFERENT glyph's outline at the right size, in the right\n // place, perfectly antialiased, which nothing downstream can see. A miss is not an error,\n // it is re-encoded and re-uploaded, and only the counter notices.\n let slot: GlyphSlot | null = renderer.resolve(\n entry.face.face,\n entry.glyphId,\n );\n if (!slot) {\n const glyph = encode(entry);\n if (!glyph || glyph.texels.length === 0) {\n stats.dropped += 1;\n continue;\n }\n slot = renderer.upload(entry.face.face, entry.glyphId, glyph);\n stats.reuploads += 1;\n if (!slot) {\n stats.dropped += 1;\n continue;\n }\n }\n renderer.push(\n slot,\n run.positions[i * 2],\n run.positions[i * 2 + 1],\n run.pixelsPerEm,\n );\n }\n\n const frame = adjacentBatchOpen\n ? { instances: 0, drawCalls: 0 }\n : renderer.end();\n stats.runs += 1;\n stats.glyphs += frame.instances;\n return { glyphs: frame.instances, drawCalls: frame.drawCalls };\n },\n\n drawRuns(runs, projection) {\n if (runs.length === 0) return { glyphs: 0, drawCalls: 0 };\n // The executor guarantees adjacency. Keeping the traversal here rather than teaching its\n // storage about an array makes the ordinary `drawRun` call byte-for-byte the fallback.\n renderer.begin();\n adjacentBatchOpen = true;\n let completed = false;\n try {\n for (const run of runs) pass.drawRun(run, projection);\n completed = true;\n } finally {\n adjacentBatchOpen = false;\n // A failed re-upload must not leave half a batch for a future caller to submit. There is\n // deliberately no `end()` on this path: it would make an exception present partial text.\n if (!completed) renderer.begin();\n }\n const frame = renderer.end();\n stats.glyphs += frame.instances;\n return { glyphs: frame.instances, drawCalls: frame.drawCalls };\n },\n\n canBatchRuns(runs) {\n if (!batchAdjacentRuns) return false;\n // A single instanced draw cannot allow a later upload to replace an earlier glyph. Only\n // admit a known, fully resident working set; individual drawRun calls retain the existing\n // submit-before-possible-eviction behaviour on a miss.\n for (const run of runs) {\n for (let i = 0; i < run.glyphCount; i += 1) {\n const id = run.slots[i];\n if (id === GLYPH_SLOT_NONE) continue;\n const entry = entries[id];\n if (!entry || !renderer.resolve(entry.face.face, entry.glyphId))\n return false;\n }\n }\n return true;\n },\n\n setViewport(width, height, bufferWidth, bufferHeight) {\n designWidth = Math.max(1, width);\n designHeight = Math.max(1, height);\n framebufferWidth = Math.max(1, bufferWidth ?? width);\n framebufferHeight = Math.max(1, bufferHeight ?? height);\n renderer.setViewport(\n designWidth,\n designHeight,\n framebufferWidth,\n framebufferHeight,\n );\n },\n\n notifyContextLost() {\n renderer.notifyContextLost();\n },\n\n rebuild() {\n return renderer.rebuild();\n },\n\n dispose() {\n shapeCache.clear();\n renderer.dispose();\n },\n };\n // Never expose the grouped entry point on the compact renderer: its 40-byte records intentionally\n // retain model/colour/spread as uniforms, so a direct grouped call would apply the final run's\n // state to preceding glyphs. The executor sees no `drawRuns` and takes its established path.\n if (!batchAdjacentRuns) {\n delete pass.drawRuns;\n delete pass.canBatchRuns;\n }\n return pass;\n}\n"],"mappings":"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"}