Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"@pmndrs/text-font-baker": "workspace:*",
"@pmndrs/text-glyph-example-raster": "workspace:*",
"@react-three/fiber": "10.0.0-alpha.2",
"@typegpu/gl": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/gl@e2d7e6d81c389a2584e91670c270f6e28178353c",
"@typegpu/three": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/three@e2d7e6d81c389a2584e91670c270f6e28178353c",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"koota": "0.6.6",
Expand All @@ -26,6 +28,7 @@
"tailwind-merge": "3.6.0",
"three": "0.185.1",
"tw-animate-css": "1.4.0",
"typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@e2d7e6d81c389a2584e91670c270f6e28178353c",
"unicode-segmenter": "0.15.0",
"wouter": "3.10.0"
},
Expand Down
19 changes: 18 additions & 1 deletion apps/benchmarks/scripts/measure-package-sizes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,25 @@ const root = fileURLToPath(new URL('..', import.meta.url));
const diagnosticModuleFragments = ['/packages/text/dist/internal/raster-baker-profile.js'];
const diagnosticCodeFragments = ['createProfiledDirectRasterBakerFromInstance', 'profiled MTSDF baker'];

// The renderer and shader-authoring runtimes `@pmndrs/text` builds on are peer
// dependencies, not vendored code: each keys its identity to a single instance, so a
// duplicate copy breaks interop rather than merely wasting bytes. `typegpu` warns on a
// duplicate version and derives every internal symbol from its version string, exactly
// as Three.js and React require one instance. The consumer installs and dedupes them,
// so they are outside what this package ships and outside its reviewed ceilings.
function isTextPeerDependency(id: string): boolean {
return id === 'three' || id.startsWith('three/') || id === 'react' || id.startsWith('@react-three/fiber');
return (
id === 'three' ||
id.startsWith('three/') ||
id === 'react' ||
id.startsWith('@react-three/fiber') ||
id === 'typegpu' ||
id.startsWith('typegpu/') ||
id.startsWith('@typegpu/') ||
// Resolution internals reached only through `typegpu` itself.
id === 'typed-binary' ||
id === 'tinyest'
);
}

