@plasius/gpu-worker 0.1.9 → 0.1.11

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/CHANGELOG.md CHANGED
@@ -20,6 +20,47 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
20
20
  - **Security**
21
21
  - (placeholder)
22
22
 
23
+ ## [0.1.11] - 2026-03-15
24
+
25
+ - **Added**
26
+ - ADR, TDR, and test-first planning coverage for snapshot-driven
27
+ scene-preparation DAG manifests in the ray-tracing-first world pipeline.
28
+ - Added `createScenePreparationManifest(...)` plus scene-preparation
29
+ representation-band and stage-family exports for snapshot-driven chunk DAG
30
+ planning.
31
+ - Added contract tests covering stable snapshot roots, multi-chunk priority
32
+ lanes, local joins, and render-preparation safety guards.
33
+
34
+ - **Changed**
35
+ - TDR-0004 now reflects the implemented scene-preparation manifest helper.
36
+
37
+ - **Fixed**
38
+ - (placeholder)
39
+
40
+ - **Security**
41
+ - (placeholder)
42
+
43
+ ## [0.1.10] - 2026-03-14
44
+
45
+ - **Added**
46
+ - (placeholder)
47
+
48
+ - **Changed**
49
+ - DAG queue guidance now treats package manifests as explicit multi-root DAG
50
+ node definitions with priority-lane mapping, rather than loose dependency
51
+ hints.
52
+ - Raised the minimum `@plasius/gpu-lock-free-queue` dependency to `^0.2.14`
53
+ so published installs resolve the new DAG graph metadata through npm.
54
+ - Updated GitHub Actions workflows to run JavaScript actions on Node 24,
55
+ refreshed core workflow action versions, and switched Codecov uploads to
56
+ the Codecov CLI.
57
+
58
+ - **Fixed**
59
+ - (placeholder)
60
+
61
+ - **Security**
62
+ - (placeholder)
63
+
23
64
  ## [0.1.9] - 2026-03-13
24
65
 
25
66
  - **Added**
@@ -189,7 +230,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
189
230
 
190
231
  ---
191
232
 
192
- [Unreleased]: https://github.com/Plasius-LTD/gpu-worker/compare/v0.1.9...HEAD
233
+ [Unreleased]: https://github.com/Plasius-LTD/gpu-worker/compare/v0.1.11...HEAD
193
234
  [0.1.0-beta.1]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.0-beta.1
194
235
  [0.1.0]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.0
195
236
  [0.1.2]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.2
@@ -211,3 +252,5 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
211
252
  [0.1.4]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.4
212
253
  [0.1.8]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.8
213
254
  [0.1.9]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.9
255
+ [0.1.10]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.10
256
+ [0.1.11]: https://github.com/Plasius-LTD/gpu-worker/releases/tag/v0.1.11
package/README.md CHANGED
@@ -158,6 +158,39 @@ const loop = createWorkerLoop({
158
158
  });
