@woosh/meep-engine 3.14.6 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/package.json +1 -1
  2. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +48 -0
  3. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  4. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +246 -3
  5. package/src/shade/RENDERER_CONTRACT.md +584 -580
  6. package/src/shade/playground/avboit/README.md +117 -0
  7. package/src/shade/playground/avboit/index.html +215 -0
  8. package/src/shade/playground/avboit/main.d.ts +2 -0
  9. package/src/shade/playground/avboit/main.d.ts.map +1 -0
  10. package/src/shade/playground/avboit/main.js +875 -0
  11. package/src/shade/renderer/Renderer.d.ts +16 -0
  12. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  13. package/src/shade/renderer/Renderer.js +29 -28
  14. package/src/shade/renderer/ShadeTransparencyMode.d.ts +13 -0
  15. package/src/shade/renderer/ShadeTransparencyMode.d.ts.map +1 -0
  16. package/src/shade/renderer/ShadeTransparencyMode.js +30 -0
  17. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.d.ts +9 -0
  18. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.d.ts.map +1 -1
  19. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.js +26 -0
  20. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts +7 -2
  21. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  22. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +12 -1
  23. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +1 -1
  24. package/src/shade/renderer/rasterize/forward/FORWARD_VERTEX_SHADER_STRUCT.d.ts +3 -0
  25. package/src/shade/renderer/rasterize/forward/FORWARD_VERTEX_SHADER_STRUCT.d.ts.map +1 -0
  26. package/src/shade/renderer/rasterize/{native/oit → forward}/FORWARD_VERTEX_SHADER_STRUCT.js +4 -4
  27. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.d.ts +3 -0
  28. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.d.ts.map +1 -0
  29. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.js +17 -0
  30. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_forward_shade_standard_fragment_brick4.d.ts +1 -1
  31. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -0
  32. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_forward_shade_standard_fragment_brick4.js +16 -16
  33. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_forward_shade_standard_fragment_ibl.d.ts +1 -1
  34. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -0
  35. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_forward_shade_standard_fragment_ibl.js +17 -17
  36. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_material_side_should_draw.d.ts +1 -1
  37. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts.map +1 -0
  38. package/src/shade/renderer/rasterize/{native/oit → forward}/chunk_material_side_should_draw.js +2 -2
  39. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts +3 -0
  40. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -0
  41. package/src/shade/renderer/rasterize/{native/oit → forward}/vertex_shader_chunk.js +4 -4
  42. package/src/shade/renderer/rasterize/native/avboit/AVBOITSettings.d.ts +27 -0
  43. package/src/shade/renderer/rasterize/native/avboit/AVBOITSettings.d.ts.map +1 -0
  44. package/src/shade/renderer/rasterize/native/avboit/AVBOITSettings.js +27 -0
  45. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_CONSTANTS.d.ts +107 -0
  46. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_CONSTANTS.d.ts.map +1 -0
  47. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_CONSTANTS.js +120 -0
  48. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +463 -0
  49. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +509 -0
  50. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_WARP_STRUCT.d.ts +15 -0
  51. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_WARP_STRUCT.d.ts.map +1 -0
  52. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_WARP_STRUCT.js +33 -0
  53. package/src/shade/renderer/rasterize/native/avboit/avboit_build_depth_curve.d.ts +20 -0
  54. package/src/shade/renderer/rasterize/native/avboit/avboit_build_depth_curve.d.ts.map +1 -0
  55. package/src/shade/renderer/rasterize/native/avboit/avboit_build_depth_curve.js +38 -0
  56. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.d.ts +38 -0
  57. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.d.ts.map +1 -0
  58. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.js +130 -0
  59. package/src/shade/renderer/rasterize/native/avboit/avboit_spec_support.d.ts +37 -0
  60. package/src/shade/renderer/rasterize/native/avboit/avboit_spec_support.d.ts.map +1 -0
  61. package/src/shade/renderer/rasterize/native/avboit/avboit_spec_support.js +73 -0
  62. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_counts_to_extinction.d.ts +9 -0
  63. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_counts_to_extinction.d.ts.map +1 -0
  64. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_counts_to_extinction.js +17 -0
  65. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_dilate_word.d.ts +11 -0
  66. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_dilate_word.d.ts.map +1 -0
  67. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_dilate_word.js +18 -0
  68. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_counts.d.ts +9 -0
  69. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_counts.d.ts.map +1 -0
  70. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_counts.js +17 -0
  71. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_transmittance.d.ts +9 -0
  72. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_transmittance.d.ts.map +1 -0
  73. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_extinction_to_transmittance.js +17 -0
  74. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.d.ts +15 -0
  75. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.d.ts.map +1 -0
  76. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.js +25 -0
  77. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_integral_depth_coordinate.d.ts +17 -0
  78. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_integral_depth_coordinate.d.ts.map +1 -0
  79. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_integral_depth_coordinate.js +35 -0
  80. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_range.d.ts +11 -0
  81. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_range.d.ts.map +1 -0
  82. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_range.js +21 -0
  83. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_word_mask.d.ts +9 -0
  84. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_word_mask.d.ts.map +1 -0
  85. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_word_mask.js +26 -0
  86. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_overflows.d.ts +11 -0
  87. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_overflows.d.ts.map +1 -0
  88. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_overflows.js +22 -0
  89. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_pack_counts.d.ts +10 -0
  90. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_pack_counts.d.ts.map +1 -0
  91. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_pack_counts.js +21 -0
  92. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_sample_transmittance.d.ts +10 -0
  93. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_sample_transmittance.d.ts.map +1 -0
  94. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_sample_transmittance.js +32 -0
  95. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmission_tint.d.ts +20 -0
  96. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmission_tint.d.ts.map +1 -0
  97. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmission_tint.js +27 -0
  98. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmittance_to_extinction.d.ts +10 -0
  99. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmittance_to_extinction.d.ts.map +1 -0
  100. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_transmittance_to_extinction.js +18 -0
  101. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unpack_counts.d.ts +9 -0
  102. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unpack_counts.d.ts.map +1 -0
  103. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_unpack_counts.js +21 -0
  104. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_physical.d.ts +9 -0
  105. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_physical.d.ts.map +1 -0
  106. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_physical.js +23 -0
  107. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_virtual.d.ts +13 -0
  108. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_virtual.d.ts.map +1 -0
  109. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_view_depth_to_virtual.js +27 -0
  110. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_warp.d.ts +22 -0
  111. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_warp.d.ts.map +1 -0
  112. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_warp.js +38 -0
  113. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +117 -0
  114. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -0
  115. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +935 -0
  116. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_build_warp.d.ts +8 -0
  117. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_build_warp.d.ts.map +1 -0
  118. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_build_warp.js +135 -0
  119. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.d.ts +3 -0
  120. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.d.ts.map +1 -0
  121. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.js +101 -0
  122. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.d.ts +3 -0
  123. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.d.ts.map +1 -0
  124. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.js +96 -0
  125. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_integrate.d.ts +3 -0
  126. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_integrate.d.ts.map +1 -0
  127. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_integrate.js +83 -0
  128. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_occupancy.d.ts +9 -0
  129. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_occupancy.d.ts.map +1 -0
  130. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_occupancy.js +119 -0
  131. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_resolve.d.ts +3 -0
  132. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_resolve.d.ts.map +1 -0
  133. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_resolve.js +81 -0
  134. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts +15 -0
  135. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts.map +1 -0
  136. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.js +237 -0
  137. package/src/shade/renderer/rasterize/native/material_draw_indirect.d.ts +22 -0
  138. package/src/shade/renderer/rasterize/native/material_draw_indirect.d.ts.map +1 -0
  139. package/src/shade/renderer/rasterize/native/material_draw_indirect.js +69 -0
  140. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  141. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +665 -722
  142. package/src/shade/renderer/rasterize/native/oit/shader_oit_generate_moments.js +3 -3
  143. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.js +4 -4
  144. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.js +4 -4
  145. package/src/shade/renderer/rasterize/native/oit/FORWARD_VERTEX_SHADER_STRUCT.d.ts +0 -3
  146. package/src/shade/renderer/rasterize/native/oit/FORWARD_VERTEX_SHADER_STRUCT.d.ts.map +0 -1
  147. package/src/shade/renderer/rasterize/native/oit/SHADING_SCENE_RESOURCE_GROUP.d.ts +0 -3
  148. package/src/shade/renderer/rasterize/native/oit/SHADING_SCENE_RESOURCE_GROUP.d.ts.map +0 -1
  149. package/src/shade/renderer/rasterize/native/oit/SHADING_SCENE_RESOURCE_GROUP.js +0 -17
  150. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.d.ts.map +0 -1
  151. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.d.ts.map +0 -1
  152. package/src/shade/renderer/rasterize/native/oit/chunk_material_side_should_draw.d.ts.map +0 -1
  153. package/src/shade/renderer/rasterize/native/oit/vertex_shader_chunk.d.ts +0 -3
  154. package/src/shade/renderer/rasterize/native/oit/vertex_shader_chunk.d.ts.map +0 -1
