@driftengine/core 4.8.1 → 4.8.2

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.
Files changed (62) hide show
  1. package/CHANGELOG.json +86 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +2 -0
  4. package/dist/input/actionMap.d.ts +6 -1
  5. package/dist/input/actionMap.js +20 -7
  6. package/dist/input/input.d.ts +28 -0
  7. package/dist/input/input.js +71 -9
  8. package/dist/input/inputHarness.d.ts +9 -0
  9. package/dist/input/inputHarness.js +34 -2
  10. package/dist/render/backend/webgl2/renderer.d.ts +52 -3
  11. package/dist/render/backend/webgl2/renderer.js +151 -5
  12. package/dist/render/backend/webgpu/buffers.d.ts +2 -0
  13. package/dist/render/backend/webgpu/buffers.js +2 -0
  14. package/dist/render/backend/webgpu/flatPass.js +16 -7
  15. package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
  16. package/dist/render/backend/webgpu/pipelineCache.js +76 -9
  17. package/dist/render/backend/webgpu/renderer.d.ts +56 -1
  18. package/dist/render/backend/webgpu/renderer.js +151 -26
  19. package/dist/render/mapsWithoutUvs.d.ts +24 -0
  20. package/dist/render/mapsWithoutUvs.js +24 -0
  21. package/dist/render/mesh.d.ts +2 -0
  22. package/dist/render/mesh.js +3 -0
  23. package/dist/render/particlePool.d.ts +15 -0
  24. package/dist/render/particlePool.js +72 -37
  25. package/dist/render/pass.d.ts +12 -0
  26. package/dist/render/renderQuality.d.ts +7 -6
  27. package/dist/render/shaders/flat/glassTint.d.ts +28 -0
  28. package/dist/render/shaders/flat/glassTint.js +31 -0
  29. package/dist/render/shaders/flat/index.d.ts +12 -0
  30. package/dist/render/shaders/flat/index.js +7 -1
  31. package/dist/render/shaders/flat/main.d.ts +1 -1
  32. package/dist/render/shaders/flat/main.js +60 -25
  33. package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
  34. package/dist/render/shaders/flat/surfaceEffects.js +6 -2
  35. package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
  36. package/dist/render/shaders/generated/flat.wgsl.js +102 -38
  37. package/dist/render/surfaceTexture.d.ts +2 -0
  38. package/dist/render/surfaceTexture.js +4 -0
  39. package/dist/render/viewModel.d.ts +29 -0
  40. package/dist/render/viewModel.js +35 -0
  41. package/package.json +6 -6
  42. package/src/index.ts +9 -1
  43. package/src/input/actionMap.ts +23 -8
  44. package/src/input/input.ts +79 -9
  45. package/src/input/inputHarness.ts +27 -2
  46. package/src/render/backend/webgl2/renderer.ts +166 -13
  47. package/src/render/backend/webgpu/buffers.ts +5 -0
  48. package/src/render/backend/webgpu/flatPass.ts +41 -25
  49. package/src/render/backend/webgpu/pipelineCache.ts +82 -8
  50. package/src/render/backend/webgpu/renderer.ts +151 -26
  51. package/src/render/mapsWithoutUvs.ts +43 -0
  52. package/src/render/mesh.ts +3 -0
  53. package/src/render/particlePool.ts +101 -54
  54. package/src/render/pass.ts +12 -0
  55. package/src/render/renderQuality.ts +7 -6
  56. package/src/render/shaders/flat/glassTint.ts +48 -0
  57. package/src/render/shaders/flat/index.ts +19 -1
  58. package/src/render/shaders/flat/main.ts +60 -25
  59. package/src/render/shaders/flat/surfaceEffects.ts +6 -2
  60. package/src/render/shaders/generated/flat.wgsl.ts +102 -38
  61. package/src/render/surfaceTexture.ts +5 -0
  62. package/src/render/viewModel.ts +41 -0
