# Shield Panel: GPU Fracture Simulation Cut Map Date: 2026-09-18 Status: Imagination pass, cut map. Nothing here has landed. Repo anchors are against `codex/item-provenance` HEAD `491811bd`. The six incoming sources are not in the repo yet; they are anchored as `files/:` against the operator's drop at `/files/`. Mechanism claims marked **(probe)** were run, not reasoned. The probe was a throwaway Unity 6000.3.24f1 project in scratch (`/probe`), holding only the six files, a built-in port of the shader, and one editor script driven by `-batchmode -executeMethod`. Nothing was built in the Aetheria tree and no Unity session was opened against it; Hands was concurrently editing `Assets/Content`, `Packages/manifest.json` and the Addressables surfaces. Logs: `/probe{,2..6}.log`. The probe project is scratch and will be removed with the session; §1 records what it proved so no one has to re-run it to trust the map. ## 0. Target Operator, 2026-09-18: **wire it up to real hits.** The absorb capability in the `FieldShieldTest` scene drives the fracture panel; no standalone click-to-hit harness is built first. Ends: - A `ShieldPanel` exists in `FieldShieldTest`, materialises on load, and fractures when the absorb capability publishes an `AbsorbEvent`. - The event is the only input to the simulation. Presentation converts; it decides nothing about the rule, matching `Assets/Scripts/Gameplay/CapabilityPresenters.cs:8` and the fork-L contract in `Assets/Scripts/ServerShared/CapabilityEvents.cs:9-20`. - The sources compile and render on the built-in render pipeline. Aetheria has no URP: `ProjectSettings/GraphicsSettings.asset:41` is `m_CustomRenderPipeline: {fileID: 0}` and `Packages/manifest.json` carries no render-pipeline package. Invariants: - **One owner of the hit.** `ShieldPanel.Hit` is the only entry point into the simulation. The presenter is the only caller. Nothing else reaches the panel. - **One event, one hit.** An `AbsorbEvent` produces exactly one `Hit`, carrying the event's own world position and direction unmodified. - **Presentation owns only presentation.** Damage-type-to-pattern and magnitude-to-energy are presentation mappings authored on the presenter. The absorb capability's data (the damage-type mask) stays out of scope, as ruled 2026-09-17. - **No second render pipeline.** Nothing in this cut adds a package, a pipeline asset, or a URP include path. - **No Unity.Mathematics.** The incoming sources use `UnityEngine.Vector2/Vector3` only (verified: no `Unity.Mathematics` or `using static Unity` in any of the six). The presenter converts `CultMath.float3` with `CultMath.UnityBridge`, the way `Assets/Scripts/Gameplay/FieldDriver.cs:125` already does. ## 1. Probe record All results from Unity 6000.3.24f1, Direct3D12, `srp=builtin`, batchmode with graphics. 1. **The shader ports to built-in with three substitutions.** Removing `"RenderPipeline"="UniversalPipeline"` from the SubShader tags, replacing the URP `Core.hlsl` include with `UnityCG.cginc`, and renaming the three `TransformWorldToHClip(` calls to `UnityWorldToClipPos(` is sufficient: `shader supported=True, messages=0` (probe3 had one warning, see §1.7). No other URP symbol is used — `_ScreenParams`, `noperspective`, `clip`, `saturate` and `StructuredBuffer` at `#pragma target 4.5` are all built-in surface. **(probe)** 2. **Indirect drawing works on built-in.** `Graphics.RenderPrimitivesIndirect` with `RenderParams`, a `MaterialPropertyBlock` carrying `GraphicsBuffer`s, and an append buffer counter copied in by `GraphicsBuffer.CopyCount` rendered the panel: `argsFill vertsPerInstance=24 instanceCount=253`, 38036 of 65536 pixels lit. The cut does not have to change the drawing strategy. **(probe)** 3. **`RenderParams` has no `shaderPass`.** Full property list in 6000.3.24f1: `layer renderingLayerMask rendererPriority instanceID entityId worldBounds camera motionVectorMode reflectionProbeUsage material matProps shadowCastingMode receiveShadows lightProbeUsage lightProbeProxyVolume overrideSceneCullingMask sceneCullingMask forceMeshLod meshLodSelectionBias`. `files/ShieldPanel.cs:410,415,420` do not compile. **(probe)** 4. **A multi-pass material renders every pass from one call.** A two-pass probe shader (red triangle in pass 0, green in pass 1) drawn once with `Graphics.RenderPrimitives` produced `redPixels=40992 greenPixels=40983`. Pass selection is not available at this API, so the supplied "one args buffer per pass index" design cannot work as written. **(probe)** 5. **One args buffer sized to the widest pass is equivalent.** Setting every args buffer to `MaxVerts * 2 * 6 = 96` vertices per instance and issuing a single `RenderPrimitivesIndirect` produced pixel-identical output to the three-call version: `singleCall=False litPixels=43929` vs `singleCall=True litPixels=43929`. Each pass already culls its own surplus vertices (`files/ShieldPanel.shader:238` fill, `:287` outline, `:347` conduit), and additive `Blend One One` with `ZWrite Off` makes pass order irrelevant. **(probe)** 6. **`centroid` is a reserved word in the compute path.** `ShieldSim.compute` failed with `Error 18: syntax error: unexpected token 'centroid' at kernel KInit` — the `CellStatic.centroid` member at `files/ShieldCommon.hlsl:18`. Renaming it to `center` in the HLSL, compute and shader gives `compute supported=True, messages=0`. The fragment/vertex compiler tolerates it; the compute compiler does not. **(probe)** 7. **One live shader warning survives the port:** `Warning 172: use of potentially uninitialized variable (BuildLine)`, Conduit vertex program. Warning only; the pass renders. Worth a look while touching `BuildLine`, not worth a cut of its own. 8. **The supplied tuning never fractures anything.** Hex tiling, `panelRadius 5`, `cellSize 0.35`, 253 cells, a head-on Radial hit at the centre, 60 frames after: | energy | broken | maxDamage | maxPeakTension | minTemper | | --- | --- | --- | --- | --- | | 3.5 (the supplied "strong" hit) | 0/253 | 0 | 0.058 | 0.547 | | 35 | 0/253 | 0.0012 | 0.579 | 0.505 | | 60 | 0/253 | 0.0097 | 0.743 | 0.487 | | 90 | 0/253 | 0 | 0.076 | 0.548 | | 120 | 0/253 | 0 | 0 | 0.55 | | 160 | 0/253 | 0 | 0.084 | 0.549 | | 220 | 0/253 | 0 | 0 | 0.55 | | 350 | 0/253 | 0 | 0 | 0.55 | | 3500 | 0/253 | 0 | 0 | 0.55 | A break needs `u > temper + tensileStrength*toughness ≈ 1.45` **and** `damage >= 0.75`. Peak tension is non-monotonic in energy and collapses above ~60: the field saturates into the compression clamp and never swings back into tension. Causes named in §5 (D9, D10). **(probe)** This is the reason Cut 4 exists and why it is the operator's first fork: wiring real hits to a simulation that cannot break is a pretty stress wave and nothing else. ## Cut 1. Land the sources, compiling, on built-in - **Repo/branch:** Aetheria `codex/item-provenance` from `491811bd`. Depends on nothing. Touches no file Hands is holding (`Packages/manifest.json`, `Assets/Content`, `ServerShared` asset-path fields, `GameData/Aetheria.cc`, `ActionBarSlot.cs`, `UnityHelpers.cs`). - **First:** confirm Hands' Addressables cuts have landed or are on files disjoint from `Assets/Shaders/`. Nothing here goes near `Assets/Resources`, which is mid-move to `Assets/Content`. **Location.** All six files go to `Assets/Shaders/Compute/ShieldField/`, matching the repo's own convention for compute-driven effects: `Assets/Shaders/Compute/Lightning/` holds `Lightning.compute`, `Lightning.shader`, `LightningVertex.cginc` and `LightningCompute.cs` together; `Slime/` does the same. Colocation is also load-bearing — `#include "ShieldCommon.hlsl"` resolves relative to the including file (probe). No import settings are needed: `.compute` and `.hlsl` import with no configurable state. **Assembly.** No asmdef. The files land in `Assembly-CSharp` with everything else in `Assets/Shaders/Compute/*.cs` and `Assets/Scripts/Gameplay/`. The `ShieldField` namespace collides with nothing (`rg "class ShieldPanel\b|class Tilings\b|class Limits\b|enum TilingKind|class PolygonSoup|class VertexWelder|enum FracturePattern"` over `Assets/Scripts` and `Assets/Shaders` returns nothing). See Q7 for the testability cost. **Deletes first:** - `files/ShieldPanel.cs:450-487` — `ShieldPanelTester`. It is the standalone click harness the operator ruled against, and it cannot run anyway: `ProjectSettings/ProjectSettings.asset:797` is `activeInputHandler: 1` (new Input System only), so `Input.GetMouseButtonDown` and `Input.mousePosition` throw at runtime. Delete it rather than port it; the absorb path replaces it. - `files/ShieldPanel.cs:90` — `argsOutline`, `argsConduit` and their `MakeArgs` calls at `:172-173`, plus the `drawFill`/`drawOutline`/`drawConduits` bools at `:68-70` and their branches at `:408-422`. Replaced by one args buffer and one draw (probe §1.5). Per-pass toggles come back as a fork (Q6) if the operator wants them, and then as shader uniforms, not as draw calls. **Per-file changes (against the supplied files):** - `ShieldCommon.hlsl:18` — `float2 centroid;` → `float2 center;`. Every `.centroid` in `ShieldSim.compute` and `ShieldPanel.shader` follows. Required to compile (§1.6). - `ShieldCommon.hlsl:31` — add `float pad2;` after `pad1`, taking `CellStatic` to 16 floats. - `TilingBuilder.cs:27` — add `public float pad2;` and initialise it at `:242`. Today the C# struct is 15 floats (60 bytes) while `Stride` at `:28` claims `4 * 16` (64), and `SetData` throws: *"One of C# data stride (60 bytes) and Buffer stride (64 bytes) should be multiple of other"* (probe). Padding to 16 keeps the 16-byte-aligned stride the comment intends; the alternative, `Stride = 4 * 15`, also works and is smaller. Either fixes it — do not fix only one side. - `TilingBuilder.cs:214` — rename the inner `dist` to `nbrDist` (and `:221`). CS0136: it shadows the `dist` declared at `:225` in the enclosing scope. The file does not compile as delivered (probe). - `ShieldPanel.shader:27` — drop `"RenderPipeline"="UniversalPipeline"`, add `"IgnoreProjector"="True"`. - `ShieldPanel.shader:33` — `#include "UnityCG.cginc"` in place of the URP `Core.hlsl`. - `ShieldPanel.shader:162,163,244` — `TransformWorldToHClip(` → `UnityWorldToClipPos(`. - `ShieldPanel.cs:171-173` — one `argsFill = MakeArgs((uint)(MaxV * 2 * 6));`. - `ShieldPanel.cs:369-371` — one `GraphicsBuffer.CopyCount(bVisible, argsFill, sizeof(uint));`. - `ShieldPanel.cs:408-422` — one `Graphics.RenderPrimitivesIndirect(rp, MeshTopology.Triangles, argsFill, 1, 0);`, no `rp.shaderPass`. - `ShieldPanel.cs:296-298` — hoist the `new uint[] { 0u, cap }` to a cached field. It allocates every frame today. - `ShieldPanel.cs:9` — drop `[ExecuteAlways]` (Q5). With it, `Update` runs the whole simulation and three GPU dispatch groups every editor frame in every open scene view, and `OnEnable` rebuilds the tiling on every domain reload. - Defect fixes D3-D8 from §5, each named in the commit. **Authority map:** - Owner: `ShieldPanel` owns the simulation and its GPU buffers. `TilingBuilder` owns adjacency, areas and the free-surface ghost weights. `Tilings` owns geometry generation. - Inputs: authored fields on the component; `Hit(worldPos, worldDir, energy, pattern, radius)` from outside. - Outputs: three indirect draws' worth of pixels, and GPU state readable for verification. - Derived state: `worldBounds`, `groups`, `echoSlot`, `simTime` are derived from the data and the frame clock; none is authored. - Forbidden writers: nothing outside `ShieldPanel` touches `bState`, `bUA/bUB`, `bV`, `bEcho`, `bFlow`, `bVisible`, `bBudget` or the args buffer. `ShieldPanelTester` is deleted rather than kept as a second hit source. - Shared paths: edit-mode and play-mode take the same `Update` body (once `ExecuteAlways` is settled); `Rebuild` is the only path that allocates, and always releases first. - Deletion line: `ShieldPanelTester`, the two surplus args buffers, the three draw bools, and the `rp.shaderPass` writes are gone before the presenter is written. **Verification:** - builds: Unity batchmode compile of the Aetheria project (`-quit -batchmode -projectPath . -logFile`), zero errors. `ShaderUtil.GetShaderMessages` on `ShieldPanel.shader` and `GetComputeShaderMessages` on `ShieldSim.compute` report zero errors (the §1.7 warning may remain). - test: none. The rule worth pinning is in Cut 2; there is nothing here a unit test would hold that the compiler does not. - negative: `rg "UniversalPipeline|render-pipelines\.universal|TransformWorldToHClip" Assets/Shaders/Compute/ShieldField` is empty. `rg "shaderPass" Assets` is empty (verified: no legitimate use of that identifier in the tree today). - operator: none yet. Cut 3 is the first thing to look at. ## Cut 2. The absorb presenter - **Repo/branch:** same. Depends on Cut 1. - **Adds:** `Assets/Scripts/Gameplay/ShieldPanelPresenter.cs`, one component, `MonoBehaviour, IAbsorbPresenter`. - **Deletes:** nothing. `FieldDriver.Absorb` (`Assets/Scripts/Gameplay/FieldDriver.cs:158`) stays or goes by operator ruling Q2; the binder already supports both presenters subscribing to the same stream (`Assets/Scripts/Gameplay/CapabilityPresentationBinder.cs:32-36` iterates every `IAbsorbPresenter` in the children). **Shape:** ``` public class ShieldPanelPresenter : MonoBehaviour, IAbsorbPresenter { public ShieldPanel Panel; // sibling object, not a child public float EnergyScale = 1f; // AbsorbEvent.Magnitude -> panel energy public float HitRadius = -1f; // -1 = the panel's own default public FracturePattern[] PatternByDamageType; // indexed by (int)DamageType, length 6 public void Absorb(AbsorbEvent e) { if (Panel == null) return; Panel.Hit(e.Position.ToUnity(), e.Direction.ToUnity(), e.Magnitude * EnergyScale, PatternFor(e.DamageType), HitRadius); } } ``` `AbsorbEvent.Position` is the world hit point and `Direction` is the incoming ray direction (`Assets/Scripts/UI/FieldTester.cs:76` publishes `hit.point` and `ray.direction`), which is exactly what `Hit` wants — `files/ShieldPanel.cs:257-282` transforms both into panel space itself. No projection, no normalisation, no reinterpretation on the presentation side: that is the "one event, one hit" invariant. `PatternFor` reads the serialized table and falls back to `FracturePattern.Radial` when the array is short or unset, so an unauthored component still works. The mapping itself is Q3. **Authority map:** - Owner: `ShieldPanelPresenter` owns the event-to-hit conversion, and nothing else. - Inputs: one `AbsorbEvent`; three authored fields. - Outputs: one `ShieldPanel.Hit` call. - Derived state: none. The presenter holds no per-hit state, no queue, no timers — the panel already queues hits (`files/ShieldPanel.cs:82`). - Forbidden writers: the presenter never touches `CapabilityEvents`; the binder owns subscription. It never reads `FieldDriver`. Nothing writes `ShieldPanel` fields at runtime except its own inspector values. - Shared paths: the operator's click and any later real absorb capability reach the panel through the same `Absorb` method, because both go through `CapabilityEvents.Absorb`. - Deletion line: none; this cut is purely additive, and buys the only wiring the operator asked for. **Verification:** - builds: project compiles. - test: nothing in `Assets/Scripts/Tests` can reach `Assembly-CSharp` (`Tests.asmdef` references only `UnityEngine.TestRunner`, `UnityEditor.TestRunner` and `Aetheria.Shared.Unity`, and `autoReferenced: false`). So the rule is pinned by a batchmode probe instead, and the probe checks it at the layer where it would break: build a `CapabilityEvents`, a `CapabilityPresentationBinder`, the presenter and a real panel; publish one `AbsorbEvent` at a known off-centre position; step the sim; read `bState` back and assert the injected region is centred where the event said and that a second identical publish doubles it. A presenter with `Panel == null` publishes and does not throw. See Q7 for the alternative that buys NUnit coverage. - negative: `rg "ShieldPanel" Assets/Scripts --glob '!ShieldPanelPresenter.cs'` finds no second caller of `Hit`. ## Cut 3. The scene rig - **Repo/branch:** same. Depends on Cuts 1-2. - **Touches:** `Assets/Scenes/FieldShieldTest.unity` only. **Geometry.** The existing field is `Assets/Prefabs/cubesphere Variant.prefab`, a unit cubesphere scaled `(10, 10, 20)` at the origin, with the camera at `(0, 0, -50)` looking `+Z`. `ShieldPanel` builds a flat disc in local XY with its normal on `+Z`. So the panel becomes a **top-level scene object** at `(0, 0, -10)` with identity rotation and unit scale, `panelRadius = 10` — the near face of the field, same silhouette, undistorted. It must not be parented to the field: the field's non-uniform `(10,10,20)` scale would ride into `_PanelToWorld` and shear every shard. Clicks already land on the field's `MeshCollider` and the absorb event carries that world point, whose X/Y is what the panel consumes. The panel therefore needs no collider of its own and no change to `ClickableCollider` or the click catcher. **Wiring.** The presenter component goes on the **field** GameObject, next to `FieldDriver`, `ClickableCollider` and `CapabilityPresentationBinder`, because the binder finds presenters with `GetComponentsInChildren` on its own object (`CapabilityPresentationBinder.cs:32`). Its `Panel` field points at the separate panel object. One cross-reference, no scale contamination. **How the scene is changed.** Hands cannot click, and hand-editing the YAML means inventing fileIDs next to a prefab instance with an override list — the exact shape that produced `1428ada3` ("Repair the Addressables map after a bad sed prefixed every line"). So: a throwaway editor script under `Assets/Scripts/Editor/`, run once by `-batchmode -executeMethod`, that opens the scene, creates the panel object, adds and configures `ShieldPanel` (assigning the shader and compute assets by `AssetDatabase.LoadAssetAtPath`), adds the presenter to the field object, assigns the reference, and `EditorSceneManager.SaveScene`s. **The script is deleted in the same commit; the scene diff is the artifact.** Keeping it would mean a second authority over the rig that nobody would remember to re-run. Q4 offers the alternatives. **Authority map:** - Owner: the scene owns the rig. The editor script is a one-shot actuator with no standing authority and no life after the commit. - Inputs: the two prefab/asset GUIDs and the transform numbers above. - Outputs: one new GameObject, one new component on the field object, one reference. - Derived state: none. - Forbidden writers: nothing may create a `ShieldPanel` at runtime; the scene is the only source. `FieldTester` is not modified — it already publishes the absorb events, and giving it knowledge of the panel would re-split the authority fork L just merged. - Shared paths: the scene load path and the editor script write the same objects, because the script is what writes the scene. - Deletion line: the editor script, before the commit closes. **Verification:** - builds: project compiles; the scene opens with no missing-script warnings. - test: a batchmode headless open of `FieldShieldTest.unity` asserting that exactly one `ShieldPanel` exists, that its `sim` and `panelShader` are non-null, and that the field object's presenter resolves to it. - negative: `rg "ShieldPanelTester" Assets` is empty. - operator: enter play mode in `FieldShieldTest`; the panel materialises as a lattice filling the field silhouette; clicking the field produces a visible stress wave travelling out from the click and reflecting off the rim; the properties panel is unchanged. Whether anything shatters is Cut 4. ## Cut 4. Make a hit actually break the panel - **Repo/branch:** same. Depends on Cut 3 and on ruling Q1. - **Blocked:** this cut is a tuning and mechanism pass, not a wiring pass. §1.8 shows no energy fractures the panel at the supplied defaults. Do not tune by eye first; the two mechanism defects below bound what tuning can reach. **Per-file changes:** - `ShieldSim.compute:226-232` — `u` is clamped to `±_StressClamp` while `v` is not. A hard hit drives `v` far past what the clamp permits, `u` pins at `-_StressClamp`, and the field never swings back into tension: that is the collapse above energy 60 in §1.8, and why 3500 behaves like 120. Clamp `v` as well, or normalise the injection so energy maps to a bounded velocity. - `ShieldPanel.cs:301` and `:359` — `_Dt` is set to `fixedSubstepDt` before the injection loop and not restored until after `KFracture` has been dispatched, so `KFracture` integrates damage and temper erosion with a substep `dt` while running **once per frame**. Damage accumulates roughly six times slower than the wave it is reading (`substeps = 6`). Either dispatch `KFracture` inside the substep loop, or hand it the frame `dt`. The first is physically honest and costs five more dispatches per frame; the second is one line. Pick one and say which in the commit — this is the "which clock owns fracture" question, not a constant. - `ShieldPanel.cs:34,53,56` and the presenter's `EnergyScale` — the tuning itself, once the two above are settled. **Verification:** - test: a batchmode probe that sweeps energy across the authored range, reads `bState` back, and asserts that the weak energy leaves `broken == 0` while the strong energy breaks a bounded, non-zero fraction of cells — the same readback shape as §1.8, which is cheap and already written once. - negative: no energy in the authored range leaves `maxPeakTension == 0` with `minTemper == temperInit`, the saturation signature. - operator: strong clicks dice the panel; repeated weak clicks eventually break through (the temper mechanism at `ShieldSim.compute:286-298` is the whole point of the design and is untested until a break is reachable). ## 5. Defects in the supplied code Confirmed by compiling and running them, not by reading. D1-D4 are fatal; the cut does not exist without them. | # | Where | Finding | | --- | --- | --- | | D1 | `files/TilingBuilder.cs:214` | CS0136: `dist` shadows the `dist` at `:225`. The file does not compile. **(probe)** | | D2 | `files/ShieldPanel.cs:410,415,420` | `RenderParams.shaderPass` does not exist in 6000.3.24f1. **(probe)** | | D3 | `files/TilingBuilder.cs:28` vs `:12-27` | `Stride = 4 * 16` but the struct is 15 floats; `SetData` throws on the stride mismatch. **(probe)** | | D4 | `files/ShieldCommon.hlsl:18` | `centroid` is reserved in the compute compiler; `ShieldSim.compute` does not compile. **(probe)** | | D5 | `files/ShieldPanel.cs:411-421` + probe §1.4 | Pass selection does not exist at this API, so the three per-pass args buffers cannot work as designed, independent of D2. | | D6 | `files/ShieldSim.compute:38` | Comment says `_BreakBudget` is a "single element"; the C# allocates two (`ShieldPanel.cs:162`) and `:304` reads index 1. **Not a bug — a lying comment.** The earlier reading of this as an overrun is refuted. | | D7 | `files/Tilings.cs:302-333` | `VoronoiCells` is O(n² log n): every site scans every other site and sorts the whole list, then `Voronoi` runs it `lloydPasses + 1` times (4 for `TilingKind.Voronoi`). Confirmed by reading; not a blocker for the Hex/Penrose default, a real wait at a few thousand sites. Use a partial selection (`nth_element`-style) or reuse the grid `ChaosGame` already builds at `:212`. | | D8 | `files/Tilings.cs:286-288` | `Vector2.zero` is both the "invalid cell" sentinel and a legal centroid, so a cell centred on the origin is silently refused relaxation. Use a nullable or a parallel validity flag. `idx` at `:292,297` is dead. | | D9 | `files/ShieldSim.compute:226-232` | `u` is clamped, `v` is not; hard hits pin the field in compression and no tension ever reflects. The measured collapse in §1.8. **(probe)** | | D10 | `files/ShieldPanel.cs:301,356,359` | `KFracture` runs once per frame but integrates with the substep `dt`, so damage and temper erosion accrue ~6× slow. | | D11 | `files/ShieldSim.compute:326-330` | `_V[e.nbr] +=` from many threads is a non-atomic read-modify-write; the cascade is nondeterministic run to run. Acceptable for an effect, worth knowing before anyone tries to write a test that pins an exact shard count. | | D12 | `files/ShieldPanel.cs:296-298` | A two-element `uint[]` is allocated every frame for the break budget. Cache it. | | D13 | `files/ShieldPanel.cs:9` | `[ExecuteAlways]` runs the full simulation, all dispatches and all draws every editor frame, and rebuilds the tiling on every domain reload. See Q5. | | D14 | `files/ShieldPanel.cs:426-440` | `ReleaseAll` does not clear `pending`, so hits queued before a rebuild are injected into the new tiling. Harmless today; wrong in principle. | | D15 | `files/ShieldPanel.shader` Conduit pass | `Warning 172: use of potentially uninitialized variable (BuildLine)`. Warning only. **(probe)** | | D16 | `files/Tilings.cs:91-116` | The TriHex interstitial triangles are placed by a separate polar construction rather than from the hex corners they should share, so welding is not guaranteed and the dual graph can come out with spurious free surfaces. Untested; `Hex` and `PenroseP3` are the safe defaults until someone looks. | Buffer lifetime, `SetCounterValue` and `CopyCount` were checked specifically and are **correct**: `bVisible` is created `Append | Structured` (`files/ShieldPanel.cs:164`), reset at `:366` before the dispatch that appends, copied at `:369` into offset `sizeof(uint)` — which is `instanceCount` in `GraphicsBuffer.IndirectDrawArgs` — and every buffer is released by `ReleaseAll` on both `OnDisable` and the head of `Rebuild`. The probe's readback of `argsFill instanceCount=253` against `cells=253` confirms the counter path end to end. **(probe)** Per-frame cost, measured shape: 253 cells → 1 inject dispatch per queued hit, 12 dispatches for six wave substeps plus their echoes, 1 fracture, 1 update, 1 `CopyCount`, 1 buffer upload, 1 indirect draw. At the scene's panel radius of 10 with `cellSize 0.35` expect roughly four times the cells and the same dispatch count. Nothing here is close to a budget problem in a test scene; the only per-frame CPU waste is D12. ## 6. Subtraction ledger | Cut | Removed | Added | Deps/targets | | --- | --- | --- | --- | | 1 | `ShieldPanelTester` (~38 lines), 2 args buffers, 3 draw bools, 2 draw calls, `ExecuteAlways` | 6 files (~5.4k lines) | none added; no package, no pipeline, no asmdef | | 2 | — | 1 file (~35 lines) | none | | 3 | the one-shot editor script, in the same commit | 1 scene object, 1 component instance | none | | 4 | — | ~5 lines changed | none | Net: strongly additive, and it should be. The capability bought is a simulation that does not exist in the tree in any form; nothing currently in the repo can be deleted in exchange for it, and the only existing machine it might replace — `FieldDriver`'s absorb visual — is the operator's call in Q2, not a subtraction this map may assume. ## 7. Operator questions Most blocking first. **Q1 — Does the first landing have to fracture?** §1.8 says no energy breaks a cell at the supplied defaults, and two mechanism defects (D9, D10) bound what tuning alone can reach. A: land Cuts 1-3 now, ship a visibly-materialising panel with a real stress wave and no fracture, and take Cut 4 as its own pass with its own probe. B: hold the whole thing until Cut 4 lands too, so the first thing seen is the effect as designed. **Recommended: A.** Wiring is the thing ruled on, it is verifiable on its own, and separating it means the tuning pass gets judged against a rig that already works instead of against itself. The cost is one operator look that ends in "nothing broke," which is the honest state. **Q2 — Does the panel replace the field shield's absorb visual or sit alongside it?** `FieldDriver.Absorb` (`FieldDriver.cs:158`) feeds the existing ellipsoid ripple, and the binder subscribes every `IAbsorbPresenter` it finds, so both can run from the same event with no code change. A: both, for comparison. B: panel only — remove `IAbsorbPresenter` from `FieldDriver` and let its hit buffer machinery (`:76,105-132,204-217`) go with it. **Recommended: A for the first landing, then B as a deliberate deletion once the panel has been looked at.** Keeping both permanently would be two absorb visuals with no rule about which owns the read; that is exactly the split this map should not create. **Q3 — Which fracture pattern does each damage type get, and where is it authored?** Six of each, no natural ordering. Proposed table: Kinetic→Radial, Optical→Lance, Electric→Star, Thermal→Crystal, Corrosive→Fizzle, Ionizing→Spiral. Authoring: A: a serialized `FracturePattern[]` on the presenter, indexed by `(int)DamageType`. B: a switch in code. C: gameplay settings or item data. **Recommended: A.** It is presentation choice, it belongs with the presentation, and the operator can retune it without a compile. C is wrong today: the absorb capability owns no data yet, and `CapabilityEvents.cs:17-20` explicitly parks that. **Q4 — How is the scene edited?** A: a one-shot editor script run in batchmode and deleted in the same commit. B: hand-edited scene YAML. C: the operator clicks it together in the editor. **Recommended: A.** B is the failure mode that produced `1428ada3`; C costs the operator's hands for something deterministic. If the operator would rather keep the script as a menu item, say so — that turns it into a standing second authority over the rig and should be a deliberate choice, not a leftover. **Q5 — `[ExecuteAlways]`?** A: drop it; the panel is inert until play mode. B: keep it, so the lattice is visible while authoring the scene. **Recommended: A**, with `Rebuild` kept on the context menu so a deliberate editor preview is still one click. Keeping it runs a full GPU simulation in every open scene view forever, for authoring convenience that a context menu already provides. **Q6 — Per-pass draw toggles?** The supplied `drawFill`/`drawOutline`/`drawConduits` bools die with the single-call draw (probe §1.5). A: gone. B: back as shader uniforms that zero each pass's alpha. **Recommended: A**, until someone wants them; B is two minutes' work the day that happens, and the toggles have no consumer today. **Q7 — Buy NUnit coverage with an asmdef?** `Tests.asmdef` cannot reference `Assembly-CSharp`, so no unit test can see the presenter as mapped. A: no asmdef; pin the rule with the batchmode probe in Cut 2, which checks the whole path from published event to GPU state. B: add `Aetheria.ShieldField.asmdef` over the six files plus the presenter, referencing `Aetheria.Shared.Unity`, and let `Tests` reference it. **Recommended: A.** B buys a test of the arithmetic in a one-line method while adding a compile boundary and a reference edge to a project that has two test files total; the probe checks the thing that would actually break, at the layer it would break. **Q8 — Which tiling is the default?** The component ships `PenroseP3` with `mergeRhombs`. `Hex` is what the probe exercised and the only one whose adjacency was verified. `Voronoi` costs D7 at init and `TriHex` carries D16. A: Penrose, as authored. B: Hex for the first landing, Penrose once the rig is looked at. **Recommended: B**, then move to whatever looks right — this is a look-at-it decision, not a correctness one.