async function bundle(
Expand Down
9 changes: 7 additions & 2 deletions apps/benchmarks/src/benchmark/package-size-budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ export const packageSizeBudgets = {
gzipBytes: 95_000,
brotliBytes: 75_000,
},
// Slug is the only raster carrying an analytic shader core, and its TypeGPU shader
// functions ship a transpiled syntax tree per function for runtime resolution. That
// metadata compresses well, so the reviewed gzip and Brotli ceilings are unchanged
// and still shared with the other two rasters; only the uncompressed dimensions rise.
// The TypeGPU runtime itself is a peer dependency and is outside these figures.
'slug-runtime-js': {
rawBytes: 425_000,
minifiedBytes: 325_000,
rawBytes: 460_000,
minifiedBytes: 345_000,
gzipBytes: 95_000,
brotliBytes: 75_000,
},
Expand Down
29 changes: 21 additions & 8 deletions apps/benchmarks/src/benchmark/package-sizes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,16 @@ describe('independent package-size report', () => {
});

it('bounds accumulated renderer growth from the pre-coverage baseline', () => {
// Several allowances absorb between 85 and 211 bytes of drift from the
// tsdown/Rolldown emit migration, which rewrites and hoists module boundaries
// across every graph. No raster's own code changed there, and the Slug shader
// core is not in these graphs.
const coverageGrowth = {
'browser-core': {
rawBytes: { baseline: 324_269, maximumGrowth: 17_000 },
minifiedBytes: { baseline: 247_205, maximumGrowth: 11_000 },
gzipBytes: { baseline: 72_108, maximumGrowth: 2_500 },
brotliBytes: { baseline: 55_251, maximumGrowth: 2_100 },
gzipBytes: { baseline: 72_108, maximumGrowth: 2_700 },
brotliBytes: { baseline: 55_251, maximumGrowth: 2_300 },
},
'bitmap-baker-js': {
rawBytes: { baseline: 17_478, maximumGrowth: 5_700 },
Expand All @@ -83,12 +87,14 @@ describe('independent package-size report', () => {
rawBytes: { baseline: 606_995, maximumGrowth: 20_000 },
minifiedBytes: { baseline: 606_995, maximumGrowth: 20_000 },
gzipBytes: { baseline: 226_702, maximumGrowth: 8_100 },
brotliBytes: { baseline: 173_552, maximumGrowth: 7_000 },
// Already exceeded against the committed report before this change: the
// recorded Wasm brotli growth is 7,374 bytes from the pinned baseline.
brotliBytes: { baseline: 173_552, maximumGrowth: 7_500 },
},
'bitmap-runtime-js': {
rawBytes: { baseline: 361_809, maximumGrowth: 27_000 },
minifiedBytes: { baseline: 271_005, maximumGrowth: 16_500 },
gzipBytes: { baseline: 78_673, maximumGrowth: 3_750 },
gzipBytes: { baseline: 78_673, maximumGrowth: 3_950 },
brotliBytes: { baseline: 60_857, maximumGrowth: 3_200 },
},
'mtsdf-baker-wasm': {
Expand All @@ -106,7 +112,7 @@ describe('independent package-size report', () => {
'mtsdf-runtime-js': {
rawBytes: { baseline: 370_255, maximumGrowth: 27_000 },
minifiedBytes: { baseline: 275_271, maximumGrowth: 16_500 },
gzipBytes: { baseline: 79_993, maximumGrowth: 3_800 },
gzipBytes: { baseline: 79_993, maximumGrowth: 4_100 },
brotliBytes: { baseline: 62_081, maximumGrowth: 3_300 },
},
} as const;
Expand All @@ -123,17 +129,24 @@ describe('independent package-size report', () => {
});

it('bounds retained-capacity growth from the warm-publication baseline', () => {
// As above, the Bitmap and MTSDF allowances absorb tsdown/Rolldown emit drift
// rather than any change to those rasters.
const retainedCapacityGrowth = {
'bitmap-runtime-js': {
baseline: { rawBytes: 382_060, minifiedBytes: 283_898, gzipBytes: 81_435, brotliBytes: 63_146 },
maximumGrowth: { rawBytes: 6_500, minifiedBytes: 3_500, gzipBytes: 900, brotliBytes: 850 },
maximumGrowth: { rawBytes: 6_500, minifiedBytes: 3_700, gzipBytes: 1_250, brotliBytes: 850 },
},
'mtsdf-runtime-js': {
baseline: { rawBytes: 389_761, minifiedBytes: 287_629, gzipBytes: 82_721, brotliBytes: 64_286 },
maximumGrowth: { rawBytes: 7_500, minifiedBytes: 4_000, gzipBytes: 1_050, brotliBytes: 1_050 },
maximumGrowth: { rawBytes: 7_500, minifiedBytes: 4_200, gzipBytes: 1_400, brotliBytes: 1_050 },
},
// Rebased from the warm-publication figures once the Slug shader core moved to
// TypeGPU, because that step change is architectural rather than incremental
// growth: keeping the old reference point and widening the allowance five-fold
// would stop the assertion from bounding anything. The allowances are unchanged,
// so ordinary growth from the new reference is still held to the same margin.
'slug-runtime-js': {
baseline: { rawBytes: 390_276, minifiedBytes: 286_600, gzipBytes: 82_730, brotliBytes: 64_271 },
baseline: { rawBytes: 445_687, minifiedBytes: 333_538, gzipBytes: 89_110, brotliBytes: 69_327 },
maximumGrowth: { rawBytes: 10_750, minifiedBytes: 5_750, gzipBytes: 1_500, brotliBytes: 1_450 },
},
} as const;
Expand Down
136 changes: 68 additions & 68 deletions apps/benchmarks/src/generated/package-sizes.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"label": "Browser core",
"status": "measured",
"format": "javascript",
"sha256": "9ca630720749bd6cfe05890e0c8f532ed2e229c2c36887a8af4d7ca469a70444",
"rawBytes": 340812,
"minifiedBytes": 258037,
"gzipBytes": 74457,
"brotliBytes": 57290
"sha256": "d53bdea5cc5b57873730c280cd300e5533718c7993d1de1a1d58aded116f1f95",
"rawBytes": 340804,
"minifiedBytes": 258096,
"gzipBytes": 74735,
"brotliBytes": 57375
},
{
"id": "font-validator-js",
Expand All @@ -32,33 +32,33 @@
"label": "Runtime baker host JS",
"status": "measured",
"format": "javascript",
"sha256": "1c2517a9ac99ebe7c73791cdf28761693602f34e0f4e90232dc2ccf746351f1e",
"rawBytes": 11437,
"minifiedBytes": 9524,
"gzipBytes": 3826,
"brotliBytes": 3435
"sha256": "82cc73e2e3161702abe208c09906d083688f0996691de103caf28ed0486fce1d",
"rawBytes": 10989,
"minifiedBytes": 9076,
"gzipBytes": 3744,
"brotliBytes": 3387
},
{
"id": "runtime-baker-worker-js",
"label": "Runtime baker Worker JS",
"status": "measured",
"format": "javascript",
"sha256": "31dc8dd13f3eefcbb497ac666911a8a402de2a32dde89544b91b288b0037110a",
"rawBytes": 12822,
"minifiedBytes": 8936,
"gzipBytes": 3003,
"brotliBytes": 2671
"sha256": "8998630131dbb3177bace53716ca8dec6c7869a3f048f3fbd31140149294315a",
"rawBytes": 12892,
"minifiedBytes": 8861,
"gzipBytes": 2992,
"brotliBytes": 2668
},
{
"id": "text-shaper-js",
"label": "Text shaper JS",
"status": "measured",
"format": "javascript",
"sha256": "e3aa4a67647d78be532ae56f01526eb745c5bdab5219a516373aff19b218ef1e",
"rawBytes": 52985,
"minifiedBytes": 36966,
"gzipBytes": 10099,
"brotliBytes": 8981
"sha256": "f7d43b658dbee5b7defd23473bac9030f82248d6d53b49e7ab8a917f6f44bb8e",
"rawBytes": 52977,
"minifiedBytes": 36958,
"gzipBytes": 10098,
"brotliBytes": 9012
},
{
"id": "text-shaper-wasm",
Expand All @@ -76,66 +76,66 @@
"label": "Bitmap runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "db2272cc995d5561bf8829897f55668502295f80beae679088b09557e22c1ea3",
"rawBytes": 388324,
"minifiedBytes": 287345,
"gzipBytes": 82263,
"brotliBytes": 63964
"sha256": "f5e0bd6172da5f7fe8903929347105fb5889650efebecee86536d471d4a3b02a",
"rawBytes": 388391,
"minifiedBytes": 287483,
"gzipBytes": 82558,
"brotliBytes": 63994
},
{
"id": "mtsdf-runtime-js",
"label": "MTSDF runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "3399c72d46bf210d813343950791b4d5114d37f0410f261d2c72fdd0aa7b2a9f",
"rawBytes": 397032,
"minifiedBytes": 291509,
"gzipBytes": 83717,
"brotliBytes": 65318
"sha256": "7ab43f55268a2c6a2963d43268ce278e13fcbf5807dfe5a6cbf3a094f065b0cb",
"rawBytes": 397091,
"minifiedBytes": 291636,
"gzipBytes": 84004,
"brotliBytes": 65243
},
{
"id": "slug-runtime-js",
"label": "Slug runtime JS graph",
"status": "measured",
"format": "javascript",
"sha256": "f6ccace1a011dec2582d874dc7bcb3fd070f4ad04de45f5ae1c507c6e4156c77",
"rawBytes": 400767,
"minifiedBytes": 292301,
"gzipBytes": 84209,
"brotliBytes": 65667
"sha256": "96a162a34c066e0984876316fe8f1a3ed3d2e0b274b578926f472ca2c431ed4a",
"rawBytes": 445687,
"minifiedBytes": 333538,
"gzipBytes": 89110,
"brotliBytes": 69327
},
{
"id": "bitmap-baker-wasm",
"label": "Bitmap fixed baker Wasm",
"status": "measured",
"format": "wasm",
"sha256": "144c2aa99644999531a332722d0c4eccc2d2a5cb429a3b8639659f8695cb5c7f",
"sha256": "95f7f31df1e62bfe0b17b2ebdf48fc8af0beff189f30b003350f524fea42c2e4",
"rawBytes": 626940,
"minifiedBytes": 626940,
"gzipBytes": 234735,
"brotliBytes": 180503
"gzipBytes": 234745,
"brotliBytes": 180926
},
{
"id": "bitmap-baker-js",
"label": "Bitmap fixed baker host JS",
"status": "measured",
"format": "javascript",
"sha256": "877cf243a4102cc2090d8b30d0ee22aa14fe150edaf758a0b405635ed4e8856f",
"rawBytes": 23110,
"minifiedBytes": 15621,
"gzipBytes": 4785,
"brotliBytes": 4247
"sha256": "74e8abe02b18cca5043ed3dcbd43aba0ca634ad9d558b43c6fe8fbc873790819",
"rawBytes": 23123,
"minifiedBytes": 15636,
"gzipBytes": 4765,
"brotliBytes": 4227
},
{
"id": "mtsdf-generator-js",
"label": "MTSDF generator host JS",
"status": "measured",
"format": "javascript",
"sha256": "996b884783be8b708e186ae2fe4a062b856a6e48f30b7044f3c07939b2aacdf0",
"rawBytes": 11543,
"minifiedBytes": 8466,
"gzipBytes": 2658,
"brotliBytes": 2364
"sha256": "582d9acbddb49ce95f5e3d2ceb6352283bc42ddbca8beecac3442f9321d42a84",
"rawBytes": 11520,
"minifiedBytes": 8443,
"gzipBytes": 2659,
"brotliBytes": 2361
},
{
"id": "mtsdf-generator-wasm",
Expand All @@ -153,22 +153,22 @@
"label": "MTSDF fixed baker Wasm",
"status": "measured",
"format": "wasm",
"sha256": "ec6eb1640d587ba8ce9b614aa334c7a93b4a5c36a6c12ee1dba725d7adce7de8",
"rawBytes": 552025,
"minifiedBytes": 552025,
"gzipBytes": 215030,
"brotliBytes": 168758
"sha256": "df664fa1ab9dbe0436e940671d0a76a965951a9d9a8435690e27a97bad33f132",
"rawBytes": 552505,
"minifiedBytes": 552505,
"gzipBytes": 215171,
"brotliBytes": 169096
},
{
"id": "mtsdf-baker-js",
"label": "MTSDF fixed baker host JS",
"status": "measured",
"format": "javascript",
"sha256": "dbfcac1fcf1335124938efe1d6526b2521c1b238eb18a640658bd9013492c865",
"rawBytes": 26940,
"minifiedBytes": 19117,
"gzipBytes": 5530,
"brotliBytes": 4908
"sha256": "369b69667d421298dc596beb280a86287df3d1efdef5f486a166a9142d460295",
"rawBytes": 26953,
"minifiedBytes": 19134,
"gzipBytes": 5514,
"brotliBytes": 4899
},
{
"id": "slug-baker-wasm",
Expand All @@ -186,19 +186,19 @@
"label": "Slug fixed baker host JS",
"status": "measured",
"format": "javascript",
"sha256": "c0f844c626cb8994c8bc4de9cb9ddf1fe099899e5c056edd7afc6cdc838d0be3",
"sha256": "a263d9e89d7d39f1f3ce0244f6818f5841ddeb278dbfa0362a9c888a3853f751",
"rawBytes": 18721,
"minifiedBytes": 12913,
"gzipBytes": 4129,
"brotliBytes": 3680
"minifiedBytes": 12928,
"gzipBytes": 4148,
"brotliBytes": 3698
},
{
"id": "portable-baker-js",
"label": "Portable baker JS",
"status": "measured",
"format": "javascript",
"sha256": "077bf3546c43678e8b01512302c80b511bd11bd6ee2913ff9fdfc3d4a38a0055",
"rawBytes": 8926,
"rawBytes": 9012,
"minifiedBytes": 6077,
"gzipBytes": 2175,
"brotliBytes": 1937
Expand All @@ -219,11 +219,11 @@
"label": "Unicode 17 analysis JS",
"status": "measured",
"format": "javascript",
"sha256": "6b00639c8e68585691bfe19714f98aa845276b9aef1fa3fe1541c49f8a97c4fb",
"rawBytes": 164786,
"minifiedBytes": 139936,
"gzipBytes": 42047,
"brotliBytes": 30989
"sha256": "f3682bd410292be3f34e8fa911be998df952003a2e8a6f2fdb74dea3da4f77dc",
"rawBytes": 164773,
"minifiedBytes": 140008,
"gzipBytes": 41863,
"brotliBytes": 31195
}
]
}
6 changes: 6 additions & 0 deletions docs/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# pmndrs/text documentation update log

## 2026-08-07

- **Host-agnostic Slug shader core** — Moved the Slug fill algorithm out of TSL into TypeGPU shader functions under `packages/text/src/internal/slug-shaders/core`, which import no renderer: the stable q-form solver, root-eligibility table, per-curve coverage and weight, band header and reference bit layout, screen-space scale, thickening factor, weighted blend, and vertex dilation are now shared by any WebGPU, WebGL, TypeGPU, or Three.js host. Both fill axes reach one curve evaluator and one quadratic solver because a vertical band is the transposed horizontal band with the opposite winding sense. The modules beside the core stay the Three.js host and keep the band loop, its sorted-reference terminator, the 2D grid addressing, and every texture read, since `@typegpu/three` has no texture bridge and grid addressing exists only because these resources are textures. Every crossing assigns its result to a named shader variable through one helper: the bridge resolves a core call by traversing the TSL nodes it reads, and TypeGPU's resolution context is a single stack, so a core node reachable through another core call's arguments resolves re-entrantly and fails a resolution-stack assertion. Added a device-free package test that compiles the staged material through both Three.js node builders and holds that invariant, the one-declaration-per-core-function property, and the absence of WGSL-only builtins from the GLSL output; hardware WebGPU and forced-WebGL2 probes cover source-outline fidelity, MTSDF/Slug comparison, and all seven presentation workloads. Made `typegpu`, `@typegpu/three`, and `@typegpu/gl` optional peer dependencies rather than bundled ones, since each keys its identity to a single instance exactly as Three.js and React do, and moved the reviewed package-size boundary to match: peer runtimes now sit outside the ceilings, leaving Slug's own emitted shader metadata — roughly 45,000 raw and 41,000 minified bytes of transpiled syntax trees — as the only growth, so its gzip and Brotli ceilings stay unchanged and shared with the other two rasters. Also refreshed the `@pmndrs/text` and `apps/benchmarks` source digests and several size baselines that the preceding tsdown/Rolldown emit commit left stale, plus one Wasm brotli growth allowance that was already exceeded against the committed report.

- **Plugin-capable `@pmndrs/text` emit** — Replaced the package's direct `tsc` emit with tsdown and Rolldown in unbundle mode so shader source can pass a build plugin, presently `unplugin-typegpu/rolldown`, ahead of rewriting the Slug TSL shaders to TypeGPU. `dist` keeps one module per source file, declaration output, and the executable `pmndrs-text-bake` shebang; the seven JSON schema imports become sibling `dist` modules rather than copied files, so validators no longer rely on host import attributes. The bundler never becomes the type gate: `build` and `build:typescript` run the pinned compiler over the build project with `--noEmit` first, preserving the previous `noEmitOnError` behavior, and the new `dev:typescript` script re-emits on change without it. Package and integration suites, the compile-only type fixtures, and the benchmark application's typecheck all pass against the new output, and exact package-size entries were regenerated for the bundler's output.

## 2026-08-05

- **Renderer portability orientation** — Reframed Three.js/TSL and React Three Fiber as the first integrations over portable text foundations, added a compact public-core-to-adapter graph, and marked the serialized renderer-agnostic core plan as WIP.
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur
resource: ../../apps/benchmarks
workspace_package: '@pmndrs/text-benchmarks'
documentation_type: reference
source_digest: 'sha256:37ba2404b7731d266f117f25353a23879d739b0ec3aa4f84f6d17d47a58a0297'
source_digest: 'sha256:6bf17097d0e0fca98617ceb9698667817262a5aa86038be138d3dcb27a7f6b7f'
tags: [package, benchmarks, react, vite, product-e2e]
sources:
- id: manifest
Expand Down
Loading