DRIFTLANDS co-op survival on drifting tides — studio progress

ADR-0013: Tide-Tint Render Pipeline — Global-Scope Shader Properties, DriftLayer Taxonomy, Build Overlay

Status

Accepted

Accepted 2026-09-01 (/architecture-review 2026-09-01 — blocking findings E14/E15/E16 and note F2 applied; see Amendment History)

Date

2026-09-01

Engine Compatibility

Field Value
EngineUnity 6.3 LTS (6000.3) · URP 2D Renderer (Render Graph) · Shader Graph 17.x
DomainRendering
Knowledge Risk⚠️ HIGH — architecture.md row 13 flags this domain and names the verification: "verify Shader Graph 2D global-property read in 6.3". URP Compatibility Mode was removed in 6.3.
References Consulteddocs/engine-reference/unity/VERSION.md, breaking-changes.md (URP/Graphics, §6.2 SetupRenderPasses), deprecated-apis.md, modules/rendering.md; live docs (below); design/art/art-bible.md §4.3, §8.A.4, §8.B.1–B.4; ADR-0001 ITideClock.TintFloats
Post-Cutoff APIs UsedNone that require a post-cutoff surface. Shader.SetGlobalColor/SetGlobalFloat are pre-cutoff and unchanged; Shader Graph blackboard properties are pre-cutoff. The only version-sensitive item is the name of the blackboard Scope control (§1). This is deliberate: the decision's whole point is to stay off 6.3's changed surfaces.
Verification RequiredV1 a Global-scope blackboard property receives Shader.SetGlobal* on 6.3 and does not appear in the Material Inspector · V2 SRP-batcher compatibility asserted structurally: ShaderUtil.GetSRPBatcherCompatibilityCode(DriftLayerSprite) == 0 after one rendered frame, beside a deliberately incompatible control shader that must read non-zero (observed 10) — UnityStats.batches is not the meter (it scales 1:1 with sprite count on 6.3 URP 2D; the SRP Batcher removes per-draw CBUFFER setup, it does not merge draws; review E21) — run against the Global-scope shader · V3 a DriftLayer sprite tints while a character sprite in the same frame does not · V4 entering build mode at max reach with the ghost dashed adds ≤ 2 draw calls in the editor (grid ≤ 1, ghost ≤ 1) as a marginal delta; the atlas-batched ghost-0 is a built-player claim deferred to a Frame Debugger check (review E20) · V5 violet is held through EvictionGrace (ADR-0001 F7) with no pop at teardown · V8 EditorSettings.spritePackerMode = 3 (build-time packing only) is a load-bearing project setting — no Play Mode measurement can observe atlas batching; any change re-opens V2/V4 (review E23)

Live documentation verified (2026-09-01)

The HIGH-risk claim architecture.md row 13 demanded was verified, and it changed the design:

Unity shader property precedence is per-instance → material → global. A value set with Shader.SetGlobalColor/SetGlobalFloat is used only if the material does not define that property: "Global properties are used if a shader needs them but the material does not have them defined", and "per-instance data overrides everything; then Material data is used; and finally if shader property does not exist in these two places then global property value is used." Sources: Shader.SetGlobalColor, Shader.SetGlobalFloat, Accessing shader properties in Cg/HLSL.

Consequence — the naïve implementation fails silently. A Shader Graph blackboard property added the ordinary way defaults to Per Material scope: it is declared in the shader's Properties block and serialized onto the material. Under the precedence rule the material's own value then wins, and Shader.SetGlobalColor("_TideTint", …) does nothing — no error, no warning, no tint. The art bible says "tint is a global-property read in-shader" (§8.B.3), which is true but not sufficient: it does not say the property must be declared at Global scope for that sentence to hold. §1 makes it a rule.

The resolution — set the blackboard property's Scope to Global. On the Shader Graph blackboard, Scope and Show In Inspector are two different controls, and only one of them does what this design needs:

Control What it actually does
Scope = Global \Per Material \Hybrid Per InstanceDeclaration scope. Global — "Makes the property editable at a global level, through a C# script only" — takes the property out of the material's property block, so nothing shadows the global and Shader.SetGlobal* reaches it. This is the control the design requires.
Show In InspectorVisibility only. Disabling it "includes an [HideInInspector] attribute to the material property" — the property is still a material property, merely hidden.

Hiding is not excluding, and this distinction is the whole finding (architecture review E14, 2026-09-01). A [HideInInspector] property remains declared in the shader's Properties block, so the material does have it defined — and by the precedence rule quoted above, the material's value wins and Shader.SetGlobal is a silent no-op. A property does not need anyone to write* it to shadow a global: it is serialized with the shader's default the moment the material is created. So Show In Inspector alone would ship precisely the bug this section exists to prevent.

Global scope is also the batching-safe path the art bible's hard rule requires: the value leaves the UnityPerMaterial CBuffer entirely rather than diverging per material. Because changing scope alters that CBuffer's layout, V2 must be run against the Global-scope shader specifically, not treated as a formality.

V1 confirms this empirically before anything is built on it — the mechanism is the load-bearing assumption of this entire ADR. The blackboard Reference name (not the display name) is what Shader.SetGlobal* addresses.

Sources (verified 2026-09-01): Shader Graph 17.0 Property Types for both control definitions; Shader.SetGlobalColor (6000.3) for the precedence rule.

Why this domain's HIGH risk does not bite

6.3 removed URP Compatibility Mode and all custom rendering must be Render Graph-native (AddRenderPasses; SetupRenderPasses deprecated in 6.2). This decision uses no custom render pass at all — the tint is a global property read inside an ordinary Shader Graph sprite shader. The HIGH-risk surface is avoided rather than navigated, which is exactly why the art bible chose it (§8.B.3). Any future full-screen tide post-process is Render-Graph-native from day one (§6).

ADR Dependencies

Field Value
Depends OnADR-0001 (Accepted — ITideClock.TintFloats(socket) is the sole tint source), ADR-0003 (assembly boundaries — the tint driver is Presentation), ADR-0005 (Accepted — per-template atlases, the batching unit)
EnablesThe friend-group playtest's anchor test ("the world says it, no UI timer") — the minimal pipeline must ship at MVP for that test to be runnable at all (art bible §8.B.3, systems index row 14)
BlocksThe tide-tint shader story; the BB ghost/grid overlay story (TR-bb-009)
Ordering NoteIndependent of ADR-0014 (UI stack) — world-space UI sprites are DriftLayer-untagged by construction (§2), so the two never touch the same material. Independent of ADR-0008: nothing here crosses the wire (§7).

Context

Problem Statement

The tide is the game's central mechanic, and the anchor promise is that the world states it — no UI timer. That promise is only testable if the world visibly changes colour with the tide, which makes the minimal tint pipeline an MVP requirement, not polish (ruled 2026-08-27, CD + AD). The art bible specifies the approach in detail; what no document settles is (a) the exact mechanism that makes a global property actually reach the shader on 6.3, and (b) what happens when more than one land is docked at once. Both turn out to be load-bearing.

Constraints

Requirements

Decision

1. The two globals are Global-scope blackboard properties (the rule that makes this work)

One Shader Graph sprite shader, DriftLayerSprite, shared by every DriftLayer sprite in every atlas. It declares exactly two tint inputs on the blackboard:

Blackboard Reference Type Scope Set by
_TideTintColorGlobalShader.SetGlobalColor
_TideAmountFloatGlobalShader.SetGlobalFloat

Both must be declared at Global scope. This is not a style preference — a Per Material property is serialized onto the material, the material's value then wins under Unity's precedence rule, and the global write becomes a silent no-op. It is the specific bug this ADR exists to prevent, because it fails with no error message and looks like "the tint doesn't work" long after the shader was signed off.

**Ticking Show In Inspector off is not a substitute** and must not be mistaken for one: it only adds [HideInInspector], leaving the property in the material's property block, still shadowing the global. The property would be invisible and broken — the worst combination, because the symptom hides the cause. Set Scope, not visibility.