@@ -0,0 +1,875 @@
1
+ import package_json from "../../../../package.json";
2
+ import { Color } from "../../../core/color/Color.js";
3
+ import Quaternion from "../../../core/geom/Quaternion.js";
4
+ import Vector3 from "../../../core/geom/Vector3.js";
5
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
6
+ import { GPUProfileLevel } from "../../device/timing/profile/GPUProfileLevel.js";
7
+ import { GPUProfileSession } from "../../device/timing/profile/GPUProfileSession.js";
8
+ import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
9
+ import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
10
+ import { create_frame_loop } from "../../renderer/create_frame_loop.js";
11
+ import { MeshoptClusterizer } from "../../renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.js";
12
+ import { meshlet_geometry_build_from_geometry } from "../../renderer/geometry/meshlet_geometry_build_from_geometry.js";
13
+ import { MeshletGeometry } from "../../renderer/geometry/MeshletGeometry.js";
14
+ import { make_plane_geometry } from "../../renderer/geometry/primitives/make_plane_geometry.js";
15
+ import { make_sunlight } from "../../renderer/light/make_sunlight.js";
16
+ import { ShadeDrawSide } from "../../renderer/material/ShadeDrawSide.js";
17
+ import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
18
+ import { TransparencyMode } from "../../renderer/material/TransparencyMode.js";
19
+ import { shader_avboit_build_warp } from "../../renderer/rasterize/native/avboit/shader_avboit_build_warp.js";
20
+ import { shader_avboit_draw_brick4 } from "../../renderer/rasterize/native/avboit/shader_avboit_draw_brick4.js";
21
+ import { shader_avboit_draw_ibl } from "../../renderer/rasterize/native/avboit/shader_avboit_draw_ibl.js";
22
+ import { shader_avboit_integrate } from "../../renderer/rasterize/native/avboit/shader_avboit_integrate.js";
23
+ import { shader_avboit_occupancy } from "../../renderer/rasterize/native/avboit/shader_avboit_occupancy.js";
24
+ import { shader_avboit_resolve } from "../../renderer/rasterize/native/avboit/shader_avboit_resolve.js";
25
+ import { shader_avboit_splat } from "../../renderer/rasterize/native/avboit/shader_avboit_splat.js";
26
+ import { Renderer } from "../../renderer/Renderer.js";
27
+ import { Mesh } from "../../renderer/scene/Mesh.js";
28
+ import { Scene } from "../../renderer/scene/Scene.js";
29
+ import { ShadeIndirectLightingMode } from "../../renderer/ShadeIndirectLightingMode.js";
30
+ import { ShadeTransparencyMode } from "../../renderer/ShadeTransparencyMode.js";
31
+ import { load_default_environment } from "../load_default_environment.js";
32
+
33
+ /**
34
+ * AVBOIT against MBOIT on the deck's own test case: a stack of near-opaque cards whose depth
35
+ * separation is a control, plus a tinted glass pane and an opaque surface pushed through the
36
+ * transparency pipeline, over an opaque backdrop.
37
+ *
38
+ * What to look for (deck slides 35–45 and 86–92):
39
+ *
40
+ * - With `alpha` near 1 and a few units of spacing, the front card should hide everything behind
41
+ * it. MBOIT is not depth-complexity invariant and lets the cards behind bleed through as the stack
42
+ * deepens; AVBOIT should not, because each card lands in its own slice.
43
+ * - As `spacing` drops toward zero the cards share a slice and AVBOIT degrades to weighted
44
+ * blending — the "frequency soup" the deck describes. The spacing at which that starts is the
45
+ * slice thickness at that depth, and the adaptive warp is what keeps it small.
46
+ * - The glass pane tints what is behind it under AVBOIT (per-channel transmittance) once
47
+ * "tint transmitted light by base colour" is on — `renderer.avboit.transmission_tint_from_base_color`,
48
+ * off by default so the three modes agree on what the material means — and never under MBOIT
49
+ * or the tracer, which have no per-channel transmission.
50
+ * - The "opaque quad, transparent material" is the deck's own fail case for moments (slides 35–38):
51
+ * an alpha-1 surface pushed through the transparency pipeline. Both pipelines clamp coverage at
52
+ * 0.997, so it should read as opaque, with a hard edge against whatever is behind it. Moments
53
+ * smear that step — the surface leaks what is behind it and its edges tear where the depth
54
+ * jumps — while a voxel slice holds it.
55
+ * - "path traced (reference)" hands the frame to the accumulating path tracer, which rolls each
56
+ * transparent hit against its alpha and so converges to the exact order-independent answer for
57
+ * the card stack. It has no per-channel transmission tint, so for the glass pane it is a
58
+ * reference for the Fresnel coverage only, not for the tint (see the README).
59
+ *
60
+ * The camera orbits (drag), zooms (wheel) and pans (right drag); its state copies to the clipboard
61
+ * as JSON and pastes back, so a view that shows something can be handed over exactly. A GPU profile
62
+ * recording downloads an `.sgpt` capture the gpu-inspector-tool reads.
63
+ *
64
+ * The camera, its controller and every control are wired *before* the device is requested, so on
65
+ * a browser with no adapter the page still orbits and copies its camera — which is also what lets
66
+ * the input path be exercised where the renderer cannot run.
67
+ *
68
+ * `npm run dev`, then `/src/shade/playground/avboit/`. Everything is also reachable from the console
69
+ * as `renderer`, `scene`, `camera`, `controller`, `configure(settings)`, `camera_state()`,
70
+ * `apply_camera(state)`, `compile_check()`, `start_profile()` and `stop_profile()`.
71
+ */
72
+
73
+ const CAMERA = {
74
+ position: new Vector3(0, 2.5, 12),
75
+ target: new Vector3(0, 1.5, 0),
76
+ fov_degrees: 45,
77
+ near: 0.1,
78
+ far: 200,
79
+ };
80
+
81
+ const VIEWPORT = { width: 1280, height: 720 };
82
+
83
+ /**
84
+ * Card colours, front to back; strong hues so bleed-through is unmistakable.
85
+ * @type {Color[]}
86
+ */
87
+ const CARD_COLORS = [
88
+ new Color(0.95, 0.25, 0.2),
89
+ new Color(0.2, 0.55, 0.95),
90
+ new Color(0.25, 0.9, 0.3),
91
+ new Color(0.95, 0.85, 0.2),
92
+ new Color(0.9, 0.3, 0.9),
93
+ new Color(0.3, 0.9, 0.9),
94
+ ];
95
+
96
+ const GLASS_TINTS = {
97
+ red: new Color(1.0, 0.15, 0.15, 1),
98
+ green: new Color(0.15, 1.0, 0.15, 1),
99
+ blue: new Color(0.15, 0.15, 1.0, 1),
100
+ none: new Color(1, 1, 1, 1),
101
+ };
102
+
103
+ /**
104
+ * The modules the compile check pushes through the device, by name.
105
+ * @type {[string, () => string][]}
106
+ */
107
+ const AVBOIT_MODULES = [
108
+ ["occupancy", () => shader_avboit_occupancy.descriptor.source],
109
+ ["build warp", () => shader_avboit_build_warp.descriptor.source],
110
+ ["splat / vertex", () => shader_avboit_splat.pipeline.vertex.module.code],
111
+ ["splat / fragment", () => shader_avboit_splat.pipeline.fragment.module.code],
112
+ ["integrate", () => shader_avboit_integrate.descriptor.source],
113
+ ["draw IBL / vertex", () => shader_avboit_draw_ibl.pipeline.vertex.module.code],
114
+ ["draw IBL / fragment", () => shader_avboit_draw_ibl.pipeline.fragment.module.code],
115
+ ["draw Brick4 / vertex", () => shader_avboit_draw_brick4.pipeline.vertex.module.code],
116
+ ["draw Brick4 / fragment", () => shader_avboit_draw_brick4.pipeline.fragment.module.code],
117
+ ["resolve", () => shader_avboit_resolve.descriptor.source],
118
+ ];
119
+
120
+ /**
121
+ * How much a profile capture records: pass spans, the frame graph, dispatch and draw counts — the
122
+ * level the inspector's timeline and pass detail read. See `volumetrics_froxel/main.js` for the
123
+ * per-frame cost; a hand-driven recording of a few seconds does not notice it.
124
+ * @type {number}
125
+ */
126
+ const PROFILE_LEVEL = GPUProfileLevel.WORKLOAD;
127
+
128
+ /**
129
+ * The running-mean floor while tracing. The tracer's default 0.01 turns the mean into a 100-sample
130
+ * exponential average, which keeps a viewport responsive but never settles below that noise floor;
131
+ * a reference wants it lower. Restored when leaving reference mode.
132
+ * @type {number}
133
+ */
134
+ const REFERENCE_MIN_ACCUMULATION_ALPHA = 0.002;
135
+
136
+ /**
137
+ * A meshlet plane, the only geometry the renderer draws.
138
+ *
139
+ * @param {number} width
140
+ * @param {number} height
141
+ * @returns {MeshletGeometry}
142
+ */
143
+ function make_plane(width, height) {
144
+ const geometry = make_plane_geometry(width, height, 1, 1);
145
+ const meshlets = new MeshletGeometry();
146
+
147
+ meshlet_geometry_build_from_geometry(geometry, meshlets);
148
+
149
+ return meshlets;
150
+ }
151
+
152
+ /**
153
+ * @param {Color} diffuse rgb and alpha
154
+ * @param {object} [options]
155
+ * @param {number} [options.transmission]
156
+ * @param {boolean} [options.transparent]
157
+ * @returns {StandardShadeMaterial}
158
+ */
159
+ function make_material(diffuse, { transmission = 0, transparent = true } = {}) {
160
+ const material = new StandardShadeMaterial();
161
+
162
+ material.diffuse_color.copy(diffuse);
163
+ material.roughness_factor = 0.6;
164
+ material.metallic_factor = 0;
165
+ material.transmission_factor = transmission;
166
+ material.ior_factor = 1.5;
167
+ material.transparency_mode = transparent ? TransparencyMode.Transparent : TransparencyMode.Opaque;
168
+ material.draw_side = ShadeDrawSide.Double;
169
+
170
+ return material;
171
+ }
172
+
173
+ /**
174
+ * @param {MeshletGeometry} geometry
175
+ * @param {StandardShadeMaterial} material
176
+ * @param {Vector3} position
177
+ * @param {Quaternion} [rotation]
178
+ * @returns {Mesh}
179
+ */
180
+ function place(geometry, material, position, rotation = new Quaternion()) {
181
+ const mesh = Mesh.from(geometry, material);
182
+
183
+ mesh.transform_local.setTranslation(position.x, position.y, position.z);
184
+ mesh.transform_local.setRotation(rotation.x, rotation.y, rotation.z, rotation.w);
185
+ mesh.updateMatrices();
186
+
187
+ return mesh;
188
+ }
189
+
190
+ /**
191
+ * Compile every AVBOIT module on the live device and collect what the compiler said.
192
+ *
193
+ * `pushErrorScope("validation")` catches the module-creation error; `getCompilationInfo` carries the
194
+ * per-line messages. Both are reported, because a module that fails to create has its reason in the
195
+ * second and only a one-line summary in the first.
196
+ *
197
+ * @param {GPUDevice} device
198
+ * @returns {Promise<{name: string, ok: boolean, messages: string[]}[]>}
199
+ */
200
+ async function compile_all(device) {
201
+ const results = [];
202
+
203
+ for (const [name, get_source] of AVBOIT_MODULES) {
204
+ device.pushErrorScope("validation");
205
+
206
+ const module = device.createShaderModule({ label: `avboit playground / ${name}`, code: get_source() });
207
+
208
+ const error = await device.popErrorScope();
209
+ const info = await module.getCompilationInfo();
210
+
211
+ const messages = info.messages
212
+ .filter(m => m.type !== "info")
213
+ .map(m => `${m.type} ${m.lineNum}:${m.linePos} ${m.message}`);
214
+
215
+ if (error !== null) {
216
+ messages.unshift(`validation: ${error.message.split("\n")[0]}`);
217
+ }
218
+
219
+ results.push({ name, ok: error === null && !info.messages.some(m => m.type === "error"), messages });
220
+ }
221
+
222
+ return results;
223
+ }
224
+
225
+ /**
226
+ * Hand an ArrayBuffer to the browser as a file.
227
+ *
228
+ * @param {ArrayBuffer} bytes
229
+ * @param {string} filename
230
+ */
231
+ function download_bytes(bytes, filename) {
232
+ const url = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
233
+
234
+ const anchor = document.createElement('a');
235
+
236
+ anchor.href = url;
237
+ anchor.download = filename;
238
+
239
+ document.body.appendChild(anchor);
240
+ anchor.click();
241
+ anchor.remove();
242
+
243
+ // Revoking in the same task cancels the download in some browsers — the click only schedules it.
244
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
245
+ }
246
+
247
+ /**
248
+ * `YYYYMMDD-HHMMSS` in local time, the naming the other `.sgpt` captures use.
249
+ *
250
+ * @returns {string}
251
+ */
252
+ function file_timestamp() {
253
+ const now = new Date();
254
+
255
+ const pad = value => String(value).padStart(2, '0');
256
+
257
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
258
+ + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
259
+ }
260
+
261
+ /**
262
+ * @param {number} bytes
263
+ * @returns {string}
264
+ */
265
+ function format_bytes(bytes) {
266
+ if (bytes < 1024) {
267
+ return `${bytes} B`;
268
+ }
269
+
270
+ if (bytes < 1024 * 1024) {
271
+ return `${(bytes / 1024).toFixed(1)} KiB`;
272
+ }
273
+
274
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
275
+ }
276
+
277
+ export async function start() {
278
+
279
+ const element = id => document.getElementById(id);
280
+
281
+ const report_element = element('report');
282
+
283
+ /**
284
+ * @param {string} text
285
+ * @param {string} [state] pass | fail | ''
286
+ */
287
+ function report(text, state = '') {
288
+ report_element.textContent = text;
289
+ report_element.className = state;
290
+ console.log(text);
291
+ }
292
+
293
+ /**
294
+ * Run a control's handler and put its failure in the panel. A handler that throws otherwise
295
+ * leaves the page looking as if the button did nothing, with the reason only in the console.
296
+ *
297
+ * @param {string} what
298
+ * @param {() => (void|Promise<void>)} action
299
+ * @returns {Promise<void>}
300
+ */
301
+ async function guarded(what, action) {
302
+ try {
303
+ await action();
304
+ } catch (e) {
305
+ console.error(`${what} failed`, e);
306
+ report(`${what} failed: ${e.message}`, 'fail');
307
+ }
308
+ }
309
+
310
+ const canvas = element('view');
311
+
312
+ canvas.width = VIEWPORT.width;
313
+ canvas.height = VIEWPORT.height;
314
+
315
+ // ---- camera, before anything needs a device ----------------------------------------------
316
+
317
+ const camera = new PerspectiveCamera();
318
+
319
+ camera.near = CAMERA.near;
320
+ camera.far = CAMERA.far;
321
+ camera.fov = CAMERA.fov_degrees * DEG_TO_RAD;
322
+ camera.aspect = VIEWPORT.width / VIEWPORT.height;
323
+ camera.transform.position.copy(CAMERA.position);
324
+ camera.transform.lookAt(CAMERA.target);
325
+ camera.transform.updateMatrix();
326
+
327
+ // document.body rather than the canvas: the controller starts a keyboard device, and only a
328
+ // focusable element can receive key events — body carries tabindex for exactly that
329
+ const controller = new OrbitalCameraController(camera, document.body);
330
+ controller.look(CAMERA.position, CAMERA.target);
331
+ controller.movement_speed_scale = 4;
332
+
333
+ // handy from the console, and on the page as early as they exist
334
+ Object.assign(globalThis, { camera, controller });
335
+
336
+ /**
337
+ * Where the camera is and what it orbits, as plain numbers: enough to put another instance of
338
+ * this page in exactly the same view.
339
+ *
340
+ * @returns {{position: number[], target: number[], fov_degrees: number}}
341
+ */
342
+ globalThis.camera_state = () => {
343
+ const p = camera.transform.position;
344
+ const t = controller.target;
345
+
346
+ const round = v => Math.round(v * 1000) / 1000;
347
+
348
+ return {
349
+ position: [round(p.x), round(p.y), round(p.z)],
350
+ target: [round(t.x), round(t.y), round(t.z)],
351
+ fov_degrees: round(camera.fov / DEG_TO_RAD),
352
+ };
353
+ };
354
+
355
+ /** @type {Renderer|null} the renderer once its device is up; null before and on failure */
356
+ let renderer = null;
357
+
358
+ /**
359
+ * Put the camera where a {@link camera_state} says. The controller moves the camera on its next
360
+ * update, so the frame after this one is the one that shows it.
361
+ *
362
+ * @param {{position: number[], target: number[], fov_degrees?: number}|string} state
363
+ */
364
+ globalThis.apply_camera = state => {
365
+ const parsed = typeof state === 'string' ? JSON.parse(state) : state;
366
+
367
+ if (parsed.fov_degrees !== undefined) {
368
+ camera.fov = parsed.fov_degrees * DEG_TO_RAD;
369
+ }
370
+
371
+ controller.look(
372
+ new Vector3(parsed.position[0], parsed.position[1], parsed.position[2]),
373
+ new Vector3(parsed.target[0], parsed.target[1], parsed.target[2])
374
+ );
375
+
376
+ renderer?.indicate_view_change();
377
+ };
378
+
379
+ element('copy_camera').addEventListener('click', () => guarded('copy camera', async () => {
380
+ const text = JSON.stringify(globalThis.camera_state());
381
+
382
+ element('camera_state').value = text;
383
+
384
+ try {
385
+ await navigator.clipboard.writeText(text);
386
+ report(`camera copied: ${text}`);
387
+ } catch (e) {
388
+ // clipboard access wants a secure context and a user gesture; the box has it anyway
389
+ report(`camera (clipboard unavailable, copy from the box): ${text}`);
390
+ }
391
+ }));
392
+
393
+ element('apply_camera').addEventListener('click', () => guarded('apply camera', () => {
394
+ globalThis.apply_camera(element('camera_state').value);
395
+ report(`camera applied: ${element('camera_state').value}`);
396
+ }));
397
+
398
+ // the controller's keyboard device listens on body, and only a focused element gets key events
399
+ document.body.focus();
400
+
401
+ // ---- the scene, which needs no device either ----------------------------------------------
402
+
403
+ // The meshlet builder runs on a WASM instance that instantiates asynchronously at import; the
404
+ // renderer's own initialize used to give it the time, and building geometry before that meant
405
+ // reading an instance that was not there yet.
406
+ await MeshoptClusterizer.ready;
407
+
408
+ const scene = new Scene();
409
+
410
+ scene.lights.add(make_sunlight({
411
+ intensity: 1.2,
412
+ direction: new Vector3(0.3, -1, -0.4),
413
+ temperature: 5500,
414
+ }));
415
+
416
+ const flat = Quaternion.fromAxisAngle(Vector3.right, -Math.PI / 2);
417
+
418
+ const backdrop = place(
419
+ make_plane(40, 16),
420
+ make_material(new Color(0.55, 0.55, 0.58, 1), { transparent: false }),
421
+ new Vector3(0, 6, -12)
422
+ );
423
+ const floor = place(
424
+ make_plane(40, 40),
425
+ make_material(new Color(0.35, 0.33, 0.3, 1), { transparent: false }),
426
+ new Vector3(0, 0, 0),
427
+ flat
428
+ );
429
+
430
+ // a dark opaque bar behind the stack, so bleed-through of the cards' colours has a reference
431
+ const bar = place(
432
+ make_plane(1.2, 6),
433
+ make_material(new Color(0.05, 0.05, 0.06, 1), { transparent: false }),
434
+ new Vector3(0, 3, -8)
435
+ );
436
+
437
+ scene.add(backdrop);
438
+ scene.add(floor);
439
+ scene.add(bar);
440
+
441
+ /** @type {Mesh[]} */
442
+ let cards = [];
443
+ /** @type {Mesh|undefined} */
444
+ let glass;
445
+ /** @type {Mesh|undefined} */
446
+ let opaque_quad;
447
+
448
+ /**
449
+ * Rebuild the stack, the pane and the quad. Materials are rebuilt too, so a change in alpha
450
+ * reaches the material buffer rather than a stale copy.
451
+ *
452
+ * @param {number} count
453
+ * @param {number} spacing world units between consecutive cards, along the view axis
454
+ * @param {number} alpha
455
+ * @param {boolean} with_glass
456
+ * @param {string} tint key of GLASS_TINTS
457
+ * @param {boolean} with_opaque_quad
458
+ */
459
+ function rebuild(count, spacing, alpha, with_glass, tint, with_opaque_quad) {
460
+ for (const card of cards) {
461
+ scene.remove(card);
462
+ }
463
+ cards = [];
464
+
465
+ if (glass !== undefined) {
466
+ scene.remove(glass);
467
+ glass = undefined;
468
+ }
469
+
470
+ if (opaque_quad !== undefined) {
471
+ scene.remove(opaque_quad);
472
+ opaque_quad = undefined;
473
+ }
474
+
475
+ const card_geometry = make_plane(3, 3);
476
+
477
+ for (let i = 0; i < count; i++) {
478
+ const color = CARD_COLORS[i % CARD_COLORS.length];
479
+ const material = make_material(new Color(color.r, color.g, color.b, alpha));
480
+
481
+ // a slight lateral offset per card so the stack reads as a stack from this camera
482
+ const card = place(
483
+ card_geometry,
484
+ material,
485
+ new Vector3(-1.5 + i * 0.45, 2.2 + i * 0.25, 2 - i * spacing)
486
+ );
487
+
488
+ cards.push(card);
489
+ scene.add(card);
490
+ }
491
+
492
+ if (with_glass) {
493
+ glass = place(
494
+ make_plane(3.5, 4.5),
495
+ make_material(GLASS_TINTS[tint], { transmission: 1 }),
496
+ new Vector3(3.6, 2.4, 3)
497
+ );
498
+ scene.add(glass);
499
+ }
500
+
501
+ if (with_opaque_quad) {
502
+ /*
503
+ One unit behind the last card, high and to the right of the stack's centre, so part of it
504
+ is seen through the cards and part stands clear against the backdrop and the dark bar.
505
+ Alpha 1 through `TransparencyMode.Transparent` (the default of make_material): both
506
+ pipelines clamp coverage at OIT_COVERAGE_MAX, so this is the "0 % transmittance card" of
507
+ the deck's harness, and its edges are the depth discontinuities moments cannot hold.
508
+ */
509
+ const z_last = 2 - (count - 1) * spacing;
510
+
511
+ opaque_quad = place(
512
+ make_plane(1.6, 1.6),
513
+ make_material(new Color(0.95, 0.9, 0.8, 1)),
514
+ new Vector3(0.9, 3.5, z_last - 1)
515
+ );
516
+ scene.add(opaque_quad);
517
+ }
518
+
519
+ renderer?.indicate_view_change();
520
+ }
521
+
522
+ // ---- settings ----------------------------------------------------------------------------
523
+
524
+ /**
525
+ * The recording in progress, or null. `Renderer.profile_session` is a nullable field and the
526
+ * null checks around it are the whole integration; the two imports at the top of this file are
527
+ * the opt-in.
528
+ *
529
+ * @type {GPUProfileSession|null}
530
+ */
531
+ let profile_session = null;
532
+
533
+ /** the tracer's own floor, read once the renderer exists */
534
+ let default_min_accumulation_alpha = 0.01;
535
+
536
+ function apply_settings() {
537
+ const mode = element('mode').value;
538
+ const reference = mode === 'REFERENCE';
539
+
540
+ VIEWPORT.width = Math.max(2, Number(element('width').value) | 0);
541
+ VIEWPORT.height = Math.max(2, Number(element('height').value) | 0);
542
+
543
+ rebuild(
544
+ Math.max(1, Number(element('count').value) | 0),
545
+ Number(element('spacing').value),
546
+ Math.min(0.997, Math.max(0.01, Number(element('alpha').value))),
547
+ element('glass').checked,
548
+ element('tint').value,
549
+ element('opaque_quad').checked
550
+ );
551
+
552
+ if (renderer === null) {
553
+ report(`no device — mode ${mode} set aside; the camera still orbits and copies`, 'fail');
554
+ return;
555
+ }
556
+
557
+ renderer.transparency_mode = mode === 'MBOIT'
558
+ ? ShadeTransparencyMode.MBOIT
559
+ : ShadeTransparencyMode.AVBOIT;
560
+
561
+ renderer.avboit.transmission_tint_from_base_color = element('tint_from_base_color').checked;
562
+
563
+ renderer.indirect_lighting_mode = element('indirect').value === 'Brick4'
564
+ ? ShadeIndirectLightingMode.Brick4
565
+ : ShadeIndirectLightingMode.IBL;
566
+
567
+ // The tracer accumulates on its own and resets when the camera moves; TAA on top of it is a
568
+ // second temporal filter over the same image. The checkbox keeps the user's choice for the
569
+ // rasterized modes, it is only overridden while tracing.
570
+ renderer.feature_path_tracing_enabled = reference;
571
+ renderer.path_tracer.min_accumulation_alpha = reference
572
+ ? REFERENCE_MIN_ACCUMULATION_ALPHA
573
+ : default_min_accumulation_alpha;
574
+ renderer.path_tracer.clear_history = true;
575
+
576
+ renderer.feature_taa_enabled = element('taa').checked && !reference;
577
+ renderer.feature_shadows_enabled = element('shadows').checked;
578
+ renderer.feature_automatic_exposure_enabled = element('exposure').checked;
579
+ renderer.exposure_compensation = Number(element('exposure_compensation').value);
580
+
581
+ report(`mode ${mode}, ${element('indirect').value} indirect, ${element('count').value} cards, spacing ${element('spacing').value}, alpha ${element('alpha').value}`);
582
+ }
583
+
584
+ element('apply').addEventListener('click', () => guarded('apply', apply_settings));
585
+ element('mode').addEventListener('change', () => guarded('apply', apply_settings));
586
+ element('indirect').addEventListener('change', () => guarded('apply', apply_settings));
587
+
588
+ element('reset').addEventListener('click', () => guarded('reset accumulation', () => {
589
+ if (renderer === null) {
590
+ return;
591
+ }
592
+ renderer.path_tracer.clear_history = true;
593
+ renderer.indicate_view_change();
594
+ }));
595
+
596
+ /**
597
+ * While tracing, the report carries how settled the reference is: the sample count of the tile
598
+ * that has had the fewest, which is the floor on convergence anywhere in the image. Updated on
599
+ * its own clock rather than per frame so the panel stays readable. Silent while a profile is
600
+ * recording, whose own status owns the report then.
601
+ */
602
+ setInterval(() => {
603
+ if (renderer === null || !renderer.feature_path_tracing_enabled || profile_session !== null) {
604
+ return;
605
+ }
606
+
607
+ const samples = renderer.path_tracer.min_sample_count;
608
+ const floor = Math.round(1 / renderer.path_tracer.min_accumulation_alpha);
609
+
610
+ report(
611
+ `mode REFERENCE — ${samples} samples/pixel accumulated (running mean up to ${floor}, then exponential); `
612
+ + `hold the camera still, or press Reset accumulation after a change`
613
+ );
614
+ }, 1000);
615
+
616
+ /**
617
+ * Set controls by name and apply them, for driving an A/B from a script rather than by hand.
618
+ * A `camera` entry takes the shape {@link camera_state} returns.
619
+ *
620
+ * @param {Record<string, string|number|boolean|object>} settings keyed by input id
621
+ */
622
+ globalThis.configure = settings => {
623
+ for (const [id, value] of Object.entries(settings)) {
624
+ if (id === 'camera') {
625
+ globalThis.apply_camera(value);
626
+ continue;
627
+ }
628
+
629
+ const input = element(id);
630
+
631
+ if (input === null) {
632
+ throw new Error(`no control named '${id}'`);
633
+ }
634
+
635
+ if (input.type === 'checkbox') {
636
+ input.checked = Boolean(value);
637
+ } else {
638
+ input.value = String(value);
639
+ }
640
+ }
641
+
642
+ apply_settings();
643
+ };
644
+
645
+ // ---- compile check -----------------------------------------------------------------------
646
+
647
+ /**
648
+ * Compile every AVBOIT module on this device and print the result.
649
+ *
650
+ * @returns {Promise<{name: string, ok: boolean, messages: string[]}[]>}
651
+ */
652
+ globalThis.compile_check = async () => {
653
+ if (renderer === null) {
654
+ throw new Error('no device');
655
+ }
656
+
657
+ report('compiling…');
658
+
659
+ const results = await compile_all(renderer.graphics.device);
660
+ const failed = results.filter(r => !r.ok);
661
+
662
+ const lines = results.map(r => `${r.ok ? 'ok ' : 'FAIL'} ${r.name}${r.messages.length ? '\n ' + r.messages.join('\n ') : ''}`);
663
+
664
+ report(
665
+ `${results.length - failed.length}/${results.length} AVBOIT modules compiled\n${lines.join('\n')}`,
666
+ failed.length === 0 ? 'pass' : 'fail'
667
+ );
668
+
669
+ globalThis.__last_compile = results;
670
+
671
+ return results;
672
+ };
673
+
674
+ element('compile').addEventListener('click', () => guarded('compile check', () => globalThis.compile_check()));
675
+
676
+ // ---- profiling ---------------------------------------------------------------------------
677
+
678
+ /** Guards against the button being leaned on while a stop is still draining. */
679
+ let profile_busy = false;
680
+
681
+ globalThis.start_profile = () => {
682
+ if (renderer === null) {
683
+ throw new Error('no device');
684
+ }
685
+
686
+ const device = renderer.graphics.device;
687
+
688
+ const settings = [
689
+ element('mode').value,
690
+ `${element('indirect').value} indirect`,
691
+ `${element('count').value} cards`,
692
+ `spacing ${element('spacing').value}`,
693
+ `alpha ${element('alpha').value}`,
694
+ `${VIEWPORT.width}x${VIEWPORT.height}`,
695
+ element('taa').checked ? 'TAA' : 'no TAA',
696
+ ];
697
+
698
+ const session = new GPUProfileSession({
699
+ level: PROFILE_LEVEL,
700
+ note: `avboit — ${settings.join(' · ')}`,
701
+ });
702
+
703
+ // Populate before start(): the meta record is written into the stream there. A pass taking
704
+ // 0.4 ms says nothing until you know which GPU and at what resolution.
705
+ const meta = session.meta;
706
+ const info = device.adapterInfo;
707
+
708
+ if (info !== undefined && info !== null) {
709
+ meta.adapter_vendor = info.vendor ?? "";
710
+ meta.adapter_architecture = info.architecture ?? "";
711
+ meta.adapter_device = info.device ?? "";
712
+ meta.adapter_description = info.description ?? "";
713
+ }
714
+
715
+ meta.features = [...device.features];
716
+ meta.engine_version = package_json.version;
717
+
718
+ session.onBytesWritten.add(bytes => {
719
+ report(`● recording ${element('mode').value} — ${session.frames_recorded} frames, ${format_bytes(bytes)} · press Profile again to stop`);
720
+ });
721
+
722
+ renderer.profile_session = session;
723
+ session.start();
724
+
725
+ profile_session = session;
726
+
727
+ const timed = device.features.has('timestamp-query');
728
+
729
+ report(timed
730
+ ? `● recording ${element('mode').value} — 0 frames · press Profile again to stop`
731
+ : '● recording without timestamp-query — structure only, no durations · press Profile again to stop');
732
+
733
+ element('profile').textContent = 'Stop profile and download';
734
+ element('profile').classList.add('recording');
735
+ };
736
+
737
+ /**
738
+ * Wait for frames already submitted to land in the session; stopping the instant the button is
739
+ * pressed would drop the last frames, which are the ones being looked at.
740
+ *
741
+ * @param {GPUProfileSession} session
742
+ */
743
+ async function drain_profile(session) {
744
+ const DEADLINE_MS = 1000;
745
+ const QUIET_MS = 60;
746
+
747
+ await renderer.graphics.device.queue.onSubmittedWorkDone();
748
+
749
+ const deadline = performance.now() + DEADLINE_MS;
750
+
751
+ let previous = session.frames_recorded;
752
+
753
+ while (performance.now() < deadline) {
754
+ await new Promise(resolve => setTimeout(resolve, QUIET_MS));
755
+
756
+ if (session.frames_recorded === previous) {
757
+ return;
758
+ }
759
+
760
+ previous = session.frames_recorded;
761
+ }
762
+ }
763
+
764
+ globalThis.stop_profile = async () => {
765
+ const session = profile_session;
766
+
767
+ if (session === null) {
768
+ return;
769
+ }
770
+
771
+ profile_session = null;
772
+
773
+ // detach first, so nothing encoded during the drain joins the recording
774
+ renderer.profile_session = null;
775
+
776
+ report('stopping — draining in-flight frames…');
777
+
778
+ await drain_profile(session);
779
+
780
+ const bytes = session.stop();
781
+
782
+ const filename = `avboit-${element('mode').value}-${file_timestamp()}.sgpt`;
783
+
784
+ download_bytes(bytes, filename);
785
+
786
+ const summary = `${session.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, `
787
+ + `${session.topology_count} topolog${session.topology_count === 1 ? 'y' : 'ies'} -> ${filename}`;
788
+
789
+ console.log(`profile: ${summary}`);
790
+ report(`profile saved: ${summary}\nopen it in packages/gpu-inspector-tool (npm run dev --workspace @woosh/gpu-inspector-tool)`, 'pass');
791
+
792
+ element('profile').textContent = 'Profile (record GPU timings)';
793
+ element('profile').classList.remove('recording');
794
+ };
795
+
796
+ element('profile').addEventListener('click', () => guarded('profile', async () => {
797
+ if (profile_busy) {
798
+ return;
799
+ }
800
+
801
+ profile_busy = true;
802
+
803
+ try {
804
+ if (profile_session === null) {
805
+ globalThis.start_profile();
806
+ } else {
807
+ await globalThis.stop_profile();
808
+ }
809
+ } finally {
810
+ profile_busy = false;
811
+ }
812
+ }));
813
+
814
+ globalThis.scene = scene;
815
+
816
+ // ---- the device, last -------------------------------------------------------------------
817
+
818
+ const context = canvas.getContext('webgpu');
819
+
820
+ if (context === null) {
821
+ report('no WebGPU context — this browser has none, or the page is not a secure context', 'fail');
822
+ }
823
+
824
+ if (context !== null) {
825
+ const candidate = new Renderer();
826
+
827
+ report('acquiring a device…');
828
+
829
+ try {
830
+ await candidate.initialize({ context, pixelRatio: 1 });
831
+
832
+ renderer = candidate;
833
+
834
+ // eye adaptation keeps drifting for seconds after any change, which makes an A/B unreadable
835
+ renderer.feature_automatic_exposure_enabled = false;
836
+ renderer.exposure_compensation = 1.5;
837
+
838
+ default_min_accumulation_alpha = renderer.path_tracer.min_accumulation_alpha;
839
+
840
+ globalThis.renderer = renderer;
841
+
842
+ load_default_environment(scene).then(() => renderer.indicate_view_change());
843
+ } catch (e) {
844
+ report(`device: ${e.message}`, 'fail');
845
+ console.error(e);
846
+ }
847
+ }
848
+
849
+ await guarded('apply', apply_settings);
850
+
851
+ if (renderer !== null) {
852
+ report(
853
+ `${report_element.textContent}
854
+ `
855
+ + 'camera: drag to orbit, wheel to zoom, right-drag to pan, W/A/S/D to move (click the page first)'
856
+ );
857
+ }
858
+
859
+ // ---- frame loop --------------------------------------------------------------------------
860
+
861
+ create_frame_loop(time_delta_s => {
862
+ if (canvas.width !== VIEWPORT.width || canvas.height !== VIEWPORT.height) {
863
+ canvas.width = VIEWPORT.width;
864
+ canvas.height = VIEWPORT.height;
865
+ camera.aspect = VIEWPORT.width / VIEWPORT.height;
866
+ renderer?.resize(VIEWPORT.width, VIEWPORT.height);
867
+ }
868
+
869
+ controller.update();
870
+
871
+ if (renderer !== null) {
872
+ renderer.render(camera, scene, time_delta_s);
873
+ }
874
+ });
875
+ }