159
159
  ```
160
160
 
161
+ `@plasius/gpu-worker` also now exposes a scene-preparation manifest helper for
162
+ snapshot-driven chunk DAG planning:
163
+
164
+ ```js
165
+ import { createScenePreparationManifest } from "@plasius/gpu-worker";
166
+
167
+ const manifest = createScenePreparationManifest({
168
+ snapshotId: "visual-snapshot-42",
169
+ chunks: [
170
+ {
171
+ chunkId: "chunk-near-0",
172
+ representationBand: "near",
173
+ gameplayImportance: "critical",
174
+ visible: true,
175
+ playerRelevant: true,
176
+ },
177
+ {
178
+ chunkId: "chunk-far-3",
179
+ representationBand: "far",
180
+ gameplayImportance: "medium",
181
+ visible: false,
182
+ },
183
+ ],
184
+ });
185
+
186
+ console.log(manifest.graph.chunkRoots);
187
+ console.log(manifest.graph.priorityLanes[0]);
188
+ ```
189
+
190
+ The helper publishes one chunk-local DAG per stable snapshot, keeps joins local
191
+ to chunk stage boundaries, and rejects render-preparation manifests that try to
192
+ mutate authoritative simulation state.
193
+
161
194
  ## What this is
162
195
  - A minimal GPU worker layer that combines a lock-free queue with user WGSL jobs.
163
196
  - A helper to assemble WGSL modules with queue helpers included.
@@ -176,6 +209,14 @@ budgets or decide priorities itself. Instead it exposes the queue mode and the
176
209
  completion hook needed by package manifests and `@plasius/gpu-performance` to
177
210
  coordinate DAG-shaped workloads.
178
211
 
212
+ Package manifests should be treated as explicit DAG node definitions, not just
213
+ loose hints. In practice that means:
214
+
215
+ - multiple roots are allowed and expected,
216
+ - manifest labels act as dependency ids,
217
+ - priorities map to ready-queue lanes,
218
+ - downstream jobs are unlocked only when every upstream dependency completes.
219
+
179
220
  ## Package Integration Model
180
221
 
181
222
  `@plasius/gpu-worker` is the preferred execution plane for discrete GPU work
package/dist/index.cjs CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
32
  assembleWorkerWgsl: () => assembleWorkerWgsl,
33
+ createScenePreparationManifest: () => createScenePreparationManifest,
33
34
  createWorkerLoop: () => createWorkerLoop,
34
35
  loadJobWgsl: () => loadJobWgsl,
35
36
  loadQueueWgsl: () => loadQueueWgsl,
36
37
  loadWorkerWgsl: () => loadWorkerWgsl,
38
+ scenePreparationRepresentationBands: () => scenePreparationRepresentationBands,
39
+ scenePreparationStageFamilies: () => scenePreparationStageFamilies,
37
40
  workerWgslUrl: () => workerWgslUrl
38
41
  });
39
42
  module.exports = __toCommonJS(index_exports);
@@ -693,13 +696,324 @@ function createWorkerLoop(options = {}) {
693
696
  }
694
697
  };
695
698
  }
699
+ var scenePreparationRepresentationBands = Object.freeze([
700
+ "near",
701
+ "mid",
702
+ "far",
703
+ "horizon"
704
+ ]);
705
+ var scenePreparationStageFamilies = Object.freeze([
706
+ "snapshotSelection",
707
+ "transformPropagation",
708
+ "animationPose",
709
+ "proceduralAnimation",
710
+ "skinningOrDeformation",
711
+ "boundsUpdate",
712
+ "lodSelection",
713
+ "rtRepresentationSelection",
714
+ "visibility",
715
+ "lightAssignment",
716
+ "renderProxyBuild",
717
+ "rtInstancePreparation"
718
+ ]);
719
+ var scenePreparationDefaultStageDependencies = Object.freeze({
720
+ snapshotSelection: Object.freeze([]),
721
+ transformPropagation: Object.freeze(["snapshotSelection"]),
722
+ animationPose: Object.freeze(["transformPropagation"]),
723
+ proceduralAnimation: Object.freeze(["animationPose"]),
724
+ skinningOrDeformation: Object.freeze([
725
+ "animationPose",
726
+ "proceduralAnimation"
727
+ ]),
728
+ boundsUpdate: Object.freeze(["skinningOrDeformation"]),
729
+ lodSelection: Object.freeze(["boundsUpdate"]),
730
+ rtRepresentationSelection: Object.freeze(["lodSelection"]),
731
+ visibility: Object.freeze(["boundsUpdate", "lodSelection"]),
732
+ lightAssignment: Object.freeze(["visibility"]),
733
+ renderProxyBuild: Object.freeze(["visibility", "lodSelection"]),
734
+ rtInstancePreparation: Object.freeze([
735
+ "visibility",
736
+ "rtRepresentationSelection"
737
+ ])
738
+ });
739
+ var scenePreparationBandPriorityWeights = Object.freeze({
740
+ near: 400,
741
+ mid: 300,
742
+ far: 200,
743
+ horizon: 100
744
+ });
745
+ var scenePreparationImportanceWeights = Object.freeze({
746
+ low: 0,
747
+ medium: 15,
748
+ high: 30,
749
+ critical: 60
750
+ });
751
+ var scenePreparationStagePriorityWeights = Object.freeze({
752
+ snapshotSelection: 60,
753
+ transformPropagation: 54,
754
+ animationPose: 50,
755
+ proceduralAnimation: 46,
756
+ skinningOrDeformation: 42,
757
+ boundsUpdate: 38,
758
+ lodSelection: 32,
759
+ rtRepresentationSelection: 28,
760
+ visibility: 34,
761
+ lightAssignment: 24,
762
+ renderProxyBuild: 22,
763
+ rtInstancePreparation: 26
764
+ });
765
+ function assertScenePreparationIdentifier(name, value) {
766
+ if (typeof value !== "string" || value.trim().length === 0) {
767
+ throw new Error(`${name} must be a non-empty string.`);
768
+ }
769
+ return value.trim();
770
+ }
771
+ function assertScenePreparationEnum(name, value, allowed) {
772
+ const normalized = assertScenePreparationIdentifier(name, value);
773
+ if (!allowed.includes(normalized)) {
774
+ throw new Error(`${name} must be one of: ${allowed.join(", ")}.`);
775
+ }
776
+ return normalized;
777
+ }
778
+ function normalizeScenePreparationStages(stages, chunkLabel) {
779
+ const requested = stages === void 0 ? scenePreparationStageFamilies : stages;
780
+ if (!Array.isArray(requested) || requested.length === 0) {
781
+ throw new Error(`${chunkLabel}.stages must be a non-empty array when provided.`);
782
+ }
783
+ const normalized = [...new Set(
784
+ requested.map(
785
+ (stage, index) => assertScenePreparationEnum(
786
+ `${chunkLabel}.stages[${index}]`,
787
+ stage,
788
+ scenePreparationStageFamilies
789
+ )
790
+ )
791
+ )];
792
+ return normalized.sort(
793
+ (left, right) => scenePreparationStageFamilies.indexOf(left) - scenePreparationStageFamilies.indexOf(right)
794
+ );
795
+ }
796
+ function collectScenePreparationDependencies(stageFamily, includedStages, seen = /* @__PURE__ */ new Set()) {
797
+ const dependencies = scenePreparationDefaultStageDependencies[stageFamily] ?? [];
798
+ for (const dependency of dependencies) {
799
+ if (includedStages.has(dependency)) {
800
+ seen.add(dependency);
801
+ continue;
802
+ }
803
+ collectScenePreparationDependencies(dependency, includedStages, seen);
804
+ }
805
+ return [...seen].sort(
806
+ (left, right) => scenePreparationStageFamilies.indexOf(left) - scenePreparationStageFamilies.indexOf(right)
807
+ );
808
+ }
809
+ function buildScenePreparationPriority(chunk, stageFamily) {
810
+ const bandWeight = scenePreparationBandPriorityWeights[chunk.representationBand] ?? 0;
811
+ const importanceWeight = scenePreparationImportanceWeights[chunk.gameplayImportance] ?? 0;
812
+ const stageWeight = scenePreparationStagePriorityWeights[stageFamily] ?? 0;
813
+ return bandWeight + importanceWeight + stageWeight + (chunk.visible ? 20 : 0) + (chunk.playerRelevant ? 20 : 0) + (chunk.imageCritical ? 15 : 0);
814
+ }
815
+ function buildScenePreparationPriorityLanes(jobs) {
816
+ const lanes = /* @__PURE__ */ new Map();
817
+ for (const job of jobs) {
818
+ const lane = lanes.get(job.priority) ?? {
819
+ priority: job.priority,
820
+ jobIds: [],
821
+ chunkIds: []
822
+ };
823
+ lane.jobIds.push(job.id);
824
+ if (!lane.chunkIds.includes(job.chunkId)) {
825
+ lane.chunkIds.push(job.chunkId);
826
+ }
827
+ lanes.set(job.priority, lane);
828
+ }
829
+ return Object.freeze(
830
+ [...lanes.values()].sort((left, right) => right.priority - left.priority).map(
831
+ (lane) => Object.freeze({
832
+ priority: lane.priority,
833
+ jobIds: Object.freeze([...lane.jobIds]),
834
+ chunkIds: Object.freeze([...lane.chunkIds]),
835
+ jobCount: lane.jobIds.length
836
+ })
837
+ )
838
+ );
839
+ }
840
+ function buildScenePreparationTopologicalOrder(jobs) {
841
+ const indegree = new Map(jobs.map((job) => [job.id, job.dependencies.length]));
842
+ const dependentsById = new Map(jobs.map((job) => [job.id, []]));
843
+ for (const job of jobs) {
844
+ for (const dependency of job.dependencies) {
845
+ dependentsById.get(dependency)?.push(job.id);
846
+ }
847
+ }
848
+ const queue = jobs.filter((job) => job.dependencies.length === 0).sort((left, right) => right.priority - left.priority).map((job) => job.id);
849
+ const jobById = new Map(jobs.map((job) => [job.id, job]));
850
+ const order = [];
851
+ while (queue.length > 0) {
852
+ const currentId = queue.shift();
853
+ if (!currentId) {
854
+ continue;
855
+ }
856
+ order.push(currentId);
857
+ const unlocked = [];
858
+ for (const dependentId of dependentsById.get(currentId) ?? []) {
859
+ const next = (indegree.get(dependentId) ?? 0) - 1;
860
+ indegree.set(dependentId, next);
861
+ if (next === 0) {
862
+ unlocked.push(dependentId);
863
+ }
864
+ }
865
+ unlocked.sort(
866
+ (left, right) => (jobById.get(right)?.priority ?? 0) - (jobById.get(left)?.priority ?? 0)
867
+ ).forEach((jobId) => {
868
+ queue.push(jobId);
869
+ });
870
+ }
871
+ if (order.length !== jobs.length) {
872
+ throw new Error("Scene-preparation manifest contains a cycle.");
873
+ }
874
+ return Object.freeze(order);
875
+ }
876
+ function createScenePreparationManifest(options = {}) {
877
+ const snapshotId = assertScenePreparationIdentifier(
878
+ "snapshotId",
879
+ options.snapshotId
880
+ );
881
+ const chunkEntries = Array.isArray(options.chunks) ? options.chunks : [];
882
+ if (chunkEntries.length === 0) {
883
+ throw new Error("createScenePreparationManifest requires at least one chunk.");
884
+ }
885
+ const normalizedChunks = chunkEntries.map((chunk, index) => {
886
+ if (!chunk || typeof chunk !== "object" || Array.isArray(chunk)) {
887
+ throw new Error(`chunks[${index}] must be an object.`);
888
+ }
889
+ const chunkLabel = `chunks[${index}]`;
890
+ if (chunk.mutatesSimulation === true) {
891
+ throw new Error(
892
+ `${chunkLabel}.mutatesSimulation cannot be true for render preparation.`
893
+ );
894
+ }
895
+ return Object.freeze({
896
+ chunkId: assertScenePreparationIdentifier(`${chunkLabel}.chunkId`, chunk.chunkId),
897
+ representationBand: assertScenePreparationEnum(
898
+ `${chunkLabel}.representationBand`,
899
+ chunk.representationBand ?? "mid",
900
+ scenePreparationRepresentationBands
901
+ ),
902
+ gameplayImportance: assertScenePreparationEnum(
903
+ `${chunkLabel}.gameplayImportance`,
904
+ chunk.gameplayImportance ?? "medium",
905
+ Object.keys(scenePreparationImportanceWeights)
906
+ ),
907
+ visible: chunk.visible !== false,
908
+ playerRelevant: chunk.playerRelevant === true,
909
+ imageCritical: chunk.imageCritical === true,
910
+ stages: normalizeScenePreparationStages(chunk.stages, chunkLabel)
911
+ });
912
+ });
913
+ const chunkIds = /* @__PURE__ */ new Set();
914
+ for (const chunk of normalizedChunks) {
915
+ if (chunkIds.has(chunk.chunkId)) {
916
+ throw new Error(`Duplicate scene-preparation chunk id detected: ${chunk.chunkId}`);
917
+ }
918
+ chunkIds.add(chunk.chunkId);
919
+ }
920
+ const jobs = [];
921
+ for (const chunk of normalizedChunks) {
922
+ const includedStages = new Set(chunk.stages);
923
+ for (const stageFamily of chunk.stages) {
924
+ const dependencies = collectScenePreparationDependencies(
925
+ stageFamily,
926
+ includedStages
927
+ ).map(
928
+ (dependency) => `${snapshotId}:${chunk.chunkId}:${chunk.representationBand}:${dependency}`
929
+ );
930
+ jobs.push(
931
+ Object.freeze({
932
+ id: `${snapshotId}:${chunk.chunkId}:${chunk.representationBand}:${stageFamily}`,
933
+ snapshotId,
934
+ chunkId: chunk.chunkId,
935
+ representationBand: chunk.representationBand,
936
+ stageFamily,
937
+ priority: buildScenePreparationPriority(chunk, stageFamily),
938
+ dependencies: Object.freeze(dependencies),
939
+ dependencyCount: dependencies.length,
940
+ root: dependencies.length === 0,
941
+ authority: "visual",
942
+ mutatesSimulation: false,
943
+ gameplayImportance: chunk.gameplayImportance,
944
+ visible: chunk.visible,
945
+ playerRelevant: chunk.playerRelevant,
946
+ imageCritical: chunk.imageCritical
947
+ })
948
+ );
949
+ }
950
+ }
951
+ const jobById = new Map(jobs.map((job) => [job.id, job]));
952
+ let crossChunkDependencyCount = 0;
953
+ let localJoinCount = 0;
954
+ const finalizedJobs = Object.freeze(
955
+ jobs.map((job) => {
956
+ const dependents = jobs.filter((candidate) => candidate.dependencies.includes(job.id)).map((candidate) => candidate.id);
957
+ const crossChunkDependencies = job.dependencies.filter((dependency) => {
958
+ const parent = jobById.get(dependency);
959
+ return parent && parent.chunkId !== job.chunkId;
960
+ });
961
+ crossChunkDependencyCount += crossChunkDependencies.length;
962
+ if (job.dependencies.length > 1 && crossChunkDependencies.length === 0) {
963
+ localJoinCount += 1;
964
+ }
965
+ return Object.freeze({
966
+ ...job,
967
+ dependents: Object.freeze(dependents),
968
+ dependentCount: dependents.length,
969
+ unresolvedDependencyCount: job.dependencies.length,
970
+ localJoin: job.dependencies.length > 1 && crossChunkDependencies.length === 0
971
+ });
972
+ })
973
+ );
974
+ const graph = Object.freeze({
975
+ schedulerMode: "dag",
976
+ jobCount: finalizedJobs.length,
977
+ chunkCount: normalizedChunks.length,
978
+ chunkIds: Object.freeze(normalizedChunks.map((chunk) => chunk.chunkId)),
979
+ representationBands: Object.freeze(
980
+ [...new Set(normalizedChunks.map((chunk) => chunk.representationBand))]
981
+ ),
982
+ roots: Object.freeze(
983
+ finalizedJobs.filter((job) => job.root).map((job) => job.id)
984
+ ),
985
+ chunkRoots: Object.freeze(
986
+ Object.fromEntries(
987
+ normalizedChunks.map((chunk) => [
988
+ chunk.chunkId,
989
+ finalizedJobs.filter((job) => job.chunkId === chunk.chunkId && job.root).map((job) => job.id)
990
+ ])
991
+ )
992
+ ),
993
+ topologicalOrder: buildScenePreparationTopologicalOrder(finalizedJobs),
994
+ priorityLanes: buildScenePreparationPriorityLanes(finalizedJobs),
995
+ localJoinCount,
996
+ crossChunkDependencyCount
997
+ });
998
+ return Object.freeze({
999
+ schemaVersion: 1,
1000
+ owner: "scene-preparation",
1001
+ schedulerMode: "dag",
1002
+ snapshotId,
1003
+ jobs: finalizedJobs,
1004
+ graph
1005
+ });
1006
+ }
696
1007
  // Annotate the CommonJS export names for ESM import in node:
697
1008
  0 && (module.exports = {
698
1009
  assembleWorkerWgsl,
1010
+ createScenePreparationManifest,
699
1011
  createWorkerLoop,
700
1012
  loadJobWgsl,
701
1013
  loadQueueWgsl,
702
1014
  loadWorkerWgsl,
1015
+ scenePreparationRepresentationBands,
1016
+ scenePreparationStageFamilies,
703
1017
  workerWgslUrl
704
1018
  });
705
1019
  //# sourceMappingURL=index.cjs.map