Enforcement: V1 (a Global-scope property receives the global write) and an editor test asserting the shipped shader's property block does not contain _TideTint/_TideAmount at all (§8) — absence from the block, not merely hidden-ness, is the invariant.

2. The DriftLayer taxonomy is an exclusion by construction

Tagged (tinted): land-template terrain, harvest nodes, tide-affected props on docked lands.

Never tagged, never tinted: players, enemies, gear, telegraph VFX (combat GDD, telegraph contract: "Combat sprites and telegraph VFX are never tide-tinted … telegraphs must read identically under amber, neutral, and violet" — tinting one would make threat-reading tide-dependent, which is a gameplay defect, not a visual one), home-island structures (untagged by category), and all UI — including the world-space UI sprites ADR-0014 §1 introduces.

"By construction" means the exclusion is a material fact, not a runtime branch: excluded sprites use a different shader that has no tint inputs at all, so there is no code path that could tint them by mistake and no per-frame check to get wrong. V3 asserts it in a single frame.

3. MVP is N = 1, and the globals are exactly right there

ITideClock.TintFloats(int socket) (ADR-0001, Accepted) returns (amber, violet) per socket. At MVP the tide has N = 1 (tide GDD Tuning Knobs: range 1–8, "MVP 1, Tier 1 3–4") — exactly one shore socket, therefore at most one docked land. One global pair represents TintFloats(0) faithfully, costs one SetGlobalColor + one SetGlobalFloat per frame, and keeps SRP batching whole.

There is no contradiction between the art bible's "one shader global per land instance" (§4.3) and "must be global properties" (§8.B.1): at N = 1 those are the same statement.

4. ⚠️ The N > 1 cliff — named now, not discovered at Tier 1

At N > 1 the globals approach breaks by construction. Tide GDD Rule 4 draws socket phase offsets as a permutation of evenly-spaced C/N slots, so no two sockets are ever in phase — that is structural, not a tuning artefact. With 3–4 sockets, several lands are docked simultaneously at different tide phases, and a single frame-global _TideAmount cannot represent more than one of them. Every docked land would show the same phase, which is precisely the information the tint exists to convey.

This is a hard architectural cliff with a known trigger (N > 1), not a deferred nicety. The three ways across it, assessed now so Tier 1 does not rediscover them under time pressure:

Route Batching Verdict
Per-material instances (one material per land)breaks SRP batching — §8.B.1's flagged riskRejected unless measured to fit
Per-renderer MPBsbreaks SRP batching — banned outright by §8.B.1Forbidden
Per-sprite phase attribute — carry the socket index in a vertex stream / UV2, keep one material, and index small global arrays in-shaderpreserved — one material, one batch per atlasThe only batching-preserving path identified. Design it before N > 1 ships — with the three caveats below, which are the difference between a plan and a trap.

Route 3 caveats (verified 2026-09-01 — read these before building it):

MVP ships route 0 (single global pair). The array form is deliberately not built now — YAGNI, and N = 1 cannot exercise or test it. But the cliff is recorded with its trigger so the Tier 1 story is "implement the designed route", not "discover the design is wrong".

5. The tint driver

One TideTintDriver in Driftlands.Presentation (ADR-0003: Presentation may reference Rules/Tide; the driver is an adapter, not a formula).

6. No custom render pass at MVP

The tint is a property read inside the sprite shader. No Scriptable Renderer Feature, no custom Render Graph pass, no post-process. This is what keeps a HIGH-risk domain at effectively zero risk.

⚠️ Do not use docs/engine-reference/unity/modules/rendering.md as the reference for that future work — it mixes a Unity-6-correct RecordRenderGraph snippet with a pre-Render-Graph AddRenderPasses(ScriptableRenderer, ref RenderingData) signature in the same document and cites 6000.0 URLs. It would reintroduce the exact deprecated shape this ADR avoids. Flagged for a doc fix-pass.

If a full-screen tide atmosphere effect is ever added (Tier 1 "Tide-Tint Polish", systems index row 15), it must be Render-Graph-native (AddRenderPasses) from day one — Compatibility Mode is gone in 6.3 and SetupRenderPasses was deprecated in 6.2. Recorded as a forbidden pattern so nobody ports a tutorial written against the old API.

