@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +243 -0
- package/dist/index.js.map +1 -0
- package/dist/webgl.d.ts +523 -0
- package/dist/webgl.d.ts.map +1 -0
- package/dist/webgl.js +1624 -0
- package/dist/webgl.js.map +1 -0
- package/package.json +60 -0
- package/vendor/LICENSE-emscripten +102 -0
- package/vendor/LICENSE-harfbuzz +42 -0
- package/vendor/VENDOR.md +115 -0
- package/vendor/hb-gpu.d.mts +29 -0
- package/vendor/hb-gpu.mjs +2 -0
- package/vendor/hb-gpu.wasm +0 -0
package/dist/webgl.d.ts
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { EncodedGlyph, HbGpu, HbGpuFailure, HbGpuFont } from "./index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/webgl.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Upstream's `ATLAS_TEX_WIDTH`, and the width this renderer PREFERS rather than the one it gets.
|
|
6
|
+
*
|
|
7
|
+
* Wide and short keeps the unwrap arithmetic in `int` range. WebGL2 only guarantees
|
|
8
|
+
* `MAX_TEXTURE_SIZE >= 2048`, so a device can refuse 4096 — read {@link HbGpuRenderer.atlasWidth}
|
|
9
|
+
* for the width an instance actually built, and never assume this constant describes it. The
|
|
10
|
+
* shader is handed the achieved width as `hb_gpu_atlas_width`, so any width is legal.
|
|
11
|
+
*/
|
|
12
|
+
declare const ATLAS_WIDTH = 4096;
|
|
13
|
+
/**
|
|
14
|
+
* THE DILATION'S WHOLE TAP BUDGET, as one number, and the reason it is one number.
|
|
15
|
+
*
|
|
16
|
+
* It bounds the flat loop in {@link FRAGMENT_MAIN} — one tap per iteration — so the compile-time
|
|
17
|
+
* ceiling IS the worst-case tap count: 64 unrolled sites, plus `hb_gpu_draw`'s own centre tap, is 65
|
|
18
|
+
* evaluations for the most expensive fragment there is. That was also the ceiling of the nested
|
|
19
|
+
* `4 rings x 16 steps` loops this replaced, so the redistribution below is free at the top end.
|
|
20
|
+
*
|
|
21
|
+
* WHY THE SHAPE CHANGED RATHER THAN THE NUMBER. The nested form clamped EVERY ring to the same 16
|
|
22
|
+
* steps, so past `radius * t > 2.5 px` all four rings ran 16 taps — and the outermost one, which is
|
|
23
|
+
* the only ring that decides where the dilated boundary lands, was then the SPARSEST: at radius 12
|
|
24
|
+
* its taps sit 4.71 px of arc apart, while ring 1 spends the same 16 on a circle a quarter the size
|
|
25
|
+
* and puts them 1.18 px apart. Giving
|
|
26
|
+
* the outer ring more steps in that shape would have meant raising the per-ring cap, and 4 x 28 is
|
|
27
|
+
* 112 unrolled sites for a budget most fragments never spend. A flat loop makes "how many taps may a
|
|
28
|
+
* fragment cost" and "how are they arranged" two independent decisions, and only the first one is a
|
|
29
|
+
* perf number.
|
|
30
|
+
*
|
|
31
|
+
* INTERPOLATED INTO THE GLSL rather than restated there, so `spreadBudget.test.ts` can assert on the
|
|
32
|
+
* arithmetic that divides it between rings and on the ceiling reaching the shader, from one source.
|
|
33
|
+
*/
|
|
34
|
+
declare const HB_GPU_SPREAD_MAX_TAPS = 64;
|
|
35
|
+
declare 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 = 64;\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 \u2014 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 \u2014 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 * \u4E2D 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 draw the run N times at N offsets \u2014 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 \u2014 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 \u2014 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 \u2014 fattening for dark ink, thinning for light \u2014 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 \u2014 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 \u2014 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 \u2014 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";
|
|
36
|
+
/**
|
|
37
|
+
* A face registered with the renderer: the namespace every one of its glyph keys carries.
|
|
38
|
+
*
|
|
39
|
+
* THE ATLAS KEY IS `(face, glyph)` AND NOT `glyph`, AND THAT IS THE WHOLE POINT OF THIS TYPE.
|
|
40
|
+
* Glyph 42 of Noto Sans SC and glyph 42 of Roboto are unrelated outlines; an atlas keyed on the
|
|
41
|
+
* glyph id alone hands the second one the first one's texels and renders fluent, crisp, WRONG
|
|
42
|
+
* text — the one failure mode nothing downstream measures. That namespacing used to live in the
|
|
43
|
+
* perf-harness's key strings, one layer above a renderer that could not tell two faces apart.
|
|
44
|
+
*
|
|
45
|
+
* Registering also fixes the `upem`, so {@link HbGpuRenderer.upload} no longer takes one and a
|
|
46
|
+
* glyph cannot be uploaded under one face's scale and drawn at another's.
|
|
47
|
+
*/
|
|
48
|
+
interface HbGpuFace {
|
|
49
|
+
/** Dense index assigned at registration. Namespaces every key of this face. */
|
|
50
|
+
readonly id: number;
|
|
51
|
+
/** Whatever the embedder called it. Appears in failure messages, nowhere else. */
|
|
52
|
+
readonly label: string;
|
|
53
|
+
/** Units per em, taken from the registered font and never from a caller's argument. */
|
|
54
|
+
readonly upem: number;
|
|
55
|
+
}
|
|
56
|
+
/** Where one encoded glyph landed, and the box to draw it in. */
|
|
57
|
+
interface GlyphSlot {
|
|
58
|
+
/**
|
|
59
|
+
* Numeric identity of the resident glyph. Optional so structural slots made by older callers
|
|
60
|
+
* remain accepted by the cold compatibility path in {@link HbGpuRenderer.push}.
|
|
61
|
+
*/
|
|
62
|
+
faceId?: number;
|
|
63
|
+
glyphId?: number;
|
|
64
|
+
/**
|
|
65
|
+
* The allocator key — `<face id>/<glyph id>`, so a draw can find its allocation without a
|
|
66
|
+
* reverse lookup. Internal shape: read it for debugging, never construct one.
|
|
67
|
+
*/
|
|
68
|
+
key: string;
|
|
69
|
+
/**
|
|
70
|
+
* Which ALLOCATION this slot describes, stamped by the allocator and never reused.
|
|
71
|
+
*
|
|
72
|
+
* THE ANSWER TO THE WORST BUG THIS PACKAGE HAD. The ring evicts, so a slot handed out an hour
|
|
73
|
+
* ago can name texels that some other glyph now owns. `push` used to draw it anyway, at the
|
|
74
|
+
* right size, in the right place, perfectly antialiased — a different glyph's outline, invisible
|
|
75
|
+
* to every check downstream. `push` now compares this against the live allocation and skips on a
|
|
76
|
+
* mismatch (see {@link AtlasStats.staleSkips}); a plain key comparison could not, because the
|
|
77
|
+
* key of a re-uploaded glyph is the same key.
|
|
78
|
+
*/
|
|
79
|
+
generation: number;
|
|
80
|
+
/** Atlas texel index of the blob's first texel — the shader's `glyphLoc`. */
|
|
81
|
+
loc: number;
|
|
82
|
+
/** Units per em of the face these coordinates are in. */
|
|
83
|
+
upem: number;
|
|
84
|
+
/** Em-space ink box, y-UP, as `hb_gpu_draw_encode` reported it. */
|
|
85
|
+
minX: number;
|
|
86
|
+
minY: number;
|
|
87
|
+
maxX: number;
|
|
88
|
+
maxY: number;
|
|
89
|
+
/** Texels this blob occupies. */
|
|
90
|
+
texels: number;
|
|
91
|
+
}
|
|
92
|
+
interface AtlasStats {
|
|
93
|
+
/**
|
|
94
|
+
* What the live allocations actually occupy: texels x 8.
|
|
95
|
+
*
|
|
96
|
+
* The honest "how much glyph data is resident" number, and the one to compare against a baked
|
|
97
|
+
* atlas's occupied bytes.
|
|
98
|
+
*/
|
|
99
|
+
liveBytes: number;
|
|
100
|
+
/**
|
|
101
|
+
* `width x height x 8` — the whole texture.
|
|
102
|
+
*
|
|
103
|
+
* THE NUMBER THAT SITS NEXT TO `hb-atlas`'s 1.45 MiB. A driver allocates the texture, not the
|
|
104
|
+
* used part of it, so this is what is actually held whatever the occupancy says. Reported
|
|
105
|
+
* separately from `liveBytes` because quoting only the smaller one is how a renderer appears to
|
|
106
|
+
* cost less than it does.
|
|
107
|
+
*/
|
|
108
|
+
reservationBytes: number;
|
|
109
|
+
/** Live allocations, and the texels they hold. */
|
|
110
|
+
entries: number;
|
|
111
|
+
liveTexels: number;
|
|
112
|
+
capacityTexels: number;
|
|
113
|
+
/** Allocations overwritten by a wrapped cursor since creation. */
|
|
114
|
+
evictions: number;
|
|
115
|
+
/** Faces registered. Keys are namespaced by these — see {@link HbGpuFace}. */
|
|
116
|
+
faces: number;
|
|
117
|
+
/**
|
|
118
|
+
* Glyphs `push` declined to draw because their slot no longer matches a live allocation.
|
|
119
|
+
*
|
|
120
|
+
* NON-ZERO IS A REAL FINDING, not noise: it means the embedder is holding slots across an
|
|
121
|
+
* eviction (its atlas is too small for its working set, or it caches slots it should re-`upload`)
|
|
122
|
+
* and the frame is short of glyphs. Counted rather than thrown because `push` is the hot loop.
|
|
123
|
+
*/
|
|
124
|
+
staleSkips: number;
|
|
125
|
+
}
|
|
126
|
+
interface BlobStats {
|
|
127
|
+
/** Distinct glyphs uploaded. */
|
|
128
|
+
glyphs: number;
|
|
129
|
+
/** Sum of every uploaded blob's length, in bytes. */
|
|
130
|
+
totalBytes: number;
|
|
131
|
+
/**
|
|
132
|
+
* THE PREDICTION UNDER TEST. `docs/text-rendering.md` records "~5.4 KB per Han glyph against
|
|
133
|
+
* ~1.4 KB for a 38x38 R8 atlas cell". This is the left-hand side of it, self-counted.
|
|
134
|
+
*/
|
|
135
|
+
bytesPerGlyph: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The contrast correction the fragment stage applies to the coverage it computed.
|
|
139
|
+
*
|
|
140
|
+
* THE TWO FIELDS REACH DIFFERENT PASSES, which is the one thing to read before setting either:
|
|
141
|
+
* `gamma` is applied to every draw, `stemDarkening` only to an undilated one. The reason is under
|
|
142
|
+
* {@link HbGpuContrast.stemDarkening}.
|
|
143
|
+
*
|
|
144
|
+
* WHY IT EXISTS, AND WHY THE DEFAULT IS ON. Raw analytic coverage is the AREA of the pixel the
|
|
145
|
+
* outline covers, and compositing it linearly is not what a browser does: measured on one fixed
|
|
146
|
+
* crop of the word "Breakthrough" at 1600x900 / DPR 1.25, the hb-gpu path and the DOM path agree on
|
|
147
|
+
* peak darkness (51 vs 50), on mean luminance (120.5 vs 121.0) and on total ink (2736 vs 2744) —
|
|
148
|
+
* and DOM still puts **66% more pixels** in the deep-dark end (1285 below luma 80 against 775).
|
|
149
|
+
* That gap is entirely in the middle of the ramp: a sub-pixel stem lands mid-grey here and near the
|
|
150
|
+
* ink colour there, which reads as washed out at exactly the sizes a UI uses. Every shipping
|
|
151
|
+
* consumer wants the correction; the exception is a harness.
|
|
152
|
+
*
|
|
153
|
+
* BOTH FIELDS ARE REQUIRED rather than optional, which is deliberate. A half-specified
|
|
154
|
+
* `{ gamma: 1.2 }` would silently inherit a stem-darkening default the author never considered, and
|
|
155
|
+
* this is a knob whose whole purpose is that somebody thought about it. Use
|
|
156
|
+
* {@link HB_GPU_CONTRAST_DEFAULT} / {@link HB_GPU_CONTRAST_NONE} rather than writing the pair out.
|
|
157
|
+
*/
|
|
158
|
+
interface HbGpuContrast {
|
|
159
|
+
/**
|
|
160
|
+
* Exponent applied to the coverage. `1` is the identity and is the default.
|
|
161
|
+
*
|
|
162
|
+
* BELOW 1 IS DARKER (a coverage of 0.5 moves toward 1), above 1 lighter. It is polarity-BLIND —
|
|
163
|
+
* unlike `stemDarkening`, which reads the foreground — so a value that helps dark-on-light text
|
|
164
|
+
* hurts light-on-dark by the same amount. HarfBuzz's own demo flips it by theme
|
|
165
|
+
* (`demo-view.cc`: `dark_mode ? 1/2.2 : 2.2`) for that reason, and a renderer here draws both
|
|
166
|
+
* polarities in one frame and cannot. Hence 1: the size- and polarity-aware half of the
|
|
167
|
+
* correction is `stemDarkening`, and this is the manual override next to it.
|
|
168
|
+
*
|
|
169
|
+
* Non-finite or non-positive values are refused and reported (`"degenerate-contrast"`), because
|
|
170
|
+
* `pow` with such an exponent is a NaN alpha over the whole quad rather than a wrong picture.
|
|
171
|
+
*/
|
|
172
|
+
gamma: number;
|
|
173
|
+
/**
|
|
174
|
+
* Run `hb_gpu_stem_darken` on the coverage OF A FILL. Default `true`.
|
|
175
|
+
*
|
|
176
|
+
* IT IS THE SIZE-AWARE AND POLARITY-AWARE HALF. The library's exponent is
|
|
177
|
+
* `mix (pow (2, brightness - 0.5), 1, smoothstep (8, 48, ppem))`, so it fattens dark text
|
|
178
|
+
* (brightness 0 -> exponent 0.707), thins light text (brightness 1 -> 1.414) and RAMPS ITSELF OFF
|
|
179
|
+
* by ppem 48, where a stem is wide enough that no pixel of it is partially covered anyway. The
|
|
180
|
+
* brightness comes from the per-instance colour and the ppem from `hb_gpu_ppem`, so it costs no
|
|
181
|
+
* per-frame decision.
|
|
182
|
+
*
|
|
183
|
+
* IT DOES NOT APPLY TO A DILATED RUN, whatever this flag says: a draw with a non-zero
|
|
184
|
+
* {@link HbGpuRenderer.setSpread} emits raw coverage. The correction exists because a sub-pixel
|
|
185
|
+
* STEM sits at mid-grey under linear coverage where a browser puts it near the ink colour, and a
|
|
186
|
+
* dilated fragment's coverage is not that number — the taps are an inside test, so the boundary
|
|
187
|
+
* is nearly binary before any curve touches it. More decisively, Godot's outline is a plain
|
|
188
|
+
* FreeType raster with no curve of its own, so an exponent on a dilated rim reads as a HALO
|
|
189
|
+
* against it: fattening for dark ink, thinning for light. Measured against the committed Godot
|
|
190
|
+
* golden, it took a 14 px outline's rim from 0.662 to 1.965 px of equivalent ramp where Godot's
|
|
191
|
+
* is 0.851 (`packages/hb-gpu/test/goldens/godot-outline-metrics.json`, `docs/text-rendering.md`).
|
|
192
|
+
*
|
|
193
|
+
* THE EDGE THAT LEAVES: a consumer drawing a spread run as the ONLY ink — an outline with no fill
|
|
194
|
+
* composited over it — now gets uncorrected coverage for that text and no way to ask for
|
|
195
|
+
* otherwise. That is the intended picture for a stroke and the wrong one for a glyph body, so a
|
|
196
|
+
* caller in that position should draw the fill it is standing in for. `gamma` is NOT gated and
|
|
197
|
+
* remains available for a deliberate transfer curve over both passes.
|
|
198
|
+
*/
|
|
199
|
+
stemDarkening: boolean;
|
|
200
|
+
}
|
|
201
|
+
/** Stem darkening on, gamma neutral. What a renderer built without a `contrast` option gets. */
|
|
202
|
+
declare const HB_GPU_CONTRAST_DEFAULT: HbGpuContrast;
|
|
203
|
+
/**
|
|
204
|
+
* No contrast curve at all: the fragment writes the coverage it computed.
|
|
205
|
+
*
|
|
206
|
+
* FOR MEASUREMENT ARMS, and they should say so where they pass it. A fidelity probe that grades an
|
|
207
|
+
* arm against an 8x area-coverage reference is grading the RASTERIZER, and an arm carrying a
|
|
208
|
+
* contrast curve scores the curve instead — `docs/text-rendering.md`'s distortion figures (0.196
|
|
209
|
+
* Han at ppem 14, 0.017 at ppem 49) only mean what they say against raw coverage.
|
|
210
|
+
*/
|
|
211
|
+
declare const HB_GPU_CONTRAST_NONE: HbGpuContrast;
|
|
212
|
+
interface HbGpuRendererOptions {
|
|
213
|
+
/**
|
|
214
|
+
* The context to draw in. NOT created here and never destroyed here.
|
|
215
|
+
*
|
|
216
|
+
* It must have been created with `premultipliedAlpha: true` — see this file's header. Nothing
|
|
217
|
+
* can check that from inside (`getContextAttributes` reports what was ASKED for, and the failure
|
|
218
|
+
* is a picture that is merely darker), so it is stated rather than validated.
|
|
219
|
+
*/
|
|
220
|
+
gl: WebGL2RenderingContext;
|
|
221
|
+
/**
|
|
222
|
+
* The extent of OBJECT SPACE — the units {@link HbGpuRenderer.push} takes, y measured DOWN.
|
|
223
|
+
*
|
|
224
|
+
* This pair builds the design->clip projection and nothing else. On a standalone canvas it is
|
|
225
|
+
* the device-pixel size and equals the framebuffer pair below; on a DPR-scaled stage it is the
|
|
226
|
+
* scene's own coordinate extent (`packages/canvas/src/present.ts`'s `designWidth`), and the two
|
|
227
|
+
* pairs differ by the ratio.
|
|
228
|
+
*/
|
|
229
|
+
designWidth: number;
|
|
230
|
+
designHeight: number;
|
|
231
|
+
/**
|
|
232
|
+
* The ACHIEVED drawing-buffer size, in DEVICE pixels. Defaults to the design pair.
|
|
233
|
+
*
|
|
234
|
+
* NOT THE SAME NUMBER AS THE DESIGN PAIR, and the default is only correct for a stage whose
|
|
235
|
+
* device-pixel ratio is 1. This pair is `u_viewport`, which is what `hb_gpu_dilate` measures half
|
|
236
|
+
* a SCREEN pixel against — see this file's header for what feeding it design units does.
|
|
237
|
+
*
|
|
238
|
+
* PASS `gl.drawingBufferWidth`, not the size you asked the canvas for. Setting `canvas.width`
|
|
239
|
+
* only REQUESTS an allocation and an implementation may hand back less, and a viewport that is
|
|
240
|
+
* wrong by a few pixels is a dilation that is wrong by a fraction of one — a rim of clipped
|
|
241
|
+
* antialiasing around every glyph rather than anything that looks like a size error.
|
|
242
|
+
*/
|
|
243
|
+
framebufferWidth?: number;
|
|
244
|
+
framebufferHeight?: number;
|
|
245
|
+
/**
|
|
246
|
+
* Atlas capacity in TEXELS. Rounded up to a whole number of {@link HbGpuRenderer.atlasWidth}-wide
|
|
247
|
+
* rows.
|
|
248
|
+
*
|
|
249
|
+
* Default 256 rows = 1 Mi texels = 8 MiB, which holds ~1500 Han outlines at the ~5.4 KB the
|
|
250
|
+
* prediction expects. Sized in texels rather than bytes because that is the unit the shader
|
|
251
|
+
* indexes in and the unit the allocator wraps in.
|
|
252
|
+
*/
|
|
253
|
+
atlasTexels?: number;
|
|
254
|
+
/**
|
|
255
|
+
* Give each DILATION TAP the same five-sample average `_hb_gpu_slug` gives a fill below ppem 16.
|
|
256
|
+
*
|
|
257
|
+
* **Default `false`, and that default is a measured trade rather than an oversight.**
|
|
258
|
+
*
|
|
259
|
+
* WHAT IT COSTS TO LEAVE ON. `hb_gpu_spread_tap` mirrors `_hb_gpu_slug`, so below ppem 16 one ring
|
|
260
|
+
* tap becomes FIVE `_hb_gpu_slug_single` evaluations — and a dilated fragment takes up to 65 taps.
|
|
261
|
+
* Measured on S9's `text-render` scenario (RTX 2060, 1280x800 at DPR 1, 40 outlined runs at 14 px,
|
|
262
|
+
* `outlinePx` 6): **12.27 Hz with it on, 70.47 Hz with it off**, 5.74x, and 8.05x at `outlinePx`
|
|
263
|
+
* 10. The font-size ladder isolates it — the same 3 px radius and the same tap set (47 steps when
|
|
264
|
+
* that was measured, 50 under the budget split that replaced the per-ring clamp) reads 17.44 Hz at
|
|
265
|
+
* `fontSize` 18 and 74.76 at 20, because 20 is where the ppem the shader computes crosses 16 and
|
|
266
|
+
* the branch stops firing. It is one branch, not the tap count: capping the rings and steps at
|
|
267
|
+
* 3x12 bought only 1.5x, and 2x8 bought 2.3x by punching holes through thin features.
|
|
268
|
+
*
|
|
269
|
+
* WHY IT IS DEFENSIBLE TO LEAVE OFF, which is a claim about pixels and is pinned by
|
|
270
|
+
* `test/glyphPixelXvfb.test.ts`'s low-ppem pair. The outline is a solid silhouette that a fill is
|
|
271
|
+
* then drawn on top of, so the only thing the taps decide is its OUTER rim; a max over ~47 taps is
|
|
272
|
+
* itself a smoothing operator; and the rim of a thick outline is the least legible place in a
|
|
273
|
+
* glyph. Measured at 14 px, rotated 10 degrees, spread 3, both programs on one GPU in one frame:
|
|
274
|
+
* the two differ on **267 pixels, RMS 33.4 levels of 255, worst 85** — and the one WITHOUT the
|
|
275
|
+
* MSAA is the one closer to an 8x dilated reference (rim RMS 77.9 against 85.1, ink 36102 against
|
|
276
|
+
* 30192 where the ideal grown shape is 58081). The extra smoothing was deepening a shortfall, not
|
|
277
|
+
* repairing one: `max` over coverage cannot exceed the peak coverage near a fragment, and at
|
|
278
|
+
* ppem 14 a Han glyph's strokes never reach 1, so the outline is a translucent mottle at ~60% of
|
|
279
|
+
* the ideal either way. That is the honest shape of this trade — not "5.74x for a slightly
|
|
280
|
+
* coarser rim" but "5.74x for a differently-wrong outline at a size where the outline is already
|
|
281
|
+
* wrong". Above ppem 16 it is free in both directions: the blend weight is
|
|
282
|
+
* `smoothstep (16, 8, ppem)`, which is exactly 0 there, so NO pixel of text at or above ppem 16
|
|
283
|
+
* can change — measured, by widening the gate to `ppem < 200` and reading a byte-identical frame.
|
|
284
|
+
*
|
|
285
|
+
* WHAT IT DOES NOT TOUCH, structurally. The FILL's coverage is `hb_gpu_draw` -> `_hb_gpu_slug`
|
|
286
|
+
* inside the vendored library, guarded by the library's own `HB_GPU_NO_MSAA`. This flag defines
|
|
287
|
+
* `HB_GPU_SPREAD_TAP_NO_MSAA`, a different name that only this file's function reads, so no
|
|
288
|
+
* setting of it can reach the fill. `spread 0` never calls the tap at all.
|
|
289
|
+
*
|
|
290
|
+
* Set `true` for very small outlined text where the rim matters more than the frame budget.
|
|
291
|
+
*/
|
|
292
|
+
spreadTapMsaa?: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* The contrast curve applied to the FINAL coverage. Defaults to {@link HB_GPU_CONTRAST_DEFAULT},
|
|
295
|
+
* which has stem darkening ON.
|
|
296
|
+
*
|
|
297
|
+
* PASS {@link HB_GPU_CONTRAST_NONE} IF YOU ARE MEASURING FIDELITY, and only then. See
|
|
298
|
+
* {@link HbGpuContrast} for the whole argument.
|
|
299
|
+
*/
|
|
300
|
+
contrast?: HbGpuContrast;
|
|
301
|
+
/**
|
|
302
|
+
* Store model/colour/spread beside every glyph so adjacent runs may share one draw. Off by
|
|
303
|
+
* default: the established renderer keeps its 40-byte record and uniform state unchanged.
|
|
304
|
+
*/
|
|
305
|
+
perInstanceRunState?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Where a refusal goes.
|
|
308
|
+
*
|
|
309
|
+
* `createHbGpuRenderer` returns `null` rather than throwing, following `createCanvasStage`, so a
|
|
310
|
+
* consumer can fall back to a DOM text path. But a renderer that declined silently reports as a
|
|
311
|
+
* cheap one — in a perf arm literally so — so every `null` and every declined upload also comes
|
|
312
|
+
* through here with a reason a human can act on.
|
|
313
|
+
*/
|
|
314
|
+
onError?(failure: HbGpuFailure): void;
|
|
315
|
+
}
|
|
316
|
+
interface HbGpuRenderer {
|
|
317
|
+
/** The borrowed context. Owned by the embedder; `dispose` does not touch it. */
|
|
318
|
+
readonly gl: WebGL2RenderingContext;
|
|
319
|
+
/**
|
|
320
|
+
* The atlas width this instance actually built — NOT necessarily {@link ATLAS_WIDTH}.
|
|
321
|
+
*
|
|
322
|
+
* WebGL2 guarantees only 2048, so a device can force a narrower texture. The shader gets this as
|
|
323
|
+
* `hb_gpu_atlas_width` and the row-wrap arithmetic uses it, so a non-4096 width is correct and
|
|
324
|
+
* merely costs more `texSubImage2D` calls per blob.
|
|
325
|
+
*/
|
|
326
|
+
readonly atlasWidth: number;
|
|
327
|
+
/**
|
|
328
|
+
* True between {@link notifyContextLost} and a successful {@link rebuild}.
|
|
329
|
+
*
|
|
330
|
+
* While it is true every method here is a no-op that touches no GL: `upload` returns `null`,
|
|
331
|
+
* `push` skips, `end` reports a zero frame. Nothing polls `gl.isContextLost()` — that is a query
|
|
332
|
+
* in the hot path for an event the embedder already receives — so a consumer that does not wire
|
|
333
|
+
* `webglcontextlost` through to {@link notifyContextLost} will draw into dead objects forever,
|
|
334
|
+
* which is permanently blank text with no signal. That wiring is not optional.
|
|
335
|
+
*/
|
|
336
|
+
readonly contextLost: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Register a face, taking its `upem`, and get the namespace its glyph keys carry.
|
|
339
|
+
*
|
|
340
|
+
* THE FONT IS RETAINED, and that is the memory decision this package makes. A context loss
|
|
341
|
+
* destroys the atlas texture, so {@link rebuild} has to be able to put the same texels back at
|
|
342
|
+
* the same offsets — and the two ways to do that are to keep a copy of every uploaded blob or to
|
|
343
|
+
* keep the encoder that produced them. This keeps the ENCODER: the embedder already holds the
|
|
344
|
+
* `HbGpuFont` (it cannot encode without one), so the retained reference costs zero additional
|
|
345
|
+
* bytes, where retaining blobs would hold a second resident copy of the glyph data forever —
|
|
346
|
+
* 1.3 MB on S9's 300-glyph Han pool — to make a once-in-a-session event faster. The cost is paid
|
|
347
|
+
* on restore instead: `rebuild` re-runs `hb_gpu_draw_encode` for every resident glyph (~180 ms
|
|
348
|
+
* for that same pool).
|
|
349
|
+
*
|
|
350
|
+
* Consequence, stated because it is a lifetime rule and not a preference: the font must outlive
|
|
351
|
+
* the renderer, or a rebuild silently drops that face's glyphs.
|
|
352
|
+
*
|
|
353
|
+
* `null` when the font's `upem` is not usable — see {@link HbGpuFailureReason} `"degenerate-upem"`.
|
|
354
|
+
*/
|
|
355
|
+
registerFace(font: HbGpuFont, label?: string): HbGpuFace | null;
|
|
356
|
+
/**
|
|
357
|
+
* Upload one encoded glyph of `face`, or return the slot it already has.
|
|
358
|
+
*
|
|
359
|
+
* `null` for a glyph with no ink — a space encodes to a zero-length blob, which is a legitimate
|
|
360
|
+
* result and must not become a zero-texel allocation — and also for a blob this renderer
|
|
361
|
+
* declines (malformed, or larger than the whole atlas), which is reported through `onError`.
|
|
362
|
+
*
|
|
363
|
+
* THROWS in exactly one case; see {@link HbGpuRendererOptions.atlasTexels} and the in-use guard
|
|
364
|
+
* inside `allocate`.
|
|
365
|
+
*/
|
|
366
|
+
upload(face: HbGpuFace, glyphId: number, glyph: EncodedGlyph): GlyphSlot | null;
|
|
367
|
+
/**
|
|
368
|
+
* The live slot for a glyph that is already resident, or `null` when it is not.
|
|
369
|
+
*
|
|
370
|
+
* THE CHEAP HALF OF {@link HbGpuRenderer.upload}, AND THE REASON AN EMBEDDER CAN KEEP HANDLES
|
|
371
|
+
* INSTEAD OF SLOTS. A retained draw list records a glyph as an id and has to turn it back into a
|
|
372
|
+
* `GlyphSlot` every frame; the only way to do that used to be `upload`, which needs an
|
|
373
|
+
* {@link EncodedGlyph} — and encoding a Han working set costs ~180 ms, so a per-frame encode is
|
|
374
|
+
* not a path anybody can take. This is a map lookup and a touch.
|
|
375
|
+
*
|
|
376
|
+
* `null` means "never uploaded, or evicted", and those are the same answer for a caller: encode
|
|
377
|
+
* and `upload` again. It is NOT an error and nothing is reported — a ring allocator evicting is
|
|
378
|
+
* the mechanism working.
|
|
379
|
+
*
|
|
380
|
+
* The returned slot is built by the same expression `upload` uses, so a resolved slot and a
|
|
381
|
+
* freshly uploaded one cannot differ; in particular it carries the CURRENT
|
|
382
|
+
* {@link GlyphSlot.generation}, which is what makes it safe to `push`.
|
|
383
|
+
*/
|
|
384
|
+
resolve(face: HbGpuFace, glyphId: number): GlyphSlot | null;
|
|
385
|
+
/** Start a frame. */
|
|
386
|
+
begin(): void;
|
|
387
|
+
/**
|
|
388
|
+
* Queue one glyph, its em ORIGIN (the pen position, on the baseline) at object-space `(x, y)`,
|
|
389
|
+
* at `pixelsPerEm` object units per em.
|
|
390
|
+
*
|
|
391
|
+
* Object space is device pixels and y measures DOWN, matching every other renderer in the repo.
|
|
392
|
+
* Rotation is not here on purpose: it belongs in {@link setModel}, so that one matrix rotates the
|
|
393
|
+
* quad AND is seen by `hb_gpu_dilate`, which computes its half-pixel dilation in SCREEN space
|
|
394
|
+
* through that same matrix. A quad rotated on the CPU behind the shader's back would be dilated
|
|
395
|
+
* along the wrong axes.
|
|
396
|
+
*
|
|
397
|
+
* A slot whose allocation has been evicted is SKIPPED and counted — see {@link GlyphSlot.generation}.
|
|
398
|
+
*/
|
|
399
|
+
push(slot: GlyphSlot, x: number, y: number, pixelsPerEm: number): void;
|
|
400
|
+
/** Object-space 2x3 (`[xx, xy, yx, yy, tx, ty]`), applied before the projection. */
|
|
401
|
+
setModel(model: ArrayLike<number>): void;
|
|
402
|
+
/** STRAIGHT rgba in 0..1. The fragment premultiplies it, once. */
|
|
403
|
+
setColor(r: number, g: number, b: number, a: number): void;
|
|
404
|
+
/**
|
|
405
|
+
* Grow every glyph of the next frame outward by `px` OBJECT units. `0` (the default) is the
|
|
406
|
+
* plain fill.
|
|
407
|
+
*
|
|
408
|
+
* WHAT AN OUTLINED LABEL IS: this run in the outline colour at `spread`, then the SAME run in the
|
|
409
|
+
* fill colour at spread 0, in that order. A centred `ctx.strokeText` of width `W` reaches `W / 2`
|
|
410
|
+
* outward, so a caller matching one passes `W / 2`; the arithmetic is the caller's, because only
|
|
411
|
+
* the caller knows whether its stroke is centred, inner or outer.
|
|
412
|
+
*
|
|
413
|
+
* A DILATION FILLS THE INTERIOR AND A CENTRED STROKE DOES NOT, AND THAT DIFFERENCE IS REAL. This
|
|
414
|
+
* paints the whole glyph plus a band of `spread` around it, where a stroke paints only a band
|
|
415
|
+
* straddling the contour. Under an OPAQUE fill the two are pixel-identical, because the fill
|
|
416
|
+
* covers every pixel they disagree about. Under a TRANSLUCENT fill they are not: the outline
|
|
417
|
+
* colour shows through the glyph's middle here and would not through a stroke. That is the honest
|
|
418
|
+
* limit of a coverage-max dilation and there is no distance field to do better with — see
|
|
419
|
+
* {@link FRAGMENT_MAIN}.
|
|
420
|
+
*
|
|
421
|
+
* OBJECT UNITS, BEFORE {@link HbGpuRenderer.setModel}, matching every other length `push` takes.
|
|
422
|
+
* A model that scales scales the outline with the text, which is what a caller rotating a label
|
|
423
|
+
* wants.
|
|
424
|
+
*
|
|
425
|
+
* IT PERSISTS, exactly like {@link HbGpuRenderer.setModel} and {@link HbGpuRenderer.setColor} —
|
|
426
|
+
* `begin` does not reset it. The failure that buys is worth stating: a caller that sets a spread
|
|
427
|
+
* for one run and does not clear it draws every LATER run fat, which looks like a font-weight bug
|
|
428
|
+
* rather than a missing call. Per-run callers should set it per run, which is what
|
|
429
|
+
* `packages/canvas`'s glyph pass does.
|
|
430
|
+
*
|
|
431
|
+
* A NON-ZERO SPREAD ALSO TURNS STEM DARKENING OFF for that run, whatever
|
|
432
|
+
* {@link HbGpuContrast.stemDarkening} says — Godot's outline carries no contrast curve, and one
|
|
433
|
+
* on a dilated rim reads as a halo. The reason is under that field; the pair "outline then fill"
|
|
434
|
+
* above is unaffected, because the fill run is the one that keeps the correction.
|
|
435
|
+
*
|
|
436
|
+
* COST. Bounded, but not free: a fragment that is neither solid ink nor near any evaluates up to
|
|
437
|
+
* 65 coverage taps (and five times that below ppem 16), and the quad it does so over grows by
|
|
438
|
+
* `spread` on every side. Beyond roughly 4 device pixels of radius the tap ceiling is reached —
|
|
439
|
+
* {@link HB_GPU_SPREAD_MAX_TAPS} — and the dilated rim starts to scallop: measured against a Godot
|
|
440
|
+
* 4.5.1 golden at radius 12, the 50% contour wobbles 0.17 px where the engine's wobbles 0.11. The
|
|
441
|
+
* cost does not grow past that point; only the scallop does. Negative and non-finite values are
|
|
442
|
+
* clamped to 0 rather than reported: this is a per-run hot-path setter with no error channel.
|
|
443
|
+
*/
|
|
444
|
+
setSpread(px: number): void;
|
|
445
|
+
/**
|
|
446
|
+
* Re-state both sizes after the embedder resized its stage or its drawing buffer.
|
|
447
|
+
*
|
|
448
|
+
* Does NOT call `gl.viewport` — this renderer never touches it. It updates the design->clip
|
|
449
|
+
* projection (from the DESIGN pair) and the `u_viewport` the dilation is measured in (from the
|
|
450
|
+
* FRAMEBUFFER pair), both of which must describe the viewport the embedder will have set by the
|
|
451
|
+
* time `end` runs. The framebuffer pair defaults to the design pair, which is right only at a
|
|
452
|
+
* device-pixel ratio of 1 — see {@link HbGpuRendererOptions.framebufferWidth}.
|
|
453
|
+
*/
|
|
454
|
+
setViewport(designWidth: number, designHeight: number, framebufferWidth?: number, framebufferHeight?: number): void;
|
|
455
|
+
/**
|
|
456
|
+
* Submit. Returns what the frame cost.
|
|
457
|
+
*
|
|
458
|
+
* WHAT THIS DOES NOT DO, because the context is borrowed: it does not set `gl.viewport` and it
|
|
459
|
+
* does not clear. Both were here while this file owned a stage, and both are actively wrong in a
|
|
460
|
+
* shared context — a clear inside a glyph pass erases everything the embedder's executor already
|
|
461
|
+
* drew, and a viewport call silently overrides a scissored or letterboxed pass.
|
|
462
|
+
*
|
|
463
|
+
* WHAT IT LEAVES DIRTY, exhaustively, so an embedder's restore code can be written against it.
|
|
464
|
+
* On a frame that drew anything (`instances > 0`):
|
|
465
|
+
*
|
|
466
|
+
* - `BLEND` is ENABLED, `blendEquation` is `FUNC_ADD`, `blendFunc` is
|
|
467
|
+
* `(ONE, ONE_MINUS_SRC_ALPHA)` — premultiplied MIX. Note `blendFunc`/`blendEquation`, not
|
|
468
|
+
* the `*Separate` forms, so both the RGB and the alpha halves are set.
|
|
469
|
+
* - The current program is this renderer's.
|
|
470
|
+
* - `ARRAY_BUFFER` is bound to the instance buffer.
|
|
471
|
+
* - `ACTIVE_TEXTURE` is `TEXTURE0` and `TEXTURE_BINDING_2D` on unit 0 is the atlas.
|
|
472
|
+
* - `VERTEX_ARRAY_BINDING` is null (unbound, not restored to whatever was bound before —
|
|
473
|
+
* WebGL2 has no cheap way to read it back).
|
|
474
|
+
* - Uniforms of this renderer's program only — `u_viewProjection`, `u_viewport`, `u_gamma`,
|
|
475
|
+
* `u_stemDarken`, `hb_gpu_atlas` and `hb_gpu_atlas_width`. Per-run model, colour and spread
|
|
476
|
+
* are captured into each instance record; their setters still persist until replaced and
|
|
477
|
+
* `begin` deliberately does not reset them (see {@link HbGpuRenderer.setSpread}).
|
|
478
|
+
*
|
|
479
|
+
* Untouched: viewport, scissor box and `SCISSOR_TEST`, clear colour, depth/stencil state,
|
|
480
|
+
* framebuffer bindings, the three unpack flags — `UNPACK_ALIGNMENT`, `UNPACK_FLIP_Y_WEBGL` and
|
|
481
|
+
* `UNPACK_PREMULTIPLY_ALPHA_WEBGL`, all saved and restored around every upload — and every
|
|
482
|
+
* texture unit but 0.
|
|
483
|
+
*
|
|
484
|
+
* On an empty frame it returns immediately and leaves ALL of the above untouched too, which is
|
|
485
|
+
* why an embedder must restore unconditionally rather than only when `instances > 0`.
|
|
486
|
+
*/
|
|
487
|
+
end(): {
|
|
488
|
+
instances: number;
|
|
489
|
+
drawCalls: number;
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* The context is gone: drop every GL handle WITHOUT calling into GL to free it.
|
|
493
|
+
*
|
|
494
|
+
* Call this from `webglcontextlost` (`packages/canvas/src/present.ts` offers exactly that hook,
|
|
495
|
+
* and `preventDefault` on that event is what makes a restore possible at all). The allocation
|
|
496
|
+
* table survives — offsets, faces and glyph ids — because {@link rebuild} puts the same texels
|
|
497
|
+
* back at the same offsets, which is what keeps every {@link GlyphSlot} the embedder is holding
|
|
498
|
+
* valid across the loss.
|
|
499
|
+
*/
|
|
500
|
+
notifyContextLost(): void;
|
|
501
|
+
/**
|
|
502
|
+
* The context is back: recreate the program, the buffers, the VAO and the texture, then re-encode
|
|
503
|
+
* and re-upload every resident glyph at the offset it already had.
|
|
504
|
+
*
|
|
505
|
+
* SAME OFFSETS, DELIBERATELY. Repacking would be simpler and would invalidate every slot the
|
|
506
|
+
* embedder holds — which the generation guard would then turn into a silently empty frame rather
|
|
507
|
+
* than garbage, but empty is still wrong. Re-materialising the atlas byte for byte means a
|
|
508
|
+
* restore needs no cooperation from the caller beyond this one call.
|
|
509
|
+
*
|
|
510
|
+
* Returns `false` (and reports) if the GL objects could not be rebuilt. A face whose font has
|
|
511
|
+
* been destroyed, or a glyph that no longer encodes to the same length, loses its allocation:
|
|
512
|
+
* those slots then fail the generation check and are skipped rather than drawn wrong.
|
|
513
|
+
*/
|
|
514
|
+
rebuild(): boolean;
|
|
515
|
+
readonly atlas: AtlasStats;
|
|
516
|
+
readonly blobs: BlobStats;
|
|
517
|
+
/** Delete this renderer's GL objects. Does NOT touch the context or the canvas. */
|
|
518
|
+
dispose(): void;
|
|
519
|
+
}
|
|
520
|
+
declare function createHbGpuRenderer(module: HbGpu, options: HbGpuRendererOptions): HbGpuRenderer | null;
|
|
521
|
+
//#endregion
|
|
522
|
+
export { ATLAS_WIDTH, AtlasStats, BlobStats, FRAGMENT_MAIN, GlyphSlot, HB_GPU_CONTRAST_DEFAULT, HB_GPU_CONTRAST_NONE, HB_GPU_SPREAD_MAX_TAPS, HbGpuContrast, HbGpuFace, HbGpuRenderer, HbGpuRendererOptions, createHbGpuRenderer };
|
|
523
|
+
//# sourceMappingURL=webgl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webgl.d.ts","names":[],"sources":["../src/webgl.ts"],"mappings":";;;;;AA+EA;;;;AAAwB;AAgExB;cAhEa,WAAA;;;AAgEsB;AA6dnC;;;;AAA0B;AA4T1B;;;;;;;;AAMe;AAIf;;;;cAnyBa,sBAAA;AAAA,cA6dA,aAAA;;;;;;;;;;;AAuWL;AAGR;UA9CiB,SAAA;;WAEN,EAAA;EAmDT;EAAA,SAjDS,KAAA;EA4DT;EAAA,SA1DS,IAAA;AAAA;;UAIM,SAAA;EAoEf;;AAAU;AAGZ;EAlEE,MAAA;EACA,OAAA;EAiEwB;;;;EA5DxB,GAAA;EAqEa;AAwBf;;;;AAyCe;AAIf;;;;EA/HE,UAAA;EA4IW;EA1IX,GAAA;;EAEA,IAAA;EA2IA;EAzIA,IAAA;EACA,IAAA;EACA,IAAA;EACA,IAAA;EAgOW;EA9NX,MAAA;AAAA;AAAA,UAGe,UAAA;EA2If;;;;;;EApIA,SAAA;EA4MA;;;;;;;;EAnMA,gBAAA;EA4Ne;EA1Nf,OAAA;EACA,UAAA;EACA,cAAA;EA+PmB;EA7PnB,SAAA;EAyQQ;EAvQR,KAAA;EA0QG;;;;;;;EAlQH,UAAA;AAAA;AAAA,UAGe,SAAA;EA2MF;EAzMb,MAAA;EA2NS;EAzNT,UAAA;EA4OmB;;;;EAvOnB,aAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;UAwBe,aAAA;EAmQK;;;;;;;;;;;;;EArPpB,KAAA;EAsWA;;;;;;;AAIO;AA+FT;;;;;;;;;;;;;;AAGgB;;;;EAjbd,aAAa;AAAA;;cAIF,uBAAA,EAAyB,aAGpC;;;;;;;;;cAUW,oBAAA,EAAsB,aAGjC;AAAA,UAEe,oBAAA;;;;;;;;EAQf,EAAA,EAAI,sBAAA;;;;;;;;;EASJ,WAAA;EACA,YAAA;;;;;;;;;;;;;EAaA,gBAAA;EACA,iBAAA;;;;;;;;;EASA,WAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCA,aAAA;;;;;;;;EAQA,QAAA,GAAW,aAAA;;;;;EAKX,mBAAA;;;;;;;;;EASA,OAAA,EAAS,OAAA,EAAS,YAAA;AAAA;AAAA,UAGH,aAAA;;WAEN,EAAA,EAAI,sBAAA;;;;;;;;WAQJ,UAAA;;;;;;;;;;WAUA,WAAA;;;;;;;;;;;;;;;;;;;EAmBT,YAAA,CAAa,IAAA,EAAM,SAAA,EAAW,KAAA,YAAiB,SAAA;;;;;;;;;;;EAW/C,MAAA,CACE,IAAA,EAAM,SAAA,EACN,OAAA,UACA,KAAA,EAAO,YAAA,GACN,SAAA;;;;;;;;;;;;;;;;;;EAkBH,OAAA,CAAQ,IAAA,EAAM,SAAA,EAAW,OAAA,WAAkB,SAAA;;EAE3C,KAAA;;;;;;;;;;;;;EAaA,IAAA,CAAK,IAAA,EAAM,SAAA,EAAW,CAAA,UAAW,CAAA,UAAW,WAAA;;EAE5C,QAAA,CAAS,KAAA,EAAO,SAAA;;EAEhB,QAAA,CAAS,CAAA,UAAW,CAAA,UAAW,CAAA,UAAW,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyC1C,SAAA,CAAU,EAAA;;;;;;;;;;EAUV,WAAA,CACE,WAAA,UACA,YAAA,UACA,gBAAA,WACA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkCF,GAAA;IAAS,SAAA;IAAmB,SAAA;EAAA;;;;;;;;;;EAU5B,iBAAA;;;;;;;;;;;;;;EAcA,OAAA;EAAA,SACS,KAAA,EAAO,UAAA;EAAA,SACP,KAAA,EAAO,SAAA;;EAEhB,OAAA;AAAA;AAAA,iBA+Fc,mBAAA,CACd,MAAA,EAAQ,KAAA,EACR,OAAA,EAAS,oBAAA,GACR,aAAA"}
|