@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":"webgl.js","names":[],"sources":["../src/webgl.ts"],"sourcesContent":["// The pipeline hb-gpu does not ship: a WebGL2 program, an RGBA16I atlas and a bump allocator.\n//\n// HarfBuzz gives you an encoder and two halves of a shader. It does not give you a renderer —\n// `util/gpu/demo-atlas.cc` and `util/gpu/demo-renderer-gl.cc` are a GLFW demo, 344 lines between\n// them, and they are the reference this file is ported from rather than the mechanism it is.\n//\n// THE THREE THINGS THIS FILE OWNS, AND WHY EACH IS NOT UPSTREAM'S:\n//\n// 1. A 2-D ATLAS BECAUSE WEBGL2 HAS NO TEXTURE BUFFERS. The blob format is a flat 1-D stream of\n// RGBA16I texels and the natural binding is `isamplerBuffer`, which WebGL2 does not have at\n// all. `HB_GPU_ATLAS_2D` exists for exactly this: the stream is wrapped into a wide\n// `TEXTURE_2D` and the shader unwraps it with `offset % width, offset / width`. Which is why\n// `hb_gpu_atlas_width` is a uniform and not a constant, and why the upload goes row by row.\n// 2. AN ALLOCATOR WITH EVICTION. `demo_atlas_alloc` calls `die (\"Ran out of atlas memory\")`.\n// That is a demo. A Han pool is thousands of outlines against a fixed texture, so the cursor\n// wraps and overwrites, and the only question is what it is allowed to overwrite.\n// 3. INSTANCED QUADS. Upstream emits six vertices per glyph with `position`, `texcoord`,\n// `normal`, `emPerPos` AND the atlas offset replicated into every one of them — 192 bytes a\n// glyph. Here the four corner signs are a static 4-vertex buffer (the corner sign IS the\n// outward normal, which is what makes that substitution exact, not a shortcut) and everything\n// per-glyph is one 40-byte instance record with `a_glyphLoc` at divisor 1.\n//\n// IT BORROWS A CONTEXT; IT DOES NOT OWN ONE. This file used to call `createCanvasStage` out of\n// `@godot-scene-web/canvas`, which made `hb-gpu` depend on `canvas`. That edge points the wrong\n// way: `canvas` is where a glyph path belongs, so `canvas` has to be able to depend on THIS. The\n// package therefore has zero workspace dependencies and takes a `WebGL2RenderingContext` it did\n// not create, on a canvas it never sizes, next to draws it did not issue.\n//\n// Everything that follows from that is stated where it bites: {@link HbGpuRenderer.end} does not\n// set the viewport and does not clear (a clear inside a glyph pass erases whatever the embedder's\n// executor already drew) and documents exactly which GL state it leaves dirty;\n// {@link HbGpuRendererOptions.framebufferWidth} is the embedder's ACHIEVED drawing-buffer size, not\n// a size this file requests; and context loss is an explicit lifecycle\n// ({@link HbGpuRenderer.notifyContextLost} / {@link HbGpuRenderer.rebuild}) rather than a canvas\n// event listener, because the canvas belongs to somebody else.\n//\n// DESIGN SIZE AND FRAMEBUFFER SIZE ARE TWO NUMBERS, NOT ONE. They were one while this file owned\n// its canvas, because it sized that canvas in device pixels and design space WAS device pixels. On\n// a borrowed stage they differ by the device-pixel ratio, and they feed two different things: the\n// design pair builds the design->clip projection, and the framebuffer pair is `u_viewport`, which\n// is what `hb_gpu_dilate` measures half a SCREEN pixel against.\n//\n// THE DAMAGE IS ASYMMETRIC, AND IT WAS MEASURED RATHER THAN REASONED — the reasoning was wrong.\n// Over-dilation is INVISIBLE: `hb_gpu_dilate` moves `position` and `texcoord` along the same\n// affine map, so `fwidth (renderCoord)` — where `hb_gpu_draw` gets its ppem — does not move with\n// it, and the extra fragments a too-large quad covers evaluate to coverage 0. Feeding the design\n// size on a 2x-DPR stage therefore changes nothing on screen; it was measured at zero differing\n// bytes on ANGLE/NVIDIA. UNDER-dilation is what bites: when the drawing buffer is SMALLER than\n// design space the quad is too tight and the antialiased rim is clipped off, which at a ratio of\n// 1/4 measures RMS 1.79 against a correct render. `packages/test-harness/test/canvasGlyphPixelXvfb.test.ts`\n// pins both directions and documents the calibration. Neither reads as a size error, which is why\n// the two are separate parameters here.\n//\n// PREMULTIPLIED OUT, ALWAYS. The fragment writes `vec4 (rgb * a * cov, a * cov)`, so the borrowed\n// context MUST have been created with `premultipliedAlpha: true` — which is what\n// `packages/canvas/src/present.ts` asks for, and the reason to borrow that stage's context rather\n// than open a second one. Getting this wrong is SILENT — the picture is merely darker — and\n// `packages/test-harness/test/canvasPixelXvfb.test.ts` is where that contract is pinned for the\n// rest of the repo.\n\nimport {\n type EncodedGlyph,\n HB_GPU_SHADER_STAGE_FRAGMENT,\n HB_GPU_SHADER_STAGE_VERTEX,\n HB_GPU_TEXEL_BYTES,\n type HbGpu,\n type HbGpuFailure,\n type HbGpuFailureReason,\n type HbGpuFont,\n} from \"./index\";\n\n/**\n * Upstream's `ATLAS_TEX_WIDTH`, and the width this renderer PREFERS rather than the one it gets.\n *\n * Wide and short keeps the unwrap arithmetic in `int` range. WebGL2 only guarantees\n * `MAX_TEXTURE_SIZE >= 2048`, so a device can refuse 4096 — read {@link HbGpuRenderer.atlasWidth}\n * for the width an instance actually built, and never assume this constant describes it. The\n * shader is handed the achieved width as `hb_gpu_atlas_width`, so any width is legal.\n */\nexport const ATLAS_WIDTH = 4096;\n\n/**\n * Below this the renderer refuses rather than clamps.\n *\n * A 5 KB Han blob is ~675 texels: at 1024 wide the upload is at most a couple of `texSubImage2D`\n * calls, and narrower rows turn one glyph into dozens of them. A device reporting below WebGL2's\n * own guaranteed 2048 is broken or emulated, and is not one this renderer can be measured on.\n */\nconst MIN_ATLAS_WIDTH = 1024;\n\nconst LEGACY_INSTANCE_FLOATS = 10;\nconst BATCHED_INSTANCE_FLOATS = 21;\n\n/**\n * The GLSL preamble every stage gets, and the one line in it that is load-bearing.\n *\n * `#define HB_GPU_ATLAS_2D` selects `uniform highp isampler2D hb_gpu_atlas` plus the\n * `hb_gpu_atlas_width` unwrap over the `isamplerBuffer` path. It is defined HERE, in the shader,\n * and not by the wasm build: the C macro of the same name only reaches `util/gpu/demo-atlas.cc`,\n * which this package does not compile, so the `-DHB_GPU_ATLAS_2D` on the em++ line does not select\n * anything. The `#ifdef` that matters lives inside the GLSL the module hands back.\n *\n * `highp int` is not decoration either: atlas offsets are absolute texel indices into a stream that\n * runs to hundreds of thousands, and `mediump int` is only guaranteed 16 bits. Worse, a precision\n * that disagrees across the two stages fails to LINK with no diagnostic on some drivers, so this\n * string is shared by both on purpose.\n */\nconst GLSL_PREAMBLE =\n \"#version 300 es\\nprecision highp float;\\nprecision highp int;\\n#define HB_GPU_ATLAS_2D\\n\";\n\n/**\n * OUR macro, and the reason it is not HarfBuzz's `HB_GPU_NO_MSAA`, which is the whole safety\n * argument for {@link HbGpuRendererOptions.spreadTapMsaa}.\n *\n * `HB_GPU_NO_MSAA` is defined by the vendored library and guards `_hb_gpu_slug` — the FILL. Reusing\n * it to switch the dilation's taps would switch the fill's five-sample average off at the same time,\n * silently, and the fill is the one thing this trade is not allowed to touch. A separate name makes\n * that structural rather than a thing to remember: `hb_gpu_draw` reaches `_hb_gpu_slug` inside the\n * library's own source, which this define cannot reach into.\n */\nconst SPREAD_TAP_NO_MSAA_DEFINE = \"#define HB_GPU_SPREAD_TAP_NO_MSAA\\n\";\n\n/**\n * THE DILATION'S WHOLE TAP BUDGET, as one number, and the reason it is one number.\n *\n * It bounds the flat loop in {@link FRAGMENT_MAIN} — one tap per iteration — so the compile-time\n * ceiling IS the worst-case tap count: 64 unrolled sites, plus `hb_gpu_draw`'s own centre tap, is 65\n * evaluations for the most expensive fragment there is. That was also the ceiling of the nested\n * `4 rings x 16 steps` loops this replaced, so the redistribution below is free at the top end.\n *\n * WHY THE SHAPE CHANGED RATHER THAN THE NUMBER. The nested form clamped EVERY ring to the same 16\n * steps, so past `radius * t > 2.5 px` all four rings ran 16 taps — and the outermost one, which is\n * the only ring that decides where the dilated boundary lands, was then the SPARSEST: at radius 12\n * its taps sit 4.71 px of arc apart, while ring 1 spends the same 16 on a circle a quarter the size\n * and puts them 1.18 px apart. Giving\n * the outer ring more steps in that shape would have meant raising the per-ring cap, and 4 x 28 is\n * 112 unrolled sites for a budget most fragments never spend. A flat loop makes \"how many taps may a\n * fragment cost\" and \"how are they arranged\" two independent decisions, and only the first one is a\n * perf number.\n *\n * INTERPOLATED INTO THE GLSL rather than restated there, so `spreadBudget.test.ts` can assert on the\n * arithmetic that divides it between rings and on the ceiling reaching the shader, from one source.\n */\nexport const HB_GPU_SPREAD_MAX_TAPS = 64;\n\n/**\n * OUR entry point for the vertex stage. Roughly 30 lines, and unavoidably ours.\n *\n * HarfBuzz ships `hb_gpu_dilate` and no `main`, because it cannot know what a consumer's\n * attributes are called. Upstream writes the same wrapper for its demo in\n * `util/gpu/demo-vertex.glsl`; this one is written against `hb-gpu-vertex.glsl`'s documented\n * contract rather than copied from it, and it differs where the pipeline does — a corner is\n * interpolated out of an instance record here, where upstream reads four expanded vertices.\n *\n * `jac` IS THE INVERSE OF THE EM-TO-OBJECT LINEAR PART, and the y term is negative because em\n * space is y-up and this renderer's object space is y-down device pixels. HarfBuzz's header spells\n * out the case: em-to-object `[[s, 0], [0, -s]]` gives `jac = (1/s, 0, 0, -1/s)`, and `1/s` is\n * exactly `a_emPerPos`. Drop the sign and the dilation pushes the texcoord the wrong way in y,\n * which shows up as a half-pixel of missing ink along horizontal edges only.\n *\n * IT ALSO GROWS THE QUAD BY {@link HbGpuRenderer.setSpread}, AND WITHOUT THAT THERE IS NO OUTLINE\n * AT ALL. `a_position` / `a_texcoord` are the glyph's INK box: the fragment stage's dilation is a\n * max of coverage taps up to `spread` away, so ink that is `spread` OUTSIDE the box has to have a\n * fragment to be found from. Left out, every tap that would have reached ink is simply never\n * rasterised and the whole feature fails as \"the outline did not appear\" — silently, with a\n * perfectly correct-looking fill. Measured by deleting these two lines: the dilated ink box then\n * grows 0-1 px instead of 4, while the superset and interior checks stay green.\n *\n * THE EXPANSION GOES THROUGH `jac`, not through a hand-written sign pattern, and that is worth the\n * one extra line. `a_normal` IS the object-space outward normal at this corner (the file header\n * says why), so `a_normal * a_spreadPx` is the object-space displacement; `jac` is by definition\n * the map from an object displacement to the em displacement that matches it, which is exactly\n * what `hb_gpu_dilate` uses it for two lines below. Spelling the em half out separately would be a\n * second chance to get the y flip backwards, and a backwards y flip here reads as an outline that\n * is present but shifted — not as an error.\n */\nconst BATCHED_VERTEX_MAIN = `\nuniform mat4 u_viewProjection;\nuniform vec2 u_viewport;\n\n/* Per-vertex: the corner sign, which IS the outward normal at that corner. */\nin vec2 a_normal;\n\n/* Per-instance, divisor 1. */\nin vec4 a_position; /* object-space box: (x at cx=0, y at cy=0, x at cx=1, y at cy=1) */\nin vec4 a_texcoord; /* em-space ink box: (minX, minY, maxX, maxY) */\nin float a_emPerPos; /* em units per object unit, i.e. upem / pixelsPerEm */\nin uint a_glyphLoc; /* first texel of this glyph's blob in the atlas */\nin vec2 a_model0;\nin vec2 a_model1;\nin vec2 a_model2;\nin vec4 a_color;\nin float a_spreadPx;\n\nout vec2 v_texcoord;\nflat out uint v_glyphLoc;\n/* The spread in EM units for THIS instance. Flat, and a varying rather than a second uniform,\n * because the object-to-em factor is a_emPerPos and that is per-instance: one uniform in em units\n * could not mean the same number of pixels for two glyphs pushed at different pixelsPerEm. */\nflat out float v_spreadEm;\nflat out float v_spreadPx;\nflat out vec4 v_color;\n\nvoid main ()\n{\n /* (-1, +1) -> (0, 0) and (+1, -1) -> (1, 1): the y term is flipped because a corner's outward\n * normal points UP in object space exactly when it is the box's minimum em coordinate. */\n vec2 corner = vec2 (a_normal.x, -a_normal.y) * 0.5 + 0.5;\n\n vec2 pos = mix (a_position.xy, a_position.zw, corner);\n vec2 tex = mix (a_texcoord.xy, a_texcoord.zw, corner);\n\n vec4 jac = vec4 (a_emPerPos, 0.0, 0.0, -a_emPerPos);\n mat4 model = mat4 (\n vec4 (a_model0, 0.0, 0.0),\n vec4 (a_model1, 0.0, 0.0),\n vec4 (0.0, 0.0, 1.0, 0.0),\n vec4 (a_model2, 0.0, 1.0));\n mat4 mvp = u_viewProjection * model;\n float spreadPx = a_spreadPx;\n vec4 color = a_color;\n\n /* At spread 0 both of these add 0.0, which is the identity for every finite value — the fill\n * path really is byte-for-byte what it was before the outline path existed. */\n vec2 spreadPos = a_normal * spreadPx;\n pos += spreadPos;\n tex += vec2 (dot (spreadPos, jac.xy), dot (spreadPos, jac.zw));\n\n hb_gpu_dilate (pos, tex, a_normal, jac, mvp, u_viewport);\n\n gl_Position = mvp * vec4 (pos, 0.0, 1.0);\n v_texcoord = tex;\n v_glyphLoc = a_glyphLoc;\n v_spreadEm = spreadPx * a_emPerPos;\n v_spreadPx = spreadPx;\n v_color = color;\n}\n`;\n\nconst VERTEX_MAIN = `\nuniform mat4 u_matViewProjection;\nuniform vec2 u_viewport;\nuniform float u_spreadPx; /* OBJECT units, pre-model. 0 disables the whole path. */\n\n/* Per-vertex: the corner sign, which IS the outward normal at that corner. */\nin vec2 a_normal;\n\n/* Per-instance, divisor 1. */\nin vec4 a_position; /* object-space box: (x at cx=0, y at cy=0, x at cx=1, y at cy=1) */\nin vec4 a_texcoord; /* em-space ink box: (minX, minY, maxX, maxY) */\nin float a_emPerPos; /* em units per object unit, i.e. upem / pixelsPerEm */\nin uint a_glyphLoc; /* first texel of this glyph's blob in the atlas */\n\nout vec2 v_texcoord;\nflat out uint v_glyphLoc;\n/* The spread in EM units for THIS instance. Flat, and a varying rather than a second uniform,\n * because the object-to-em factor is a_emPerPos and that is per-instance: one uniform in em units\n * could not mean the same number of pixels for two glyphs pushed at different pixelsPerEm. */\nflat out float v_spreadEm;\n\nvoid main ()\n{\n /* (-1, +1) -> (0, 0) and (+1, -1) -> (1, 1): the y term is flipped because a corner's outward\n * normal points UP in object space exactly when it is the box's minimum em coordinate. */\n vec2 corner = vec2 (a_normal.x, -a_normal.y) * 0.5 + 0.5;\n\n vec2 pos = mix (a_position.xy, a_position.zw, corner);\n vec2 tex = mix (a_texcoord.xy, a_texcoord.zw, corner);\n\n vec4 jac = vec4 (a_emPerPos, 0.0, 0.0, -a_emPerPos);\n\n /* At spread 0 both of these add 0.0, which is the identity for every finite value — the fill\n * path really is byte-for-byte what it was before the outline path existed. */\n vec2 spreadPos = a_normal * u_spreadPx;\n pos += spreadPos;\n tex += vec2 (dot (spreadPos, jac.xy), dot (spreadPos, jac.zw));\n\n hb_gpu_dilate (pos, tex, a_normal, jac, u_matViewProjection, u_viewport);\n\n gl_Position = u_matViewProjection * vec4 (pos, 0.0, 1.0);\n v_texcoord = tex;\n v_glyphLoc = a_glyphLoc;\n v_spreadEm = u_spreadPx * a_emPerPos;\n}\n`;\n\n/**\n * OUR entry point for the fragment stage. Same ownership note as {@link VERTEX_MAIN}.\n *\n * STEM DARKENING AND GAMMA ARE HERE AND ON BY DEFAULT, and the thing that is now a MEASUREMENT\n * decision is switching them OFF — see {@link HbGpuRendererOptions.contrast} and\n * {@link HB_GPU_CONTRAST_NONE}. Both are contrast corrections that move coverage AWAY from correct\n * area coverage, which is exactly what a fidelity arm grades against (`docs/text-rendering.md`: the\n * reference is an 8x render box-downsampled, \"a ceiling, not a mechanism\"), so a HARNESS that left\n * them on would be scoring its own contrast curve. A shipping consumer is not a harness: raw\n * coverage puts a sub-pixel stem at mid-grey where a browser puts it near the ink colour, which is\n * measurably why DOM text out-reads this path at small sizes.\n *\n * The block below is `util/gpu/demo-fragment.glsl`'s, with two differences that are ours:\n * `brightness` comes off per-instance `v_color.rgb` directly (which is STRAIGHT, so upstream's divide by the\n * premultiplied alpha is already done) and the ppem is HarfBuzz's own `hb_gpu_ppem` rather than\n * `1.0 / max (fwidth (v_texcoord).xy)` — this file's render coordinates are FONT UNITS, so the\n * reciprocal of their derivative is pixels per font unit and would put the `smoothstep (8, 48)`\n * ramp a factor of `upem` off. `hb_gpu_ppem` folds the glyph's own scale in and is what\n * `hb_gpu_spread_tap` above already agrees with.\n *\n * THE OUTLINE IS A MAX OF COVERAGE TAPS, BECAUSE THERE IS NO DISTANCE FIELD TO OFFSET. HarfBuzz's\n * entire public GLSL surface is `hb_gpu_draw` (coverage), `hb_gpu_ppem` and `hb_gpu_stem_darken` —\n * there is no signed distance anywhere in the format, so \"dilate by r\" cannot be a threshold shift\n * and has to be \"is any point within r of this one inside the glyph\", sampled. See\n * {@link HbGpuRenderer.setSpread} for what that costs and where it differs from a real stroke.\n *\n * EXPORTED SO THE TAP BUDGET CAN BE ASSERTED WITHOUT A GPU. `spreadBudget.test.ts` reads the\n * interpolated {@link HB_GPU_SPREAD_MAX_TAPS} back out of this text; a compiled shader is the one\n * place the constant has to be right, and the pixel suite that compiles it is gated on a display.\n */\nconst BATCHED_FRAGMENT_MAIN = `\nuniform float u_gamma; /* exponent on the final coverage; 1.0 is off */\nuniform float u_stemDarken; /* > 0 runs hb_gpu_stem_darken; 0 is off */\n\nin vec2 v_texcoord;\nflat in uint v_glyphLoc;\nflat in float v_spreadEm;\nflat in float v_spreadPx;\nflat in vec4 v_color;\n\nout vec4 fragColor;\n\nconst float HB_GPU_SPREAD_TAU = 6.2831853;\n/* Hard ceilings so the loop is bounded at compile time. One tap per iteration, so\n * HB_GPU_SPREAD_MAX_TAPS + the centre tap is the worst case, and it is only reached by a fragment\n * that is neither solid ink nor near any. See the TS constant of the same name. */\nconst int HB_GPU_SPREAD_MAX_RINGS = 4;\nconst int HB_GPU_SPREAD_MAX_TAPS = ${HB_GPU_SPREAD_MAX_TAPS};\n/* \"Already saturated\": no tap can raise this, so stop. Not 1.0, because the coverage estimator\n * lands a hair under it on a deep-interior fragment and an exact test would never fire. */\nconst float HB_GPU_SPREAD_SOLID = 0.999;\n\n/*\n * WHERE A TAP STOPS MEANING \"how much ink is at this offset\" AND STARTS MEANING \"is this fragment\n * inside the dilated silhouette\". The knee of a smoothstep, and the whole of the fix below.\n *\n * NO BACKTICKS ANYWHERE IN THIS COMMENT, and that is not a style note: this whole string is a JS\n * template literal, so one backtick ends the shader mid-sentence and the package fails to PARSE.\n *\n * THE BUG IT REMOVES. A dilated shape is the union of a disk of radius r swept along the outline:\n * a BINARY shape, whose only partial coverage is at its own boundary. A max of raw coverage taps\n * cannot produce that, because a max cannot exceed the largest coverage near the fragment — and at\n * ppem 14 a Han stroke is thinner than a pixel, so its coverage PEAKS at 0.42 and the whole\n * silhouette came out a translucent mottle at 0.62 of the ideal's ink.\n *\n * THE RANGE IS 0 TO 0.5, AND \"A TAP ABOVE HALF COVERAGE IS INSIDE\" IS THE RULE THAT FAILS. That is\n * the obvious reading and it makes this case measurably WORSE, which is why the knee is a swept\n * number rather than an argued one. Half of a PIXEL is not half of a sub-pixel STROKE: at ppem 14\n * the fixture's peak coverage is 0.42, so a knee centred on 0.5 sits above anything the glyph can\n * reach and ERASES the outline. What 0.5 is the right value for is the top of the range — a pixel\n * centred exactly ON the outline reads 0.5, so \"as covered as a pixel on the boundary\" is the point\n * at which a tap is fully inside, and everything below it ramps.\n *\n * SWEPT ON THE RTX 2060 THROUGH ANGLE, both fixtures, against 8x grown references. Low-ppem is\n * 中 at 14 px per em rotated 10 degrees, spread 3 (SHALLOW COVERAGE); thin is a full stop at 96 px\n * per em, spread 12 (SPARSE COVERING — no 64-tap set tiles a disk of that radius; the sweep was run\n * when those taps were four rings of 16, and the rim column moved again when they were resplit).\n *\n * knee low rim rms low ink ratio low interior short thin rim rms thin ink ratio\n * none 77.89 0.622 38.5% 80.90 0.965\n * 0.35 - 0.65 128.57 0.574 (worse still) 102.52 0.966\n * 0.25 - 0.75 112.35 0.604 --- 99.72 0.965\n * 0.20 - 0.50 76.87 0.870 --- 94.27 0.974\n * 0.15 - 0.45 73.85 0.968 --- 90.15 0.978\n * 0.10 - 0.40 84.75 1.037 --- 85.82 0.985\n * 0.05 - 0.45 79.40 1.011 4.1% 82.16 0.986\n * 0.05 - 0.50 72.64 0.968 7.4% 82.81 0.983\n * 0.00 - 0.45 83.29 1.026 3.3% 78.86 0.990\n * 0.00 - 0.55 69.73 0.947 9.3% 79.96 0.984\n * 0.00 - 0.50 75.66 0.988 6.1% 79.10 0.987 <-\n *\n * The two upper rows are the \"roughly half\" hypothesis and both are worse than shipping nothing.\n * 0 - 0.5 is the only row that improves EVERY column at once, and it is also the one with a\n * sentence behind it rather than a fit.\n *\n * WHAT IT COSTS AT LARGE PPEM. A well-resolved glyph's tap coverage IS the area, so a tap sitting\n * exactly on the outline reads 0.5 — and 0.5 is also the ideal answer at the dilated boundary,\n * where this maps it to 1. So the boundary moves outward by a fraction of a pixel: measured, the\n * 96 px per em ink box grows 5 px on one side for a spread of 4 instead of 4. Real, inside the\n * fixtures' SPREAD_TOLERANCE_PX, and the price of an interior that is no longer translucent.\n *\n * PER TAP RATHER THAN ON THE MAX, AND NOT FOR THE REASON IT LOOKS LIKE. smoothstep is MONOTONE, so\n * it commutes with max and the two placements give the same silhouette — measured, not reasoned:\n * moving it after the loop reads ink ratio 0.989 against 0.988 and the same rim RMS to two decimal\n * places. What the placement actually buys is the two things a monotone identity does not cover.\n * First, the FILL IS THE FLOOR: cov enters the loop as hb_gpu_draw's own coverage and is never\n * sharpened, so a dilated run stays a strict SUPERSET of the same run at spread 0 — sharpening the\n * max would put the fill through the knee too, and smoothstep(0, 0.5, x) is BELOW x for x under\n * ~0.08, so a faint fill pixel would come back dimmer than it was drawn. Second, the early-out\n * below tests cov INSIDE the loop, and only a per-tap value can raise it early.\n *\n * IT DOES NOT MAKE THE EARLY-OUT FIRE AT 14 px, WHICH THE ROUND EXPECTED IT TO. A tap saturates to\n * exactly 1 only once its raw coverage reaches HB_GPU_SPREAD_INSIDE_HIGH, and at ppem 14 the\n * fixture's peak raw coverage is 0.42, which sharpens to 0.931 — still under\n * HB_GPU_SPREAD_SOLID. So a fragment at that size still walks the whole tap set, and the frame-cost\n * side effect that was predicted here IS NOT THERE. Above ppem 16 taps reached 1 before this change\n * as well, so nothing moved there either. Lowering HB_GPU_SPREAD_SOLID would collect it, and is\n * deliberately not done here: it is a cost decision with its own pixels to grade, on a rung where\n * the tap budget is a device ceiling.\n */\nconst float HB_GPU_SPREAD_INSIDE_LOW = 0.0;\nconst float HB_GPU_SPREAD_INSIDE_HIGH = 0.5;\n\n/*\n * One coverage tap, WITH NO DERIVATIVE IN IT — which is the whole reason this exists.\n *\n * It is _hb_gpu_slug (hb-gpu-fragment.glsl, 14.4.0) with ppem lifted into a parameter. The\n * library's own _hb_gpu_slug advertises itself as callable \"from non-uniform control flow\", and\n * for GLSL it is not: it calls hb_gpu_ppem, which calls fwidth. The disk below has a per-fragment\n * early-out, so every tap after that point IS non-uniform control flow, and a fwidth there is\n * undefined by GLSL ES 3.00.\n *\n * MIRRORED RATHER THAN AVOIDED so an outline tap and a fill fragment agree. Lifting ppem is exact\n * rather than an approximation: it is fwidth(v_texcoord) and the glyph's own scale, and fwidth of an\n * interpolated varying is constant across an affine quad, so its value at a tap equals its value at\n * the centre.\n *\n * THE MSAA HALF IS SWITCHABLE AND THE DEFAULT IS OFF — see\n * {@link HbGpuRendererOptions.spreadTapMsaa}, which carries the measurement. Note the macro is\n * HB_GPU_SPREAD_TAP_NO_MSAA and NOT the library's HB_GPU_NO_MSAA: that one guards _hb_gpu_slug,\n * i.e. the FILL, which this trade must not touch.\n *\n * The vendored wasm is digest-pinned (vendor/VENDOR.md, test/vendor.test.ts), so the source this\n * mirrors cannot move without a deliberate vendor bump.\n */\nfloat hb_gpu_spread_tap (vec2 rc, vec2 pixelsPerEm, float ppem, uint glyphLoc_)\n{\n float c = _hb_gpu_slug_single (rc, pixelsPerEm, glyphLoc_);\n#ifndef HB_GPU_SPREAD_TAP_NO_MSAA\n if (ppem < 16.0)\n {\n vec2 emsPerPixel = 1.0 / pixelsPerEm;\n vec2 d = emsPerPixel * (1.0 / 3.0);\n float msaa = 0.25 *\n (_hb_gpu_slug_single (rc + vec2 (-d.x, -d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 ( d.x, -d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 (-d.x, d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 ( d.x, d.y), pixelsPerEm, glyphLoc_));\n c = mix (c, msaa, smoothstep (16.0, 8.0, ppem));\n }\n#endif\n return c;\n}\n\nvoid main ()\n{\n float cov = hb_gpu_draw (v_texcoord, v_glyphLoc);\n\n /* PER-PRIMITIVE CONTROL FLOW: the two derivative-taking calls inside see flat run state, so all\n * fragments of one glyph primitive take the same branch. v_spreadPx and v_spreadEm are flat\n * (constant over a primitive, which is what a derivative\n * quad belongs to), so it is uniform by construction rather than by luck. v_spreadEm is also a\n * genuine guard: a NaN or zero a_emPerPos fails it and takes the single-tap path. */\n if (v_spreadPx > 0.0 && v_spreadEm > 0.0)\n {\n vec2 pixelsPerEm = 1.0 / fwidth (v_texcoord);\n float ppem = hb_gpu_ppem (v_texcoord, v_glyphLoc);\n /* The spread in DEVICE pixels — fwidth is a screen-space derivative, so this already carries\n * the model scale and the device-pixel ratio. It only ever picks tap counts; the tap OFFSETS\n * are in em units and are exact. */\n float radiusPx = v_spreadEm * max (pixelsPerEm.x, pixelsPerEm.y);\n\n /* CONCENTRIC RINGS, NOT ONE, and that is not a refinement. A dilated fragment is covered iff\n * SOME offset within the disk lands on ink; taps on a single ring of radius r can all overshoot\n * a feature narrower than 2r, which punches holes through the outline exactly where a glyph is\n * thin — a comma, a hairline serif, a full stop. Ring spacing is held near 2/3 px, so the RADIAL\n * half of the covering is sub-pixel out to the clamp at HB_GPU_SPREAD_MAX_RINGS.\n *\n * THE RADII STAY EQUALLY SPACED, which is worth saying because equal AREA is the obvious\n * alternative and it is worse here. Pushing the rings outward concentrates them where the taps\n * are already densest, and combined with the budget split below it both doubles the outward bias\n * of the whole tap set and halves the radial margin the small-feature case relies on — the one\n * where a full stop smaller than the tap radius has to be found by an INNER ring. */\n int rings = clamp (int (ceil (radiusPx * 1.5)), 1, HB_GPU_SPREAD_MAX_RINGS);\n /* 1 + 2 + ... + rings, the denominator of the budget split below. */\n int denom = rings * (rings + 1) / 2;\n\n /* ONE FLAT LOOP OVER THE WHOLE BUDGET, AND THE OUTER RING GETS MOST OF IT.\n *\n * Every ring used to be capped at the same number of steps, which sounds neutral and is not: a\n * ring's taps are spread over a circumference proportional to its radius, so an equal share puts\n * the WIDEST arc gaps on the outermost ring — the only one that decides where the dilated\n * boundary lands. At radius 12 that was 4.71 px of arc between the taps that draw the edge,\n * against 1.18 px on ring 1, and the boundary followed the tap count: measured against a Godot\n * 4.5.1 golden, 0.3099 px of wobble at exactly 16 cycles per revolution where the engine has\n * 0.0164.\n *\n * So the budget is split in proportion to ring RADIUS, i.e. to circumference: ring k of rings\n * may spend (MAX_TAPS * k + denom/2) / denom taps, which at four rings is 6 / 13 / 19 / 26 and\n * sums to exactly MAX_TAPS. It sums to exactly MAX_TAPS at one, two and three rings as well\n * (64; 21 + 43; 11 + 21 + 32), so the flat bound is never the thing that truncates a ring — it\n * is a hedge against a driver that insists on unrolling, not a second policy. At radius 12 the\n * outer arc is then 2.90 px rather than 4.71.\n *\n * The lower clamp of 6 steps is what keeps a SMALL radius honest, and it is the reason the cap\n * enters as max (cap, 6) rather than as cap: ring 1's share at four rings is exactly 6, and a\n * hexagon is the coarsest ring that still surrounds its centre.\n *\n * ONE TAP PER ITERATION, so HB_GPU_SPREAD_MAX_TAPS is simultaneously the loop bound and the\n * fragment's worst-case cost — the two used to be 4 x 16 and 65 and had to be reasoned about\n * separately. Dynamic bounds and breaks are legal ESSL 3.00; the GLSL ES 1.00 Appendix A\n * restriction that forced the nested constant shape does not apply to version 300 es. */\n int ring = 0;\n int step = 0;\n int steps = 0;\n float ringEm = 0.0;\n float phase = 0.0;\n for (int i = 0; i < HB_GPU_SPREAD_MAX_TAPS; i++)\n {\n /* THE INTERIOR EARLY-OUT: a fragment already covered by its own centre tap is trivially\n * within r of ink, and interior fragments are most of a glyph. */\n if (cov >= HB_GPU_SPREAD_SOLID) break;\n if (step >= steps)\n {\n ring += 1;\n /* The other exit: the rings this radius actually asked for are done. */\n if (ring > rings) break;\n float t = float (ring) / float (rings);\n ringEm = v_spreadEm * t;\n int cap = (HB_GPU_SPREAD_MAX_TAPS * ring + denom / 2) / denom;\n steps = clamp (int (ceil (HB_GPU_SPREAD_TAU * radiusPx * t)), 6, max (cap, 6));\n /* THE GOLDEN ANGLE, so no two rings put their taps on the same radii — which would leave\n * wedge-shaped gaps between the rings rather than a covering. A fixed fraction of a step\n * would do that for one pair of ring counts and line up for another; 137.5 degrees per ring\n * is the rotation with no small-integer commensurability with any of them. */\n phase = 2.39996 * float (ring);\n step = 0;\n }\n float angle = phase + HB_GPU_SPREAD_TAU * float (step) / float (steps);\n vec2 at = v_texcoord + ringEm * vec2 (cos (angle), sin (angle));\n /* MAX, NOT A SUM, AND THE MAX IS WHY THIS IS IN THE SHADER. The alternative a caller could\n * build without it — draw the run N times at N offsets — composites N times, so a\n * translucent outline is N overlapping translucent copies and reads far darker than one\n * stroke. One fragment, one coverage, one blend.\n *\n * SHARPENED BEFORE THE MAX, not after: the max is over a set of INSIDE tests, and the union\n * of disks it approximates is a binary shape. Sharpening the max instead would sharpen a\n * number that had already been flattened to the peak coverage nearby, which is the value\n * that is wrong. See HB_GPU_SPREAD_INSIDE_LOW. */\n cov = max (cov, smoothstep (HB_GPU_SPREAD_INSIDE_LOW,\n HB_GPU_SPREAD_INSIDE_HIGH,\n hb_gpu_spread_tap (at, pixelsPerEm, ppem, v_glyphLoc)));\n step += 1;\n }\n }\n\n /* CONTRAST, ON THE FINAL COVERAGE — and STEM DARKENING ONLY WHEN THE DILATION DID NOT RUN.\n *\n * An outline's rim is NOT a coverage ramp with the same problem the fill's has, which is what\n * this block assumed when it shipped. Stem darkening exists because a sub-pixel STEM lands at\n * mid-grey under linear coverage where a browser puts it near the ink colour; the fix is an\n * exponent that pushes the middle of the ramp toward the foreground. A dilated fragment's\n * coverage is not that number. The taps above are an INSIDE test (HB_GPU_SPREAD_INSIDE_LOW\n * sharpens each one before the max), so the dilated boundary is already very nearly binary — and\n * the engine this mirrors applies no curve at all to its outline: Godot strokes the glyph and\n * hands the result to FreeType's plain grayscale raster. Measured against that golden, the curve\n * took a 14 px outline's rim from 0.662 px to 1.965 px of equivalent ramp, three times the width,\n * where Godot's own is 0.851. That is a halo — fattening for dark ink, thinning for light — and\n * it is the one thing in this shader that made an outline softer than the engine's.\n *\n * THE GATE IS THE DILATION BRANCH'S OWN CONDITION, CHARACTER FOR CHARACTER, so the two cannot\n * disagree about which fragments dilated. A bare v_spreadPx > 0.0 would also strip the\n * correction from a DEGENERATE run — one whose a_emPerPos is zero or NaN, which fails\n * v_spreadEm > 0.0 and takes the single-tap path. Such a fragment is a fill in every way that\n * reaches the framebuffer, and it should keep a fill's darkening rather than lose it to a uniform\n * that ended up doing nothing.\n *\n * GAMMA IS NOT GATED. It is an explicit consumer knob, defaults to 1 (skipped entirely below),\n * and is polarity- and size-blind by construction; a consumer that deliberately sets one is\n * asking for a transfer curve on the text, and putting the fill and the outline of the same run\n * on different curves would be a stranger thing than either choice.\n *\n * THE PPEM IS FETCHED IN UNIFORM CONTROL FLOW AND THE COVERAGE TEST IS NOT ALLOWED TO CONTAIN IT.\n * hb_gpu_ppem calls fwidth, which GLSL ES 3.00 leaves undefined once the 2x2 derivative quad can\n * disagree about whether to run it — and \"0.0 < cov < 1.0\" is precisely a per-fragment condition.\n * u_stemDarken is uniform while v_spreadPx and v_spreadEm are flat, which is the same\n * argument the dilation branch above makes for its own hb_gpu_ppem call; darken is that branch's\n * predicate negated, so a quad coherent enough to run the dilation is exactly as coherent about\n * skipping the darkening. Upstream's demo puts its fwidth inside the coverage test; that is a\n * desktop-GLSL liberty this cannot take.\n */\n bool fillPass = !(v_spreadPx > 0.0 && v_spreadEm > 0.0);\n bool darken = u_stemDarken > 0.0 && fillPass;\n\n float darkenPpem = 0.0;\n if (darken)\n darkenPpem = hb_gpu_ppem (v_texcoord, v_glyphLoc);\n\n /* EDGE ONLY, which is upstream's guard and is not merely an optimisation here. Both corrections\n * fix 0 and 1, so the interior and the background cannot move whatever the exponents are — but\n * pow (0.0, y) is UNDEFINED for y <= 0 in GLSL ES 3.00, and u_gamma is a number a consumer chose.\n * Skipping cov == 0.0 is what makes a hostile gamma a picture that is wrong rather than a NaN\n * alpha over the whole quad. No derivative is taken inside, so a non-uniform branch is legal. */\n if (cov > 0.0 && cov < 1.0)\n {\n float adj = cov;\n if (darken)\n {\n /* v_color is STRAIGHT, so this IS upstream's dot (c.rgb, 1/3) / c.a with the divide already\n * done. A flat 1/3 rather than a Rec.709 luma on purpose: hb_gpu_stem_darken's exponent\n * curve is calibrated against HarfBuzz's own definition of \"brightness\", and weighting the\n * channels differently would silently retune somebody else's constants. */\n adj = hb_gpu_stem_darken (adj, dot (v_color.rgb, vec3 (1.0 / 3.0)), darkenPpem);\n }\n if (u_gamma != 1.0)\n adj = pow (adj, u_gamma);\n cov = adj;\n }\n\n float a = v_color.a * cov;\n fragColor = vec4 (v_color.rgb * a, a);\n}\n`;\n\nexport const FRAGMENT_MAIN = `\nuniform vec4 u_color; /* STRAIGHT rgba; premultiplied exactly once, below */\nuniform float u_spreadPx;\nuniform float u_gamma; /* exponent on the final coverage; 1.0 is off */\nuniform float u_stemDarken; /* > 0 runs hb_gpu_stem_darken; 0 is off */\n\nin vec2 v_texcoord;\nflat in uint v_glyphLoc;\nflat in float v_spreadEm;\n\nout vec4 fragColor;\n\nconst float HB_GPU_SPREAD_TAU = 6.2831853;\n/* Hard ceilings so the loop is bounded at compile time. One tap per iteration, so\n * HB_GPU_SPREAD_MAX_TAPS + the centre tap is the worst case, and it is only reached by a fragment\n * that is neither solid ink nor near any. See the TS constant of the same name. */\nconst int HB_GPU_SPREAD_MAX_RINGS = 4;\nconst int HB_GPU_SPREAD_MAX_TAPS = ${HB_GPU_SPREAD_MAX_TAPS};\n/* \"Already saturated\": no tap can raise this, so stop. Not 1.0, because the coverage estimator\n * lands a hair under it on a deep-interior fragment and an exact test would never fire. */\nconst float HB_GPU_SPREAD_SOLID = 0.999;\n\n/*\n * WHERE A TAP STOPS MEANING \"how much ink is at this offset\" AND STARTS MEANING \"is this fragment\n * inside the dilated silhouette\". The knee of a smoothstep, and the whole of the fix below.\n *\n * NO BACKTICKS ANYWHERE IN THIS COMMENT, and that is not a style note: this whole string is a JS\n * template literal, so one backtick ends the shader mid-sentence and the package fails to PARSE.\n *\n * THE BUG IT REMOVES. A dilated shape is the union of a disk of radius r swept along the outline:\n * a BINARY shape, whose only partial coverage is at its own boundary. A max of raw coverage taps\n * cannot produce that, because a max cannot exceed the largest coverage near the fragment — and at\n * ppem 14 a Han stroke is thinner than a pixel, so its coverage PEAKS at 0.42 and the whole\n * silhouette came out a translucent mottle at 0.62 of the ideal's ink.\n *\n * THE RANGE IS 0 TO 0.5, AND \"A TAP ABOVE HALF COVERAGE IS INSIDE\" IS THE RULE THAT FAILS. That is\n * the obvious reading and it makes this case measurably WORSE, which is why the knee is a swept\n * number rather than an argued one. Half of a PIXEL is not half of a sub-pixel STROKE: at ppem 14\n * the fixture's peak coverage is 0.42, so a knee centred on 0.5 sits above anything the glyph can\n * reach and ERASES the outline. What 0.5 is the right value for is the top of the range — a pixel\n * centred exactly ON the outline reads 0.5, so \"as covered as a pixel on the boundary\" is the point\n * at which a tap is fully inside, and everything below it ramps.\n *\n * SWEPT ON THE RTX 2060 THROUGH ANGLE, both fixtures, against 8x grown references. Low-ppem is\n * 中 at 14 px per em rotated 10 degrees, spread 3 (SHALLOW COVERAGE); thin is a full stop at 96 px\n * per em, spread 12 (SPARSE COVERING — no 64-tap set tiles a disk of that radius; the sweep was run\n * when those taps were four rings of 16, and the rim column moved again when they were resplit).\n *\n * knee low rim rms low ink ratio low interior short thin rim rms thin ink ratio\n * none 77.89 0.622 38.5% 80.90 0.965\n * 0.35 - 0.65 128.57 0.574 (worse still) 102.52 0.966\n * 0.25 - 0.75 112.35 0.604 --- 99.72 0.965\n * 0.20 - 0.50 76.87 0.870 --- 94.27 0.974\n * 0.15 - 0.45 73.85 0.968 --- 90.15 0.978\n * 0.10 - 0.40 84.75 1.037 --- 85.82 0.985\n * 0.05 - 0.45 79.40 1.011 4.1% 82.16 0.986\n * 0.05 - 0.50 72.64 0.968 7.4% 82.81 0.983\n * 0.00 - 0.45 83.29 1.026 3.3% 78.86 0.990\n * 0.00 - 0.55 69.73 0.947 9.3% 79.96 0.984\n * 0.00 - 0.50 75.66 0.988 6.1% 79.10 0.987 <-\n *\n * The two upper rows are the \"roughly half\" hypothesis and both are worse than shipping nothing.\n * 0 - 0.5 is the only row that improves EVERY column at once, and it is also the one with a\n * sentence behind it rather than a fit.\n *\n * WHAT IT COSTS AT LARGE PPEM. A well-resolved glyph's tap coverage IS the area, so a tap sitting\n * exactly on the outline reads 0.5 — and 0.5 is also the ideal answer at the dilated boundary,\n * where this maps it to 1. So the boundary moves outward by a fraction of a pixel: measured, the\n * 96 px per em ink box grows 5 px on one side for a spread of 4 instead of 4. Real, inside the\n * fixtures' SPREAD_TOLERANCE_PX, and the price of an interior that is no longer translucent.\n *\n * PER TAP RATHER THAN ON THE MAX, AND NOT FOR THE REASON IT LOOKS LIKE. smoothstep is MONOTONE, so\n * it commutes with max and the two placements give the same silhouette — measured, not reasoned:\n * moving it after the loop reads ink ratio 0.989 against 0.988 and the same rim RMS to two decimal\n * places. What the placement actually buys is the two things a monotone identity does not cover.\n * First, the FILL IS THE FLOOR: cov enters the loop as hb_gpu_draw's own coverage and is never\n * sharpened, so a dilated run stays a strict SUPERSET of the same run at spread 0 — sharpening the\n * max would put the fill through the knee too, and smoothstep(0, 0.5, x) is BELOW x for x under\n * ~0.08, so a faint fill pixel would come back dimmer than it was drawn. Second, the early-out\n * below tests cov INSIDE the loop, and only a per-tap value can raise it early.\n *\n * IT DOES NOT MAKE THE EARLY-OUT FIRE AT 14 px, WHICH THE ROUND EXPECTED IT TO. A tap saturates to\n * exactly 1 only once its raw coverage reaches HB_GPU_SPREAD_INSIDE_HIGH, and at ppem 14 the\n * fixture's peak raw coverage is 0.42, which sharpens to 0.931 — still under\n * HB_GPU_SPREAD_SOLID. So a fragment at that size still walks the whole tap set, and the frame-cost\n * side effect that was predicted here IS NOT THERE. Above ppem 16 taps reached 1 before this change\n * as well, so nothing moved there either. Lowering HB_GPU_SPREAD_SOLID would collect it, and is\n * deliberately not done here: it is a cost decision with its own pixels to grade, on a rung where\n * the tap budget is a device ceiling.\n */\nconst float HB_GPU_SPREAD_INSIDE_LOW = 0.0;\nconst float HB_GPU_SPREAD_INSIDE_HIGH = 0.5;\n\n/*\n * One coverage tap, WITH NO DERIVATIVE IN IT — which is the whole reason this exists.\n *\n * It is _hb_gpu_slug (hb-gpu-fragment.glsl, 14.4.0) with ppem lifted into a parameter. The\n * library's own _hb_gpu_slug advertises itself as callable \"from non-uniform control flow\", and\n * for GLSL it is not: it calls hb_gpu_ppem, which calls fwidth. The disk below has a per-fragment\n * early-out, so every tap after that point IS non-uniform control flow, and a fwidth there is\n * undefined by GLSL ES 3.00.\n *\n * MIRRORED RATHER THAN AVOIDED so an outline tap and a fill fragment agree. Lifting ppem is exact\n * rather than an approximation: it is fwidth(v_texcoord) and the glyph's own scale, and fwidth of an\n * interpolated varying is constant across an affine quad, so its value at a tap equals its value at\n * the centre.\n *\n * THE MSAA HALF IS SWITCHABLE AND THE DEFAULT IS OFF — see\n * {@link HbGpuRendererOptions.spreadTapMsaa}, which carries the measurement. Note the macro is\n * HB_GPU_SPREAD_TAP_NO_MSAA and NOT the library's HB_GPU_NO_MSAA: that one guards _hb_gpu_slug,\n * i.e. the FILL, which this trade must not touch.\n *\n * The vendored wasm is digest-pinned (vendor/VENDOR.md, test/vendor.test.ts), so the source this\n * mirrors cannot move without a deliberate vendor bump.\n */\nfloat hb_gpu_spread_tap (vec2 rc, vec2 pixelsPerEm, float ppem, uint glyphLoc_)\n{\n float c = _hb_gpu_slug_single (rc, pixelsPerEm, glyphLoc_);\n#ifndef HB_GPU_SPREAD_TAP_NO_MSAA\n if (ppem < 16.0)\n {\n vec2 emsPerPixel = 1.0 / pixelsPerEm;\n vec2 d = emsPerPixel * (1.0 / 3.0);\n float msaa = 0.25 *\n (_hb_gpu_slug_single (rc + vec2 (-d.x, -d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 ( d.x, -d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 (-d.x, d.y), pixelsPerEm, glyphLoc_) +\n _hb_gpu_slug_single (rc + vec2 ( d.x, d.y), pixelsPerEm, glyphLoc_));\n c = mix (c, msaa, smoothstep (16.0, 8.0, ppem));\n }\n#endif\n return c;\n}\n\nvoid main ()\n{\n float cov = hb_gpu_draw (v_texcoord, v_glyphLoc);\n\n /* UNIFORM CONTROL FLOW, AND IT HAS TO BE: the two derivative-taking calls inside are legal only\n * because every fragment of a 2x2 derivative quad takes this branch together. u_spreadPx is a\n * real uniform and v_spreadEm is flat (constant over a primitive, which is what a derivative\n * quad belongs to), so it is uniform by construction rather than by luck. v_spreadEm is also a\n * genuine guard: a NaN or zero a_emPerPos fails it and takes the single-tap path. */\n if (u_spreadPx > 0.0 && v_spreadEm > 0.0)\n {\n vec2 pixelsPerEm = 1.0 / fwidth (v_texcoord);\n float ppem = hb_gpu_ppem (v_texcoord, v_glyphLoc);\n /* The spread in DEVICE pixels — fwidth is a screen-space derivative, so this already carries\n * the model scale and the device-pixel ratio. It only ever picks tap counts; the tap OFFSETS\n * are in em units and are exact. */\n float radiusPx = v_spreadEm * max (pixelsPerEm.x, pixelsPerEm.y);\n\n /* CONCENTRIC RINGS, NOT ONE, and that is not a refinement. A dilated fragment is covered iff\n * SOME offset within the disk lands on ink; taps on a single ring of radius r can all overshoot\n * a feature narrower than 2r, which punches holes through the outline exactly where a glyph is\n * thin — a comma, a hairline serif, a full stop. Ring spacing is held near 2/3 px, so the RADIAL\n * half of the covering is sub-pixel out to the clamp at HB_GPU_SPREAD_MAX_RINGS.\n *\n * THE RADII STAY EQUALLY SPACED, which is worth saying because equal AREA is the obvious\n * alternative and it is worse here. Pushing the rings outward concentrates them where the taps\n * are already densest, and combined with the budget split below it both doubles the outward bias\n * of the whole tap set and halves the radial margin the small-feature case relies on — the one\n * where a full stop smaller than the tap radius has to be found by an INNER ring. */\n int rings = clamp (int (ceil (radiusPx * 1.5)), 1, HB_GPU_SPREAD_MAX_RINGS);\n /* 1 + 2 + ... + rings, the denominator of the budget split below. */\n int denom = rings * (rings + 1) / 2;\n\n /* ONE FLAT LOOP OVER THE WHOLE BUDGET, AND THE OUTER RING GETS MOST OF IT.\n *\n * Every ring used to be capped at the same number of steps, which sounds neutral and is not: a\n * ring's taps are spread over a circumference proportional to its radius, so an equal share puts\n * the WIDEST arc gaps on the outermost ring — the only one that decides where the dilated\n * boundary lands. At radius 12 that was 4.71 px of arc between the taps that draw the edge,\n * against 1.18 px on ring 1, and the boundary followed the tap count: measured against a Godot\n * 4.5.1 golden, 0.3099 px of wobble at exactly 16 cycles per revolution where the engine has\n * 0.0164.\n *\n * So the budget is split in proportion to ring RADIUS, i.e. to circumference: ring k of rings\n * may spend (MAX_TAPS * k + denom/2) / denom taps, which at four rings is 6 / 13 / 19 / 26 and\n * sums to exactly MAX_TAPS. It sums to exactly MAX_TAPS at one, two and three rings as well\n * (64; 21 + 43; 11 + 21 + 32), so the flat bound is never the thing that truncates a ring — it\n * is a hedge against a driver that insists on unrolling, not a second policy. At radius 12 the\n * outer arc is then 2.90 px rather than 4.71.\n *\n * The lower clamp of 6 steps is what keeps a SMALL radius honest, and it is the reason the cap\n * enters as max (cap, 6) rather than as cap: ring 1's share at four rings is exactly 6, and a\n * hexagon is the coarsest ring that still surrounds its centre.\n *\n * ONE TAP PER ITERATION, so HB_GPU_SPREAD_MAX_TAPS is simultaneously the loop bound and the\n * fragment's worst-case cost — the two used to be 4 x 16 and 65 and had to be reasoned about\n * separately. Dynamic bounds and breaks are legal ESSL 3.00; the GLSL ES 1.00 Appendix A\n * restriction that forced the nested constant shape does not apply to version 300 es. */\n int ring = 0;\n int step = 0;\n int steps = 0;\n float ringEm = 0.0;\n float phase = 0.0;\n for (int i = 0; i < HB_GPU_SPREAD_MAX_TAPS; i++)\n {\n /* THE INTERIOR EARLY-OUT: a fragment already covered by its own centre tap is trivially\n * within r of ink, and interior fragments are most of a glyph. */\n if (cov >= HB_GPU_SPREAD_SOLID) break;\n if (step >= steps)\n {\n ring += 1;\n /* The other exit: the rings this radius actually asked for are done. */\n if (ring > rings) break;\n float t = float (ring) / float (rings);\n ringEm = v_spreadEm * t;\n int cap = (HB_GPU_SPREAD_MAX_TAPS * ring + denom / 2) / denom;\n steps = clamp (int (ceil (HB_GPU_SPREAD_TAU * radiusPx * t)), 6, max (cap, 6));\n /* THE GOLDEN ANGLE, so no two rings put their taps on the same radii — which would leave\n * wedge-shaped gaps between the rings rather than a covering. A fixed fraction of a step\n * would do that for one pair of ring counts and line up for another; 137.5 degrees per ring\n * is the rotation with no small-integer commensurability with any of them. */\n phase = 2.39996 * float (ring);\n step = 0;\n }\n float angle = phase + HB_GPU_SPREAD_TAU * float (step) / float (steps);\n vec2 at = v_texcoord + ringEm * vec2 (cos (angle), sin (angle));\n /* MAX, NOT A SUM, AND THE MAX IS WHY THIS IS IN THE SHADER. The alternative a caller could\n * build without it — draw the run N times at N offsets — composites N times, so a\n * translucent outline is N overlapping translucent copies and reads far darker than one\n * stroke. One fragment, one coverage, one blend.\n *\n * SHARPENED BEFORE THE MAX, not after: the max is over a set of INSIDE tests, and the union\n * of disks it approximates is a binary shape. Sharpening the max instead would sharpen a\n * number that had already been flattened to the peak coverage nearby, which is the value\n * that is wrong. See HB_GPU_SPREAD_INSIDE_LOW. */\n cov = max (cov, smoothstep (HB_GPU_SPREAD_INSIDE_LOW,\n HB_GPU_SPREAD_INSIDE_HIGH,\n hb_gpu_spread_tap (at, pixelsPerEm, ppem, v_glyphLoc)));\n step += 1;\n }\n }\n\n /* CONTRAST, ON THE FINAL COVERAGE — and STEM DARKENING ONLY WHEN THE DILATION DID NOT RUN.\n *\n * An outline's rim is NOT a coverage ramp with the same problem the fill's has, which is what\n * this block assumed when it shipped. Stem darkening exists because a sub-pixel STEM lands at\n * mid-grey under linear coverage where a browser puts it near the ink colour; the fix is an\n * exponent that pushes the middle of the ramp toward the foreground. A dilated fragment's\n * coverage is not that number. The taps above are an INSIDE test (HB_GPU_SPREAD_INSIDE_LOW\n * sharpens each one before the max), so the dilated boundary is already very nearly binary — and\n * the engine this mirrors applies no curve at all to its outline: Godot strokes the glyph and\n * hands the result to FreeType's plain grayscale raster. Measured against that golden, the curve\n * took a 14 px outline's rim from 0.662 px to 1.965 px of equivalent ramp, three times the width,\n * where Godot's own is 0.851. That is a halo — fattening for dark ink, thinning for light — and\n * it is the one thing in this shader that made an outline softer than the engine's.\n *\n * THE GATE IS THE DILATION BRANCH'S OWN CONDITION, CHARACTER FOR CHARACTER, so the two cannot\n * disagree about which fragments dilated. A bare u_spreadPx > 0.0 would also strip the\n * correction from a DEGENERATE run — one whose a_emPerPos is zero or NaN, which fails\n * v_spreadEm > 0.0 and takes the single-tap path. Such a fragment is a fill in every way that\n * reaches the framebuffer, and it should keep a fill's darkening rather than lose it to a uniform\n * that ended up doing nothing.\n *\n * GAMMA IS NOT GATED. It is an explicit consumer knob, defaults to 1 (skipped entirely below),\n * and is polarity- and size-blind by construction; a consumer that deliberately sets one is\n * asking for a transfer curve on the text, and putting the fill and the outline of the same run\n * on different curves would be a stranger thing than either choice.\n *\n * THE PPEM IS FETCHED IN UNIFORM CONTROL FLOW AND THE COVERAGE TEST IS NOT ALLOWED TO CONTAIN IT.\n * hb_gpu_ppem calls fwidth, which GLSL ES 3.00 leaves undefined once the 2x2 derivative quad can\n * disagree about whether to run it — and \"0.0 < cov < 1.0\" is precisely a per-fragment condition.\n * u_stemDarken and u_spreadPx are real uniforms and v_spreadEm is flat, which is the same\n * argument the dilation branch above makes for its own hb_gpu_ppem call; darken is that branch's\n * predicate negated, so a quad coherent enough to run the dilation is exactly as coherent about\n * skipping the darkening. Upstream's demo puts its fwidth inside the coverage test; that is a\n * desktop-GLSL liberty this cannot take.\n */\n bool fillPass = !(u_spreadPx > 0.0 && v_spreadEm > 0.0);\n bool darken = u_stemDarken > 0.0 && fillPass;\n\n float darkenPpem = 0.0;\n if (darken)\n darkenPpem = hb_gpu_ppem (v_texcoord, v_glyphLoc);\n\n /* EDGE ONLY, which is upstream's guard and is not merely an optimisation here. Both corrections\n * fix 0 and 1, so the interior and the background cannot move whatever the exponents are — but\n * pow (0.0, y) is UNDEFINED for y <= 0 in GLSL ES 3.00, and u_gamma is a number a consumer chose.\n * Skipping cov == 0.0 is what makes a hostile gamma a picture that is wrong rather than a NaN\n * alpha over the whole quad. No derivative is taken inside, so a non-uniform branch is legal. */\n if (cov > 0.0 && cov < 1.0)\n {\n float adj = cov;\n if (darken)\n {\n /* u_color is STRAIGHT, so this IS upstream's dot (c.rgb, 1/3) / c.a with the divide already\n * done. A flat 1/3 rather than a Rec.709 luma on purpose: hb_gpu_stem_darken's exponent\n * curve is calibrated against HarfBuzz's own definition of \"brightness\", and weighting the\n * channels differently would silently retune somebody else's constants. */\n adj = hb_gpu_stem_darken (adj, dot (u_color.rgb, vec3 (1.0 / 3.0)), darkenPpem);\n }\n if (u_gamma != 1.0)\n adj = pow (adj, u_gamma);\n cov = adj;\n }\n\n float a = u_color.a * cov;\n fragColor = vec4 (u_color.rgb * a, a);\n}\n`;\n\n/**\n * A face registered with the renderer: the namespace every one of its glyph keys carries.\n *\n * THE ATLAS KEY IS `(face, glyph)` AND NOT `glyph`, AND THAT IS THE WHOLE POINT OF THIS TYPE.\n * Glyph 42 of Noto Sans SC and glyph 42 of Roboto are unrelated outlines; an atlas keyed on the\n * glyph id alone hands the second one the first one's texels and renders fluent, crisp, WRONG\n * text — the one failure mode nothing downstream measures. That namespacing used to live in the\n * perf-harness's key strings, one layer above a renderer that could not tell two faces apart.\n *\n * Registering also fixes the `upem`, so {@link HbGpuRenderer.upload} no longer takes one and a\n * glyph cannot be uploaded under one face's scale and drawn at another's.\n */\nexport interface HbGpuFace {\n /** Dense index assigned at registration. Namespaces every key of this face. */\n readonly id: number;\n /** Whatever the embedder called it. Appears in failure messages, nowhere else. */\n readonly label: string;\n /** Units per em, taken from the registered font and never from a caller's argument. */\n readonly upem: number;\n}\n\n/** Where one encoded glyph landed, and the box to draw it in. */\nexport interface GlyphSlot {\n /**\n * Numeric identity of the resident glyph. Optional so structural slots made by older callers\n * remain accepted by the cold compatibility path in {@link HbGpuRenderer.push}.\n */\n faceId?: number;\n glyphId?: number;\n /**\n * The allocator key — `<face id>/<glyph id>`, so a draw can find its allocation without a\n * reverse lookup. Internal shape: read it for debugging, never construct one.\n */\n key: string;\n /**\n * Which ALLOCATION this slot describes, stamped by the allocator and never reused.\n *\n * THE ANSWER TO THE WORST BUG THIS PACKAGE HAD. The ring evicts, so a slot handed out an hour\n * ago can name texels that some other glyph now owns. `push` used to draw it anyway, at the\n * right size, in the right place, perfectly antialiased — a different glyph's outline, invisible\n * to every check downstream. `push` now compares this against the live allocation and skips on a\n * mismatch (see {@link AtlasStats.staleSkips}); a plain key comparison could not, because the\n * key of a re-uploaded glyph is the same key.\n */\n generation: number;\n /** Atlas texel index of the blob's first texel — the shader's `glyphLoc`. */\n loc: number;\n /** Units per em of the face these coordinates are in. */\n upem: number;\n /** Em-space ink box, y-UP, as `hb_gpu_draw_encode` reported it. */\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n /** Texels this blob occupies. */\n texels: number;\n}\n\nexport interface AtlasStats {\n /**\n * What the live allocations actually occupy: texels x 8.\n *\n * The honest \"how much glyph data is resident\" number, and the one to compare against a baked\n * atlas's occupied bytes.\n */\n liveBytes: number;\n /**\n * `width x height x 8` — the whole texture.\n *\n * THE NUMBER THAT SITS NEXT TO `hb-atlas`'s 1.45 MiB. A driver allocates the texture, not the\n * used part of it, so this is what is actually held whatever the occupancy says. Reported\n * separately from `liveBytes` because quoting only the smaller one is how a renderer appears to\n * cost less than it does.\n */\n reservationBytes: number;\n /** Live allocations, and the texels they hold. */\n entries: number;\n liveTexels: number;\n capacityTexels: number;\n /** Allocations overwritten by a wrapped cursor since creation. */\n evictions: number;\n /** Faces registered. Keys are namespaced by these — see {@link HbGpuFace}. */\n faces: number;\n /**\n * Glyphs `push` declined to draw because their slot no longer matches a live allocation.\n *\n * NON-ZERO IS A REAL FINDING, not noise: it means the embedder is holding slots across an\n * eviction (its atlas is too small for its working set, or it caches slots it should re-`upload`)\n * and the frame is short of glyphs. Counted rather than thrown because `push` is the hot loop.\n */\n staleSkips: number;\n}\n\nexport interface BlobStats {\n /** Distinct glyphs uploaded. */\n glyphs: number;\n /** Sum of every uploaded blob's length, in bytes. */\n totalBytes: number;\n /**\n * THE PREDICTION UNDER TEST. `docs/text-rendering.md` records \"~5.4 KB per Han glyph against\n * ~1.4 KB for a 38x38 R8 atlas cell\". This is the left-hand side of it, self-counted.\n */\n bytesPerGlyph: number;\n}\n\n/**\n * The contrast correction the fragment stage applies to the coverage it computed.\n *\n * THE TWO FIELDS REACH DIFFERENT PASSES, which is the one thing to read before setting either:\n * `gamma` is applied to every draw, `stemDarkening` only to an undilated one. The reason is under\n * {@link HbGpuContrast.stemDarkening}.\n *\n * WHY IT EXISTS, AND WHY THE DEFAULT IS ON. Raw analytic coverage is the AREA of the pixel the\n * outline covers, and compositing it linearly is not what a browser does: measured on one fixed\n * crop of the word \"Breakthrough\" at 1600x900 / DPR 1.25, the hb-gpu path and the DOM path agree on\n * peak darkness (51 vs 50), on mean luminance (120.5 vs 121.0) and on total ink (2736 vs 2744) —\n * and DOM still puts **66% more pixels** in the deep-dark end (1285 below luma 80 against 775).\n * That gap is entirely in the middle of the ramp: a sub-pixel stem lands mid-grey here and near the\n * ink colour there, which reads as washed out at exactly the sizes a UI uses. Every shipping\n * consumer wants the correction; the exception is a harness.\n *\n * BOTH FIELDS ARE REQUIRED rather than optional, which is deliberate. A half-specified\n * `{ gamma: 1.2 }` would silently inherit a stem-darkening default the author never considered, and\n * this is a knob whose whole purpose is that somebody thought about it. Use\n * {@link HB_GPU_CONTRAST_DEFAULT} / {@link HB_GPU_CONTRAST_NONE} rather than writing the pair out.\n */\nexport interface HbGpuContrast {\n /**\n * Exponent applied to the coverage. `1` is the identity and is the default.\n *\n * BELOW 1 IS DARKER (a coverage of 0.5 moves toward 1), above 1 lighter. It is polarity-BLIND —\n * unlike `stemDarkening`, which reads the foreground — so a value that helps dark-on-light text\n * hurts light-on-dark by the same amount. HarfBuzz's own demo flips it by theme\n * (`demo-view.cc`: `dark_mode ? 1/2.2 : 2.2`) for that reason, and a renderer here draws both\n * polarities in one frame and cannot. Hence 1: the size- and polarity-aware half of the\n * correction is `stemDarkening`, and this is the manual override next to it.\n *\n * Non-finite or non-positive values are refused and reported (`\"degenerate-contrast\"`), because\n * `pow` with such an exponent is a NaN alpha over the whole quad rather than a wrong picture.\n */\n gamma: number;\n /**\n * Run `hb_gpu_stem_darken` on the coverage OF A FILL. Default `true`.\n *\n * IT IS THE SIZE-AWARE AND POLARITY-AWARE HALF. The library's exponent is\n * `mix (pow (2, brightness - 0.5), 1, smoothstep (8, 48, ppem))`, so it fattens dark text\n * (brightness 0 -> exponent 0.707), thins light text (brightness 1 -> 1.414) and RAMPS ITSELF OFF\n * by ppem 48, where a stem is wide enough that no pixel of it is partially covered anyway. The\n * brightness comes from the per-instance colour and the ppem from `hb_gpu_ppem`, so it costs no\n * per-frame decision.\n *\n * IT DOES NOT APPLY TO A DILATED RUN, whatever this flag says: a draw with a non-zero\n * {@link HbGpuRenderer.setSpread} emits raw coverage. The correction exists because a sub-pixel\n * STEM sits at mid-grey under linear coverage where a browser puts it near the ink colour, and a\n * dilated fragment's coverage is not that number — the taps are an inside test, so the boundary\n * is nearly binary before any curve touches it. More decisively, Godot's outline is a plain\n * FreeType raster with no curve of its own, so an exponent on a dilated rim reads as a HALO\n * against it: fattening for dark ink, thinning for light. Measured against the committed Godot\n * golden, it took a 14 px outline's rim from 0.662 to 1.965 px of equivalent ramp where Godot's\n * is 0.851 (`packages/hb-gpu/test/goldens/godot-outline-metrics.json`, `docs/text-rendering.md`).\n *\n * THE EDGE THAT LEAVES: a consumer drawing a spread run as the ONLY ink — an outline with no fill\n * composited over it — now gets uncorrected coverage for that text and no way to ask for\n * otherwise. That is the intended picture for a stroke and the wrong one for a glyph body, so a\n * caller in that position should draw the fill it is standing in for. `gamma` is NOT gated and\n * remains available for a deliberate transfer curve over both passes.\n */\n stemDarkening: boolean;\n}\n\n/** Stem darkening on, gamma neutral. What a renderer built without a `contrast` option gets. */\nexport const HB_GPU_CONTRAST_DEFAULT: HbGpuContrast = Object.freeze({\n gamma: 1,\n stemDarkening: true,\n});\n\n/**\n * No contrast curve at all: the fragment writes the coverage it computed.\n *\n * FOR MEASUREMENT ARMS, and they should say so where they pass it. A fidelity probe that grades an\n * arm against an 8x area-coverage reference is grading the RASTERIZER, and an arm carrying a\n * contrast curve scores the curve instead — `docs/text-rendering.md`'s distortion figures (0.196\n * Han at ppem 14, 0.017 at ppem 49) only mean what they say against raw coverage.\n */\nexport const HB_GPU_CONTRAST_NONE: HbGpuContrast = Object.freeze({\n gamma: 1,\n stemDarkening: false,\n});\n\nexport interface HbGpuRendererOptions {\n /**\n * The context to draw in. NOT created here and never destroyed here.\n *\n * It must have been created with `premultipliedAlpha: true` — see this file's header. Nothing\n * can check that from inside (`getContextAttributes` reports what was ASKED for, and the failure\n * is a picture that is merely darker), so it is stated rather than validated.\n */\n gl: WebGL2RenderingContext;\n /**\n * The extent of OBJECT SPACE — the units {@link HbGpuRenderer.push} takes, y measured DOWN.\n *\n * This pair builds the design->clip projection and nothing else. On a standalone canvas it is\n * the device-pixel size and equals the framebuffer pair below; on a DPR-scaled stage it is the\n * scene's own coordinate extent (`packages/canvas/src/present.ts`'s `designWidth`), and the two\n * pairs differ by the ratio.\n */\n designWidth: number;\n designHeight: number;\n /**\n * The ACHIEVED drawing-buffer size, in DEVICE pixels. Defaults to the design pair.\n *\n * NOT THE SAME NUMBER AS THE DESIGN PAIR, and the default is only correct for a stage whose\n * device-pixel ratio is 1. This pair is `u_viewport`, which is what `hb_gpu_dilate` measures half\n * a SCREEN pixel against — see this file's header for what feeding it design units does.\n *\n * PASS `gl.drawingBufferWidth`, not the size you asked the canvas for. Setting `canvas.width`\n * only REQUESTS an allocation and an implementation may hand back less, and a viewport that is\n * wrong by a few pixels is a dilation that is wrong by a fraction of one — a rim of clipped\n * antialiasing around every glyph rather than anything that looks like a size error.\n */\n framebufferWidth?: number;\n framebufferHeight?: number;\n /**\n * Atlas capacity in TEXELS. Rounded up to a whole number of {@link HbGpuRenderer.atlasWidth}-wide\n * rows.\n *\n * Default 256 rows = 1 Mi texels = 8 MiB, which holds ~1500 Han outlines at the ~5.4 KB the\n * prediction expects. Sized in texels rather than bytes because that is the unit the shader\n * indexes in and the unit the allocator wraps in.\n */\n atlasTexels?: number;\n /**\n * Give each DILATION TAP the same five-sample average `_hb_gpu_slug` gives a fill below ppem 16.\n *\n * **Default `false`, and that default is a measured trade rather than an oversight.**\n *\n * WHAT IT COSTS TO LEAVE ON. `hb_gpu_spread_tap` mirrors `_hb_gpu_slug`, so below ppem 16 one ring\n * tap becomes FIVE `_hb_gpu_slug_single` evaluations — and a dilated fragment takes up to 65 taps.\n * Measured on S9's `text-render` scenario (RTX 2060, 1280x800 at DPR 1, 40 outlined runs at 14 px,\n * `outlinePx` 6): **12.27 Hz with it on, 70.47 Hz with it off**, 5.74x, and 8.05x at `outlinePx`\n * 10. The font-size ladder isolates it — the same 3 px radius and the same tap set (47 steps when\n * that was measured, 50 under the budget split that replaced the per-ring clamp) reads 17.44 Hz at\n * `fontSize` 18 and 74.76 at 20, because 20 is where the ppem the shader computes crosses 16 and\n * the branch stops firing. It is one branch, not the tap count: capping the rings and steps at\n * 3x12 bought only 1.5x, and 2x8 bought 2.3x by punching holes through thin features.\n *\n * WHY IT IS DEFENSIBLE TO LEAVE OFF, which is a claim about pixels and is pinned by\n * `test/glyphPixelXvfb.test.ts`'s low-ppem pair. The outline is a solid silhouette that a fill is\n * then drawn on top of, so the only thing the taps decide is its OUTER rim; a max over ~47 taps is\n * itself a smoothing operator; and the rim of a thick outline is the least legible place in a\n * glyph. Measured at 14 px, rotated 10 degrees, spread 3, both programs on one GPU in one frame:\n * the two differ on **267 pixels, RMS 33.4 levels of 255, worst 85** — and the one WITHOUT the\n * MSAA is the one closer to an 8x dilated reference (rim RMS 77.9 against 85.1, ink 36102 against\n * 30192 where the ideal grown shape is 58081). The extra smoothing was deepening a shortfall, not\n * repairing one: `max` over coverage cannot exceed the peak coverage near a fragment, and at\n * ppem 14 a Han glyph's strokes never reach 1, so the outline is a translucent mottle at ~60% of\n * the ideal either way. That is the honest shape of this trade — not \"5.74x for a slightly\n * coarser rim\" but \"5.74x for a differently-wrong outline at a size where the outline is already\n * wrong\". Above ppem 16 it is free in both directions: the blend weight is\n * `smoothstep (16, 8, ppem)`, which is exactly 0 there, so NO pixel of text at or above ppem 16\n * can change — measured, by widening the gate to `ppem < 200` and reading a byte-identical frame.\n *\n * WHAT IT DOES NOT TOUCH, structurally. The FILL's coverage is `hb_gpu_draw` -> `_hb_gpu_slug`\n * inside the vendored library, guarded by the library's own `HB_GPU_NO_MSAA`. This flag defines\n * `HB_GPU_SPREAD_TAP_NO_MSAA`, a different name that only this file's function reads, so no\n * setting of it can reach the fill. `spread 0` never calls the tap at all.\n *\n * Set `true` for very small outlined text where the rim matters more than the frame budget.\n */\n spreadTapMsaa?: boolean;\n /**\n * The contrast curve applied to the FINAL coverage. Defaults to {@link HB_GPU_CONTRAST_DEFAULT},\n * which has stem darkening ON.\n *\n * PASS {@link HB_GPU_CONTRAST_NONE} IF YOU ARE MEASURING FIDELITY, and only then. See\n * {@link HbGpuContrast} for the whole argument.\n */\n contrast?: HbGpuContrast;\n /**\n * Store model/colour/spread beside every glyph so adjacent runs may share one draw. Off by\n * default: the established renderer keeps its 40-byte record and uniform state unchanged.\n */\n perInstanceRunState?: boolean;\n /**\n * Where a refusal goes.\n *\n * `createHbGpuRenderer` returns `null` rather than throwing, following `createCanvasStage`, so a\n * consumer can fall back to a DOM text path. But a renderer that declined silently reports as a\n * cheap one — in a perf arm literally so — so every `null` and every declined upload also comes\n * through here with a reason a human can act on.\n */\n onError?(failure: HbGpuFailure): void;\n}\n\nexport interface HbGpuRenderer {\n /** The borrowed context. Owned by the embedder; `dispose` does not touch it. */\n readonly gl: WebGL2RenderingContext;\n /**\n * The atlas width this instance actually built — NOT necessarily {@link ATLAS_WIDTH}.\n *\n * WebGL2 guarantees only 2048, so a device can force a narrower texture. The shader gets this as\n * `hb_gpu_atlas_width` and the row-wrap arithmetic uses it, so a non-4096 width is correct and\n * merely costs more `texSubImage2D` calls per blob.\n */\n readonly atlasWidth: number;\n /**\n * True between {@link notifyContextLost} and a successful {@link rebuild}.\n *\n * While it is true every method here is a no-op that touches no GL: `upload` returns `null`,\n * `push` skips, `end` reports a zero frame. Nothing polls `gl.isContextLost()` — that is a query\n * in the hot path for an event the embedder already receives — so a consumer that does not wire\n * `webglcontextlost` through to {@link notifyContextLost} will draw into dead objects forever,\n * which is permanently blank text with no signal. That wiring is not optional.\n */\n readonly contextLost: boolean;\n /**\n * Register a face, taking its `upem`, and get the namespace its glyph keys carry.\n *\n * THE FONT IS RETAINED, and that is the memory decision this package makes. A context loss\n * destroys the atlas texture, so {@link rebuild} has to be able to put the same texels back at\n * the same offsets — and the two ways to do that are to keep a copy of every uploaded blob or to\n * keep the encoder that produced them. This keeps the ENCODER: the embedder already holds the\n * `HbGpuFont` (it cannot encode without one), so the retained reference costs zero additional\n * bytes, where retaining blobs would hold a second resident copy of the glyph data forever —\n * 1.3 MB on S9's 300-glyph Han pool — to make a once-in-a-session event faster. The cost is paid\n * on restore instead: `rebuild` re-runs `hb_gpu_draw_encode` for every resident glyph (~180 ms\n * for that same pool).\n *\n * Consequence, stated because it is a lifetime rule and not a preference: the font must outlive\n * the renderer, or a rebuild silently drops that face's glyphs.\n *\n * `null` when the font's `upem` is not usable — see {@link HbGpuFailureReason} `\"degenerate-upem\"`.\n */\n registerFace(font: HbGpuFont, label?: string): HbGpuFace | null;\n /**\n * Upload one encoded glyph of `face`, or return the slot it already has.\n *\n * `null` for a glyph with no ink — a space encodes to a zero-length blob, which is a legitimate\n * result and must not become a zero-texel allocation — and also for a blob this renderer\n * declines (malformed, or larger than the whole atlas), which is reported through `onError`.\n *\n * THROWS in exactly one case; see {@link HbGpuRendererOptions.atlasTexels} and the in-use guard\n * inside `allocate`.\n */\n upload(\n face: HbGpuFace,\n glyphId: number,\n glyph: EncodedGlyph,\n ): GlyphSlot | null;\n /**\n * The live slot for a glyph that is already resident, or `null` when it is not.\n *\n * THE CHEAP HALF OF {@link HbGpuRenderer.upload}, AND THE REASON AN EMBEDDER CAN KEEP HANDLES\n * INSTEAD OF SLOTS. A retained draw list records a glyph as an id and has to turn it back into a\n * `GlyphSlot` every frame; the only way to do that used to be `upload`, which needs an\n * {@link EncodedGlyph} — and encoding a Han working set costs ~180 ms, so a per-frame encode is\n * not a path anybody can take. This is a map lookup and a touch.\n *\n * `null` means \"never uploaded, or evicted\", and those are the same answer for a caller: encode\n * and `upload` again. It is NOT an error and nothing is reported — a ring allocator evicting is\n * the mechanism working.\n *\n * The returned slot is built by the same expression `upload` uses, so a resolved slot and a\n * freshly uploaded one cannot differ; in particular it carries the CURRENT\n * {@link GlyphSlot.generation}, which is what makes it safe to `push`.\n */\n resolve(face: HbGpuFace, glyphId: number): GlyphSlot | null;\n /** Start a frame. */\n begin(): void;\n /**\n * Queue one glyph, its em ORIGIN (the pen position, on the baseline) at object-space `(x, y)`,\n * at `pixelsPerEm` object units per em.\n *\n * Object space is device pixels and y measures DOWN, matching every other renderer in the repo.\n * Rotation is not here on purpose: it belongs in {@link setModel}, so that one matrix rotates the\n * quad AND is seen by `hb_gpu_dilate`, which computes its half-pixel dilation in SCREEN space\n * through that same matrix. A quad rotated on the CPU behind the shader's back would be dilated\n * along the wrong axes.\n *\n * A slot whose allocation has been evicted is SKIPPED and counted — see {@link GlyphSlot.generation}.\n */\n push(slot: GlyphSlot, x: number, y: number, pixelsPerEm: number): void;\n /** Object-space 2x3 (`[xx, xy, yx, yy, tx, ty]`), applied before the projection. */\n setModel(model: ArrayLike<number>): void;\n /** STRAIGHT rgba in 0..1. The fragment premultiplies it, once. */\n setColor(r: number, g: number, b: number, a: number): void;\n /**\n * Grow every glyph of the next frame outward by `px` OBJECT units. `0` (the default) is the\n * plain fill.\n *\n * WHAT AN OUTLINED LABEL IS: this run in the outline colour at `spread`, then the SAME run in the\n * fill colour at spread 0, in that order. A centred `ctx.strokeText` of width `W` reaches `W / 2`\n * outward, so a caller matching one passes `W / 2`; the arithmetic is the caller's, because only\n * the caller knows whether its stroke is centred, inner or outer.\n *\n * A DILATION FILLS THE INTERIOR AND A CENTRED STROKE DOES NOT, AND THAT DIFFERENCE IS REAL. This\n * paints the whole glyph plus a band of `spread` around it, where a stroke paints only a band\n * straddling the contour. Under an OPAQUE fill the two are pixel-identical, because the fill\n * covers every pixel they disagree about. Under a TRANSLUCENT fill they are not: the outline\n * colour shows through the glyph's middle here and would not through a stroke. That is the honest\n * limit of a coverage-max dilation and there is no distance field to do better with — see\n * {@link FRAGMENT_MAIN}.\n *\n * OBJECT UNITS, BEFORE {@link HbGpuRenderer.setModel}, matching every other length `push` takes.\n * A model that scales scales the outline with the text, which is what a caller rotating a label\n * wants.\n *\n * IT PERSISTS, exactly like {@link HbGpuRenderer.setModel} and {@link HbGpuRenderer.setColor} —\n * `begin` does not reset it. The failure that buys is worth stating: a caller that sets a spread\n * for one run and does not clear it draws every LATER run fat, which looks like a font-weight bug\n * rather than a missing call. Per-run callers should set it per run, which is what\n * `packages/canvas`'s glyph pass does.\n *\n * A NON-ZERO SPREAD ALSO TURNS STEM DARKENING OFF for that run, whatever\n * {@link HbGpuContrast.stemDarkening} says — Godot's outline carries no contrast curve, and one\n * on a dilated rim reads as a halo. The reason is under that field; the pair \"outline then fill\"\n * above is unaffected, because the fill run is the one that keeps the correction.\n *\n * COST. Bounded, but not free: a fragment that is neither solid ink nor near any evaluates up to\n * 65 coverage taps (and five times that below ppem 16), and the quad it does so over grows by\n * `spread` on every side. Beyond roughly 4 device pixels of radius the tap ceiling is reached —\n * {@link HB_GPU_SPREAD_MAX_TAPS} — and the dilated rim starts to scallop: measured against a Godot\n * 4.5.1 golden at radius 12, the 50% contour wobbles 0.17 px where the engine's wobbles 0.11. The\n * cost does not grow past that point; only the scallop does. Negative and non-finite values are\n * clamped to 0 rather than reported: this is a per-run hot-path setter with no error channel.\n */\n setSpread(px: number): void;\n /**\n * Re-state both sizes after the embedder resized its stage or its drawing buffer.\n *\n * Does NOT call `gl.viewport` — this renderer never touches it. It updates the design->clip\n * projection (from the DESIGN pair) and the `u_viewport` the dilation is measured in (from the\n * FRAMEBUFFER pair), both of which must describe the viewport the embedder will have set by the\n * time `end` runs. The framebuffer pair defaults to the design pair, which is right only at a\n * device-pixel ratio of 1 — see {@link HbGpuRendererOptions.framebufferWidth}.\n */\n setViewport(\n designWidth: number,\n designHeight: number,\n framebufferWidth?: number,\n framebufferHeight?: number,\n ): void;\n /**\n * Submit. Returns what the frame cost.\n *\n * WHAT THIS DOES NOT DO, because the context is borrowed: it does not set `gl.viewport` and it\n * does not clear. Both were here while this file owned a stage, and both are actively wrong in a\n * shared context — a clear inside a glyph pass erases everything the embedder's executor already\n * drew, and a viewport call silently overrides a scissored or letterboxed pass.\n *\n * WHAT IT LEAVES DIRTY, exhaustively, so an embedder's restore code can be written against it.\n * On a frame that drew anything (`instances > 0`):\n *\n * - `BLEND` is ENABLED, `blendEquation` is `FUNC_ADD`, `blendFunc` is\n * `(ONE, ONE_MINUS_SRC_ALPHA)` — premultiplied MIX. Note `blendFunc`/`blendEquation`, not\n * the `*Separate` forms, so both the RGB and the alpha halves are set.\n * - The current program is this renderer's.\n * - `ARRAY_BUFFER` is bound to the instance buffer.\n * - `ACTIVE_TEXTURE` is `TEXTURE0` and `TEXTURE_BINDING_2D` on unit 0 is the atlas.\n * - `VERTEX_ARRAY_BINDING` is null (unbound, not restored to whatever was bound before —\n * WebGL2 has no cheap way to read it back).\n * - Uniforms of this renderer's program only — `u_viewProjection`, `u_viewport`, `u_gamma`,\n * `u_stemDarken`, `hb_gpu_atlas` and `hb_gpu_atlas_width`. Per-run model, colour and spread\n * are captured into each instance record; their setters still persist until replaced and\n * `begin` deliberately does not reset them (see {@link HbGpuRenderer.setSpread}).\n *\n * Untouched: viewport, scissor box and `SCISSOR_TEST`, clear colour, depth/stencil state,\n * framebuffer bindings, the three unpack flags — `UNPACK_ALIGNMENT`, `UNPACK_FLIP_Y_WEBGL` and\n * `UNPACK_PREMULTIPLY_ALPHA_WEBGL`, all saved and restored around every upload — and every\n * texture unit but 0.\n *\n * On an empty frame it returns immediately and leaves ALL of the above untouched too, which is\n * why an embedder must restore unconditionally rather than only when `instances > 0`.\n */\n end(): { instances: number; drawCalls: number };\n /**\n * The context is gone: drop every GL handle WITHOUT calling into GL to free it.\n *\n * Call this from `webglcontextlost` (`packages/canvas/src/present.ts` offers exactly that hook,\n * and `preventDefault` on that event is what makes a restore possible at all). The allocation\n * table survives — offsets, faces and glyph ids — because {@link rebuild} puts the same texels\n * back at the same offsets, which is what keeps every {@link GlyphSlot} the embedder is holding\n * valid across the loss.\n */\n notifyContextLost(): void;\n /**\n * The context is back: recreate the program, the buffers, the VAO and the texture, then re-encode\n * and re-upload every resident glyph at the offset it already had.\n *\n * SAME OFFSETS, DELIBERATELY. Repacking would be simpler and would invalidate every slot the\n * embedder holds — which the generation guard would then turn into a silently empty frame rather\n * than garbage, but empty is still wrong. Re-materialising the atlas byte for byte means a\n * restore needs no cooperation from the caller beyond this one call.\n *\n * Returns `false` (and reports) if the GL objects could not be rebuilt. A face whose font has\n * been destroyed, or a glyph that no longer encodes to the same length, loses its allocation:\n * those slots then fail the generation check and are skipped rather than drawn wrong.\n */\n rebuild(): boolean;\n readonly atlas: AtlasStats;\n readonly blobs: BlobStats;\n /** Delete this renderer's GL objects. Does NOT touch the context or the canvas. */\n dispose(): void;\n}\n\ninterface Allocation {\n key: string;\n /** Which face and glyph this is, so `rebuild` can encode it again. */\n faceId: number;\n glyphId: number;\n offset: number;\n texels: number;\n /** Stamped once, never reused. The other half of {@link GlyphSlot.generation}. */\n generation: number;\n /** This allocation still owns its atlas range. */\n live: boolean;\n /** Renderer identity; prevents structurally copied slots crossing renderers. */\n rid: number;\n /** The immutable owned slot returned on hot paths. */\n slot: GlyphSlot;\n /** Monotonic touch counter — the LRU order. */\n usedAt: number;\n /** Frame index this was last drawn in; the in-use guard reads it. */\n usedFrame: number;\n /**\n * The face's units per em and the glyph's em ink box, KEPT HERE rather than re-read.\n *\n * These six numbers used to be taken off the {@link EncodedGlyph} at every call, which meant the\n * only way to obtain a slot was to hold — or re-run — the encoder. {@link HbGpuRenderer.resolve}\n * exists precisely so a caller does not have to, and it can only exist if the allocation knows\n * its own box. They are six floats per resident glyph against a blob that averages 5.4 KB.\n */\n upem: number;\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\ninterface RegisteredFace extends HbGpuFace {\n font: HbGpuFont;\n}\n\n/** A build step's refusal: the machine-readable half and the sentence for a human. */\ninterface BuildFailure {\n reason: HbGpuFailureReason;\n message: string;\n}\n\n/** A compiled shader, or the reason there is not one. Never a throw — see `createHbGpuRenderer`. */\nfunction compileShader(\n gl: WebGL2RenderingContext,\n type: number,\n source: string,\n): { shader: WebGLShader } | BuildFailure {\n const stage = type === gl.VERTEX_SHADER ? \"vertex\" : \"fragment\";\n const shader = gl.createShader(type);\n // `gl-object`, not `shader-compile`: nothing was compiled. `createShader` returning null means\n // the context is gone or out of resources, which is a different thing for a caller to react to\n // than GLSL that will not build.\n if (!shader) {\n return {\n reason: \"gl-object\",\n message: `gl.createShader(${stage}) returned null — the context is lost or out of resources`,\n };\n }\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader) || \"(no log)\";\n gl.deleteShader(shader);\n return {\n reason: \"shader-compile\",\n message: `${stage} shader failed to compile — ${log}`,\n };\n }\n return { shader };\n}\n\n/**\n * A WebGL2 renderer for hb-gpu blobs, in a context somebody else owns.\n *\n * `null`, NOT A THROW, for every construction failure — the idiom `createCanvasStage` already sets\n * in this repo, and the difference between a measurement arm and a shipped renderer. A consumer\n * that cannot have this one falls back to its DOM text path; a consumer that WANTS to be loud\n * passes `onError` and is told the reason, because a silently skipped renderer reports as a cheap\n * one.\n *\n * DESIGN SPACE IS DEVICE PIXELS, and not by preference: `u_viewport` is the framebuffer size and\n * `hb_gpu_dilate` uses it with the projection to work out how far half a screen pixel is in object\n * units. Any scale in the projection would make the dilation and the quad disagree, and a dilation\n * that is wrong by a fraction is a rim of clipped antialiasing around every glyph.\n */\nlet rendererIdentity = 0;\n/** Non-enumerable provenance survives a direct slot hand-off but deliberately not `{ ...slot }`. */\nconst SLOT_OWNER = Symbol(\"hb-gpu-slot-owner\");\n\nexport function createHbGpuRenderer(\n module: HbGpu,\n options: HbGpuRendererOptions,\n): HbGpuRenderer | null {\n const rid = ++rendererIdentity;\n const gl = options.gl;\n const report = (reason: HbGpuFailureReason, message: string): void => {\n options.onError?.({ reason, message: `hb-gpu: ${message}` });\n };\n const refuse = (reason: HbGpuFailureReason, message: string): null => {\n report(reason, message);\n return null;\n };\n\n if (gl.isContextLost()) {\n return refuse(\n \"context-lost\",\n \"the context handed to createHbGpuRenderer is already lost — every object built now would be dead on arrival\",\n );\n }\n\n // -----------------------------------------------------------------------------------------------\n // Capability probe\n // -----------------------------------------------------------------------------------------------\n\n // `ATLAS_WIDTH` used to be a bare 4096 with no check at all. WebGL2 guarantees only 2048, so on a\n // conforming-but-modest device `texImage2D` would fail with INVALID_VALUE and the atlas would\n // sample as zero — a page with no text on it, and no error anybody sees.\n const maxTextureSize = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE)) || 0;\n if (maxTextureSize < MIN_ATLAS_WIDTH) {\n return refuse(\n \"texture-size\",\n `MAX_TEXTURE_SIZE is ${maxTextureSize}, below the ${MIN_ATLAS_WIDTH} this renderer needs (WebGL2 itself guarantees 2048) — the atlas cannot be built`,\n );\n }\n const atlasWidth = Math.min(ATLAS_WIDTH, maxTextureSize);\n if (atlasWidth !== ATLAS_WIDTH) {\n report(\n \"atlas-clamped\",\n `MAX_TEXTURE_SIZE is ${maxTextureSize}, so the atlas is ${atlasWidth} texels wide instead of ${ATLAS_WIDTH} — correct (the width is a uniform) but each blob now spans more rows and costs more texSubImage2D calls`,\n );\n }\n\n // Read once, here, because `rebuild` recompiles the fragment stage after a context loss and a\n // restored program that quietly disagreed with the lost one about its own tap set would be a\n // rendering change with no call site.\n const spreadTapMsaa = options.spreadTapMsaa ?? false;\n\n // THE CONTRAST CURVE, RESOLVED ONCE AND SANITISED ONCE. Read here for `spreadTapMsaa`'s reason\n // and one more: `end` states these every frame, so a NaN gamma checked at the call site instead\n // would be a NaN written 60 times a second with nothing to report against.\n const contrast = options.contrast ?? HB_GPU_CONTRAST_DEFAULT;\n const perInstanceRunState = options.perInstanceRunState === true;\n const instanceFloatsPerRecord = perInstanceRunState\n ? BATCHED_INSTANCE_FLOATS\n : LEGACY_INSTANCE_FLOATS;\n const instanceBytes = instanceFloatsPerRecord * 4;\n let contrastGamma = contrast.gamma;\n if (!Number.isFinite(contrastGamma) || contrastGamma <= 0) {\n report(\n \"degenerate-contrast\",\n `contrast.gamma is ${String(contrast.gamma)}, which \\`pow\\` cannot take as an exponent — using 1 (no gamma). Stem darkening is unaffected and is ${contrast.stemDarkening ? \"on\" : \"off\"}.`,\n );\n contrastGamma = 1;\n }\n const contrastStemDarken = contrast.stemDarkening ? 1 : 0;\n\n const requestedTexels = Math.max(1, options.atlasTexels ?? atlasWidth * 256);\n const requestedRows = Math.max(1, Math.ceil(requestedTexels / atlasWidth));\n const atlasHeight = Math.min(requestedRows, maxTextureSize);\n if (atlasHeight !== requestedRows) {\n report(\n \"atlas-clamped\",\n `${requestedTexels} texels needs ${requestedRows} rows but MAX_TEXTURE_SIZE caps the texture at ${maxTextureSize} — the atlas holds ${atlasHeight * atlasWidth} texels, and a working set larger than that will thrash the ring`,\n );\n }\n const capacityTexels = atlasHeight * atlasWidth;\n\n // -----------------------------------------------------------------------------------------------\n // GL objects — every one of them rebuildable, because a context loss destroys all of them at once\n // -----------------------------------------------------------------------------------------------\n\n let program: WebGLProgram | null = null;\n let uViewProjection: WebGLUniformLocation | null = null;\n let uMatViewProjection: WebGLUniformLocation | null = null;\n let uColor: WebGLUniformLocation | null = null;\n let uSpreadPx: WebGLUniformLocation | null = null;\n let uViewport: WebGLUniformLocation | null = null;\n let uGamma: WebGLUniformLocation | null = null;\n let uStemDarken: WebGLUniformLocation | null = null;\n let uAtlas: WebGLUniformLocation | null = null;\n let uAtlasWidth: WebGLUniformLocation | null = null;\n let aNormal = -1;\n let aPosition = -1;\n let aTexcoord = -1;\n let aEmPerPos = -1;\n let aGlyphLoc = -1;\n let aModel0 = -1;\n let aModel1 = -1;\n let aModel2 = -1;\n let aColor = -1;\n let aSpreadPx = -1;\n let atlasTexture: WebGLTexture | null = null;\n let vao: WebGLVertexArrayObject | null = null;\n let cornerBuffer: WebGLBuffer | null = null;\n let instanceBuffer: WebGLBuffer | null = null;\n\n // The instance staging arrays outlive a context loss: they are plain memory, and re-growing them\n // on restore would be work for nothing.\n let instanceCapacity = 256;\n let instanceData = new ArrayBuffer(instanceCapacity * instanceBytes);\n let instanceFloats = new Float32Array(instanceData);\n let instanceUints = new Uint32Array(instanceData);\n\n function buildProgram(): BuildFailure | null {\n const vertex = compileShader(\n gl,\n gl.VERTEX_SHADER,\n GLSL_PREAMBLE +\n module.shaderLibrary(HB_GPU_SHADER_STAGE_VERTEX) +\n (perInstanceRunState ? BATCHED_VERTEX_MAIN : VERTEX_MAIN),\n );\n if (\"message\" in vertex) return vertex;\n const fragment = compileShader(\n gl,\n gl.FRAGMENT_SHADER,\n // The selected complete source follows the selected instance layout. Both retain the shared\n // precision preamble, so their varying precisions link identically.\n GLSL_PREAMBLE +\n (spreadTapMsaa ? \"\" : SPREAD_TAP_NO_MSAA_DEFINE) +\n module.shaderLibrary(HB_GPU_SHADER_STAGE_FRAGMENT) +\n (perInstanceRunState ? BATCHED_FRAGMENT_MAIN : FRAGMENT_MAIN),\n );\n if (\"message\" in fragment) {\n gl.deleteShader(vertex.shader);\n return fragment;\n }\n const created = gl.createProgram();\n if (!created) {\n gl.deleteShader(vertex.shader);\n gl.deleteShader(fragment.shader);\n return {\n reason: \"gl-object\",\n message: \"gl.createProgram returned null (context lost?)\",\n };\n }\n gl.attachShader(created, vertex.shader);\n gl.attachShader(created, fragment.shader);\n gl.linkProgram(created);\n gl.deleteShader(vertex.shader);\n gl.deleteShader(fragment.shader);\n if (!gl.getProgramParameter(created, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(created) || \"(no log)\";\n gl.deleteProgram(created);\n // The classic silent one: a `highp int` in one stage and a `mediump int` in the other links\n // with an empty log on some drivers. Both stages share `GLSL_PREAMBLE` so that cannot happen\n // here, and the log is quoted anyway because the next cause will not be that.\n return {\n reason: \"program-link\",\n message: `program failed to link — ${log}`,\n };\n }\n program = created;\n\n if (perInstanceRunState) {\n uViewProjection = gl.getUniformLocation(created, \"u_viewProjection\");\n } else {\n uMatViewProjection = gl.getUniformLocation(created, \"u_matViewProjection\");\n }\n uViewport = gl.getUniformLocation(created, \"u_viewport\");\n if (!perInstanceRunState) {\n uColor = gl.getUniformLocation(created, \"u_color\");\n uSpreadPx = gl.getUniformLocation(created, \"u_spreadPx\");\n }\n // A contrast uniform that no live code path reads is legally optimised away. Attributes are\n // different: absence means the linked shader does not match this file's instance format.\n // A driver is entitled to drop `u_gamma` from the\n // program when the option resolved to 1, because nothing then reads it.\n uGamma = gl.getUniformLocation(created, \"u_gamma\");\n uStemDarken = gl.getUniformLocation(created, \"u_stemDarken\");\n uAtlas = gl.getUniformLocation(created, \"hb_gpu_atlas\");\n uAtlasWidth = gl.getUniformLocation(created, \"hb_gpu_atlas_width\");\n\n aNormal = gl.getAttribLocation(created, \"a_normal\");\n aPosition = gl.getAttribLocation(created, \"a_position\");\n aTexcoord = gl.getAttribLocation(created, \"a_texcoord\");\n aEmPerPos = gl.getAttribLocation(created, \"a_emPerPos\");\n aGlyphLoc = gl.getAttribLocation(created, \"a_glyphLoc\");\n if (perInstanceRunState) {\n aModel0 = gl.getAttribLocation(created, \"a_model0\");\n aModel1 = gl.getAttribLocation(created, \"a_model1\");\n aModel2 = gl.getAttribLocation(created, \"a_model2\");\n aColor = gl.getAttribLocation(created, \"a_color\");\n aSpreadPx = gl.getAttribLocation(created, \"a_spreadPx\");\n }\n // -1 means the linker dropped the attribute, and `enableVertexAttribArray(-1)` is an\n // INVALID_VALUE that leaves a program which draws nothing. All five are read by `main`, so this\n // can only mean the shader library that came out of the wasm is not the one this file expects.\n const missing = (\n [\n [\"a_normal\", aNormal],\n [\"a_position\", aPosition],\n [\"a_texcoord\", aTexcoord],\n [\"a_emPerPos\", aEmPerPos],\n [\"a_glyphLoc\", aGlyphLoc],\n ...(perInstanceRunState\n ? [[\"a_model0\", aModel0], [\"a_model1\", aModel1], [\"a_model2\", aModel2], [\"a_color\", aColor], [\"a_spreadPx\", aSpreadPx]] as const\n : []),\n ] as const\n )\n .filter(([, location]) => location < 0)\n .map(([name]) => name);\n if (missing.length > 0) {\n return {\n reason: \"program-link\",\n message: `the linked program has no location for ${missing.join(\", \")} — the shader library does not match this file's main()`,\n };\n }\n // These are constants of this program, not of a draw. Uniform values belong to the linked\n // program even while a borrowed context switches to another program between glyph runs.\n gl.useProgram(created);\n gl.uniform1i(uAtlas, 0);\n gl.uniform1i(uAtlasWidth, atlasWidth);\n gl.uniform1f(uGamma, contrastGamma);\n gl.uniform1f(uStemDarken, contrastStemDarken);\n return null;\n }\n\n function buildAtlasTexture(): BuildFailure | null {\n const created = gl.createTexture();\n if (!created) {\n return {\n reason: \"gl-object\",\n message: \"gl.createTexture returned null (context lost?)\",\n };\n }\n atlasTexture = created;\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, created);\n // `RGBA16I` is not filterable at all — an integer texture with anything but NEAREST is an\n // incomplete texture and samples as zero, which renders as a page with no text on it.\n gl.texImage2D(\n gl.TEXTURE_2D,\n 0,\n gl.RGBA16I,\n atlasWidth,\n atlasHeight,\n 0,\n gl.RGBA_INTEGER,\n gl.SHORT,\n null,\n );\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n // No mip levels exist, so the wrap modes are the only other way to make the texture incomplete.\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n return null;\n }\n\n function bindInstanceAttributes(): void {\n gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);\n gl.enableVertexAttribArray(aPosition);\n gl.vertexAttribPointer(aPosition, 4, gl.FLOAT, false, instanceBytes, 0);\n gl.vertexAttribDivisor(aPosition, 1);\n gl.enableVertexAttribArray(aTexcoord);\n gl.vertexAttribPointer(aTexcoord, 4, gl.FLOAT, false, instanceBytes, 16);\n gl.vertexAttribDivisor(aTexcoord, 1);\n gl.enableVertexAttribArray(aEmPerPos);\n gl.vertexAttribPointer(aEmPerPos, 1, gl.FLOAT, false, instanceBytes, 32);\n gl.vertexAttribDivisor(aEmPerPos, 1);\n // `vertexAttribIPointer`, not `vertexAttribPointer`: `a_glyphLoc` is a `uint` in GLSL and a\n // float path would round every offset above 2^24 and, worse, convert the ones below it.\n gl.enableVertexAttribArray(aGlyphLoc);\n gl.vertexAttribIPointer(aGlyphLoc, 1, gl.UNSIGNED_INT, instanceBytes, 36);\n gl.vertexAttribDivisor(aGlyphLoc, 1);\n if (!perInstanceRunState) return;\n gl.enableVertexAttribArray(aModel0);\n gl.vertexAttribPointer(aModel0, 2, gl.FLOAT, false, instanceBytes, 40);\n gl.vertexAttribDivisor(aModel0, 1);\n gl.enableVertexAttribArray(aModel1);\n gl.vertexAttribPointer(aModel1, 2, gl.FLOAT, false, instanceBytes, 48);\n gl.vertexAttribDivisor(aModel1, 1);\n gl.enableVertexAttribArray(aModel2);\n gl.vertexAttribPointer(aModel2, 2, gl.FLOAT, false, instanceBytes, 56);\n gl.vertexAttribDivisor(aModel2, 1);\n gl.enableVertexAttribArray(aColor);\n gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, instanceBytes, 64);\n gl.vertexAttribDivisor(aColor, 1);\n gl.enableVertexAttribArray(aSpreadPx);\n gl.vertexAttribPointer(aSpreadPx, 1, gl.FLOAT, false, instanceBytes, 80);\n gl.vertexAttribDivisor(aSpreadPx, 1);\n }\n\n function buildGeometry(): BuildFailure | null {\n const createdVao = gl.createVertexArray();\n const createdCorner = gl.createBuffer();\n const createdInstance = gl.createBuffer();\n if (!createdVao || !createdCorner || !createdInstance) {\n return {\n reason: \"gl-object\",\n message:\n \"gl.createVertexArray/createBuffer returned null (context lost?)\",\n };\n }\n vao = createdVao;\n cornerBuffer = createdCorner;\n instanceBuffer = createdInstance;\n\n gl.bindVertexArray(createdVao);\n // The four corner signs, in TRIANGLE_STRIP order: (cx, cy) = (0,0), (0,1), (1,0), (1,1). This is\n // exactly upstream's `nx = cx ? 1 : -1, ny = cy ? -1 : 1`, and exactly its two triangles\n // (v0,v1,v2) and (v1,v2,v3) — a strip is those two triangles and no index buffer.\n gl.bindBuffer(gl.ARRAY_BUFFER, createdCorner);\n gl.bufferData(\n gl.ARRAY_BUFFER,\n new Float32Array([-1, 1, -1, -1, 1, 1, 1, -1]),\n gl.STATIC_DRAW,\n );\n gl.enableVertexAttribArray(aNormal);\n gl.vertexAttribPointer(aNormal, 2, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, createdInstance);\n gl.bufferData(gl.ARRAY_BUFFER, instanceData.byteLength, gl.DYNAMIC_DRAW);\n bindInstanceAttributes();\n gl.bindVertexArray(null);\n return null;\n }\n\n function buildGlObjects(): BuildFailure | null {\n return buildProgram() ?? buildAtlasTexture() ?? buildGeometry();\n }\n\n /** Forget every handle WITHOUT calling GL. What a lost context leaves behind. */\n function dropGlObjects(): void {\n program = null;\n atlasTexture = null;\n vao = null;\n cornerBuffer = null;\n instanceBuffer = null;\n }\n\n function deleteGlObjects(): void {\n if (vao) gl.deleteVertexArray(vao);\n if (cornerBuffer) gl.deleteBuffer(cornerBuffer);\n if (instanceBuffer) gl.deleteBuffer(instanceBuffer);\n if (atlasTexture) gl.deleteTexture(atlasTexture);\n if (program) gl.deleteProgram(program);\n dropGlObjects();\n }\n\n const initialFailure = buildGlObjects();\n if (initialFailure) {\n deleteGlObjects();\n return refuse(initialFailure.reason, initialFailure.message);\n }\n\n // -----------------------------------------------------------------------------------------------\n // Allocator\n // -----------------------------------------------------------------------------------------------\n\n const faces: RegisteredFace[] = [];\n /** Compatibility/debug lookup only; hot resolve/push use the numeric face maps below. */\n const allocations = new Map<string, Allocation>();\n const byFace: Map<number, Allocation>[] = [];\n /** Identity proof for slots this renderer minted; structural legacy slots have no entry here. */\n const ownedSlots = new WeakMap<GlyphSlot, Allocation>();\n /** Live allocations, always sorted by atlas offset. */\n const order: Allocation[] = [];\n let cursor = 0;\n let touchCounter = 0;\n /** Monotonic touch stamp. One counter, one place it advances. */\n const touch = (): number => {\n touchCounter += 1;\n return touchCounter;\n };\n /** Monotonic allocation stamp. Never reused, never reset — see {@link GlyphSlot.generation}. */\n let generationCounter = 0;\n let evictions = 0;\n let staleSkips = 0;\n let liveTexels = 0;\n let frameIndex = 0;\n let blobGlyphs = 0;\n let blobBytes = 0;\n let contextLost = false;\n\n // TWO PAIRS, AND THE SECOND DEFAULTS TO THE FIRST — see this file's header for what conflating\n // them costs. `?? designWidth` rather than `?? 0` because a standalone canvas genuinely has one\n // size, and making the caller repeat it would be the kind of ceremony people copy wrong.\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 * Place `texels` texels and return the offset, evicting whatever the cursor lands on.\n *\n * A BUMP RING, NOT `die (\"Ran out of atlas memory\")`. Allocations are laid down in touch order,\n * so sweeping the cursor forward overwrites the OLDEST region first — which is LRU for the\n * workload this exists for, a glyph pool larger than the atlas where every resident glyph is\n * touched at most once a frame. It is only an approximation once a key is re-uploaded, and the\n * approximation is not what makes this safe.\n *\n * WHAT MAKES IT SAFE IS THE IN-USE GUARD, AND THAT GUARD STAYS A THROW. Everything else in this\n * file degrades to `null` plus an `onError`, because a shipped renderer must let its consumer\n * fall back. This one does not, and the judgement is deliberate:\n *\n * - It is not a runtime condition. It says the atlas cannot hold ONE FRAME's working set, which\n * is a sizing decision the embedder made before any frame ran. `atlas.capacityTexels` and\n * `atlas.liveTexels` are published precisely so it can be made correctly.\n * - Neither repair is honest. Evicting the victim draws a DIFFERENT glyph's outline in its\n * place, at the right size, in the right position, perfectly antialiased — unreadable text\n * that looks like working text. Declining the new glyph instead leaves the frame short, every\n * frame, forever, reported only as a counter nobody reads.\n * - It is not the hot loop. `push` never throws; this is the cache-miss path.\n */\n function removeAllocation(entry: Allocation): void {\n if (!entry.live) return;\n entry.live = false;\n allocations.delete(entry.key);\n byFace[entry.faceId]?.delete(entry.glyphId);\n const index = order.indexOf(entry);\n if (index >= 0) order.splice(index, 1);\n liveTexels -= entry.texels;\n }\n\n function allocate(key: string, texels: number): number {\n if (cursor + texels > capacityTexels) cursor = 0;\n const start = cursor;\n const end = cursor + texels;\n\n // `order` is offset-sorted, so the slice which can overlap is found without allocating and\n // sorting the whole atlas on every miss. The ring can wrap, but `start..end` itself cannot.\n let first = 0;\n let last = order.length;\n while (first < last) {\n const middle = (first + last) >>> 1;\n if (order[middle]!.offset + order[middle]!.texels <= start)\n first = middle + 1;\n else last = middle;\n }\n let after = first;\n while (after < order.length && order[after]!.offset < end) after += 1;\n // Check before mutating. A same-frame victim is a sizing error, and partial eviction before\n // throwing would make the renderer's resident-table invariants lie.\n for (let i = first; i < after; i += 1) {\n const victim = order[i]!;\n if (victim.usedFrame === frameIndex) {\n throw new Error(\n `hb-gpu: the atlas (${capacityTexels} texels) cannot hold one frame's glyphs — placing \"${key}\" would overwrite \"${victim.key}\", already drawn this frame`,\n );\n }\n }\n for (let i = first; i < after; i += 1) {\n const victim = order[i]!;\n victim.live = false;\n allocations.delete(victim.key);\n byFace[victim.faceId]?.delete(victim.glyphId);\n liveTexels -= victim.texels;\n evictions += 1;\n }\n if (after > first) order.splice(first, after - first);\n\n cursor = end;\n return start;\n }\n\n /**\n * Upload one blob's texels, row by row.\n *\n * ROW BY ROW BECAUSE THE STREAM IS 1-D AND THE TEXTURE IS NOT. A blob is a run of texels at some\n * absolute offset; that run generally starts mid-row and spans several. `texSubImage2D` can only\n * write rectangles, so each row-fragment is its own call — which is upstream's loop, and the\n * single most delicate arithmetic in this file. Off by one row and the glyph's band headers read\n * as curve data.\n */\n function uploadTexels(offset: number, texels: Uint8Array): void {\n // `Int16Array` over the SAME bytes, not a conversion: `gl.SHORT` means the driver reads these\n // 8-byte texels verbatim, and the encoder already wrote them little-endian, which is the only\n // byte order WebAssembly and WebGL both have. Building an Int16Array element by element here\n // would be a byte-swap waiting to be introduced.\n // `Int16Array` cannot start on an odd byte and throws a `RangeError` from inside the\n // constructor if asked to. `encode` hands back a `.slice()`, whose `byteOffset` is 0, so this\n // never fires on the package's own path — but a caller pooling blobs into one buffer could land\n // one on an odd offset, and a bare RangeError here says nothing about why.\n const shorts =\n texels.byteOffset % 2 === 0\n ? new Int16Array(\n texels.buffer,\n texels.byteOffset,\n texels.byteLength / 2,\n )\n : new Int16Array(texels.slice().buffer);\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, atlasTexture);\n // 8-byte texels make every row start 8-byte aligned, so 4 is safe. SAVED AND RESTORED because\n // `UNPACK_ALIGNMENT` is CONTEXT state, not texture state, and this renderer no longer owns the\n // context: `packages/canvas/src/textures.ts` sets it to 1 for its tightly packed RGBA uploads\n // and would have inherited this 4 on its next respec, which reads rows at the wrong stride and\n // skews every texture uploaded after the first glyph.\n const previousAlignment = Number(gl.getParameter(gl.UNPACK_ALIGNMENT)) || 4;\n if (previousAlignment !== 4) gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);\n // THE OTHER TWO UNPACK FLAGS, AND THIS PAIR CRASHES RATHER THAN ERRORS. Both are context state\n // like the alignment, but they are not merely wrong for this upload — they are ILLEGAL for it.\n // WebGL2 defines `UNPACK_FLIP_Y_WEBGL` and `UNPACK_PREMULTIPLY_ALPHA_WEBGL` only for the\n // `texSubImage2D` overloads taking an ImageData / image / canvas / video / ImageBitmap, and\n // requires `INVALID_OPERATION` when either is true for an `ArrayBufferView` source, which is\n // the overload below.\n //\n // `packages/canvas/src/textures.ts:230` sets premultiply TRUE for its colour uploads and leaves\n // it set, exactly as it is entitled to — so any embedder that has uploaded one texture before\n // the first glyph hands this function an illegal call. On ANGLE/Vulkan the observed result is\n // not the specified error: the renderer process EXITS, with no GL error, no exception and\n // nothing on the console. That is how it was found — downstream, as a hard crash the moment a\n // glyph atlas took its first upload beside a normal texture cache.\n //\n // Restored rather than left false for the same reason the alignment is: the next embedder\n // upload after a glyph would otherwise silently stop premultiplying and composite at a².\n const previousFlipY = Boolean(gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL));\n const previousPremultiply = Boolean(\n gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL),\n );\n if (previousFlipY) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n if (previousPremultiply) {\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);\n }\n let remaining = texels.byteLength / HB_GPU_TEXEL_BYTES;\n let source = 0;\n let destination = offset;\n while (remaining > 0) {\n const x = destination % atlasWidth;\n const y = Math.floor(destination / atlasWidth);\n const run = Math.min(atlasWidth - x, remaining);\n gl.texSubImage2D(\n gl.TEXTURE_2D,\n 0,\n x,\n y,\n run,\n 1,\n gl.RGBA_INTEGER,\n gl.SHORT,\n shorts,\n // The 4 shorts per texel are what turns a texel offset into an element offset. WebGL2's\n // `srcOffset` counts ELEMENTS of the typed array, not bytes and not texels.\n source * 4,\n );\n source += run;\n destination += run;\n remaining -= run;\n }\n if (previousAlignment !== 4) {\n gl.pixelStorei(gl.UNPACK_ALIGNMENT, previousAlignment);\n }\n if (previousFlipY) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n if (previousPremultiply) {\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);\n }\n }\n\n // -----------------------------------------------------------------------------------------------\n // Frame state\n // -----------------------------------------------------------------------------------------------\n\n let instanceCount = 0;\n const model = new Float32Array([1, 0, 0, 1, 0, 0]);\n const color = new Float32Array([1, 1, 1, 1]);\n /** Object units, and 0 is the fill path. See {@link HbGpuRenderer.setSpread}. */\n let spreadPx = 0;\n const mvp = new Float32Array(16);\n let mvpDirty = true;\n let colorDirty = true;\n let spreadDirty = true;\n let viewportDirty = true;\n /**\n * Design -> clip as `(scaleX, scaleY, translateX, translateY)`, byte-identical to\n * `createCanvasStage`'s `toClip`.\n *\n * `scaleY` is NEGATIVE because design space measures y DOWNWARDS and clip space upwards. Computed\n * here rather than read off a stage because this renderer no longer has one — and restated in the\n * same four numbers, in the same order, so the two cannot silently diverge.\n *\n * THE DESIGN PAIR, NOT THE FRAMEBUFFER PAIR. The framebuffer pair is `u_viewport` and only that.\n */\n const toClip = new Float32Array(4);\n\n function refreshProjection(): void {\n toClip[0] = 2 / designWidth;\n toClip[1] = -2 / designHeight;\n toClip[2] = -1;\n toClip[3] = 1;\n }\n refreshProjection();\n\n /**\n * Design-to-clip as the column-major `mat4` GLSL wants. The batched program receives only this\n * projection; the established renderer keeps its complete model-view-projection uniform.\n *\n * ONE matrix, and it has to be this one. `hb_gpu_dilate` is handed the same `m` and works out\n * how far half a screen pixel is by pushing the vertex AND its normal through it, so any part of\n * the transform applied elsewhere — a quad rotated on the CPU, a viewport scale — is a transform\n * the dilation cannot see.\n */\n function refreshMatrix(): void {\n const sx = toClip[0];\n const sy = toClip[1];\n mvp.fill(0);\n mvp[0] = perInstanceRunState ? sx : sx * model[0];\n mvp[1] = perInstanceRunState ? 0 : sy * model[1];\n mvp[4] = perInstanceRunState ? 0 : sx * model[2];\n mvp[5] = perInstanceRunState ? sy : sy * model[3];\n mvp[10] = 1;\n mvp[12] = perInstanceRunState ? toClip[2] : sx * model[4] + toClip[2];\n mvp[13] = perInstanceRunState ? toClip[3] : sy * model[5] + toClip[3];\n mvp[15] = 1;\n }\n\n function growInstances(): void {\n instanceCapacity *= 2;\n const next = new ArrayBuffer(instanceCapacity * instanceBytes);\n new Uint8Array(next).set(new Uint8Array(instanceData));\n instanceData = next;\n instanceFloats = new Float32Array(instanceData);\n instanceUints = new Uint32Array(instanceData);\n gl.bindVertexArray(vao);\n gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, instanceData.byteLength, gl.DYNAMIC_DRAW);\n // The VAO's pointers reference the buffer by NAME, but re-specifying storage is the safe moment\n // to restate them; a bound VAO with a stale size is one of WebGL's quieter failure modes.\n bindInstanceAttributes();\n gl.bindVertexArray(null);\n }\n\n /**\n * The slot an allocation describes. ONE expression, and that is the point of it.\n *\n * A fresh slot, a slot for a glyph that was already resident and a slot handed back by\n * {@link HbGpuRenderer.resolve} all come from here, so they cannot disagree about the ink box,\n * the generation or the offset. It reads the ALLOCATION only — it takes no `EncodedGlyph` —\n * which is what lets `resolve` exist without an encoder.\n */\n function slotFor(entry: Allocation): GlyphSlot {\n return entry.slot;\n }\n\n return {\n gl,\n atlasWidth,\n\n get contextLost() {\n return contextLost;\n },\n\n registerFace(font, label) {\n const upem = font.upem;\n // The division that made this check necessary is `pixelsPerEm / slot.upem` in `push`: a upem\n // of 0 makes it Infinity, every instance record becomes NaN, and the draw is a silent no-op\n // with no error anywhere. `packages/hb-gpu/src/index.ts` rejects a degenerate face at\n // creation; this is the second gate, at the layer that does the dividing.\n if (!Number.isInteger(upem) || upem <= 0) {\n return refuse(\n \"degenerate-upem\",\n `face \"${label ?? faces.length}\" reports upem ${upem} — every glyph scaled by it would be NaN and draw nothing`,\n );\n }\n const face: RegisteredFace = {\n id: faces.length,\n label: label ?? `face${faces.length}`,\n upem,\n font,\n };\n faces.push(face);\n byFace.push(new Map());\n return face;\n },\n\n upload(face, glyphId, glyph) {\n if (contextLost) {\n report(\n \"context-lost\",\n `upload of glyph ${glyphId} ignored while the context is lost — call rebuild() from webglcontextrestored`,\n );\n return null;\n }\n const registered = faces[face.id];\n if (!registered || registered !== face) {\n return refuse(\n \"face-unregistered\",\n `face \"${face.label}\" (id ${face.id}) was not registered with this renderer — its keys would collide with whatever face holds that id`,\n );\n }\n const key = `${face.id}/${glyphId}`;\n const existing = byFace[face.id]?.get(glyphId);\n if (existing) {\n existing.usedAt = touch();\n return slotFor(existing);\n }\n // A blank glyph (a space) encodes to the empty-blob singleton. Zero texels is not an\n // allocation and drawing it would be a degenerate quad reading texel 0 — which is some other\n // glyph's header.\n if (glyph.texels.length === 0) return null;\n if (glyph.texels.length % HB_GPU_TEXEL_BYTES !== 0) {\n return refuse(\n \"blob-malformed\",\n `blob for \"${key}\" is ${glyph.texels.length} bytes, not a whole number of ${HB_GPU_TEXEL_BYTES}-byte texels — it would be uploaded a texel short and read as curve data`,\n );\n }\n\n const texels = glyph.texels.length / HB_GPU_TEXEL_BYTES;\n if (texels > capacityTexels) {\n // Data-dependent, unlike the in-use guard: one pathological outline in a font nobody chose.\n // A live app has to survive it with a hole in one run, so this declines rather than throws.\n return refuse(\n \"blob-too-large\",\n `glyph \"${key}\" needs ${texels} texels but the whole atlas is ${capacityTexels} — raise atlasTexels; this glyph will not be drawn`,\n );\n }\n const offset = allocate(key, texels);\n uploadTexels(offset, glyph.texels);\n generationCounter += 1;\n const entry = {\n key,\n faceId: face.id,\n glyphId,\n offset,\n texels,\n generation: generationCounter,\n live: true,\n rid,\n usedAt: touch(),\n usedFrame: -1,\n upem: face.upem,\n // HarfBuzz extents are y-UP with a NEGATIVE height: `yBearing` is the ink's TOP and\n // `yBearing + height` its bottom, so flipping into a min/max box swaps which is which.\n // Read ONCE, here, where the `EncodedGlyph` is in hand — everything downstream, `resolve`\n // included, reads it back off the allocation.\n minX: glyph.extents.xBearing,\n minY: glyph.extents.yBearing + glyph.extents.height,\n maxX: glyph.extents.xBearing + glyph.extents.width,\n maxY: glyph.extents.yBearing,\n } as Omit<Allocation, \"slot\">;\n const slot: GlyphSlot = {\n faceId: face.id,\n glyphId,\n key,\n generation: entry.generation,\n loc: entry.offset,\n upem: entry.upem,\n minX: entry.minX,\n minY: entry.minY,\n maxX: entry.maxX,\n maxY: entry.maxY,\n texels: entry.texels,\n };\n Object.defineProperty(slot, SLOT_OWNER, { value: rid });\n const owned: Allocation = { ...entry, slot };\n ownedSlots.set(slot, owned);\n allocations.set(key, owned);\n byFace[face.id]!.set(glyphId, owned);\n let insertion = 0;\n while (insertion < order.length && order[insertion]!.offset < offset)\n insertion += 1;\n order.splice(insertion, 0, owned);\n liveTexels += texels;\n blobGlyphs += 1;\n blobBytes += glyph.texels.length;\n\n return slotFor(owned);\n },\n\n resolve(face, glyphId) {\n // A lost context still HAS its allocation table — `notifyContextLost` keeps it so `rebuild`\n // can put the same texels back at the same offsets — but nothing may be drawn until the\n // rebuild, and handing out a slot that `push` would silently discard is worse than a miss.\n if (contextLost) return null;\n const registered = faces[face.id];\n if (!registered || registered !== face) {\n // Reported, unlike a plain miss: a foreign face handle is a bug in the embedder, and the\n // symptom without this is a run that silently draws nothing at all.\n return refuse(\n \"face-unregistered\",\n `face \"${face.label}\" (id ${face.id}) was not registered with this renderer — resolve() cannot answer for it, and every glyph of the run would be missing`,\n );\n }\n const entry = byFace[face.id]?.get(glyphId);\n if (!entry) return null;\n // Touched, exactly as the already-resident path of `upload` touches: a caller that resolves\n // its whole run before pushing any of it must not look idle to the ring.\n entry.usedAt = touch();\n return slotFor(entry);\n },\n\n begin() {\n frameIndex += 1;\n instanceCount = 0;\n },\n\n push(slot, x, y, pixelsPerEm) {\n if (contextLost) return;\n // Slots we minted take the numeric fast path and can prove renderer ownership without\n // parsing a key. A structural legacy slot has no identity metadata, so preserve source\n // compatibility with one cold key lookup.\n const owned = ownedSlots.get(slot);\n const owner = (slot as GlyphSlot & { [SLOT_OWNER]?: number })[SLOT_OWNER];\n const entry = owned\n ? owned\n : owner !== undefined && owner !== rid\n ? undefined\n : slot.faceId !== undefined && slot.glyphId !== undefined\n ? byFace[slot.faceId]?.get(slot.glyphId)\n : allocations.get(slot.key);\n // THE EVICTION GUARD, AND IT IS THREE COMPARISONS FOR A REASON. A missing entry is the plain\n // case. A LIVE entry under the same key is the nasty one: the glyph was evicted and later\n // re-uploaded somewhere else, so the key still resolves and `slot.loc` still points at a\n // plausible offset — which is now some other glyph's blob. `generation` is what distinguishes\n // \"this allocation\" from \"an allocation that once had this name\"; `offset` is a second, free\n // check that costs nothing and would catch a slot copied between renderers.\n if (\n !entry ||\n entry.rid !== rid ||\n !entry.live ||\n entry.generation !== slot.generation ||\n entry.offset !== slot.loc\n ) {\n staleSkips += 1;\n return;\n }\n if (instanceCount >= instanceCapacity) growInstances();\n entry.usedFrame = frameIndex;\n entry.usedAt = touch();\n\n // Font units to object units. An em coordinate `e` lands at `x + scale * e` in x and\n // `y - scale * e` in y; the minus is the em-space y-UP to object-space y-DOWN flip, and it\n // is why the object box's `y0` — taken at the em box's MINIMUM y — is the LARGER value.\n const scale = pixelsPerEm / slot.upem;\n const base = instanceCount * instanceFloatsPerRecord;\n instanceFloats[base] = x + scale * slot.minX;\n instanceFloats[base + 1] = y - scale * slot.minY;\n instanceFloats[base + 2] = x + scale * slot.maxX;\n instanceFloats[base + 3] = y - scale * slot.maxY;\n instanceFloats[base + 4] = slot.minX;\n instanceFloats[base + 5] = slot.minY;\n instanceFloats[base + 6] = slot.maxX;\n instanceFloats[base + 7] = slot.maxY;\n // `a_emPerPos` is the INVERSE of the scale: em units per object unit, which for a 1000-unit\n // em at 14 px is ~71. `jac` is built from it in the vertex wrapper.\n instanceFloats[base + 8] = 1 / scale;\n instanceUints[base + 9] = slot.loc >>> 0;\n if (perInstanceRunState) {\n for (let i = 0; i < 6; i += 1) instanceFloats[base + 10 + i] = model[i]!;\n instanceFloats[base + 16] = color[0];\n instanceFloats[base + 17] = color[1];\n instanceFloats[base + 18] = color[2];\n instanceFloats[base + 19] = color[3];\n instanceFloats[base + 20] = spreadPx;\n }\n instanceCount += 1;\n },\n\n setModel(next) {\n for (let i = 0; i < 6; i += 1) {\n if (!perInstanceRunState && model[i] !== next[i]) mvpDirty = true;\n model[i] = next[i];\n }\n },\n\n setColor(r, g, b, a) {\n if (!perInstanceRunState) {\n colorDirty ||= color[0] !== r || color[1] !== g || color[2] !== b || color[3] !== a;\n }\n color[0] = r;\n color[1] = g;\n color[2] = b;\n color[3] = a;\n },\n\n setSpread(px) {\n // Clamped rather than reported. A negative spread would shrink the quad below the ink box and\n // clip the glyph's own antialiased rim, and a NaN would make `v_spreadPx > 0.0` false in the\n // fragment but `pos += a_normal * NaN` a degenerate quad in the vertex — a run that vanishes.\n const next = Number.isFinite(px) && px > 0 ? px : 0;\n if (!perInstanceRunState && spreadPx !== next) spreadDirty = true;\n spreadPx = next;\n },\n\n setViewport(width, height, bufferWidth, bufferHeight) {\n const nextDesignWidth = Math.max(1, width);\n const nextDesignHeight = Math.max(1, height);\n const nextFramebufferWidth = Math.max(1, bufferWidth ?? width);\n const nextFramebufferHeight = Math.max(1, bufferHeight ?? height);\n if (\n designWidth !== nextDesignWidth ||\n designHeight !== nextDesignHeight\n ) {\n designWidth = nextDesignWidth;\n designHeight = nextDesignHeight;\n refreshProjection();\n mvpDirty = true;\n }\n if (\n framebufferWidth !== nextFramebufferWidth ||\n framebufferHeight !== nextFramebufferHeight\n ) {\n framebufferWidth = nextFramebufferWidth;\n framebufferHeight = nextFramebufferHeight;\n viewportDirty = true;\n }\n },\n\n end() {\n if (contextLost || instanceCount === 0) {\n return { instances: 0, drawCalls: 0 };\n }\n if (!program || !vao || !instanceBuffer || !atlasTexture) {\n return { instances: 0, drawCalls: 0 };\n }\n\n gl.useProgram(program);\n gl.bindVertexArray(vao);\n gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);\n gl.bufferSubData(\n gl.ARRAY_BUFFER,\n 0,\n instanceFloats,\n 0,\n instanceCount * instanceFloatsPerRecord,\n );\n\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, atlasTexture);\n if (mvpDirty) {\n refreshMatrix();\n gl.uniformMatrix4fv(\n perInstanceRunState ? uViewProjection : uMatViewProjection,\n false,\n mvp,\n );\n mvpDirty = false;\n }\n if (!perInstanceRunState && colorDirty) {\n gl.uniform4fv(uColor, color);\n colorDirty = false;\n }\n if (!perInstanceRunState && spreadDirty) {\n gl.uniform1f(uSpreadPx, spreadPx);\n spreadDirty = false;\n }\n // THE FRAMEBUFFER PAIR. `hb_gpu_dilate` divides by this to turn half a screen pixel into\n // object units; the design pair is already inside `mvp` and passing it here as well would\n // double-count the device-pixel ratio.\n if (viewportDirty) {\n gl.uniform2f(uViewport, framebufferWidth, framebufferHeight);\n viewportDirty = false;\n }\n\n // Premultiplied MIX, the package's blend: `src + dst * (1 - src.a)`.\n gl.enable(gl.BLEND);\n gl.blendEquation(gl.FUNC_ADD);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);\n gl.bindVertexArray(null);\n return { instances: instanceCount, drawCalls: 1 };\n },\n\n notifyContextLost() {\n contextLost = true;\n instanceCount = 0;\n // NO `gl.delete*` HERE. Every one of those handles is already invalid; calling into GL with\n // them is at best ignored and at worst an INVALID_OPERATION the embedder's own error check\n // will report as its bug.\n dropGlObjects();\n },\n\n rebuild() {\n const failure = buildGlObjects();\n if (failure) {\n deleteGlObjects();\n report(\n failure.reason,\n `rebuild after context loss failed — ${failure.message}`,\n );\n return false;\n }\n // A rebuilt program has only the constants stated by buildProgram; all per-run state must\n // be installed on the first draw after restoration.\n mvpDirty = true;\n colorDirty = true;\n spreadDirty = true;\n viewportDirty = true;\n // In OFFSET order, not touch order: the uploads then walk the texture forwards, which is the\n // one access pattern a driver can coalesce. Correctness does not depend on it — every\n // allocation goes back exactly where it was.\n const resident = [...order];\n const dropped: string[] = [];\n for (const entry of resident) {\n const face = faces[entry.faceId];\n const glyph = face ? face.font.encode(entry.glyphId) : null;\n if (\n !glyph ||\n glyph.texels.length !== entry.texels * HB_GPU_TEXEL_BYTES\n ) {\n // The font was destroyed, or the encoder no longer produces the same blob. Dropping the\n // allocation is what makes the slots the embedder holds fail the generation check and be\n // SKIPPED — the alternative is texels of the wrong length at a fixed offset, which is\n // another glyph's outline drawn in this one's place.\n removeAllocation(entry);\n dropped.push(entry.key);\n continue;\n }\n uploadTexels(entry.offset, glyph.texels);\n }\n contextLost = false;\n if (dropped.length > 0) {\n report(\n \"rebuild-incomplete\",\n `${dropped.length} of ${resident.length} resident glyphs could not be re-encoded after a context loss (${dropped.slice(0, 8).join(\", \")}${dropped.length > 8 ? \", …\" : \"\"}) — their faces' fonts must outlive the renderer; those glyphs will be skipped until re-uploaded`,\n );\n }\n return true;\n },\n\n get atlas(): AtlasStats {\n return {\n liveBytes: liveTexels * HB_GPU_TEXEL_BYTES,\n reservationBytes: atlasWidth * atlasHeight * HB_GPU_TEXEL_BYTES,\n entries: allocations.size,\n liveTexels,\n capacityTexels,\n evictions,\n faces: faces.length,\n staleSkips,\n };\n },\n\n get blobs(): BlobStats {\n return {\n glyphs: blobGlyphs,\n totalBytes: blobBytes,\n bytesPerGlyph: blobGlyphs > 0 ? blobBytes / blobGlyphs : 0,\n };\n },\n\n dispose() {\n // The context, the canvas and the registered fonts all belong to the embedder. This deletes\n // what this file created and nothing else.\n if (!contextLost) deleteGlObjects();\n else dropGlObjects();\n faces.length = 0;\n allocations.clear();\n byFace.length = 0;\n order.length = 0;\n },\n };\n}\n"],"mappings":";;;;;;;;;;AA+EA,MAAa,cAAc;;;;;;;;AAS3B,MAAM,kBAAkB;AAExB,MAAM,yBAAyB;AAC/B,MAAM,0BAA0B;;;;;;;;;;;;;;;AAgBhC,MAAM,gBACJ;;;;;;;;;;;AAYF,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;AAuBlC,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCtC,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+D5B,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EpB,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgT9B,MAAa,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2d7B,MAAa,0BAAyC,OAAO,OAAO;CAClE,OAAO;CACP,eAAe;AACjB,CAAC;;;;;;;;;AAUD,MAAa,uBAAsC,OAAO,OAAO;CAC/D,OAAO;CACP,eAAe;AACjB,CAAC;;AA2WD,SAAS,cACP,IACA,MACA,QACwC;CACxC,MAAM,QAAQ,SAAS,GAAG,gBAAgB,WAAW;CACrD,MAAM,SAAS,GAAG,aAAa,IAAI;CAInC,IAAI,CAAC,QACH,OAAO;EACL,QAAQ;EACR,SAAS,mBAAmB,MAAM;CACpC;CAEF,GAAG,aAAa,QAAQ,MAAM;CAC9B,GAAG,cAAc,MAAM;CACvB,IAAI,CAAC,GAAG,mBAAmB,QAAQ,GAAG,cAAc,GAAG;EACrD,MAAM,MAAM,GAAG,iBAAiB,MAAM,KAAK;EAC3C,GAAG,aAAa,MAAM;EACtB,OAAO;GACL,QAAQ;GACR,SAAS,GAAG,MAAM,8BAA8B;EAClD;CACF;CACA,OAAO,EAAE,OAAO;AAClB;;;;;;;;;;;;;;;AAgBA,IAAI,mBAAmB;;AAEvB,MAAM,aAAa,OAAO,mBAAmB;AAE7C,SAAgB,oBACd,QACA,SACsB;CACtB,MAAM,MAAM,EAAE;CACd,MAAM,KAAK,QAAQ;CACnB,MAAM,UAAU,QAA4B,YAA0B;EACpE,QAAQ,UAAU;GAAE;GAAQ,SAAS,WAAW;EAAU,CAAC;CAC7D;CACA,MAAM,UAAU,QAA4B,YAA0B;EACpE,OAAO,QAAQ,OAAO;EACtB,OAAO;CACT;CAEA,IAAI,GAAG,cAAc,GACnB,OAAO,OACL,gBACA,6GACF;CAUF,MAAM,iBAAiB,OAAO,GAAG,aAAa,GAAG,gBAAgB,CAAC,KAAK;CACvE,IAAI,iBAAiB,iBACnB,OAAO,OACL,gBACA,uBAAuB,eAAe,cAAc,gBAAgB,iFACtE;CAEF,MAAM,aAAa,KAAK,IAAI,aAAa,cAAc;CACvD,IAAI,eAAA,MACF,OACE,iBACA,uBAAuB,eAAe,oBAAoB,WAAW,0BAA0B,YAAY,yGAC7G;CAMF,MAAM,gBAAgB,QAAQ,iBAAiB;CAK/C,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,sBAAsB,QAAQ,wBAAwB;CAC5D,MAAM,0BAA0B,sBAC5B,0BACA;CACJ,MAAM,gBAAgB,0BAA0B;CAChD,IAAI,gBAAgB,SAAS;CAC7B,IAAI,CAAC,OAAO,SAAS,aAAa,KAAK,iBAAiB,GAAG;EACzD,OACE,uBACA,qBAAqB,OAAO,SAAS,KAAK,EAAE,uGAAuG,SAAS,gBAAgB,OAAO,MAAM,EAC3L;EACA,gBAAgB;CAClB;CACA,MAAM,qBAAqB,SAAS,gBAAgB,IAAI;CAExD,MAAM,kBAAkB,KAAK,IAAI,GAAG,QAAQ,eAAe,aAAa,GAAG;CAC3E,MAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,KAAK,kBAAkB,UAAU,CAAC;CACzE,MAAM,cAAc,KAAK,IAAI,eAAe,cAAc;CAC1D,IAAI,gBAAgB,eAClB,OACE,iBACA,GAAG,gBAAgB,gBAAgB,cAAc,iDAAiD,eAAe,qBAAqB,cAAc,WAAW,iEACjK;CAEF,MAAM,iBAAiB,cAAc;CAMrC,IAAI,UAA+B;CACnC,IAAI,kBAA+C;CACnD,IAAI,qBAAkD;CACtD,IAAI,SAAsC;CAC1C,IAAI,YAAyC;CAC7C,IAAI,YAAyC;CAC7C,IAAI,SAAsC;CAC1C,IAAI,cAA2C;CAC/C,IAAI,SAAsC;CAC1C,IAAI,cAA2C;CAC/C,IAAI,UAAU;CACd,IAAI,YAAY;CAChB,IAAI,YAAY;CAChB,IAAI,YAAY;CAChB,IAAI,YAAY;CAChB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,SAAS;CACb,IAAI,YAAY;CAChB,IAAI,eAAoC;CACxC,IAAI,MAAqC;CACzC,IAAI,eAAmC;CACvC,IAAI,iBAAqC;CAIzC,IAAI,mBAAmB;CACvB,IAAI,eAAe,IAAI,YAAY,mBAAmB,aAAa;CACnE,IAAI,iBAAiB,IAAI,aAAa,YAAY;CAClD,IAAI,gBAAgB,IAAI,YAAY,YAAY;CAEhD,SAAS,eAAoC;EAC3C,MAAM,SAAS,cACb,IACA,GAAG,eACH,gBACE,OAAO,cAAA,CAAwC,KAC9C,sBAAsB,sBAAsB,YACjD;EACA,IAAI,aAAa,QAAQ,OAAO;EAChC,MAAM,WAAW,cACf,IACA,GAAG,iBAGH,iBACG,gBAAgB,KAAK,6BACtB,OAAO,cAAA,CAA0C,KAChD,sBAAsB,wBAAwB,cACnD;EACA,IAAI,aAAa,UAAU;GACzB,GAAG,aAAa,OAAO,MAAM;GAC7B,OAAO;EACT;EACA,MAAM,UAAU,GAAG,cAAc;EACjC,IAAI,CAAC,SAAS;GACZ,GAAG,aAAa,OAAO,MAAM;GAC7B,GAAG,aAAa,SAAS,MAAM;GAC/B,OAAO;IACL,QAAQ;IACR,SAAS;GACX;EACF;EACA,GAAG,aAAa,SAAS,OAAO,MAAM;EACtC,GAAG,aAAa,SAAS,SAAS,MAAM;EACxC,GAAG,YAAY,OAAO;EACtB,GAAG,aAAa,OAAO,MAAM;EAC7B,GAAG,aAAa,SAAS,MAAM;EAC/B,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG,WAAW,GAAG;GACpD,MAAM,MAAM,GAAG,kBAAkB,OAAO,KAAK;GAC7C,GAAG,cAAc,OAAO;GAIxB,OAAO;IACL,QAAQ;IACR,SAAS,4BAA4B;GACvC;EACF;EACA,UAAU;EAEV,IAAI,qBACF,kBAAkB,GAAG,mBAAmB,SAAS,kBAAkB;OAEnE,qBAAqB,GAAG,mBAAmB,SAAS,qBAAqB;EAE3E,YAAY,GAAG,mBAAmB,SAAS,YAAY;EACvD,IAAI,CAAC,qBAAqB;GACxB,SAAS,GAAG,mBAAmB,SAAS,SAAS;GACjD,YAAY,GAAG,mBAAmB,SAAS,YAAY;EACzD;EAKA,SAAS,GAAG,mBAAmB,SAAS,SAAS;EACjD,cAAc,GAAG,mBAAmB,SAAS,cAAc;EAC3D,SAAS,GAAG,mBAAmB,SAAS,cAAc;EACtD,cAAc,GAAG,mBAAmB,SAAS,oBAAoB;EAEjE,UAAU,GAAG,kBAAkB,SAAS,UAAU;EAClD,YAAY,GAAG,kBAAkB,SAAS,YAAY;EACtD,YAAY,GAAG,kBAAkB,SAAS,YAAY;EACtD,YAAY,GAAG,kBAAkB,SAAS,YAAY;EACtD,YAAY,GAAG,kBAAkB,SAAS,YAAY;EACtD,IAAI,qBAAqB;GACvB,UAAU,GAAG,kBAAkB,SAAS,UAAU;GAClD,UAAU,GAAG,kBAAkB,SAAS,UAAU;GAClD,UAAU,GAAG,kBAAkB,SAAS,UAAU;GAClD,SAAS,GAAG,kBAAkB,SAAS,SAAS;GAChD,YAAY,GAAG,kBAAkB,SAAS,YAAY;EACxD;EAIA,MAAM,UACJ;GACE,CAAC,YAAY,OAAO;GACpB,CAAC,cAAc,SAAS;GACxB,CAAC,cAAc,SAAS;GACxB,CAAC,cAAc,SAAS;GACxB,CAAC,cAAc,SAAS;GACxB,GAAI,sBACA;IAAC,CAAC,YAAY,OAAO;IAAG,CAAC,YAAY,OAAO;IAAG,CAAC,YAAY,OAAO;IAAG,CAAC,WAAW,MAAM;IAAG,CAAC,cAAc,SAAS;GAAC,IACpH,CAAC;EACP,EAEC,QAAQ,GAAG,cAAc,WAAW,CAAC,EACrC,KAAK,CAAC,UAAU,IAAI;EACvB,IAAI,QAAQ,SAAS,GACnB,OAAO;GACL,QAAQ;GACR,SAAS,0CAA0C,QAAQ,KAAK,IAAI,EAAE;EACxE;EAIF,GAAG,WAAW,OAAO;EACrB,GAAG,UAAU,QAAQ,CAAC;EACtB,GAAG,UAAU,aAAa,UAAU;EACpC,GAAG,UAAU,QAAQ,aAAa;EAClC,GAAG,UAAU,aAAa,kBAAkB;EAC5C,OAAO;CACT;CAEA,SAAS,oBAAyC;EAChD,MAAM,UAAU,GAAG,cAAc;EACjC,IAAI,CAAC,SACH,OAAO;GACL,QAAQ;GACR,SAAS;EACX;EAEF,eAAe;EACf,GAAG,cAAc,GAAG,QAAQ;EAC5B,GAAG,YAAY,GAAG,YAAY,OAAO;EAGrC,GAAG,WACD,GAAG,YACH,GACA,GAAG,SACH,YACA,aACA,GACA,GAAG,cACH,GAAG,OACH,IACF;EACA,GAAG,cAAc,GAAG,YAAY,GAAG,oBAAoB,GAAG,OAAO;EACjE,GAAG,cAAc,GAAG,YAAY,GAAG,oBAAoB,GAAG,OAAO;EAEjE,GAAG,cAAc,GAAG,YAAY,GAAG,gBAAgB,GAAG,aAAa;EACnE,GAAG,cAAc,GAAG,YAAY,GAAG,gBAAgB,GAAG,aAAa;EACnE,OAAO;CACT;CAEA,SAAS,yBAA+B;EACtC,GAAG,WAAW,GAAG,cAAc,cAAc;EAC7C,GAAG,wBAAwB,SAAS;EACpC,GAAG,oBAAoB,WAAW,GAAG,GAAG,OAAO,OAAO,eAAe,CAAC;EACtE,GAAG,oBAAoB,WAAW,CAAC;EACnC,GAAG,wBAAwB,SAAS;EACpC,GAAG,oBAAoB,WAAW,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACvE,GAAG,oBAAoB,WAAW,CAAC;EACnC,GAAG,wBAAwB,SAAS;EACpC,GAAG,oBAAoB,WAAW,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACvE,GAAG,oBAAoB,WAAW,CAAC;EAGnC,GAAG,wBAAwB,SAAS;EACpC,GAAG,qBAAqB,WAAW,GAAG,GAAG,cAAc,eAAe,EAAE;EACxE,GAAG,oBAAoB,WAAW,CAAC;EACnC,IAAI,CAAC,qBAAqB;EAC1B,GAAG,wBAAwB,OAAO;EAClC,GAAG,oBAAoB,SAAS,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACrE,GAAG,oBAAoB,SAAS,CAAC;EACjC,GAAG,wBAAwB,OAAO;EAClC,GAAG,oBAAoB,SAAS,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACrE,GAAG,oBAAoB,SAAS,CAAC;EACjC,GAAG,wBAAwB,OAAO;EAClC,GAAG,oBAAoB,SAAS,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACrE,GAAG,oBAAoB,SAAS,CAAC;EACjC,GAAG,wBAAwB,MAAM;EACjC,GAAG,oBAAoB,QAAQ,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACpE,GAAG,oBAAoB,QAAQ,CAAC;EAChC,GAAG,wBAAwB,SAAS;EACpC,GAAG,oBAAoB,WAAW,GAAG,GAAG,OAAO,OAAO,eAAe,EAAE;EACvE,GAAG,oBAAoB,WAAW,CAAC;CACrC;CAEA,SAAS,gBAAqC;EAC5C,MAAM,aAAa,GAAG,kBAAkB;EACxC,MAAM,gBAAgB,GAAG,aAAa;EACtC,MAAM,kBAAkB,GAAG,aAAa;EACxC,IAAI,CAAC,cAAc,CAAC,iBAAiB,CAAC,iBACpC,OAAO;GACL,QAAQ;GACR,SACE;EACJ;EAEF,MAAM;EACN,eAAe;EACf,iBAAiB;EAEjB,GAAG,gBAAgB,UAAU;EAI7B,GAAG,WAAW,GAAG,cAAc,aAAa;EAC5C,GAAG,WACD,GAAG,cACH,IAAI,aAAa;GAAC;GAAI;GAAG;GAAI;GAAI;GAAG;GAAG;GAAG;EAAE,CAAC,GAC7C,GAAG,WACL;EACA,GAAG,wBAAwB,OAAO;EAClC,GAAG,oBAAoB,SAAS,GAAG,GAAG,OAAO,OAAO,GAAG,CAAC;EAExD,GAAG,WAAW,GAAG,cAAc,eAAe;EAC9C,GAAG,WAAW,GAAG,cAAc,aAAa,YAAY,GAAG,YAAY;EACvE,uBAAuB;EACvB,GAAG,gBAAgB,IAAI;EACvB,OAAO;CACT;CAEA,SAAS,iBAAsC;EAC7C,OAAO,aAAa,KAAK,kBAAkB,KAAK,cAAc;CAChE;;CAGA,SAAS,gBAAsB;EAC7B,UAAU;EACV,eAAe;EACf,MAAM;EACN,eAAe;EACf,iBAAiB;CACnB;CAEA,SAAS,kBAAwB;EAC/B,IAAI,KAAK,GAAG,kBAAkB,GAAG;EACjC,IAAI,cAAc,GAAG,aAAa,YAAY;EAC9C,IAAI,gBAAgB,GAAG,aAAa,cAAc;EAClD,IAAI,cAAc,GAAG,cAAc,YAAY;EAC/C,IAAI,SAAS,GAAG,cAAc,OAAO;EACrC,cAAc;CAChB;CAEA,MAAM,iBAAiB,eAAe;CACtC,IAAI,gBAAgB;EAClB,gBAAgB;EAChB,OAAO,OAAO,eAAe,QAAQ,eAAe,OAAO;CAC7D;CAMA,MAAM,QAA0B,CAAC;;CAEjC,MAAM,8BAAc,IAAI,IAAwB;CAChD,MAAM,SAAoC,CAAC;;CAE3C,MAAM,6BAAa,IAAI,QAA+B;;CAEtD,MAAM,QAAsB,CAAC;CAC7B,IAAI,SAAS;CACb,IAAI,eAAe;;CAEnB,MAAM,cAAsB;EAC1B,gBAAgB;EAChB,OAAO;CACT;;CAEA,IAAI,oBAAoB;CACxB,IAAI,YAAY;CAChB,IAAI,aAAa;CACjB,IAAI,aAAa;CACjB,IAAI,aAAa;CACjB,IAAI,aAAa;CACjB,IAAI,YAAY;CAChB,IAAI,cAAc;CAKlB,IAAI,cAAc,KAAK,IAAI,GAAG,QAAQ,WAAW;CACjD,IAAI,eAAe,KAAK,IAAI,GAAG,QAAQ,YAAY;CACnD,IAAI,mBAAmB,KAAK,IAC1B,GACA,QAAQ,oBAAoB,QAAQ,WACtC;CACA,IAAI,oBAAoB,KAAK,IAC3B,GACA,QAAQ,qBAAqB,QAAQ,YACvC;;;;;;;;;;;;;;;;;;;;;;;CAwBA,SAAS,iBAAiB,OAAyB;EACjD,IAAI,CAAC,MAAM,MAAM;EACjB,MAAM,OAAO;EACb,YAAY,OAAO,MAAM,GAAG;EAC5B,OAAO,MAAM,SAAS,OAAO,MAAM,OAAO;EAC1C,MAAM,QAAQ,MAAM,QAAQ,KAAK;EACjC,IAAI,SAAS,GAAG,MAAM,OAAO,OAAO,CAAC;EACrC,cAAc,MAAM;CACtB;CAEA,SAAS,SAAS,KAAa,QAAwB;EACrD,IAAI,SAAS,SAAS,gBAAgB,SAAS;EAC/C,MAAM,QAAQ;EACd,MAAM,MAAM,SAAS;EAIrB,IAAI,QAAQ;EACZ,IAAI,OAAO,MAAM;EACjB,OAAO,QAAQ,MAAM;GACnB,MAAM,SAAU,QAAQ,SAAU;GAClC,IAAI,MAAM,QAAS,SAAS,MAAM,QAAS,UAAU,OACnD,QAAQ,SAAS;QACd,OAAO;EACd;EACA,IAAI,QAAQ;EACZ,OAAO,QAAQ,MAAM,UAAU,MAAM,OAAQ,SAAS,KAAK,SAAS;EAGpE,KAAK,IAAI,IAAI,OAAO,IAAI,OAAO,KAAK,GAAG;GACrC,MAAM,SAAS,MAAM;GACrB,IAAI,OAAO,cAAc,YACvB,MAAM,IAAI,MACR,sBAAsB,eAAe,qDAAqD,IAAI,qBAAqB,OAAO,IAAI,4BAChI;EAEJ;EACA,KAAK,IAAI,IAAI,OAAO,IAAI,OAAO,KAAK,GAAG;GACrC,MAAM,SAAS,MAAM;GACrB,OAAO,OAAO;GACd,YAAY,OAAO,OAAO,GAAG;GAC7B,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO;GAC5C,cAAc,OAAO;GACrB,aAAa;EACf;EACA,IAAI,QAAQ,OAAO,MAAM,OAAO,OAAO,QAAQ,KAAK;EAEpD,SAAS;EACT,OAAO;CACT;;;;;;;;;;CAWA,SAAS,aAAa,QAAgB,QAA0B;EAS9D,MAAM,SACJ,OAAO,aAAa,MAAM,IACtB,IAAI,WACF,OAAO,QACP,OAAO,YACP,OAAO,aAAa,CACtB,IACA,IAAI,WAAW,OAAO,MAAM,EAAE,MAAM;EAC1C,GAAG,cAAc,GAAG,QAAQ;EAC5B,GAAG,YAAY,GAAG,YAAY,YAAY;EAM1C,MAAM,oBAAoB,OAAO,GAAG,aAAa,GAAG,gBAAgB,CAAC,KAAK;EAC1E,IAAI,sBAAsB,GAAG,GAAG,YAAY,GAAG,kBAAkB,CAAC;EAiBlE,MAAM,gBAAgB,QAAQ,GAAG,aAAa,GAAG,mBAAmB,CAAC;EACrE,MAAM,sBAAsB,QAC1B,GAAG,aAAa,GAAG,8BAA8B,CACnD;EACA,IAAI,eAAe,GAAG,YAAY,GAAG,qBAAqB,KAAK;EAC/D,IAAI,qBACF,GAAG,YAAY,GAAG,gCAAgC,KAAK;EAEzD,IAAI,YAAY,OAAO,aAAA;EACvB,IAAI,SAAS;EACb,IAAI,cAAc;EAClB,OAAO,YAAY,GAAG;GACpB,MAAM,IAAI,cAAc;GACxB,MAAM,IAAI,KAAK,MAAM,cAAc,UAAU;GAC7C,MAAM,MAAM,KAAK,IAAI,aAAa,GAAG,SAAS;GAC9C,GAAG,cACD,GAAG,YACH,GACA,GACA,GACA,KACA,GACA,GAAG,cACH,GAAG,OACH,QAGA,SAAS,CACX;GACA,UAAU;GACV,eAAe;GACf,aAAa;EACf;EACA,IAAI,sBAAsB,GACxB,GAAG,YAAY,GAAG,kBAAkB,iBAAiB;EAEvD,IAAI,eAAe,GAAG,YAAY,GAAG,qBAAqB,IAAI;EAC9D,IAAI,qBACF,GAAG,YAAY,GAAG,gCAAgC,IAAI;CAE1D;CAMA,IAAI,gBAAgB;CACpB,MAAM,QAAQ,IAAI,aAAa;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC,CAAC;CACjD,MAAM,QAAQ,IAAI,aAAa;EAAC;EAAG;EAAG;EAAG;CAAC,CAAC;;CAE3C,IAAI,WAAW;CACf,MAAM,MAAM,IAAI,aAAa,EAAE;CAC/B,IAAI,WAAW;CACf,IAAI,aAAa;CACjB,IAAI,cAAc;CAClB,IAAI,gBAAgB;;;;;;;;;;;CAWpB,MAAM,SAAS,IAAI,aAAa,CAAC;CAEjC,SAAS,oBAA0B;EACjC,OAAO,KAAK,IAAI;EAChB,OAAO,KAAK,KAAK;EACjB,OAAO,KAAK;EACZ,OAAO,KAAK;CACd;CACA,kBAAkB;;;;;;;;;;CAWlB,SAAS,gBAAsB;EAC7B,MAAM,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO;EAClB,IAAI,KAAK,CAAC;EACV,IAAI,KAAK,sBAAsB,KAAK,KAAK,MAAM;EAC/C,IAAI,KAAK,sBAAsB,IAAI,KAAK,MAAM;EAC9C,IAAI,KAAK,sBAAsB,IAAI,KAAK,MAAM;EAC9C,IAAI,KAAK,sBAAsB,KAAK,KAAK,MAAM;EAC/C,IAAI,MAAM;EACV,IAAI,MAAM,sBAAsB,OAAO,KAAK,KAAK,MAAM,KAAK,OAAO;EACnE,IAAI,MAAM,sBAAsB,OAAO,KAAK,KAAK,MAAM,KAAK,OAAO;EACnE,IAAI,MAAM;CACZ;CAEA,SAAS,gBAAsB;EAC7B,oBAAoB;EACpB,MAAM,OAAO,IAAI,YAAY,mBAAmB,aAAa;EAC7D,IAAI,WAAW,IAAI,EAAE,IAAI,IAAI,WAAW,YAAY,CAAC;EACrD,eAAe;EACf,iBAAiB,IAAI,aAAa,YAAY;EAC9C,gBAAgB,IAAI,YAAY,YAAY;EAC5C,GAAG,gBAAgB,GAAG;EACtB,GAAG,WAAW,GAAG,cAAc,cAAc;EAC7C,GAAG,WAAW,GAAG,cAAc,aAAa,YAAY,GAAG,YAAY;EAGvE,uBAAuB;EACvB,GAAG,gBAAgB,IAAI;CACzB;;;;;;;;;CAUA,SAAS,QAAQ,OAA8B;EAC7C,OAAO,MAAM;CACf;CAEA,OAAO;EACL;EACA;EAEA,IAAI,cAAc;GAChB,OAAO;EACT;EAEA,aAAa,MAAM,OAAO;GACxB,MAAM,OAAO,KAAK;GAKlB,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GACrC,OAAO,OACL,mBACA,SAAS,SAAS,MAAM,OAAO,iBAAiB,KAAK,0DACvD;GAEF,MAAM,OAAuB;IAC3B,IAAI,MAAM;IACV,OAAO,SAAS,OAAO,MAAM;IAC7B;IACA;GACF;GACA,MAAM,KAAK,IAAI;GACf,OAAO,qBAAK,IAAI,IAAI,CAAC;GACrB,OAAO;EACT;EAEA,OAAO,MAAM,SAAS,OAAO;GAC3B,IAAI,aAAa;IACf,OACE,gBACA,mBAAmB,QAAQ,8EAC7B;IACA,OAAO;GACT;GACA,MAAM,aAAa,MAAM,KAAK;GAC9B,IAAI,CAAC,cAAc,eAAe,MAChC,OAAO,OACL,qBACA,SAAS,KAAK,MAAM,QAAQ,KAAK,GAAG,kGACtC;GAEF,MAAM,MAAM,GAAG,KAAK,GAAG,GAAG;GAC1B,MAAM,WAAW,OAAO,KAAK,KAAK,IAAI,OAAO;GAC7C,IAAI,UAAU;IACZ,SAAS,SAAS,MAAM;IACxB,OAAO,QAAQ,QAAQ;GACzB;GAIA,IAAI,MAAM,OAAO,WAAW,GAAG,OAAO;GACtC,IAAI,MAAM,OAAO,SAAA,MAAgC,GAC/C,OAAO,OACL,kBACA,aAAa,IAAI,OAAO,MAAM,OAAO,OAAO,wGAC9C;GAGF,MAAM,SAAS,MAAM,OAAO,SAAA;GAC5B,IAAI,SAAS,gBAGX,OAAO,OACL,kBACA,UAAU,IAAI,UAAU,OAAO,iCAAiC,eAAe,mDACjF;GAEF,MAAM,SAAS,SAAS,KAAK,MAAM;GACnC,aAAa,QAAQ,MAAM,MAAM;GACjC,qBAAqB;GACrB,MAAM,QAAQ;IACZ;IACA,QAAQ,KAAK;IACb;IACA;IACA;IACA,YAAY;IACZ,MAAM;IACN;IACA,QAAQ,MAAM;IACd,WAAW;IACX,MAAM,KAAK;IAKX,MAAM,MAAM,QAAQ;IACpB,MAAM,MAAM,QAAQ,WAAW,MAAM,QAAQ;IAC7C,MAAM,MAAM,QAAQ,WAAW,MAAM,QAAQ;IAC7C,MAAM,MAAM,QAAQ;GACtB;GACA,MAAM,OAAkB;IACtB,QAAQ,KAAK;IACb;IACA;IACA,YAAY,MAAM;IAClB,KAAK,MAAM;IACX,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,QAAQ,MAAM;GAChB;GACA,OAAO,eAAe,MAAM,YAAY,EAAE,OAAO,IAAI,CAAC;GACtD,MAAM,QAAoB;IAAE,GAAG;IAAO;GAAK;GAC3C,WAAW,IAAI,MAAM,KAAK;GAC1B,YAAY,IAAI,KAAK,KAAK;GAC1B,OAAO,KAAK,IAAK,IAAI,SAAS,KAAK;GACnC,IAAI,YAAY;GAChB,OAAO,YAAY,MAAM,UAAU,MAAM,WAAY,SAAS,QAC5D,aAAa;GACf,MAAM,OAAO,WAAW,GAAG,KAAK;GAChC,cAAc;GACd,cAAc;GACd,aAAa,MAAM,OAAO;GAE1B,OAAO,QAAQ,KAAK;EACtB;EAEA,QAAQ,MAAM,SAAS;GAIrB,IAAI,aAAa,OAAO;GACxB,MAAM,aAAa,MAAM,KAAK;GAC9B,IAAI,CAAC,cAAc,eAAe,MAGhC,OAAO,OACL,qBACA,SAAS,KAAK,MAAM,QAAQ,KAAK,GAAG,sHACtC;GAEF,MAAM,QAAQ,OAAO,KAAK,KAAK,IAAI,OAAO;GAC1C,IAAI,CAAC,OAAO,OAAO;GAGnB,MAAM,SAAS,MAAM;GACrB,OAAO,QAAQ,KAAK;EACtB;EAEA,QAAQ;GACN,cAAc;GACd,gBAAgB;EAClB;EAEA,KAAK,MAAM,GAAG,GAAG,aAAa;GAC5B,IAAI,aAAa;GAIjB,MAAM,QAAQ,WAAW,IAAI,IAAI;GACjC,MAAM,QAAS,KAA+C;GAC9D,MAAM,QAAQ,QACV,QACA,UAAU,KAAA,KAAa,UAAU,MAC/B,KAAA,IACA,KAAK,WAAW,KAAA,KAAa,KAAK,YAAY,KAAA,IAC5C,OAAO,KAAK,SAAS,IAAI,KAAK,OAAO,IACrC,YAAY,IAAI,KAAK,GAAG;GAOhC,IACE,CAAC,SACD,MAAM,QAAQ,OACd,CAAC,MAAM,QACP,MAAM,eAAe,KAAK,cAC1B,MAAM,WAAW,KAAK,KACtB;IACA,cAAc;IACd;GACF;GACA,IAAI,iBAAiB,kBAAkB,cAAc;GACrD,MAAM,YAAY;GAClB,MAAM,SAAS,MAAM;GAKrB,MAAM,QAAQ,cAAc,KAAK;GACjC,MAAM,OAAO,gBAAgB;GAC7B,eAAe,QAAQ,IAAI,QAAQ,KAAK;GACxC,eAAe,OAAO,KAAK,IAAI,QAAQ,KAAK;GAC5C,eAAe,OAAO,KAAK,IAAI,QAAQ,KAAK;GAC5C,eAAe,OAAO,KAAK,IAAI,QAAQ,KAAK;GAC5C,eAAe,OAAO,KAAK,KAAK;GAChC,eAAe,OAAO,KAAK,KAAK;GAChC,eAAe,OAAO,KAAK,KAAK;GAChC,eAAe,OAAO,KAAK,KAAK;GAGhC,eAAe,OAAO,KAAK,IAAI;GAC/B,cAAc,OAAO,KAAK,KAAK,QAAQ;GACvC,IAAI,qBAAqB;IACvB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,eAAe,OAAO,KAAK,KAAK,MAAM;IACrE,eAAe,OAAO,MAAM,MAAM;IAClC,eAAe,OAAO,MAAM,MAAM;IAClC,eAAe,OAAO,MAAM,MAAM;IAClC,eAAe,OAAO,MAAM,MAAM;IAClC,eAAe,OAAO,MAAM;GAC9B;GACA,iBAAiB;EACnB;EAEA,SAAS,MAAM;GACb,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,IAAI,CAAC,uBAAuB,MAAM,OAAO,KAAK,IAAI,WAAW;IAC7D,MAAM,KAAK,KAAK;GAClB;EACF;EAEA,SAAS,GAAG,GAAG,GAAG,GAAG;GACnB,IAAI,CAAC,qBACH,eAAe,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK,MAAM,OAAO;GAEpF,MAAM,KAAK;GACX,MAAM,KAAK;GACX,MAAM,KAAK;GACX,MAAM,KAAK;EACb;EAEA,UAAU,IAAI;GAIZ,MAAM,OAAO,OAAO,SAAS,EAAE,KAAK,KAAK,IAAI,KAAK;GAClD,IAAI,CAAC,uBAAuB,aAAa,MAAM,cAAc;GAC7D,WAAW;EACb;EAEA,YAAY,OAAO,QAAQ,aAAa,cAAc;GACpD,MAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK;GACzC,MAAM,mBAAmB,KAAK,IAAI,GAAG,MAAM;GAC3C,MAAM,uBAAuB,KAAK,IAAI,GAAG,eAAe,KAAK;GAC7D,MAAM,wBAAwB,KAAK,IAAI,GAAG,gBAAgB,MAAM;GAChE,IACE,gBAAgB,mBAChB,iBAAiB,kBACjB;IACA,cAAc;IACd,eAAe;IACf,kBAAkB;IAClB,WAAW;GACb;GACA,IACE,qBAAqB,wBACrB,sBAAsB,uBACtB;IACA,mBAAmB;IACnB,oBAAoB;IACpB,gBAAgB;GAClB;EACF;EAEA,MAAM;GACJ,IAAI,eAAe,kBAAkB,GACnC,OAAO;IAAE,WAAW;IAAG,WAAW;GAAE;GAEtC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC,cAC1C,OAAO;IAAE,WAAW;IAAG,WAAW;GAAE;GAGtC,GAAG,WAAW,OAAO;GACrB,GAAG,gBAAgB,GAAG;GACtB,GAAG,WAAW,GAAG,cAAc,cAAc;GAC7C,GAAG,cACD,GAAG,cACH,GACA,gBACA,GACA,gBAAgB,uBAClB;GAEA,GAAG,cAAc,GAAG,QAAQ;GAC5B,GAAG,YAAY,GAAG,YAAY,YAAY;GAC1C,IAAI,UAAU;IACZ,cAAc;IACd,GAAG,iBACD,sBAAsB,kBAAkB,oBACxC,OACA,GACF;IACA,WAAW;GACb;GACA,IAAI,CAAC,uBAAuB,YAAY;IACtC,GAAG,WAAW,QAAQ,KAAK;IAC3B,aAAa;GACf;GACA,IAAI,CAAC,uBAAuB,aAAa;IACvC,GAAG,UAAU,WAAW,QAAQ;IAChC,cAAc;GAChB;GAIA,IAAI,eAAe;IACjB,GAAG,UAAU,WAAW,kBAAkB,iBAAiB;IAC3D,gBAAgB;GAClB;GAGA,GAAG,OAAO,GAAG,KAAK;GAClB,GAAG,cAAc,GAAG,QAAQ;GAC5B,GAAG,UAAU,GAAG,KAAK,GAAG,mBAAmB;GAE3C,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,GAAG,aAAa;GAC7D,GAAG,gBAAgB,IAAI;GACvB,OAAO;IAAE,WAAW;IAAe,WAAW;GAAE;EAClD;EAEA,oBAAoB;GAClB,cAAc;GACd,gBAAgB;GAIhB,cAAc;EAChB;EAEA,UAAU;GACR,MAAM,UAAU,eAAe;GAC/B,IAAI,SAAS;IACX,gBAAgB;IAChB,OACE,QAAQ,QACR,uCAAuC,QAAQ,SACjD;IACA,OAAO;GACT;GAGA,WAAW;GACX,aAAa;GACb,cAAc;GACd,gBAAgB;GAIhB,MAAM,WAAW,CAAC,GAAG,KAAK;GAC1B,MAAM,UAAoB,CAAC;GAC3B,KAAK,MAAM,SAAS,UAAU;IAC5B,MAAM,OAAO,MAAM,MAAM;IACzB,MAAM,QAAQ,OAAO,KAAK,KAAK,OAAO,MAAM,OAAO,IAAI;IACvD,IACE,CAAC,SACD,MAAM,OAAO,WAAW,MAAM,SAAA,GAC9B;KAKA,iBAAiB,KAAK;KACtB,QAAQ,KAAK,MAAM,GAAG;KACtB;IACF;IACA,aAAa,MAAM,QAAQ,MAAM,MAAM;GACzC;GACA,cAAc;GACd,IAAI,QAAQ,SAAS,GACnB,OACE,sBACA,GAAG,QAAQ,OAAO,MAAM,SAAS,OAAO,iEAAiE,QAAQ,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,IAAI,QAAQ,SAAS,IAAI,QAAQ,GAAG,iGAC5K;GAEF,OAAO;EACT;EAEA,IAAI,QAAoB;GACtB,OAAO;IACL,WAAW,aAAA;IACX,kBAAkB,aAAa,cAAA;IAC/B,SAAS,YAAY;IACrB;IACA;IACA;IACA,OAAO,MAAM;IACb;GACF;EACF;EAEA,IAAI,QAAmB;GACrB,OAAO;IACL,QAAQ;IACR,YAAY;IACZ,eAAe,aAAa,IAAI,YAAY,aAAa;GAC3D;EACF;EAEA,UAAU;GAGR,IAAI,CAAC,aAAa,gBAAgB;QAC7B,cAAc;GACnB,MAAM,SAAS;GACf,YAAY,MAAM;GAClB,OAAO,SAAS;GAChB,MAAM,SAAS;EACjB;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@godot-scene-web/hb-gpu",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "HarfBuzz Slug glyph-outline renderer for borrowed WebGL contexts.",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/tfoxy/godot-scene-web.git"
14
+ },
15
+ "homepage": "https://github.com/tfoxy/godot-scene-web#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/tfoxy/godot-scene-web/issues"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "development": "./src/index.ts",
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./webgl": {
26
+ "development": "./src/webgl.ts",
27
+ "types": "./dist/webgl.d.ts",
28
+ "import": "./dist/webgl.js"
29
+ },
30
+ "./vendor/hb-gpu.mjs": {
31
+ "development": "./vendor/hb-gpu.mjs",
32
+ "types": "./vendor/hb-gpu.d.mts",
33
+ "default": "./vendor/hb-gpu.mjs"
34
+ },
35
+ "./vendor/hb-gpu.wasm": {
36
+ "development": "./vendor/hb-gpu.wasm",
37
+ "default": "./vendor/hb-gpu.wasm"
38
+ },
39
+ "./vendor/*": "./vendor/*"
40
+ },
41
+ "main": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "files": [
44
+ "dist",
45
+ "vendor",
46
+ "LICENSE"
47
+ ],
48
+ "devDependencies": {
49
+ "@playwright/test": "^1.59.1",
50
+ "esbuild": "^0.28.0",
51
+ "harfbuzzjs": "1.6.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "build:wasm": "./build.sh",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit",
57
+ "test": "cd ../.. && vitest run --config vitest.config.ts packages/hb-gpu/test",
58
+ "test:glyph-pixel": "cd ../.. && GSW_HB_GPU_PIXEL=1 xvfb-run -a vitest run packages/hb-gpu/test/glyphPixelXvfb.test.ts"
59
+ }
60
+ }
@@ -0,0 +1,102 @@
1
+ Emscripten is available under 2 licenses, the MIT license and the
2
+ University of Illinois/NCSA Open Source License.
3
+
4
+ Both are permissive open source licenses, with little if any
5
+ practical difference between them.
6
+
7
+ The reason for offering both is that (1) the MIT license is
8
+ well-known, while (2) the University of Illinois/NCSA Open Source
9
+ License allows Emscripten's code to be integrated upstream into
10
+ LLVM, which uses that license, should the opportunity arise.
11
+
12
+ The full text of both licenses follows.
13
+
14
+ ==============================================================================
15
+
16
+ Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in
26
+ all copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
+ THE SOFTWARE.
35
+
36
+ ==============================================================================
37
+
38
+ Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
39
+ All rights reserved.
40
+
41
+ Permission is hereby granted, free of charge, to any person obtaining a
42
+ copy of this software and associated documentation files (the
43
+ "Software"), to deal with the Software without restriction, including
44
+ without limitation the rights to use, copy, modify, merge, publish,
45
+ distribute, sublicense, and/or sell copies of the Software, and to
46
+ permit persons to whom the Software is furnished to do so, subject to
47
+ the following conditions:
48
+
49
+ Redistributions of source code must retain the above copyright
50
+ notice, this list of conditions and the following disclaimers.
51
+
52
+ Redistributions in binary form must reproduce the above
53
+ copyright notice, this list of conditions and the following disclaimers
54
+ in the documentation and/or other materials provided with the
55
+ distribution.
56
+
57
+ Neither the names of Mozilla,
58
+ nor the names of its contributors may be used to endorse
59
+ or promote products derived from this Software without specific prior
60
+ written permission.
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
63
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
65
+ IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR
66
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
68
+ SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
69
+
70
+ ==============================================================================
71
+
72
+ This program uses portions of Node.js source code located in src/library_path.js,
73
+ in accordance with the terms of the MIT license. Node's license follows:
74
+
75
+ """
76
+ Copyright Joyent, Inc. and other Node contributors. All rights reserved.
77
+ Permission is hereby granted, free of charge, to any person obtaining a copy
78
+ of this software and associated documentation files (the "Software"), to
79
+ deal in the Software without restriction, including without limitation the
80
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
81
+ sell copies of the Software, and to permit persons to whom the Software is
82
+ furnished to do so, subject to the following conditions:
83
+
84
+ The above copyright notice and this permission notice shall be included in
85
+ all copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
88
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
89
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
90
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
91
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
93
+ IN THE SOFTWARE.
94
+ """
95
+
96
+ The musl libc project is bundled in this repo, and it has the MIT license, see
97
+ system/lib/libc/musl/COPYRIGHT
98
+
99
+ The third_party/ subdirectory contains code with other licenses. None of it is
100
+ used by default, but certain options use it (e.g., the optional closure compiler
101
+ flag will run closure compiler from third_party/).
102
+
@@ -0,0 +1,42 @@
1
+ HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
2
+ For parts of HarfBuzz that are licensed under different licenses see individual
3
+ files names COPYING in subdirectories where applicable.
4
+
5
+ Copyright © 2010-2022 Google, Inc.
6
+ Copyright © 2015-2020 Ebrahim Byagowi
7
+ Copyright © 2019,2020 Facebook, Inc.
8
+ Copyright © 2012,2015 Mozilla Foundation
9
+ Copyright © 2011 Codethink Limited
10
+ Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
11
+ Copyright © 2009 Keith Stribley
12
+ Copyright © 2011 Martin Hosken and SIL International
13
+ Copyright © 2007 Chris Wilson
14
+ Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
15
+ Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
16
+ Copyright © 1998-2005 David Turner and Werner Lemberg
17
+ Copyright © 2016 Igalia S.L.
18
+ Copyright © 2022 Matthias Clasen
19
+ Copyright © 2018,2021 Khaled Hosny
20
+ Copyright © 2018,2019,2020 Adobe, Inc
21
+ Copyright © 2013-2015 Alexei Podtelezhnikov
22
+
23
+ For full copyright notices consult the individual files in the package.
24
+
25
+
26
+ Permission is hereby granted, without written agreement and without
27
+ license or royalty fees, to use, copy, modify, and distribute this
28
+ software and its documentation for any purpose, provided that the
29
+ above copyright notice and the following two paragraphs appear in
30
+ all copies of this software.
31
+
32
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
33
+ DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
34
+ ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
35
+ IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
36
+ DAMAGE.
37
+
38
+ THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
39
+ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
40
+ FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
41
+ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
42
+ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.