7. Networking: none

The tint is pure presentation derived from a deterministic schedule. Every client computes the same tide from the same seed and wall-clock (ADR-0001), so the tint needs no replication, no authority and no reconciliation. Nothing in this ADR touches NGO, HostCommitQueue, or any command. It is independent of ADR-0008.

8. The build overlay (TR-bb-009, QQ-06's remainder)

The BB ghost and reach-grid overlay are bundled here by architecture.md row 13 because they are the other thing drawing into the world outside the sprite batch.

Citation note: art-bible.md §8.A.4 attributes the telegraph exemption to "combat GDD Rule 11". That label is wrong — the requirement is in the combat GDD's telegraph contract (two statements, one in the telegraph rules and one in the hit-feedback section), while Rule 11 is the enemy-interface rule. The requirement is real and binding; only the pointer is stale. Worth correcting in the art bible's next pass so a reader chasing "Rule 11" does not conclude it was dropped.

Alternatives Considered

Alternative A: Exposed properties + Material.SetColor per material

Alternative B: Per-renderer MaterialPropertyBlocks

Alternative C: Baked per-tide-state sprite variants

Alternative D: Full-screen post-process tint (custom Render Graph pass)

Alternative E: Global array indexed per sprite (the N > 1 route) at MVP

Consequences

Positive

Negative

Risks

GDD Requirements Addressed

GDD / Spec Requirement How This ADR Addresses It
art-bible.md §8.B.1Globals via Shader.SetGlobal*, never per-renderer MPBs§1 — and §1 supplies the missing precondition (Global scope) without which the globals silently do nothing (TR-tint-001)
art-bible.md §8.A.4DriftLayer covers terrain/nodes/props; combat + character sprites and home structures never tinted§2 — exclusion by material, not by runtime branch; V3 (TR-tint-002)
art-bible.md §8.B.3One Shader Graph sprite shader; no custom Render Graph pass; future post-process Render-Graph-native§1, §6 (TR-tint-003)
art-bible.md §4.3Amber/violet curve; t=0 at dock open; one shader global per land§3 (at N = 1 "per land" and "global" coincide) + §4 (they diverge at N > 1)
design/gdd/tide-scheduler.md F7amberIntensity/violetIntensity; violet held through EvictionGrace§5 — the driver must not fade the held violet; V5 (TR-tide-008)
design/gdd/base-building.md Visual RequirementsDashed ghost + reach grid within < 100 draw calls§8 — ghost batches with the atlas (0), grid is one mesh (1); V4 (TR-bb-009)
design/quick-specs/camera-2026-08-27.md Rule 1Orthographic, 64 px/u, PPU 32, point filter§1 — a fragment-stage tint changes no sampling or geometry (TR-cam-001)

Performance Implications

Migration Plan

No production code exists yet.

Validation Criteria

Applied on Acceptance — ✅ ALL APPLIED 2026-09-01

Historical record. While this ADR was Proposed, no registry stances were registered and no other document was amended — amending Accepted documents on the strength of a Proposed one strands those edits if /architecture-review returns it. That deferral ended when this ADR was Accepted on 2026-09-01: every item below is now applied and committed (dc7c1b0). The list is kept as the record of what landed, not as outstanding work.

Registry stances claimed (now registered in docs/registry/architecture.yaml):

Documents amended (all ✅ applied 2026-09-01): architecture.md (§4 + Required-ADR row 13 → Accepted; QQ-06 fully closed), traceability-index.md (TR-tint-001/002/003, TR-tide-008, TR-bb-009), and two art-bible.md notes: §8.B.3 gains the Global-scope precondition (§1) — the one place the art bible is silent on something its own design needs — and §4.3 gains the N > 1 caveat, since its "one shader global _TideTint … per land instance" phrasing becomes false the moment a second socket exists (§4), and §4.3 is where an artist would look.

Related Decisions

Amendment History

Amendments — 2026-09-07 (architecture-review-2026-09-07.md)