@aardworx/wombat.rendering 0.9.3 → 0.9.5
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/dist/core/buffer.d.ts.map +1 -1
- package/dist/core/buffer.js +65 -5
- package/dist/core/buffer.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/provider.d.ts +38 -0
- package/dist/core/provider.d.ts.map +1 -0
- package/dist/core/provider.js +104 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/renderObject.d.ts +17 -10
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/core/sampler.d.ts.map +1 -1
- package/dist/core/sampler.js +71 -2
- package/dist/core/sampler.js.map +1 -1
- package/dist/core/texture.d.ts +9 -0
- package/dist/core/texture.d.ts.map +1 -1
- package/dist/core/texture.js +0 -0
- package/dist/core/texture.js.map +1 -1
- package/dist/resources/adaptiveTexture.d.ts.map +1 -1
- package/dist/resources/adaptiveTexture.js +7 -0
- package/dist/resources/adaptiveTexture.js.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +20 -18
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +23 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/slots.js +4 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +72 -29
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +52 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -0
- package/dist/runtime/heapDecoder.js +363 -0
- package/dist/runtime/heapDecoder.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +300 -3
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +42 -5
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +64 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -0
- package/dist/runtime/heapIrBuilders.js +311 -0
- package/dist/runtime/heapIrBuilders.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +286 -97
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +67 -4
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +152 -18
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +4 -3
- package/src/core/buffer.ts +58 -6
- package/src/core/index.ts +11 -0
- package/src/core/provider.ts +143 -0
- package/src/core/renderObject.ts +17 -10
- package/src/core/sampler.ts +64 -2
- package/src/core/texture.ts +0 -0
- package/src/resources/adaptiveTexture.ts +7 -0
- package/src/resources/preparedRenderObject.ts +21 -16
- package/src/resources/uniformBuffer.ts +21 -0
- package/src/runtime/derivedUniforms/slots.ts +4 -0
- package/src/runtime/heapAdapter.ts +63 -29
- package/src/runtime/heapDecoder.ts +446 -0
- package/src/runtime/heapEffectIR.ts +314 -3
- package/src/runtime/heapEligibility.ts +34 -5
- package/src/runtime/heapIrBuilders.ts +365 -0
- package/src/runtime/heapScene.ts +295 -81
- package/src/runtime/textureAtlas/atlasPool.ts +152 -16
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
|
|
21
21
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
22
|
-
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns } from "@aardworx/wombat.shader/passes";
|
|
22
|
+
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs } from "@aardworx/wombat.shader/passes";
|
|
23
|
+
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
23
24
|
import {
|
|
24
25
|
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
25
26
|
type EntryDef, type EntryParameter, type ParamDecoration,
|
|
@@ -804,8 +805,17 @@ export function compileHeapEffectIR(
|
|
|
804
805
|
compileOptions: CompileOptions,
|
|
805
806
|
mode: HeapEffectEmitMode = "standalone",
|
|
806
807
|
): { vs: string; fs: string; preludeWgsl: string; vsEntry: string; fsEntry: string } {
|
|
807
|
-
//
|
|
808
|
-
//
|
|
808
|
+
// Both modes run through the composition-based heap rewrite.
|
|
809
|
+
// Family-member mode skips the megacall prelude in the decoder
|
|
810
|
+
// (wrapper VS owns it) and re-shapes the emitted @vertex into a
|
|
811
|
+
// regular function so heapShaderFamily can splice it into the family
|
|
812
|
+
// wrapper.
|
|
813
|
+
return compileHeapEffectIRViaDecoder(userEffect, layout, compileOptions, mode);
|
|
814
|
+
|
|
815
|
+
// ────────── legacy path (no longer reached) ──────────
|
|
816
|
+
// Kept below temporarily for reference during the migration. The
|
|
817
|
+
// unreachable code is dead-code-eliminated by the TS compiler.
|
|
818
|
+
// eslint-disable-next-line no-unreachable
|
|
809
819
|
const heapShaped = userEffect.substitute({
|
|
810
820
|
vertex: {
|
|
811
821
|
inputs: buildVertexAttributeMap(layout),
|
|
@@ -880,6 +890,307 @@ export function compileHeapEffectIR(
|
|
|
880
890
|
};
|
|
881
891
|
}
|
|
882
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Composition-based heap rewrite (standalone mode).
|
|
895
|
+
*
|
|
896
|
+
* Pipeline:
|
|
897
|
+
* 1. Merge user effect stages into one Module.
|
|
898
|
+
* 2. `uniformsToInputs` — every per-RO uniform name from the bucket
|
|
899
|
+
* layout gets renamed from Uniform scope to Input scope (so the
|
|
900
|
+
* decoder VS writes it into the carrier, and the rename is visible
|
|
901
|
+
* to BOTH the VS and the FS sides simultaneously). The pass also
|
|
902
|
+
* surfaces matching `EntryParameter` declarations on every entry
|
|
903
|
+
* whose body reads them — required so `extractFusedEntry`'s State-
|
|
904
|
+
* struct construction sees them as ports.
|
|
905
|
+
* 3. Synthesise the heap decoder VS Module from the layout
|
|
906
|
+
* (`synthesizeHeapDecoderModule`). The decoder owns the megacall
|
|
907
|
+
* search and writes every drawHeader field — per-vertex
|
|
908
|
+
* attributes, per-RO uniforms, per-instance variants, and atlas
|
|
909
|
+
* texture sub-fields — onto the inter-stage carrier.
|
|
910
|
+
* 4. Concatenate values: `[heapDecls, decoderEntry, ...userModule.values]`.
|
|
911
|
+
* `composeStages` (run by `compileModule`) fuses the decoder VS
|
|
912
|
+
* with the user VS via the standard State-struct path.
|
|
913
|
+
* 5. `rewriteFsAtlasTexturesViaCarrier` — FS-side substitution only:
|
|
914
|
+
* every `textureSample(t, smp, uv)` for an atlas-routed binding
|
|
915
|
+
* becomes `atlasSample(Input._h_<t>PageRef, …, uv)`. The decoder
|
|
916
|
+
* already wrote those carrier slots in step 3 so no VS-side writes
|
|
917
|
+
* are needed.
|
|
918
|
+
* 6. `liftReturns` + `injectVsBuiltins` + `compileModule`.
|
|
919
|
+
*
|
|
920
|
+
* No post-emit string mangling — `applyMegacallToEmittedVs` /
|
|
921
|
+
* `applyFamilyMemberShape` are gone for this path.
|
|
922
|
+
*/
|
|
923
|
+
function compileHeapEffectIRViaDecoder(
|
|
924
|
+
userEffect: Effect,
|
|
925
|
+
layout: BucketLayout,
|
|
926
|
+
compileOptions: CompileOptions,
|
|
927
|
+
mode: HeapEffectEmitMode = "standalone",
|
|
928
|
+
): { vs: string; fs: string; preludeWgsl: string; vsEntry: string; fsEntry: string } {
|
|
929
|
+
// 1. Merge user stages.
|
|
930
|
+
let userModule: Module = mergeStages(userEffect);
|
|
931
|
+
|
|
932
|
+
// 2. Rename per-RO uniforms (Uniform → Input). The set includes:
|
|
933
|
+
//
|
|
934
|
+
// - every uniform-ref drawHeader name (post-RO uniforms the decoder
|
|
935
|
+
// loads per draw),
|
|
936
|
+
// - every attribute-ref drawHeader name (Sg.instanced's
|
|
937
|
+
// `instanceUniforms` rewrites these from Uniform → Input scope
|
|
938
|
+
// at Effect.compile time, but `mergeStages` accesses the raw
|
|
939
|
+
// stage template that still carries the original `Uniform` decl
|
|
940
|
+
// + Uniform-scope reads; renaming covers both shapes so neither
|
|
941
|
+
// a stale Uniform decl nor a stale Uniform-scope read survives).
|
|
942
|
+
//
|
|
943
|
+
// Names not in the drawHeader stay untouched (currently none in the
|
|
944
|
+
// heap-everything path).
|
|
945
|
+
const uniformRefNames = new Set<string>();
|
|
946
|
+
for (const f of layout.drawHeaderFields) {
|
|
947
|
+
if (f.kind === "uniform-ref" || f.kind === "attribute-ref") uniformRefNames.add(f.name);
|
|
948
|
+
}
|
|
949
|
+
userModule = uniformsToInputs(userModule, uniformRefNames);
|
|
950
|
+
|
|
951
|
+
// 3. Build the decoder Module. Mode flows through: standalone owns
|
|
952
|
+
// the megacall search; family-member receives drawIdx/instId/vid
|
|
953
|
+
// from the wrapper VS as inputs and skips the search.
|
|
954
|
+
const decoderModule = synthesizeHeapDecoderModule(layout, mode);
|
|
955
|
+
|
|
956
|
+
// 4. Concatenate. Decoder values come first so the storage-buffer
|
|
957
|
+
// bindings land at @binding(0)..@binding(6) consistently across
|
|
958
|
+
// every emitted shader (the WGSL emit walks values in order and
|
|
959
|
+
// assigns binding slots).
|
|
960
|
+
let combined: Module = {
|
|
961
|
+
types: [...decoderModule.types, ...userModule.types],
|
|
962
|
+
values: [...decoderModule.values, ...userModule.values],
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// 5. Standard preprocessing.
|
|
966
|
+
combined = liftReturns(combined);
|
|
967
|
+
combined = injectVsBuiltins(combined);
|
|
968
|
+
|
|
969
|
+
// 6. FS-side atlas rewrite (texture-sample → atlas-sample, reading
|
|
970
|
+
// from the carrier varyings the decoder wrote).
|
|
971
|
+
combined = rewriteFsAtlasTexturesViaCarrier(combined, layout);
|
|
972
|
+
|
|
973
|
+
// 7. Compile.
|
|
974
|
+
const compiled = compileModule(combined, compileOptions);
|
|
975
|
+
if ((globalThis as { __HEAP_DEBUG_FULL__?: boolean }).__HEAP_DEBUG_FULL__) {
|
|
976
|
+
for (const s of compiled.stages) console.log(`[heap-debug-full] ${s.stage}:\n${s.source}`);
|
|
977
|
+
}
|
|
978
|
+
const vsStage = compiled.stages.find(s => s.stage === "vertex");
|
|
979
|
+
const fsStage = compiled.stages.find(s => s.stage === "fragment");
|
|
980
|
+
let vsSrc = vsStage?.source ?? "";
|
|
981
|
+
let fsSrc = fsStage?.source ?? "";
|
|
982
|
+
|
|
983
|
+
// 8. Family-member mode: re-shape the @vertex / @fragment entries
|
|
984
|
+
// into plain function helpers the family wrapper can call. The body
|
|
985
|
+
// is already correct (decoder + user-VS fused, all carrier values
|
|
986
|
+
// computed); the entry headers just need to lose their stage
|
|
987
|
+
// decorations and let location-decorated params become plain u32
|
|
988
|
+
// function arguments.
|
|
989
|
+
if (mode === "family-member") {
|
|
990
|
+
if (vsSrc.length > 0) vsSrc = reshapeFamilyMemberVs(vsSrc);
|
|
991
|
+
if (fsSrc.length > 0) fsSrc = reshapeFamilyMemberFs(fsSrc);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 9. Final WGSL hygiene: atlas-routed texture/sampler bindings are
|
|
995
|
+
// semantically gone (all sampling now goes through the atlas
|
|
996
|
+
// helpers + page arrays), but emit may still produce decls for
|
|
997
|
+
// them. Strip + prepend the atlas-sample helper library on FS.
|
|
998
|
+
if (layout.atlasTextureBindings.size > 0) {
|
|
999
|
+
if (vsSrc.length > 0) vsSrc = stripAtlasTextureSamplerDecls(vsSrc, layout.atlasTextureBindings);
|
|
1000
|
+
if (fsSrc.length > 0) {
|
|
1001
|
+
fsSrc = stripAtlasTextureSamplerDecls(fsSrc, layout.atlasTextureBindings);
|
|
1002
|
+
fsSrc = prependAtlasPrelude(fsSrc);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
vs: vsSrc,
|
|
1008
|
+
fs: fsSrc,
|
|
1009
|
+
vsEntry: vsStage?.entryName ?? "vs",
|
|
1010
|
+
fsEntry: fsStage?.entryName ?? "fs",
|
|
1011
|
+
preludeWgsl: "",
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Family-member VS reshape — surface `(heap_drawIdx, instId, vid)` as
|
|
1017
|
+
* plain `u32` parameters of the helper fn so the family wrapper VS
|
|
1018
|
+
* can call it directly with the megacall-search outputs.
|
|
1019
|
+
*
|
|
1020
|
+
* The decoder synthesised the three names as `@location(N)`-decorated
|
|
1021
|
+
* inputs. After composeStages they live inside the entry's input
|
|
1022
|
+
* struct as `in.heap_drawIdx` / `in.instId` / `in.vid`. This pass:
|
|
1023
|
+
*
|
|
1024
|
+
* 1. Replaces the `(in: <InputStruct>)` parameter with three
|
|
1025
|
+
* plain `u32` parameters by the same names.
|
|
1026
|
+
* 2. Inserts a body prologue that builds the input struct locally
|
|
1027
|
+
* and seeds its three megacall fields from the new params, so
|
|
1028
|
+
* the rest of the body (referring to `in.X`) keeps working.
|
|
1029
|
+
*
|
|
1030
|
+
* `@vertex`/`@fragment` decorations are NOT stripped here —
|
|
1031
|
+
* `heapShaderFamily.stripStageDecoration` does that during wrapper
|
|
1032
|
+
* assembly. Leaving them lets `readEntryName` find the helper name.
|
|
1033
|
+
*
|
|
1034
|
+
* DCE may have dropped one or both of `instId` / `vid` from the
|
|
1035
|
+
* input struct when nothing in the fused body reads them. The
|
|
1036
|
+
* pass detects this and emits a parameter only for fields that
|
|
1037
|
+
* survived; the wrapper can still pass three args (extras are
|
|
1038
|
+
* ignored by WGSL only if the signature accepts them — actually
|
|
1039
|
+
* they're not, so we DO need to keep all three names so the
|
|
1040
|
+
* wrapper's `family_X(drawIdx, instId, vid)` call typechecks).
|
|
1041
|
+
*
|
|
1042
|
+
* The three params are always emitted regardless of liveness; an
|
|
1043
|
+
* unused param is harmless and keeps the wrapper call site stable.
|
|
1044
|
+
*/
|
|
1045
|
+
function reshapeFamilyMemberVs(src: string): string {
|
|
1046
|
+
return reshapeFamilyMember(src, /^@vertex\s/m);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Family-member FS reshape — currently same shape as VS. */
|
|
1050
|
+
function reshapeFamilyMemberFs(src: string): string {
|
|
1051
|
+
return reshapeFamilyMember(src, /^@fragment\s/m);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function reshapeFamilyMember(src: string, stageRe: RegExp): string {
|
|
1055
|
+
// Locate the @vertex / @fragment fn declaration.
|
|
1056
|
+
const stageMatch = stageRe.exec(src);
|
|
1057
|
+
if (stageMatch === null) return src;
|
|
1058
|
+
const fnRe = /fn\s+(\w+)\s*\(([^)]*)\)\s*(->\s*\w+)?\s*\{/y;
|
|
1059
|
+
fnRe.lastIndex = stageMatch.index + stageMatch[0].length;
|
|
1060
|
+
// Walk back over whitespace between the stage attr and `fn`.
|
|
1061
|
+
let i = fnRe.lastIndex;
|
|
1062
|
+
while (/\s/.test(src[i - 1] ?? "")) i--;
|
|
1063
|
+
fnRe.lastIndex = i;
|
|
1064
|
+
const fnMatch = fnRe.exec(src);
|
|
1065
|
+
if (fnMatch === null) {
|
|
1066
|
+
// Try without leading-whitespace assumption — find the next `fn ...`.
|
|
1067
|
+
const loose = /fn\s+(\w+)\s*\(([^)]*)\)\s*(->\s*\w+)?\s*\{/.exec(src.slice(stageMatch.index));
|
|
1068
|
+
if (loose === null) return src;
|
|
1069
|
+
return rewriteFn(src, stageMatch.index + loose.index!, loose);
|
|
1070
|
+
}
|
|
1071
|
+
return rewriteFn(src, fnMatch.index, fnMatch);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function rewriteFn(src: string, fnStart: number, fnMatch: RegExpExecArray): string {
|
|
1075
|
+
const fnName = fnMatch[1]!;
|
|
1076
|
+
const paramsRaw = fnMatch[2]!;
|
|
1077
|
+
const returnPart = fnMatch[3] ?? "";
|
|
1078
|
+
const params = paramsRaw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1079
|
+
|
|
1080
|
+
// Identify the `in: <InputStruct>` param (if any).
|
|
1081
|
+
let inStructName: string | undefined;
|
|
1082
|
+
const otherParams: string[] = [];
|
|
1083
|
+
for (const p of params) {
|
|
1084
|
+
const m = /^in\s*:\s*(\w+)$/.exec(p);
|
|
1085
|
+
if (m !== null && inStructName === undefined) {
|
|
1086
|
+
inStructName = m[1]!;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
otherParams.push(p);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// New parameter list: three megacall params first, then any other
|
|
1093
|
+
// params (rare; usually only `in`).
|
|
1094
|
+
const newParams = [
|
|
1095
|
+
"heap_drawIdx: u32",
|
|
1096
|
+
"instId: u32",
|
|
1097
|
+
"vid: u32",
|
|
1098
|
+
...otherParams,
|
|
1099
|
+
].join(", ");
|
|
1100
|
+
|
|
1101
|
+
// Body prologue: only needed when the original entry actually had an
|
|
1102
|
+
// input struct that the body reads via `in.X`. We inject a local
|
|
1103
|
+
// `in` and seed its three megacall fields. Any other fields the
|
|
1104
|
+
// entry declared (vertex attribute Inputs, post-rename uniforms) are
|
|
1105
|
+
// satisfied via the fused State struct — they never read from `in`.
|
|
1106
|
+
const prologue = inStructName === undefined
|
|
1107
|
+
? ""
|
|
1108
|
+
: `\n var in: ${inStructName};\n in.heap_drawIdx = heap_drawIdx;\n in.instId = instId;\n in.vid = vid;\n`;
|
|
1109
|
+
|
|
1110
|
+
// Splice the new signature in.
|
|
1111
|
+
const before = src.slice(0, fnStart);
|
|
1112
|
+
const headerLen = fnMatch[0]!.length;
|
|
1113
|
+
// The match captured up to and including the `{`. Anything inside the
|
|
1114
|
+
// body follows.
|
|
1115
|
+
const bodyStart = fnStart + headerLen;
|
|
1116
|
+
const newHeader = `fn ${fnName}(${newParams})${returnPart ? " " + returnPart : ""} {${prologue}`;
|
|
1117
|
+
return before + newHeader + src.slice(bodyStart);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* FS-side atlas rewrite for the decoder-composition path.
|
|
1122
|
+
*
|
|
1123
|
+
* The decoder VS has already written `_h_<name>{PageRef,FormatBits,
|
|
1124
|
+
* Origin,Size}` onto the inter-stage carrier. This pass:
|
|
1125
|
+
* 1. Rewrites each `textureSample(t, smp, uv)` (or shipped
|
|
1126
|
+
* `texture(...)` shorthand) for an atlas-routed binding into the
|
|
1127
|
+
* atlas-sample intrinsic reading from `Input.<carrier name>`.
|
|
1128
|
+
* 2. Adds matching `EntryParameter`s on every FS Entry so the WGSL
|
|
1129
|
+
* emit produces an Input struct that actually contains the
|
|
1130
|
+
* `_h_<name><Sub>` fields the body reads.
|
|
1131
|
+
*
|
|
1132
|
+
* Unlike the legacy `rewriteFsAtlasTextures`, this does NOT inject
|
|
1133
|
+
* VS-side WriteOutputs (the decoder writes them already).
|
|
1134
|
+
*/
|
|
1135
|
+
function rewriteFsAtlasTexturesViaCarrier(m: Module, layout: BucketLayout): Module {
|
|
1136
|
+
if (layout.atlasTextureBindings.size === 0) return m;
|
|
1137
|
+
|
|
1138
|
+
// Find which atlas-routed names the FS actually samples — only those
|
|
1139
|
+
// need carrier declarations and rewrites.
|
|
1140
|
+
const used = new Set<string>();
|
|
1141
|
+
for (const v of m.values) {
|
|
1142
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
1143
|
+
visitStmtExprs(v.entry.body, e => {
|
|
1144
|
+
const hit = isAtlasSampleCall(e, layout.atlasTextureBindings);
|
|
1145
|
+
if (hit !== null) used.add(hit.name);
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
if (used.size === 0) return m;
|
|
1149
|
+
|
|
1150
|
+
// Build the four carrier inputs per used texture. Flat interpolation
|
|
1151
|
+
// — integer types require it and the floats are constant per draw.
|
|
1152
|
+
const fsExtras: EntryParameter[] = [];
|
|
1153
|
+
for (const name of used) {
|
|
1154
|
+
const v = atlasVaryingNames(name);
|
|
1155
|
+
const flat: ParamDecoration = { kind: "Interpolation", mode: "flat" };
|
|
1156
|
+
fsExtras.push({ name: v.pageRef, type: Tu32, semantic: v.pageRef, decorations: [flat] });
|
|
1157
|
+
fsExtras.push({ name: v.formatBits, type: Tu32, semantic: v.formatBits, decorations: [flat] });
|
|
1158
|
+
fsExtras.push({ name: v.origin, type: Tvec2, semantic: v.origin, decorations: [flat] });
|
|
1159
|
+
fsExtras.push({ name: v.size, type: Tvec2, semantic: v.size, decorations: [flat] });
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Add the EntryParameter declarations to every FS entry's inputs.
|
|
1163
|
+
let out = m;
|
|
1164
|
+
out = {
|
|
1165
|
+
...out,
|
|
1166
|
+
values: out.values.map((vv) => {
|
|
1167
|
+
if (vv.kind !== "Entry" || vv.entry.stage !== "fragment") return vv;
|
|
1168
|
+
const haveInput = new Set(vv.entry.inputs.map(p => p.name));
|
|
1169
|
+
const additions = fsExtras.filter(p => !haveInput.has(p.name));
|
|
1170
|
+
if (additions.length === 0) return vv;
|
|
1171
|
+
return { ...vv, entry: { ...vv.entry, inputs: [...vv.entry.inputs, ...additions] } };
|
|
1172
|
+
}),
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Body rewrite: textureSample → atlasSample(Input.pageRef, …, uv).
|
|
1176
|
+
out = mapStageEntryBodies(out, "fragment", body => mapStmt(body, {
|
|
1177
|
+
expr: e => mapExpr(e, sub => {
|
|
1178
|
+
const hit = isAtlasSampleCall(sub, layout.atlasTextureBindings);
|
|
1179
|
+
if (hit === null) return sub;
|
|
1180
|
+
const v = atlasVaryingNames(hit.name);
|
|
1181
|
+
const args: Expr[] = [
|
|
1182
|
+
readScope("Input", v.pageRef, Tu32),
|
|
1183
|
+
readScope("Input", v.formatBits, Tu32),
|
|
1184
|
+
readScope("Input", v.origin, Tvec2),
|
|
1185
|
+
readScope("Input", v.size, Tvec2),
|
|
1186
|
+
hit.uv,
|
|
1187
|
+
];
|
|
1188
|
+
return { kind: "CallIntrinsic", op: ATLAS_SAMPLE_INTRINSIC, args, type: Tvec4 };
|
|
1189
|
+
}),
|
|
1190
|
+
}));
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
883
1194
|
/**
|
|
884
1195
|
* Drop `@group(N) @binding(M) var <name>: texture_*<...>;` and the
|
|
885
1196
|
* matching sampler decl(s) for atlas-routed binding names. We keep
|
|
@@ -40,6 +40,7 @@ import type { BufferView } from "../core/bufferView.js";
|
|
|
40
40
|
import type { ITexture } from "../core/texture.js";
|
|
41
41
|
import type { ISampler } from "../core/sampler.js";
|
|
42
42
|
import type { RenderObject } from "../core/renderObject.js";
|
|
43
|
+
import { asAttributeProvider } from "../core/provider.js";
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Maximum texture extent (per side) the heap path is willing to
|
|
@@ -82,6 +83,8 @@ const isHeapServableSampler = (_s: ISampler): boolean => true;
|
|
|
82
83
|
*/
|
|
83
84
|
function isHeapServableTexture(t: ITexture): boolean {
|
|
84
85
|
if (t.kind === "gpu") return false;
|
|
86
|
+
// URL-deferred — the Sg layer hands us a placeholder while it loads.
|
|
87
|
+
if (t.kind === "url") return false;
|
|
85
88
|
const src = t.source;
|
|
86
89
|
if (src.kind === "raw") {
|
|
87
90
|
if ((src.depthOrArrayLayers ?? 1) !== 1) return false;
|
|
@@ -107,11 +110,21 @@ function isHeapServableTexture(t: ITexture): boolean {
|
|
|
107
110
|
return true;
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
// Materialise every attribute view a provider knows about. Eligibility
|
|
114
|
+
// is decided before any shader is compiled, so we can't pull
|
|
115
|
+
// shader-driven here — we enumerate. Attribute providers are map-backed
|
|
116
|
+
// in practice, so `names()` is the full set and `tryGet` is a map hit.
|
|
117
|
+
function attrViews(p: { names(): Iterable<string>; tryGet(n: string): BufferView | undefined }): BufferView[] {
|
|
118
|
+
const out: BufferView[] = [];
|
|
119
|
+
for (const n of p.names()) { const v = p.tryGet(n); if (v !== undefined) out.push(v); }
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
function bufferAvals(ro: RenderObject): aval<IBuffer>[] {
|
|
111
124
|
const out: aval<IBuffer>[] = [];
|
|
112
|
-
|
|
125
|
+
for (const v of attrViews(asAttributeProvider(ro.vertexAttributes))) out.push(v.buffer);
|
|
113
126
|
if (ro.instanceAttributes !== undefined) {
|
|
114
|
-
|
|
127
|
+
for (const v of attrViews(asAttributeProvider(ro.instanceAttributes))) out.push(v.buffer);
|
|
115
128
|
}
|
|
116
129
|
if (ro.indices !== undefined) out.push(ro.indices.buffer);
|
|
117
130
|
return out;
|
|
@@ -139,7 +152,23 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
139
152
|
if (ro.storageBuffers !== undefined && ro.storageBuffers.count > 0) {
|
|
140
153
|
return AVal.constant(false);
|
|
141
154
|
}
|
|
142
|
-
|
|
155
|
+
// Heap path serves at most one distinct texture + sampler per bucket
|
|
156
|
+
// (the megacall draw has one bind group). The Sg compile layer
|
|
157
|
+
// over-binds each texture under both `name` and `${name}_view` (so
|
|
158
|
+
// whichever name the WGSL schema lands on resolves), so dedupe by
|
|
159
|
+
// aval identity before applying the single-binding rule — otherwise
|
|
160
|
+
// every textured leaf would be `count > 1` and fall to the legacy
|
|
161
|
+
// per-RO path, which at scale (thousands of textured ROs ⇒ thousands
|
|
162
|
+
// of draw calls + bind-group switches) is far slower than the heap
|
|
163
|
+
// megacall sampling one shared atlas page.
|
|
164
|
+
const distinctTextureAvals = new Set<aval<ITexture>>();
|
|
165
|
+
ro.textures.iter((_n, av) => { distinctTextureAvals.add(av); });
|
|
166
|
+
if (distinctTextureAvals.size > 1) {
|
|
167
|
+
return AVal.constant(false);
|
|
168
|
+
}
|
|
169
|
+
const distinctSamplerAvals = new Set<aval<ISampler>>();
|
|
170
|
+
ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av); });
|
|
171
|
+
if (distinctSamplerAvals.size > 1) {
|
|
143
172
|
return AVal.constant(false);
|
|
144
173
|
}
|
|
145
174
|
|
|
@@ -155,9 +184,9 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
|
|
|
155
184
|
const isBroadcast = v.singleValue !== undefined || stride === 0;
|
|
156
185
|
if (!isBroadcast && stride !== v.elementType.byteSize) strideBlocked = true;
|
|
157
186
|
};
|
|
158
|
-
|
|
187
|
+
for (const v of attrViews(asAttributeProvider(ro.vertexAttributes))) checkStride(v);
|
|
159
188
|
if (ro.instanceAttributes !== undefined) {
|
|
160
|
-
|
|
189
|
+
for (const v of attrViews(asAttributeProvider(ro.instanceAttributes))) checkStride(v);
|
|
161
190
|
}
|
|
162
191
|
if (strideBlocked) return AVal.constant(false);
|
|
163
192
|
|