@@ -1115,6 +1115,8 @@ export declare class WebGPURenderer implements RendererApi {
1115
1115
  private readonly uvScale;
1116
1116
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
1117
1117
  private materialDoubleSided;
1118
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
1119
+ private materialHasMaps;
1118
1120
  createSurfaceTexture(source: TexImageSource, options?: SurfaceTextureOptions): GpuSurfaceTexture;
1119
1121
  /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
1120
1122
  createSurfaceTextureArray(sources: readonly TexImageSource[], options?: SurfaceTextureOptions): GpuSurfaceTexture;
@@ -1834,6 +1836,16 @@ export declare class WebGPURenderer implements RendererApi {
1834
1836
  */
1835
1837
  /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
1836
1838
  private readonly glassCasters;
1839
+ /** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
1840
+ private glassSeen;
1841
+ /**
1842
+ * The first glass caster switches every lit pipeline to reading glass. See
1843
+ * `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
1844
+ * the lit pass on every surface whether they find anything or not.
1845
+ */
1846
+ private noteGlassCaster;
1847
+ /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
1848
+ private enableLit;
1837
1849
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
1838
1850
  private readonly glassCasterScratch;
1839
1851
  private readonly casterSink;
@@ -2944,6 +2956,22 @@ export declare class WebGPURenderer implements RendererApi {
2944
2956
  */
2945
2957
  private canDraw;
2946
2958
  private openPass;
2959
+ private openAnyPass;
2960
+ /**
2961
+ * Submit what is drawn after the present on a microtask, which runs before the browser presents.
2962
+ *
2963
+ * There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
2964
+ * presentation happens when the task ends, after the microtask checkpoint. Anything that needs
2965
+ * the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
2966
+ *
2967
+ * **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
2968
+ * With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
2969
+ * queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
2970
+ * `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
2971
+ */
2972
+ private queueOverlayFlush;
2973
+ /** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
2974
+ private readonly flushOverlayBound;
2947
2975
  /**
2948
2976
  * The cache whose pipelines match the pass this draw will land in.
2949
2977
  *
@@ -3159,6 +3187,15 @@ export declare class WebGPURenderer implements RendererApi {
3159
3187
  * context is valid for the duration of the call for exactly this reason.
3160
3188
  */
3161
3189
  private readonly passContext;
3190
+ /**
3191
+ * Fill the context a contributed pass draws with, for the pass it is about to draw into.
3192
+ *
3193
+ * **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
3194
+ * at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
3195
+ * built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
3196
+ * so the two cannot disagree.
3197
+ */
3198
+ private fillPassContext;
3162
3199
  /**
3163
3200
  * Let something outside this file draw into the frame. See `Renderer.registerPass`.
3164
3201
  *
@@ -3313,9 +3350,27 @@ export declare class WebGPURenderer implements RendererApi {
3313
3350
  * **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
3314
3351
  * and cannot confine one to a scissor. See `insetPass.ts`.
3315
3352
  *
3316
- * Returns the drawing-buffer height of the inset, as the other backend does.
3353
+ * Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
3354
+ * other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
3355
+ * it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
3356
+ * projection of infinities in the caller's camera.
3317
3357
  */
3318
3358
  beginInset(rect: InsetRect, clearColor: Vec3 | null): number;
3359
+ /**
3360
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
3361
+ *
3362
+ * The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
3363
+ * depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
3364
+ * as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
3365
+ */
3366
+ beginViewModel(share?: number): void;
3367
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
3368
+ endViewModel(): void;
3369
+ /** The view model's depth bounds on the whole of the pass. */
3370
+ private squeezeViewport;
3371
+ /** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
3372
+ private viewModelActive;
3373
+ private readonly viewModelRange;
3319
3374
  /** Give the whole canvas back. Safe to call without a matching `beginInset`. */
3320
3375
  endInset(): void;
3321
3376
  /**
@@ -77,6 +77,8 @@ import { resolvePointLights } from '../../lightBudget.js';
77
77
  import { resolveRenderQuality } from '../../renderQuality.js';
78
78
  import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_STEPS, resolveGlobalMedium, } from '../../globalMedium.js';
79
79
  import { castsDepth, glassOf } from '../../shadowCasters.js';
80
+ import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
81
+ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
80
82
  import { GlassCasterList } from '../../glassCasters.js';
81
83
  import { GpuGlassTint } from './glassTintPass.js';
82
84
  import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
@@ -2131,6 +2133,8 @@ export class WebGPURenderer {
2131
2133
  uvScale = new Float32Array([1, 1]);
2132
2134
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2133
2135
  materialDoubleSided = false;
2136
+ /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
2137
+ materialHasMaps = false;
2134
2138
  createSurfaceTexture(source, options = {}) {
2135
2139
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2136
2140
  }
@@ -2257,6 +2261,7 @@ export class WebGPURenderer {
2257
2261
  this.uvScale[0] = material?.uScale ?? 1;
2258
2262
  this.uvScale[1] = material?.vScale ?? 1;
2259
2263
  this.materialDoubleSided = material?.doubleSided === true;
2264
+ this.materialHasMaps = materialHasMaps(material);
2260
2265
  const normal = (material?.normal ?? null);
2261
2266
  const orm = (material?.orm ?? null);
2262
2267
  const emissiveMap = (material?.emissive ?? null);
@@ -4400,6 +4405,29 @@ export class WebGPURenderer {
4400
4405
  */
4401
4406
  /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
4402
4407
  glassCasters = new GlassCasterList();
4408
+ /** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
4409
+ glassSeen = false;
4410
+ /**
4411
+ * The first glass caster switches every lit pipeline to reading glass. See
4412
+ * `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
4413
+ * the lit pass on every surface whether they find anything or not.
4414
+ */
4415
+ noteGlassCaster() {
4416
+ if (this.glassSeen)
4417
+ return;
4418
+ this.glassSeen = true;
4419
+ this.enableLit('GLASS_SHADOWS');
4420
+ }
4421
+ /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
4422
+ enableLit(feature) {
4423
+ if (this.pipelines.litSwitches[feature])
4424
+ return;
4425
+ void this.pipelines.enable(feature);
4426
+ if (this.overlayPipelines !== this.pipelines)
4427
+ void this.overlayPipelines.enable(feature);
4428
+ if (this.latePipelines !== this.pipelines)
4429
+ void this.latePipelines.enable(feature);
4430
+ }
4403
4431
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
4404
4432
  glassCasterScratch = {
4405
4433
  transmission: 0,
@@ -4412,6 +4440,7 @@ export class WebGPURenderer {
4412
4440
  /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
4413
4441
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4414
4442
  this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
4443
+ this.noteGlassCaster();
4415
4444
  }
4416
4445
  return;
4417
4446
  }
@@ -4451,6 +4480,7 @@ export class WebGPURenderer {
4451
4480
  if (!this.drawingGlassDepth && !castsDepth(material)) {
4452
4481
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4453
4482
  this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
4483
+ this.noteGlassCaster();
4454
4484
  }
4455
4485
  return;
4456
4486
  }
@@ -4491,6 +4521,7 @@ export class WebGPURenderer {
4491
4521
  if (!this.drawingGlassDepth && !castsDepth(material)) {
4492
4522
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4493
4523
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
4524
+ this.noteGlassCaster();
4494
4525
  }
4495
4526
  return;
4496
4527
  }
@@ -4833,7 +4864,7 @@ export class WebGPURenderer {
4833
4864
  */
4834
4865
  /* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
4835
4866
  const glassShadows = quality.glassShadows !== 'off';
4836
- this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples, glassShadows);
4867
+ this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples, glassShadows, quality.clusteredLights);
4837
4868
  /*
4838
4869
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
4839
4870
  *
@@ -4845,11 +4876,11 @@ export class WebGPURenderer {
4845
4876
  this.overlayPipelines =
4846
4877
  this.pipelines.format === surface.format && this.samples === 1
4847
4878
  ? this.pipelines
4848
- : new PipelineCache(device, surface.format, 1, glassShadows);
4879
+ : new PipelineCache(device, surface.format, 1, glassShadows, quality.clusteredLights);
4849
4880
  this.latePipelines =
4850
4881
  this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
4851
4882
  ? this.pipelines
4852
- : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
4883
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
4853
4884
  this.issueBound = this.issueCommand.bind(this);
4854
4885
  this.pipelineTargets =
4855
4886
  this.overlayPipelines === this.pipelines
@@ -5123,7 +5154,11 @@ export class WebGPURenderer {
5123
5154
  /* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
5124
5155
  The albedo is the group's key, so its table needs no key of its own. */
5125
5156
  if (name === 'uSurfaceEffects') {
5126
- return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
5157
+ const effects = this.albedo?.effectsView ?? null;
5158
+ /* The first table bound is when the lit pipelines start reading one. See `litSwitches`. */
5159
+ if (effects !== null)
5160
+ this.enableLit('SURFACE_EFFECTS');
5161
+ return { view: effects ?? blankView, sampler: blankSampler };
5127
5162
  }
5128
5163
  /*
5129
5164
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
@@ -6215,6 +6250,7 @@ export class WebGPURenderer {
6215
6250
  i[at('uLightingEnabled')] = 1;
6216
6251
  i[at('uFogEnabled')] = 1;
6217
6252
  i[at('uAlbedoEnabled')] = 0;
6253
+ this.materialHasMaps = false;
6218
6254
  /*
6219
6255
  * **The camera medium, which this pass did not upload until 2026-08-29.**
6220
6256
  *
@@ -6707,6 +6743,7 @@ export class WebGPURenderer {
6707
6743
  */
6708
6744
  if (this.tracksMotionNow())
6709
6745
  this.recordDrawMotion(mesh, model, previousModel);
6746
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6710
6747
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
6711
6748
  }
6712
6749
  /**
@@ -6801,6 +6838,7 @@ export class WebGPURenderer {
6801
6838
  this.translucentQueue.record(mesh, model, opacity, options);
6802
6839
  return;
6803
6840
  }
6841
+ noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6804
6842
  /* The tint travels in the options here, where `drawMesh` takes it positionally — see
6805
6843
  `TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
6806
6844
  this.submitMesh(mesh, model, options.tint ?? null, Math.min(opacity, 1), true, options);
@@ -10442,6 +10480,8 @@ export class WebGPURenderer {
10442
10480
  command.instances = 1;
10443
10481
  command.indirect = null;
10444
10482
  recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
10483
+ if (this.framePresented)
10484
+ this.queueOverlayFlush();
10445
10485
  return command;
10446
10486
  }
10447
10487
  /** Replay one recorded draw into an open pass. */
@@ -10457,11 +10497,7 @@ export class WebGPURenderer {
10457
10497
  const definition = passAt(this.passes, nodeState(this.arena, at));
10458
10498
  if (definition === undefined)
10459
10499
  return;
10460
- this.passContext.pass = pass;
10461
- /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
10462
- this.passContext.outputTransform = this.gradeCode();
10463
- this.passContext.outputExposure = this.gradeExposure();
10464
- definition.draw(this.passContext);
10500
+ definition.draw(this.fillPassContext(pass));
10465
10501
  return;
10466
10502
  }
10467
10503
  if (verb !== VERB_DRAW)
@@ -10558,6 +10594,15 @@ export class WebGPURenderer {
10558
10594
  return this.framePresented;
10559
10595
  }
10560
10596
  openPass() {
10597
+ const was = this.pass;
10598
+ const pass = this.openAnyPass();
10599
+ /* A pass opened while a view model is drawn starts with the whole depth range: squeeze it. */
10600
+ if (this.viewModelActive && pass !== null && pass !== was && !this.reflectionPassActive) {
10601
+ this.squeezeViewport(pass);
10602
+ }
10603
+ return pass;
10604
+ }
10605
+ openAnyPass() {
10561
10606
  /* While the replay is running this is the only pass there is. See `oitEncoder`. */
10562
10607
  if (this.oitEncoder !== null)
10563
10608
  return this.oitEncoder;
@@ -10649,18 +10694,29 @@ export class WebGPURenderer {
10649
10694
  },
10650
10695
  });
10651
10696
  this.overlayActive = true;
10652
- /*
10653
- * **Submitted on a microtask, which runs before the browser presents.** There is no second
10654
- * `endFrame` to hang this on — a consumer thinks the frame is over — and presentation happens
10655
- * when the task ends, after the microtask checkpoint. Anything that needs the pixels sooner
10656
- * flushes synchronously; `copyRegionTo` is the one that does.
10657
- */
10658
- if (!this.overlayFlushQueued) {
10659
- this.overlayFlushQueued = true;
10660
- queueMicrotask(() => this.flushOverlay());
10661
- }
10697
+ this.queueOverlayFlush();
10662
10698
  return this.pass;
10663
10699
  }
10700
+ /**
10701
+ * Submit what is drawn after the present on a microtask, which runs before the browser presents.
10702
+ *
10703
+ * There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
10704
+ * presentation happens when the task ends, after the microtask checkpoint. Anything that needs
10705
+ * the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
10706
+ *
10707
+ * **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
10708
+ * With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
10709
+ * queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
10710
+ * `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
10711
+ */
10712
+ queueOverlayFlush() {
10713
+ if (this.overlayFlushQueued)
10714
+ return;
10715
+ this.overlayFlushQueued = true;
10716
+ queueMicrotask(this.flushOverlayBound);
10717
+ }
10718
+ /** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
10719
+ flushOverlayBound = () => this.flushOverlay();
10664
10720
  /**
10665
10721
  * The cache whose pipelines match the pass this draw will land in.
10666
10722
  *
@@ -10725,6 +10781,9 @@ export class WebGPURenderer {
10725
10781
  this.encoder = null;
10726
10782
  }
10727
10783
  endFrame() {
10784
+ /* A view model left open would squeeze every pass after the world, the composite included. */
10785
+ if (this.viewModelActive)
10786
+ this.endViewModel();
10728
10787
  this.inFrame = false;
10729
10788
  if (this.surface.lost || this.encoder === null)
10730
10789
  return;
@@ -11148,7 +11207,29 @@ export class WebGPURenderer {
11148
11207
  outputTransform: 0,
11149
11208
  outputExposure: 1,
11150
11209
  jitter: this.passJitter,
11210
+ format: 'rgba8unorm',
11211
+ depthFormat: DEPTH_FORMAT,
11212
+ samples: 1,
11151
11213
  };
11214
+ /**
11215
+ * Fill the context a contributed pass draws with, for the pass it is about to draw into.
11216
+ *
11217
+ * **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
11218
+ * at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
11219
+ * built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
11220
+ * so the two cannot disagree.
11221
+ */
11222
+ fillPassContext(pass) {
11223
+ const target = this.targetPipelines();
11224
+ const context = this.passContext;
11225
+ context.pass = pass;
11226
+ /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
11227
+ context.outputTransform = this.gradeCode();
11228
+ context.outputExposure = this.gradeExposure();
11229
+ context.format = target.format;
11230
+ context.samples = target.sampleCount;
11231
+ return context;
11232
+ }
11152
11233
  /**
11153
11234
  * Let something outside this file draw into the frame. See `Renderer.registerPass`.
11154
11235
  *
@@ -11286,14 +11367,12 @@ export class WebGPURenderer {
11286
11367
  const pass = this.openPass();
11287
11368
  if (pass === null)
11288
11369
  return;
11289
- this.passContext.pass = pass;
11290
- /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
11291
- this.passContext.outputTransform = this.gradeCode();
11292
- this.passContext.outputExposure = this.gradeExposure();
11293
- definition.draw(this.passContext);
11370
+ definition.draw(this.fillPassContext(pass));
11294
11371
  return;
11295
11372
  }
11296
11373
  recordNode(this.arena, VERB_PASS, this.passReads.get(handle) ?? 0, this.currentTarget(), handle, 0);
11374
+ if (this.framePresented)
11375
+ this.queueOverlayFlush();
11297
11376
  }
11298
11377
  /** Let go of a pass. A handle kept past this draws nothing; see `PassHandle`. */
11299
11378
  unregisterPass(handle) {
@@ -11485,7 +11564,10 @@ export class WebGPURenderer {
11485
11564
  * **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
11486
11565
  * and cannot confine one to a scissor. See `insetPass.ts`.
11487
11566
  *
11488
- * Returns the drawing-buffer height of the inset, as the other backend does.
11567
+ * Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
11568
+ * other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
11569
+ * it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
11570
+ * projection of infinities in the caller's camera.
11489
11571
  */
11490
11572
  beginInset(rect, clearColor) {
11491
11573
  /*
@@ -11497,7 +11579,7 @@ export class WebGPURenderer {
11497
11579
  this.flushGraph();
11498
11580
  const pass = this.openPass();
11499
11581
  if (this.surface.lost || pass === null)
11500
- return 0;
11582
+ return Math.max(rect.width, 1) / Math.max(rect.height, 1);
11501
11583
  const canvas = this.surface.context.canvas;
11502
11584
  const cssWidth = Math.max(canvas.clientWidth, 1);
11503
11585
  const cssHeight = Math.max(canvas.clientHeight, 1);
@@ -11594,6 +11676,44 @@ export class WebGPURenderer {
11594
11676
  */
11595
11677
  return w / h;
11596
11678
  }
11679
+ /**
11680
+ * Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
11681
+ *
11682
+ * The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
11683
+ * depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
11684
+ * as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
11685
+ */
11686
+ beginViewModel(share = VIEW_MODEL_DEPTH_SHARE) {
11687
+ if (this.quality.frameGraph)
11688
+ this.flushGraph();
11689
+ viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
11690
+ this.viewModelActive = true;
11691
+ const pass = this.openPass();
11692
+ if (this.surface.lost || pass === null)
11693
+ return;
11694
+ this.squeezeViewport(pass);
11695
+ }
11696
+ /** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
11697
+ endViewModel() {
11698
+ if (!this.viewModelActive)
11699
+ return;
11700
+ if (this.quality.frameGraph)
11701
+ this.flushGraph();
11702
+ this.viewModelActive = false;
11703
+ const pass = this.pass;
11704
+ if (this.surface.lost || pass === null)
11705
+ return;
11706
+ const [width, height] = this.passSize();
11707
+ pass.setViewport(0, 0, width, height, 0, 1);
11708
+ }
11709
+ /** The view model's depth bounds on the whole of the pass. */
11710
+ squeezeViewport(pass) {
11711
+ const [width, height] = this.passSize();
11712
+ pass.setViewport(0, 0, width, height, this.viewModelRange[0], this.viewModelRange[1]);
11713
+ }
11714
+ /** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
11715
+ viewModelActive = false;
11716
+ viewModelRange = [0, 1];
11597
11717
  /** Give the whole canvas back. Safe to call without a matching `beginInset`. */
11598
11718
  endInset() {
11599
11719
  /*
@@ -12663,6 +12783,9 @@ export class WebGPURenderer {
12663
12783
  this.iesRows = this.iesAtlas.height;
12664
12784
  this.iesPlanes = this.iesAtlas.planes;
12665
12785
  this.cookieTiles = this.cookieImages.length;
12786
+ /* The first profile or cookie is when the lit pipelines start reading them. See `litSwitches`. */
12787
+ if (this.iesRows > 1 || this.cookieTiles > 0)
12788
+ this.enableLit('LIGHT_FIXTURES');
12666
12789
  return this.fixtureView;
12667
12790
  }
12668
12791
  /**
@@ -12739,6 +12862,8 @@ export class WebGPURenderer {
12739
12862
  device.queue.writeTexture({ texture: atlas }, field.atlas, { bytesPerRow: aw * 8, rowsPerImage: ah }, [aw, ah, ad]);
12740
12863
  this.driftIndex = index;
12741
12864
  this.driftAtlas = atlas;
12865
+ /* The first volume is when the lit pipelines start reading one. See `litSwitches`. */
12866
+ this.enableLit('DRIFT_LIGHT');
12742
12867
  this.driftIndexView = index.createView({ dimension: '3d' });
12743
12868
  this.driftAtlasView = atlas.createView({ dimension: '3d' });
12744
12869
  this.bakeLightField = {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Saying, once a mesh, that a material's maps cannot be read on it.
3
+ *
4
+ * **A mesh with no texture coordinates reads one constant coordinate**, so every map bound to it
5
+ * samples a single texel: an emissive map makes nothing glow, a normal map tilts nothing, and the
6
+ * frame is identical pixel for pixel with and without the material's images. Found when an example's
7
+ * burning logs never glowed — `MeshBuilder` writes no coordinates unless `build({ planarUvs: true })`
8
+ * asks — and nothing said why. One decision for both backends, each calling it from its draw.
9
+ *
10
+ * Once per mesh, held weakly: a mesh drawn every frame is said once, and a disposed one is
11
+ * forgotten. **What it gives up** is a material shared between a textured mesh and an untextured one
12
+ * on purpose, which is said once for the untextured one and is then quiet.
13
+ */
14
+ import type { SurfaceMaterial } from './surfaceTexture.ts';
15
+ /**
16
+ * Whether a material binds a map that is a picture: albedo, normal or emissive.
17
+ *
18
+ * **Not ORM**, because a one-colour ORM map is how a draw sets a constant roughness and metalness,
19
+ * and read at one texel it is exactly that constant — the materials example draws its spheres so.
20
+ * The three counted here are pictures nobody binds to be read at one point.
21
+ */
22
+ export declare function materialHasMaps(material: SurfaceMaterial<unknown> | null): boolean;
23
+ /** Warn the first time `mesh`, which has no coordinates, is drawn with a picture map bound. */
24
+ export declare function noteMapsWithoutUvs(mesh: object, hasUvs: boolean, materialHasMaps: boolean): void;
@@ -0,0 +1,24 @@
1
+ const told = new WeakSet();
2
+ /**
3
+ * Whether a material binds a map that is a picture: albedo, normal or emissive.
4
+ *
5
+ * **Not ORM**, because a one-colour ORM map is how a draw sets a constant roughness and metalness,
6
+ * and read at one texel it is exactly that constant — the materials example draws its spheres so.
7
+ * The three counted here are pictures nobody binds to be read at one point.
8
+ */
9
+ export function materialHasMaps(material) {
10
+ if (material === null)
11
+ return false;
12
+ return ((material.albedo ?? null) !== null ||
13
+ (material.normal ?? null) !== null ||
14
+ (material.emissive ?? null) !== null);
15
+ }
16
+ /** Warn the first time `mesh`, which has no coordinates, is drawn with a picture map bound. */
17
+ export function noteMapsWithoutUvs(mesh, hasUvs, materialHasMaps) {
18
+ if (hasUvs || !materialHasMaps || told.has(mesh))
19
+ return;
20
+ told.add(mesh);
21
+ console.warn('[driftengine] a mesh with no texture coordinates is drawn with an albedo, normal or emissive ' +
22
+ 'map, which it cannot read: every map samples one texel. Give the mesh `uvs` — ' +
23
+ '`MeshBuilder.build({ planarUvs: true })` writes them — or draw it without the maps.');
24
+ }
@@ -56,6 +56,8 @@ export declare class Mesh {
56
56
  readonly bounds: Bounds;
57
57
  /** Whether this mesh carries a rig, which decides which flat program draws it. */
58
58
  readonly isSkinned: boolean;
59
+ /** Whether it has texture coordinates, without which a material's maps read one texel. */
60
+ readonly hasUvs: boolean;
59
61
  /**
60
62
  * This mesh's morph deltas, or null for geometry that does not deform.
61
63
  *
@@ -123,6 +123,8 @@ export class Mesh {
123
123
  bounds = createBounds();
124
124
  /** Whether this mesh carries a rig, which decides which flat program draws it. */
125
125
  isSkinned;
126
+ /** Whether it has texture coordinates, without which a material's maps read one texel. */
127
+ hasUvs;
126
128
  /**
127
129
  * This mesh's morph deltas, or null for geometry that does not deform.
128
130
  *
@@ -171,6 +173,7 @@ export class Mesh {
171
173
  constructor(gl, data, dynamic = false, spread = false) {
172
174
  validateMeshData(data);
173
175
  this.spread = spread;
176
+ this.hasUvs = data.uvs !== undefined;
174
177
  boundsOfPositions(data.positions, this.bounds);
175
178
  this.positionFloats = data.positions.length;
176
179
  const vao = gl.createVertexArray();
@@ -98,6 +98,15 @@ export declare class ParticlePool {
98
98
  * together.
99
99
  */
100
100
  private readonly seedOf;
101
+ /**
102
+ * Where each slot's particle sits in the drawn streams, or -1 where it sits nowhere.
103
+ *
104
+ * **What lets `emit` draw a particle the frame it is born.** The streams are compacted, so a
105
+ * slot's entry is wherever `update` last put it; an emit into a slot that has one overwrites
106
+ * that entry, and an emit into one that has none appends. Without it a new particle waited for
107
+ * the next `update` to be drawn at all.
108
+ */
109
+ private readonly drawnAt;
101
110
  private next;
102
111
  constructor(options: ParticlePoolOptions);
103
112
  /** How many particles are alive. */
@@ -119,6 +128,12 @@ export declare class ParticlePool {
119
128
  * particles cost nothing but the loop iteration.
120
129
  */
121
130
  update(dt: number): void;
131
+ /**
132
+ * One particle's entry in both drawn streams, at `t` of its life: what `update` writes for every
133
+ * live particle and `emit` for a new one, so the two cannot disagree about what a particle looks
134
+ * like.
135
+ */
136
+ private writeDrawn;
122
137
  /** Drop everything, for a respawn or a scene change. */
123
138
  clear(): void;
124
139
  }