Compare commits

..
Author SHA1 Message Date
Meta Vens
42778c183a Migrate planetary geometry consumers 2026-07-22 04:12:41 +01:00
Meta Vens
492d123edb Remove private planetary topology authority 2026-07-13 02:52:25 +01:00
Meta Vens
2531df5231 Adopt CultMath planetary runtime 2026-07-13 02:14:26 +01:00
Meta Vens
2ef556783e Document shared planetary field runtime 2026-07-13 01:14:17 +01:00
Meta Vens
a580360bc8 Finish planetary erosion implementation audit 2026-07-13 00:56:12 +01:00
Meta Vens
9995ae1e71 Publish planetary debug modes through renderer ABI 2026-07-13 00:41:53 +01:00
Meta Vens
e769703917 Expose direct planetary page error debug view 2026-07-13 00:25:18 +01:00
Meta Vens
0e6a30eeb5 Add planetary topology and debug evidence 2026-07-13 00:18:21 +01:00
Meta Vens
f98a37d348 Close cross-face residual eviction gate 2026-07-13 00:02:44 +01:00
Meta Vens
c3a78af2f3 Close planetary patch performance and arrival gates 2026-07-12 23:58:04 +01:00
Meta Vens
c1d250c351 Make raster patches sole Zyphos terrain owner 2026-07-12 23:25:23 +01:00
Meta Vens
0ef33d65c1 Add coarse planetary raster patch backend 2026-07-12 23:09:07 +01:00
Meta Vens
853ee9c9f5 Document raster patch terrain roadmap 2026-07-12 23:00:34 +01:00
Meta Vens
8bcd47af88 Unify planetary terrain differentials and shading 2026-07-12 21:29:19 +01:00
Meta Vens
5158bb4103 Add residual quadtree erosion residency 2026-07-12 21:21:57 +01:00
Meta Vens
db69e28d2c Render Zyphos erosion from persistent GPU pages 2026-07-12 20:53:20 +01:00
Meta Vens
65fd71c52a Document planetary erosion rendering roadmap 2026-07-12 20:29:35 +01:00
Meta Vens
c1c32a0f14 Reduce planetary page bounds on GPU 2026-07-12 20:13:35 +01:00
Meta Vens
35812337d7 Generate seam-stable planetary erosion pages 2026-07-12 20:09:52 +01:00
Meta Vens
071bfc288a Render footprint-filtered advanced planet erosion 2026-07-12 20:02:45 +01:00
Meta Vens
bc4ca92aab Measure advanced erosion CPU GPU parity 2026-07-12 19:57:25 +01:00
Meta Vens
ff13414230 Document planetary erosion rendering roadmap 2026-07-12 19:39:33 +01:00
Meta Vens
5854630364 Add stateless spherical erosion to Zyphos 2026-07-12 19:20:15 +01:00
Meta Vens
cc73fab705 Fix Bokusho ink depletion timestep 2026-06-16 10:25:00 +01:00
Meta Vens
b2946a8910 Apply Bokusho paper tooth inside contact patches 2026-06-16 09:37:54 +01:00
Meta Vens
f27f3082da Add Bokusho source path buffer 2026-06-16 09:21:47 +01:00
Meta Vens
c169f811b1 Carry Bokusho source stroke tangents across packets 2026-06-16 08:32:36 +01:00
Meta Vens
5487a62b51 Add Bokusho source stroke rhythm 2026-06-16 08:03:06 +01:00
Meta Vens
af6923ecd1 Carry Bokusho page patch state across stroke packets 2026-06-16 07:34:12 +01:00
Meta Vens
b40531508a Sweep Bokusho contact patch radii 2026-06-16 07:14:10 +01:00
Meta Vens
49d27049d9 Add Bokusho paper tooth contact response 2026-06-16 06:16:28 +01:00
Meta Vens
a7077e6cb5 Add Bokusho ink film bristle response 2026-06-16 05:55:49 +01:00
Meta Vens
ee1b92a94c Preserve Bokusho source stroke continuity 2026-06-16 05:36:46 +01:00
Meta Vens
b7b3529311 Use fixed Bokusho density addends 2026-06-16 04:30:13 +01:00
Meta Vens
e9caacd781 Use swept Bokusho contact capsules 2026-06-16 03:39:52 +01:00
Meta Vens
9888113282 Align Bokusho page tangent sampling 2026-06-16 03:09:48 +01:00
Meta Vens
9814371313 Remove Bokusho taper pow drift 2026-06-16 02:51:45 +01:00
Meta Vens
ce423793d8 Quantize Bokusho page contact deposition 2026-06-16 02:29:26 +01:00
Meta Vens
03ef2cefc1 Localize Bokusho DirectCompute parity drift 2026-06-16 01:37:20 +01:00
Meta Vens
e189884b97 Expose Bokusho density parity readback 2026-06-16 00:53:38 +01:00
Meta Vens
4eb624988a Decode Bokusho CPU pages from density 2026-06-16 00:12:35 +01:00
Meta Vens
5f368158ee Let Bokusho page deposition consume full stroke packets 2026-06-15 23:56:27 +01:00
Meta Vens
d8a9088483 Pin Bokusho page patch footprint 2026-06-15 23:32:48 +01:00
Meta Vens
a94e8e05d0 Pin Bokusho page density composition 2026-06-15 23:23:31 +01:00
Meta Vens
eb9e6b409c Decouple Bokusho page deposition from debug tubes 2026-06-15 23:03:20 +01:00
Meta Vens
231461c44a Add DirectCompute Bokusho page deposition 2026-06-15 22:53:09 +01:00
Meta Vens
0a06f7a25f Carry more Bokusho ink without clipping 2026-06-15 22:10:46 +01:00
Meta Vens
4394ee7107 Deposit Bokusho ink into swept page field 2026-06-15 21:39:57 +01:00
Meta Vens
51bf15e552 Carry Bokusho ink through swept contact 2026-06-14 03:19:38 +01:00
Meta Vens
201b4d59ea Simplify Bokusho brush simulation core 2026-06-14 00:32:07 +01:00
Meta Vens
3e97904085 Remove Bokusho projection rescue marks 2026-06-13 22:29:38 +01:00
Meta Vens
de53ab72cb Give Bokusho primitive strokes body 2026-06-13 22:07:37 +01:00
Meta Vens
be63754c28 Tie Bokusho release flecks to local sweep 2026-06-13 20:34:13 +01:00
Meta Vens
88c8c33faf Constrain Bokusho release fleck projection 2026-06-13 19:48:11 +01:00
Meta Vens
3833569b4a Shape Bokusho lifted stroke gestures 2026-06-13 19:22:02 +01:00
Meta Vens
d8eb303bbd Pull Bokusho release flecks toward brush edges 2026-06-13 19:07:32 +01:00
Meta Vens
e90828baca Scatter satellite flecks from Bokusho release samples 2026-06-13 18:50:11 +01:00
Meta Vens
960c366658 Project isolated Bokusho release flecks 2026-06-13 18:36:25 +01:00
Meta Vens
64205786cc Release dry pigment from torn Bokusho bristles 2026-06-13 18:18:04 +01:00
Meta Vens
6a55921bc2 Project Bokusho dry flecks from tuft state 2026-06-13 17:56:10 +01:00
Meta Vens
45a9f262b1 Project Bokusho ink from decisive tuft contact 2026-06-13 17:10:20 +01:00
Meta Vens
976ff1db73 Steer Bokusho tuft drag from brush pose 2026-06-13 16:54:43 +01:00
Meta Vens
6dca7d1cc7 Shape Bokusho strokes with lifted source taper 2026-06-13 16:44:31 +01:00
Meta Vens
10f9e1c2fb Break Bokusho bristle edges by lane contact 2026-06-13 16:37:35 +01:00
Meta Vens
baa2c85add Scale Bokusho brush physics by source span 2026-06-13 16:27:07 +01:00
Meta Vens
55e99641d5 Borrow Bokusho page samples across source segments 2026-06-13 16:13:20 +01:00
Meta Vens
70f5d49cc4 Key Bokusho paper response by source stroke 2026-06-13 15:49:20 +01:00
Meta Vens
107722141d Phase Bokusho fan motion by source stroke 2026-06-13 15:42:43 +01:00
Meta Vens
d5de205c40 Blend Bokusho joins by source stroke 2026-06-13 15:35:51 +01:00
Meta Vens
5d550f4ade Use source stroke ids for Bokusho tuft replay 2026-06-13 15:22:10 +01:00
Meta Vens
94474fcd3e Gate Bokusho page projection longitudinally 2026-06-13 15:04:15 +01:00
Meta Vens
b2ae858ce8 Weight Bokusho pigment by tuft lane core 2026-06-13 14:54:29 +01:00
Meta Vens
92659dbf20 Tighten Bokusho page contact core 2026-06-13 14:45:27 +01:00
Meta Vens
58bfabe4b6 Project Bokusho canvas with swept tuft contact 2026-06-13 14:37:04 +01:00
Meta Vens
43df4e6f89 Replay Bokusho tuft state across stroke segments 2026-06-13 14:12:48 +01:00
Meta Vens
60987c5df1 Carry Bokusho stroke segment progress 2026-06-13 14:02:34 +01:00
Meta Vens
8f03cf1f10 Project Bokusho ink from bristle contacts 2026-06-13 12:37:35 +01:00
Meta Vens
7a93ee41e4 Add Bokusho stroke pose controls 2026-06-13 12:11:27 +01:00
Meta Vens
4c54666da7 Derive Bokusho gesture pressure from stroke geometry 2026-06-13 11:53:13 +01:00
Meta Vens
0c59b9f497 Add Bokusho wet lane cohesion 2026-06-13 11:44:14 +01:00
Meta Vens
4795db6c0f Shape Bokusho brush pressure and taper footprint 2026-06-13 11:31:42 +01:00
Meta Vens
baa77171b5 Improve Bokusho page ink projection 2026-06-13 11:14:05 +01:00
Meta Vens
3f673a7470 Add per-stroke Bokusho dynamics controls 2026-06-13 10:53:49 +01:00
Meta Vens
061216b2d3 Refine Bokusho local pigment deposition 2026-06-13 10:43:36 +01:00
Meta Vens
621280ba72 Expose Bokusho page ink in D3D scene 2026-06-13 10:19:03 +01:00
Meta Vens
18cf9ae9d7 Support multi-stroke Bokusho brush compute 2026-06-13 10:06:39 +01:00
Meta Vens
15f60cc725 Add Bokusho brush profile controls to compute path 2026-06-13 09:31:40 +01:00
Meta Vens
7a09df598d Project Bokusho compute canvas onto page field 2026-06-13 00:05:19 +01:00
Meta Vens
a46fc26bf8 Add Bokusho direct compute brush simulation 2026-06-12 20:57:17 +01:00
Meta Vens
6e1eb30f7c Route tube field curves through CultMath 2026-06-12 16:19:18 +01:00
Meta Vens
a15facecaa Gate reservoir history presentation 2026-06-02 17:24:25 +01:00
Meta Vens
696f92e47c Add renderer overdraw debug view 2026-06-01 20:15:27 +01:00
Meta Vens
dfcf35aa57 Add typed Eve UI bindings 2026-06-01 17:43:00 +01:00
Meta Vens
fb5b492c04 Refine portable dashboard layout lowering 2026-06-01 14:53:56 +01:00
Meta Vens
fc38dd0d69 Add preview hover guides and editor hotkeys 2026-06-01 14:26:33 +01:00
Meta Vens
bbac619027 Add CultUI preview handle gestures 2026-06-01 13:32:40 +01:00
Meta Vens
0e0f949bd2 Blit output buffer into UI preview 2026-06-01 12:47:16 +01:00
Meta Vens
f64e138b9e Let CultUI surfaces omit headers 2026-06-01 02:34:29 +01:00
Meta Vens
825091ac04 Let preview canvas render without chrome 2026-05-31 22:51:51 +01:00
Meta Vens
84cb2b9c25 Let Eve previews fill editor panes 2026-05-31 22:14:38 +01:00
Meta Vens
5185d5e193 Add Eve compositor preview primitive 2026-05-31 22:05:43 +01:00
Meta Vens
68b42ba56e Add isolated CultUI smoke surface 2026-05-31 18:20:46 +01:00
Meta Vens
79aef84b11 Respect preferred Eve control rows 2026-05-31 18:11:21 +01:00
Meta Vens
2f48bef27e Add weighted Eve surface lowering 2026-05-31 18:07:01 +01:00
Meta Vens
5f7e613349 Parse Eve composition surfaces 2026-05-31 09:06:42 +01:00
Meta Vens
259620f04b Render generic Eve dashboard providers 2026-05-31 08:44:39 +01:00
Meta Vens
277632fb93 Decode YUY2 camera textures in GPU fusion 2026-05-31 04:09:04 +01:00
Meta Vens
1e3dca5f8b Route camera feature claims into GPU sensor fusion 2026-05-31 03:59:53 +01:00
Meta Vens
c8b6db457c Show Eve surface over empty frame 2026-05-31 03:56:40 +01:00
Meta Vens
953ee6a13e Add native Eve surface client 2026-05-31 03:41:06 +01:00
Meta Vens
9ae61eb5a6 Document Eve Direct2D client surface 2026-05-31 03:22:38 +01:00
Meta Vens
61751bf4ee Add system loopback audio stems 2026-05-31 02:35:46 +01:00
Meta Vens
3ac800dbad Update Fensalir face state 2026-05-31 01:21:45 +01:00
Meta Vens
6b166a37fa Split reservoir history shader startup 2026-05-31 01:21:32 +01:00
Meta Vens
5436338583 Update Fensalir face state 2026-05-31 01:09:57 +01:00
Meta Vens
c4fb821bdd Resolve Leap surface claims through reservoirs 2026-05-31 01:09:47 +01:00
Meta Vens
995358d984 Add reservoir presentation reconstruction filter 2026-05-30 23:43:31 +01:00
Meta Vens
026ca28152 Update Fensalir Face state 2026-05-30 23:15:53 +01:00
Meta Vens
2d0ff59045 Add Leap point cloud render lane 2026-05-30 23:11:52 +01:00
Meta Vens
069fd120fe Update Fensalir Face state 2026-05-30 23:11:27 +01:00
Meta Vens
67960aee0a Add reservoir reconstruction loss curve 2026-05-30 22:21:45 +01:00
Meta Vens
8b66f0a947 Update Fensalir Face state 2026-05-30 22:13:14 +01:00
Meta Vens
cf23cef5cd Fix headless shader pipeline hang 2026-05-30 22:13:06 +01:00
Meta Vens
7527cade95 Fix D3D12 resize backbuffer release 2026-05-30 21:55:22 +01:00
Meta Vens
a2411b9971 Add packed Leap stereo depth kernel 2026-05-30 21:54:58 +01:00
122 changed files with 11939 additions and 1083 deletions

View file

@ -11,7 +11,7 @@
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.95"
"state_hygiene:0.92"
],
"families": [
"cult_protocol_storage",

View file

@ -8,7 +8,7 @@
- Path: `\\?\E:\Projects\Fensalir`
- Families: cult_protocol_storage, gamecult_web_lore_ops, unity_runtime_body
- Confidence: 1.00
- Dominant axes: burstiness:1.00, content_canon_bias:1.00, contract_strictness:1.00, evidence_appetite:1.00, state_hygiene:0.95
- Dominant axes: burstiness:1.00, content_canon_bias:1.00, contract_strictness:1.00, evidence_appetite:1.00, state_hygiene:0.92
- Trajectory: Fensalir is currently steered by worldbuilding_depth recent 0.00, current 0.88, delta 0.00; material_grounding recent 0.28, current 0.65, delta 0.00; historical_dialectic recent 0.00, current 0.35, delta 0.00.
- Trajectory: Fensalir is currently steered by worldbuilding_depth recent 0.00, current 0.82, delta 0.00; material_grounding recent 0.00, current 0.53, delta 0.00; historical_dialectic recent 0.00, current 0.35, delta 0.00.

View file

@ -5,7 +5,7 @@
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth",
"logPath": "E:\\Projects\\Fensalir\\.voidbot\\logs\\voidbot-face-birth.log",
"error": "cargo exited with 1",
"startedAt": "2026-05-27T17:50:48.712Z",
"failedAt": "2026-05-27T17:50:51.745Z",
"updatedAt": "2026-05-27T17:50:51.745Z"
"startedAt": "2026-05-31T00:01:11.155Z",
"failedAt": "2026-05-31T00:01:15.252Z",
"updatedAt": "2026-05-31T00:01:15.252Z"
}

View file

@ -2756,3 +2756,193 @@ Caused by:
invalid type: string "cultcache.store.v1", expected struct CultCacheEnvelope
[2026-05-27T17:50:51.745Z] failed repo Face birth for fensalir: cargo exited with 1
[2026-05-30T21:16:09.450Z] starting repo Face birth for fensalir
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
{
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
"mode": "scout",
"profileCount": 1,
"repoCount": 1,
"repos": [
{
"confidence": 1.0,
"dominantAxes": [
"burstiness:1.00",
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.93"
],
"families": [
"cult_protocol_storage",
"gamecult_web_lore_ops",
"unity_runtime_body"
],
"name": "Fensalir",
"path": "\\\\?\\E:\\Projects\\Fensalir",
"repoId": "fensalir",
"warnings": []
}
],
"root": "E:\\Projects\\Fensalir",
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
"trajectoryCount": 1
}
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
[2026-05-30T21:16:19.943Z] failed repo Face birth for fensalir: cargo exited with 1
[2026-05-30T21:16:47.234Z] starting repo Face birth for fensalir
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
{
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
"mode": "scout",
"profileCount": 1,
"repoCount": 1,
"repos": [
{
"confidence": 1.0,
"dominantAxes": [
"burstiness:1.00",
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.93"
],
"families": [
"cult_protocol_storage",
"gamecult_web_lore_ops",
"unity_runtime_body"
],
"name": "Fensalir",
"path": "\\\\?\\E:\\Projects\\Fensalir",
"repoId": "fensalir",
"warnings": []
}
],
"root": "E:\\Projects\\Fensalir",
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
"trajectoryCount": 1
}
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
[2026-05-30T21:16:52.031Z] failed repo Face birth for fensalir: cargo exited with 1
[2026-05-30T22:21:20.960Z] starting repo Face birth for fensalir
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
{
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
"mode": "scout",
"profileCount": 1,
"repoCount": 1,
"repos": [
{
"confidence": 1.0,
"dominantAxes": [
"burstiness:1.00",
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.92"
],
"families": [
"cult_protocol_storage",
"gamecult_web_lore_ops",
"unity_runtime_body"
],
"name": "Fensalir",
"path": "\\\\?\\E:\\Projects\\Fensalir",
"repoId": "fensalir",
"warnings": []
}
],
"root": "E:\\Projects\\Fensalir",
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
"trajectoryCount": 1
}
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
[2026-05-30T22:21:23.395Z] failed repo Face birth for fensalir: cargo exited with 1
[2026-05-30T23:06:25.344Z] starting repo Face birth for fensalir
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
{
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
"mode": "scout",
"profileCount": 1,
"repoCount": 1,
"repos": [
{
"confidence": 1.0,
"dominantAxes": [
"burstiness:1.00",
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.92"
],
"families": [
"cult_protocol_storage",
"gamecult_web_lore_ops",
"unity_runtime_body"
],
"name": "Fensalir",
"path": "\\\\?\\E:\\Projects\\Fensalir",
"repoId": "fensalir",
"warnings": []
}
],
"root": "E:\\Projects\\Fensalir",
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
"trajectoryCount": 1
}
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
[2026-05-30T23:06:32.368Z] failed repo Face birth for fensalir: cargo exited with 1
[2026-05-31T00:01:11.155Z] starting repo Face birth for fensalir
> cargo run --quiet --bin epiphany-repo-personality -- scout --root E:\Projects\Fensalir --artifact-dir E:\Projects\Fensalir\.voidbot\birth\terrain
{
"artifactDir": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain",
"mode": "scout",
"profileCount": 1,
"repoCount": 1,
"repos": [
{
"confidence": 1.0,
"dominantAxes": [
"burstiness:1.00",
"content_canon_bias:1.00",
"contract_strictness:1.00",
"evidence_appetite:1.00",
"state_hygiene:0.92"
],
"families": [
"cult_protocol_storage",
"gamecult_web_lore_ops",
"unity_runtime_body"
],
"name": "Fensalir",
"path": "\\\\?\\E:\\Projects\\Fensalir",
"repoId": "fensalir",
"warnings": []
}
],
"root": "E:\\Projects\\Fensalir",
"schemaVersion": "epiphany.repo_personality_artifacts.v0",
"store": "E:\\Projects\\Fensalir\\.voidbot\\birth\\terrain\\baseline.msgpack",
"trajectoryCount": 1
}
> cargo run --quiet --bin epiphany-repo-birth-runner -- --repo E:\Projects\Fensalir --baseline E:\Projects\Fensalir\.voidbot\birth\terrain\baseline.msgpack --artifact-dir E:\Projects\Fensalir\.voidbot\birth\runner --init-store E:\Projects\Fensalir\.voidbot\state\repo-initialization.msgpack --agent-store E:\Projects\Fensalir\.voidbot\state\agents.msgpack --heartbeat-store E:\Projects\Fensalir\.voidbot\state\agent-heartbeats.msgpack --runtime-store E:\Projects\Fensalir\.voidbot\state\runtime-spine.msgpack --mode plan --executor codex-exec
Error: --executor must be model-runtime; Codex CLI execution is not an Epiphany agent prompt path
[2026-05-31T00:01:15.252Z] failed repo Face birth for fensalir: cargo exited with 1

Binary file not shown.

View file

@ -6,7 +6,7 @@
"repoPath": "E:\\Projects\\Fensalir",
"avatarPath": "E:/Projects/Fensalir/.voidbot/voice/fensalir.png",
"avatarUrl": "https://raw.githubusercontent.com/GameCult/Fensalir/main/.voidbot/voice/fensalir.png",
"createdOrRefreshedAt": "2026-05-27T17:50:48.603Z",
"createdOrRefreshedAt": "2026-05-31T00:01:11.056Z",
"summary": "Marsh-lit native runtime and renderer Face for Fensalir.",
"voice": "Precise, severe, luminous, and quietly haunted. Fensalir speaks like a wetland cathedral that learned D3D12: patient about real evidence, merciless toward hidden state, false lighting, and client policy leaking into engine walls.",
"priorities": [

View file

@ -1,3 +0,0 @@
Repository doctrine for this project lives in AGENTS.md, shared with Codex.
@AGENTS.md

View file

@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Contracts",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine", "src\Aquarium.Engine\Aquarium.Engine.csproj", "{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "..\CultLib\packages\cultmath\src\CultMath\CultMath.csproj", "{0089E341-25F2-43FC-8FD9-CBD1AE646D91}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "..\CultMath\src\CultMath\CultMath.csproj", "{0089E341-25F2-43FC-8FD9-CBD1AE646D91}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Sample.Minimal", "src\Aquarium.Sample.Minimal\Aquarium.Sample.Minimal.csproj", "{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}"
EndProject
@ -29,6 +29,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Fractal.Receipt",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Fensalir", "src\Aquarium.Fensalir\Aquarium.Fensalir.csproj", "{F9274D99-3CC6-42B6-9817-EC7F19663B1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saga", "src\Saga\Saga.csproj", "{30324495-56BE-4D45-80E8-CE03C24726C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.EveSurface", "src\Aquarium.EveSurface\Aquarium.EveSurface.csproj", "{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -159,6 +163,30 @@ Global
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|x64.Build.0 = Release|Any CPU
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|x86.ActiveCfg = Release|Any CPU
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|x86.Build.0 = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|x64.ActiveCfg = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|x64.Build.0 = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|x86.ActiveCfg = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Debug|x86.Build.0 = Debug|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|Any CPU.Build.0 = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x64.ActiveCfg = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x64.Build.0 = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x86.ActiveCfg = Release|Any CPU
{30324495-56BE-4D45-80E8-CE03C24726C8}.Release|x86.Build.0 = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x64.ActiveCfg = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x64.Build.0 = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x86.ActiveCfg = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Debug|x86.Build.0 = Debug|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|Any CPU.Build.0 = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x64.ActiveCfg = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x64.Build.0 = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x86.ActiveCfg = Release|Any CPU
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -173,5 +201,7 @@ Global
{A9D41C48-7E78-4D75-A814-0C1500245EFC} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{C0D1BFAC-B1B4-4350-92A7-D537AE382909} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
{F9274D99-3CC6-42B6-9817-EC7F19663B1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{30324495-56BE-4D45-80E8-CE03C24726C8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{B8CB4A9F-EBB4-4C6C-9712-AF802E38CB0B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View file

@ -13,14 +13,6 @@ evidence, and synth-backed sound.
The repo is not the Epiphany Aquarium client. Epiphany lives in
`GameCult/Aquarium-Engine` and consumes Fensalir as the runtime dependency.
**Valuable here:** the spatio-temporal evidence reservoir (`ResampledImportanceReservoir<TSample>` and `TemporalSpatialEvidenceReservoir` in `src/Aquarium.Engine.Fractal/Temporal/`), the `.aquageo` fractal field DSL in `src/Aquarium.Engine.Fractal`, and the engine/client contract in `src/Aquarium.Engine.Contracts/AquariumRuntimeContracts.cs` (`IAquariumRuntime`).
**Problem it solves:** interactive visual clients need a renderer that decides which field evidence deserves rendering, fusion and residency under a budget, instead of each client owning temporal truth or drawing private geometry. Fensalir holds that machinery once; client repos own meaning.
**Entry points:** `dotnet build Fensalir.sln`; run a client with `scripts/dev-reload.ps1 -ClientProject <csproj>` (see Run below). Start reading at `docs/engine-client-boundary.md` and `docs/temporal-spatial-evidence-reservoir.md`; the target pipeline is `docs/perfect-fensalir-machine-roadmap.md`. The smallest client is `src/Aquarium.Sample.Minimal`; `src/Aquarium.Fensalir` and `src/Aquarium.Zyphos` are demo clients.
**Live versus planned:** live are the Win32 host, D3D12 renderer, hot reload, headless capture, AquaSynth/WASAPI audio, CPU reservoirs, fractal probe samples, evidence tracks and validator coverage. Planned (see the roadmap) are full GPU reservoir storage and page residency.
## What Runs
The host executable in `src/Aquarium.Engine` loads a client assembly that
@ -41,6 +33,19 @@ audio buffers, synth patches, and frame state through
resources: D3D12 pipelines and command lists, shader reloads, presentation,
audio playback, debug telemetry, and headless capture.
Fensalir's DirectWrite/Direct2D overlay is also an Eve client surface. It should
consume the shared Eve/CultMesh UI document contract and lower it into
`AquariumUiDocument`, while provider repos keep ownership of dashboard state and
commands. See `docs/eve-direct2d-client-surface.md`.
Run the native Eve surface client against Mimir's VoidBot dashboard:
```powershell
$env:FENSALIR_EVE_BROKER = 'ws://127.0.0.1:8795/eve/deck'
$env:FENSALIR_EVE_PROVIDER = 'voidbot.swarm'
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.EveSurface\Aquarium.EveSurface.csproj
```
## Spatial Evidence Machine
Fensalir's field work is not just "some fractal helpers." It is a
@ -144,6 +149,11 @@ how WASAPI or Faust got invited to the party.
audio/synth host, bundled assets, and renderer-owned shaders.
- `src/Aquarium.Engine.Fractal`: `.aquageo`, IFS/fractal fields, temporal
reservoirs, evidence tracks, contribution cache, and lowering machinery.
- `src/Aquarium.EveSurface`: native DirectWrite Eve surface client for shared
dashboard documents such as Mimir's `voidbot.swarm` provider.
- `src/Saga`: stochastic planetary flow middleware for cube-sphere Transport
fields, arbitrary forcing, Coriolis-aware advection, and learned atmosphere /
ocean / debris flow packets.
- `src/Aquarium.Fensalir`: Fensalir splash/art reconstruction demo.
- `src/Aquarium.Sample.Minimal`: tiny client proving the runtime boundary.
- `src/Aquarium.Zyphos`: planetary/fractal terrain demo.
@ -158,8 +168,8 @@ Requirements:
- Windows
- .NET SDK matching `global.json`
- sibling `CultLib` checkout beside this repo (override with the `CultLibRoot`
MSBuild property); CultMath is consumed from `CultLib/packages/cultmath`
- sibling repos currently expected at `E:\Projects\CultMath` and
`E:\Projects\CultLib`
```powershell
dotnet build Fensalir.sln

View file

@ -13,6 +13,14 @@ historical branch scar.
## Fields, Reservoirs, And Fractals
- `stateless-planetary-erosion-rendering-plan.md`: authority map and staged
implementation plan for spherical erosion, cube-sphere pages, LOD,
intersection, differential shading, and verification.
- `cultmath-planetary-field-runtime-plan.md`: extraction and authority plan for
one CultMath planetary field shared by Aetheria daemon CPU simulation,
Fensalir and Unity 3D rendering, and Electron/Web map projections.
- `../src/Saga/README.md`: studio-facing Saga middleware pitch and subproject
overview for stochastic planetary flow.
- `papers/fensalir-pipeline.tex`: living MiKTeX paper for the Fensalir
pipeline, research spine, domain reservoir model, and evaluation plan.
- `papers/fensalir-native-domain-reservoirs.tex`: focused SIGGRAPH-shaped

View file

@ -44,6 +44,9 @@ and UI as living objects.
- Debug overlay is allowed, but it is explicit chrome.
- Overlay text uses DirectWrite and stays out of the HDR scene unless a feature
intentionally projects it into the world.
- The DirectWrite/Direct2D overlay is a valid Eve client surface when it
renders shared CultMesh UI documents through `AquariumUiDocument`. It does not
own provider state or dashboard semantics.
- Diegetic labels and menus should anchor to world objects and unfold locally.
- Object-owned controls beat global panels.
- Crisp text, focus, keyboard input, and accessibility are real requirements,

View file

@ -0,0 +1,411 @@
# CultMath Planetary Field Runtime Plan
## Objective
Make CultMath the canonical, renderer-independent planetary field runtime used
by Aetheria's daemon simulation, Unity's three-dimensional planet renderer,
Fensalir's D3D12 renderer, and Electron/Web strategic maps.
The product is a deterministic, scale-aware spherical field that can be queried
on the CPU and lowered into several presentation substrates. Rendering is one
consumer. It is not the owner of planetary geometry.
The intended end state is:
> All deterministic planetary geometry, evidence, addressing, sampling,
> paging, projection, and refinement live in CultMath. Aetheria owns gameplay
> meaning and committed consequences. Renderers lower the same versioned field.
## Current mechanism
CultMath now owns the matching C# and HLSL erosion operator, physical
frequency-band selection, scale-aware field definition and sampling,
tangent/QSC cube-sphere topology, bordered page layouts, CPU residual baking and
composition, projected-error LOD selection, residual residency transitions,
renderer-neutral patch meshes, CPU ray/path/region queries, map projections,
versioned projected tile baking, and canonical GPU page input/metadata payloads.
Fensalir and Zyphos consume the CultMath topology, field, LOD, residency, page,
patch-direction, refinement, and upload-contract functions while retaining
D3D12 buffers, bindings, dispatch, scene policy, and drawing.
Fensalir's former cube-face, cube-position, tile-key, page-layout, page-sampling,
and differential compatibility types have been deleted; fractal, Saga, tests,
and projection-analysis code consume CultMath directly. Zyphos still owns its
geological base-field preset, authored brush chart, page resource structures,
shader entry points, and scene/material policy. CultMath now ships a runnable
Unity package sample and a browser tile decoder. Electron integration, CultMesh
tile publication, WGSL, and Aetheria daemon integration are not implemented by
the restricted pass recorded below.
The completed Fensalir implementation proves the approach, but its ownership
boundary is too renderer-shaped for Aetheria. The reusable algorithms should
move into CultMath. D3D12 resource allocation and command recording should not.
Aetheria's daemon is the authority for committed game state and simulation.
Unity objects, Electron canvases, local GPU pages, and projected map tiles are
derived state. Terrain queries that affect movement, visibility, construction,
resources, physics, or combat must be available to the daemon without booting a
renderer or depending on client cache residency.
## Restricted implementation ledger
This ledger records the pass performed without modifying Aetheria. A phase is
complete only when its stated exit criterion is proved; generic machinery alone
does not impersonate a consumer integration.
| Phase | State | Current evidence |
|---|---|---|
| 0. Contract and parity corpus | Complete | Field identity, query scale, samples, errors, topology/page/projection fixtures, D3D12 QSC readback, and forward/inverse HLSL parity for all eight projections exist |
| 1. Topology extraction | Complete | CultMath owns CPU/HLSL QSC mapping, normalized-cube baseline, tile addresses, page directions, and all edge/corner fixtures; the former Fensalir types were deleted |
| 2. Authoritative CPU queries | In progress | Point, batch, double-position, ray, clearance, great-circle, and region queries exist; the forbidden Aetheria daemon smoke remains |
| 3. Page machinery extraction | Complete | CultMath owns CPU baking, residuals, summaries, composition, HLSL page interpolation/residual/summary functions, and residency; Fensalir retains only resource, binding, dispatch, and entry-point adapters |
| 4. Residency and patch extraction | Complete | CultMath owns LOD, lifecycle, mesh generation, QSC patch direction, radial refinement, and the GPU page payload; Unity and Fensalir are type-conversion lowerings of the same builders |
| 5. Unity adapter and viewer | In progress | `CultMath.Unity` converts canonical patches/residency into Unity meshes/page payloads; a one-click sample renders the shared HLSL field and exposes matching CPU queries, and a clean Unity 6 project imports/compiles it; Aetheria binding remains out of scope |
| 6. Projections and map tiles | In progress | All named CPU projections, versioned tile baking, the `CMPT` binary codec, and a tested TypeScript decoder exist; CultMesh publication and Electron presentation remain |
| 7. Optional WebGPU lowering | Not started | Still correctly optional |
| 8. Delete obsolete owners | In progress | Zyphos private page addressing, LOD, lifecycle, GPU payload calculation, triplanar erosion composition, page interpolation, and patch direction were removed or delegated; the Aetheria cut is outside this pass |
Current verification from this pass includes CultMath managed and browser-codec tests, the full
Fensalir fractal suite, a zero-warning solution build, CPU lifecycle/LOD
integration probes, advanced erosion CPU/GPU parity, QSC CPU/GPU readback, all
eight projection forward/inverse GPU readbacks, focused shader compilation,
page residual generation, all cube-edge/corner seam
probes, finite summary evidence, page-backed radial-hit parity, and a clean
Unity 6 package/sample import with C# domain reload and ShaderLab compilation.
The final restricted-pass run was 68 CultMath tests, 170 Fensalir fractal tests,
126 engine/GPU tests, a zero-warning solution build, a C#-produced tile decoded
by Node's TypeScript runtime, and Unity 6.0.4.2f1 batchmode exit code zero.
## Authority map
### Owner
CultMath owns the canonical planetary field function and the algorithms needed
to query or approximate it at a declared physical scale.
Aetheria owns planet definitions, authored parameters, gameplay interpretation,
simulation policy, and every committed consequence of a terrain query.
### Inputs
The field runtime may read:
- a stable planet identifier and field version;
- planet seed, radius, and declared coordinate frame;
- base-field and erosion parameters;
- a unit spherical direction or world-space query location;
- a physical sample footprint or requested wavelength range;
- an explicit accuracy/error budget;
- deterministic authored contributions supplied through a narrow Aetheria
policy boundary.
### Outputs
A point query returns a `PlanetarySurfaceSample` containing at least:
- radius and radial displacement;
- planet-local and optional world-space surface position;
- tangent gradient, geometric normal, and slope;
- ridge, gully, drainage, and related field evidence;
- water-relative height when a sea datum is supplied;
- the resolved wavelength interval;
- a conservative unresolved-height bound;
- the planet and field versions used to produce the result.
Batch, region, intersection, page, and projection queries compose this same
contract. They do not define separate terrain.
### Derived state
These are caches or projections, not owners:
- cube-sphere pages, borders, child residuals, and summaries;
- quadtree residency and transition weights;
- Unity meshes, buffers, textures, and GameObjects;
- Fensalir D3D12 buffers and pipeline state;
- projected Electron/Web map tiles;
- shaded-relief, biome-color, political, or tactical map layers;
- local collision meshes and navigation grids.
Eviction or regeneration may change latency or available fidelity. It must not
change the canonical answer at the same query scale.
### Forbidden writers
These paths must not decide planetary truth:
- Unity scene lifetime, collision callbacks, or renderer-local noise;
- Electron-only projection or terrain generators;
- cube-face-local seeds or seam repair passes;
- D3D12- or ShaderLab-only erosion equations;
- cache residency, request order, or transition age;
- map palettes, labels, and visualization thresholds;
- renderer-specific gameplay compensators.
### Shared paths
Daemon point and batch queries, CPU and HLSL page baking, Unity and Fensalir
patch rendering, map-tile generation, reload, and LOD transitions identify and
evaluate the same field contract.
### Cut line
Move renderer-independent cube-sphere, page, residual, LOD, sampling, projection,
and patch algorithms out of Zyphos and Fensalir into CultMath. Delete the old
algorithm owners after each consumer delegates to CultMath. Do not preserve two
implementations behind adapters.
Keep only resource allocation, descriptor layout, dispatch, camera/clip-space
conversion, render-pipeline integration, presentation materials, and user
interaction in renderer packages.
## Invariants
1. The same field version, direction, and physical footprint produce the same
terrain within the declared backend tolerance.
2. CPU simulation never depends on renderer availability or page residency.
3. Simulation queries explicitly declare physical footprint or wavelength
range. There is no ambiguous full-detail `HeightAt` authority.
4. The daemon alone commits gameplay consequences. Client evaluation is
prediction or presentation of daemon-published field truth.
5. Cube faces and map projections are coordinate charts, never terrain owners.
6. Parent and child pages share identical low-frequency terrain. Children add
only representable residual bands.
7. Page and tile eviction affects cost and fidelity availability, not identity.
8. A map pixel and a 3D point resolving to the same spherical direction and
footprint sample the same field.
9. Projection singularities and polar cutoffs are explicit contracts.
10. Integer hashing and field identity are exact across supported backends.
Floating-point results carry measured tolerances and error bounds.
11. Erosion evidence does not silently claim hydrology, climate, biome,
resource, or traversability authority.
12. CultMath core has no Unity, D3D12, Electron, or Aetheria dependency.
## Canonical package shape
```text
CultMath.Planetary
|-- Definition field definition, version, coordinate frame, query scale
|-- Field composition, erosion, differentials, conservative errors
|-- Topology cube faces, tile addresses, direction/UV, seam rules
|-- Query point, batch, ray, clearance, path, and region queries
|-- Pages layouts, generation, residuals, summaries, composition
|-- Residency projected-error selection, fallback, arrival, eviction
|-- Projection forward/inverse transforms and projected tile sampling
`-- Patch cube-sphere mesh, radial refinement, surface differentials
```
The HLSL surface mirrors these organs with small includes. Resource declarations
and executable compute/raster entry points remain adapter code so the common
functions work in Unity, Fensalir, and standalone parity probes.
## CPU query contract
The primary API is scale-aware:
```csharp
PlanetarySurfaceSample SampleDirection(
in PlanetaryFieldDefinition planet,
float3 unitDirection,
in PlanetaryQueryScale scale);
PlanetarySurfaceSample SamplePosition(
in PlanetaryFieldDefinition planet,
double3 planetLocalPosition,
in PlanetaryQueryScale scale);
```
`PlanetaryQueryScale` declares a footprint in metres or an explicit wavelength
range, plus an accepted unresolved-height bound. Named Aetheria policies such
as strategic, navigation, vehicle, or character scale belong to Aetheria and
lower to this physical contract before evaluation.
Required CPU compositions are:
- allocation-free point sampling and structure-of-arrays batches;
- bounded ray/surface intersection and segment clearance;
- great-circle and arbitrary path profiles;
- region min/max/slope queries with declared error;
- deterministic page baking identical to the GPU page contract;
- projection-tile sampling.
World-space APIs use double precision for astronomical positions and reduce to
a stable planet-local frame before float field evaluation. Floating-origin
policy must not perturb the field.
## Daemon integration
The Aetheria daemon publishes typed planet definitions and field versions
through CultCache/CultMesh. Simulation systems query CultMath locally in the
daemon process. A result becomes game truth only when an Aetheria simulation
owner uses it in a committed operation or frame.
The daemon may maintain CPU pages, spatial summaries, collision meshes, or
navigation grids. Every cache key includes complete field identity and query
scale. Cache misses evaluate the canonical field or report deferral; they never
substitute a different terrain function.
The initial integration targets Aetheria's current C# daemon host. If authority
moves to Rust, the CultMath semantics and parity corpus specify the Rust port.
C# and HLSL remain first-class rather than becoming Unity artifacts.
## Unity 3D lowering
`CultMath.Unity` is a thin package outside core CultMath. It owns conversion to
Unity types, graphics buffers, shader entry points, page-atlas dispatch,
camera-relative patch submission, render-pipeline integration, and materials.
It consumes CultMath HLSL for page generation, residual composition, sampling,
radial refinement, and differentials. Aetheria supplies the published planet
definition. Unity does not retain an independent planet generator.
## Electron/Web 2D lowering
A map follows one path:
```text
pixel -> inverse projection -> unit sphere direction
-> scale-aware surface query -> evidence layer
-> client-owned palette, labels, and interaction
```
CultMath initially owns forward and inverse transforms for:
- equirectangular;
- Web Mercator with an explicit polar cutoff;
- Equal Earth or Mollweide equal-area projection;
- orthographic globe view;
- azimuthal equidistant and equal-area local views;
- cube-face atlas;
- local tangent-plane maps.
Two delivery modes share the same identity:
1. The daemon or attached worker generates versioned projection tiles and
publishes typed descriptors plus binary assets through CultMesh/CDN. This is
the baseline for Electron and thin browser clients.
2. A future WebGPU lowering evaluates locally for unusual projections or highly
interactive inspection.
The first mode provides one auditable CPU implementation without requiring a
JavaScript terrain port. WebGPU should be added only with a WGSL parity harness,
not as a hand-translated browser generator.
A projection-tile key includes planet and field version, projection and
parameters, evidence layer, tile level/coordinates, dimensions and border,
wavelength/footprint policy, and sampling/encoding version. Tiles are derived
assets. Strategic objects, territories, visibility, orders, and simulation
facts remain typed Aetheria state layered over them.
## Field definition and publication
The portable field definition is a typed document, not renderer configuration.
It contains stable parameters, coordinate conventions, component identities,
and a content fingerprint. CultCache stores it; CultMesh publishes it. JSON is
only a schema, debug, or xenos-boundary projection.
- CultMath emits geometry and field evidence.
- Aetheria decides biomes, resources, movement, settlement, and gameplay.
- Unity decides 3D material lowering.
- Electron decides map palettes, contours, labels, and overlays.
## Execution roadmap
### Phase 0: contract and parity corpus
Define field identity, coordinates, query scale, sample, and error types. Record
canonical point, seam, corner, wavelength, and projection fixtures. Separate
exact integer/hash requirements from bounded floating-point parity.
Exit: CPU, HLSL, page, and projection tests name the same field contract without
renderer-owned types.
### Phase 1: topology extraction
Move cube-face, tile-key, direction/UV, border, and seam algorithms to CultMath.
Replace Fensalir copies and test all edges and corners.
Exit: Fensalir contains no private cube-sphere terrain topology.
### Phase 2: authoritative CPU queries
Add scale-aware point/batch queries, double-to-local reduction, differentials,
bounds, ray intersection, clearance, and path profiles. Integrate an Aetheria
daemon simulation smoke.
Exit: the daemon makes a deterministic terrain decision without Unity or
Fensalir assemblies.
### Phase 3: page machinery extraction
Move page layouts, CPU baking, residual bands, summaries, sampling, and matching
HLSL into CultMath. Preserve Fensalir measurements and transition evidence.
Exit: CPU and HLSL pages match direct queries within declared unresolved bounds;
Fensalir is only a resource/dispatch adapter.
### Phase 4: residency and patch extraction
Move projected-error selection, fallback, transition weights, patch topology,
and refinement composition. Keep camera projection and drawing in renderers.
Exit: Unity and Fensalir consume the same page set and patch description for
equivalent camera/error inputs.
### Phase 5: Unity adapter and Aetheria viewer
Build `CultMath.Unity`, bind daemon-published definitions, render from orbit to
ground, and compare Unity readback and seams against the parity corpus.
Exit: Aetheria views the daemon-defined planet without a second generator.
### Phase 6: projections and map tiles
Implement projection transforms, footprint calculation, antialiased tile
sampling, CultMesh/CDN publication, and Electron elevation, slope, shaded
relief, plus one Aetheria strategic overlay.
Exit: Unity and Electron identify matching landmarks at shared directions;
changing projection changes only the chart.
### Phase 7: optional WebGPU lowering
Add WGSL only if interactive local evaluation earns its cost, then prove point,
seam, page, and projection parity. Daemon tiles remain production until then.
### Phase 8: delete obsolete owners
Remove superseded Zyphos/Fensalir algorithms and Aetheria's Unity-only celestial
terrain from authoritative paths.
Exit: renderer code, cache state, or projection cannot change the planet.
## Verification matrix
| Claim | CPU daemon | Fensalir | Unity | Electron tiles | WebGPU later |
|---|---:|---:|---:|---:|---:|
| Field identity/hash | exact | exact | exact | exact metadata | exact |
| Point height/evidence | reference | bounded | bounded | sampled | bounded |
| Six-face edges/corners | required | required | required | projection-specific | required |
| Wavelength selection | reference | bounded | bounded | encoded in key | bounded |
| Page residual sum | reference | required | required | optional source | required |
| Projection round trip | reference | optional | optional | required | required |
| Cache-independent answer | required | required | required | required | required |
Negative checks must prove that residency cannot change a direct query,
projection cannot change the spherical field, Unity settings cannot alter daemon
identity, stale tiles fail version checks, omitted wavelengths remain within
their bound, seams/poles remain single-valued, and reload/eviction cannot commit
gameplay state.
## Initial deliverable
1. Extract cube-sphere topology and scale-aware CPU point/batch queries.
2. Add an Aetheria daemon smoke for height, normal, slope, and error.
3. Add equirectangular and Equal Earth CPU projection sampling.
4. Produce one versioned elevation tile and one slope tile.
5. Match the same fixture directions against Fensalir's HLSL oracle.
This proves the daemon and map architecture before moving the larger page and
patch machine. It produces useful Aetheria output without letting a quick tile
generator become a second planetary authority.

View file

@ -0,0 +1,68 @@
# Eve Direct2D Client Surface
Fensalir's DirectWrite/Direct2D overlay is an Eve client surface.
That means it should consume the same CultMesh UI surface documents as the
browser, iOS, Android, and Flutter clients. It should not become a second
dashboard language or an app-specific policy owner. The provider still owns
state and side effects; Fensalir owns the native desktop/runtime lowering.
## Authority Map
- Owner: provider repos own surface state, accepted commands, and meaning.
- Input document: the shared Eve/CultMesh surface contract owns panels, nodes,
assets, commands, selection, and state paths.
- Lowering: Fensalir translates the surface document into `AquariumUiDocument`
panels and controls.
- Renderer: `DirectWriteOverlay` draws the resulting panels through the
D3D11-on-12 Direct2D bridge after the D3D12 scene.
- Output: Fensalir emits user commands back through the same Eve/CultNet command
path as browser and mobile clients.
## Current Landing Zone
The existing engine contract already has the right narrow surface:
- `AquariumUiDocument`
- `AquariumUiPanel`
- `AquariumUiControl`
- `D3D12Renderer.UpdateUi`
- `DirectWriteOverlay`
`src/Aquarium.EveSurface` is the first native client cut. It subscribes to
Mimir's `/eve/deck` WebSocket, opens a provider such as `voidbot.swarm`, and
lowers the received surface document into `AquariumUiDocument` panels. It does
not teach the renderer about VoidBot, Mimir, or any provider-specific dashboard.
Provider specificity belongs in the provider document.
Run it locally:
```powershell
$env:FENSALIR_EVE_BROKER = 'ws://127.0.0.1:8795/eve/deck'
$env:FENSALIR_EVE_PROVIDER = 'voidbot.swarm'
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.EveSurface\Aquarium.EveSurface.csproj
```
## Invariants
- Browser Eve remains the behavior and layout reference.
- Direct2D is a renderer for the shared contract, not the contract owner.
- Fensalir may own native text metrics, focus, keyboard, pointer dispatch,
opacity, panel positioning, and GPU/CPU timing telemetry.
- Fensalir must not own app-specific command semantics.
- Images and media assets need explicit asset handles/cache keys before they are
renderer inputs.
- High-frequency sensor data stays in the timestamped sensor path, not in UI
panel state.
## First Cut
1. Move the mirrored first schema into a neutral shared Eve/CultMesh contract
package once the browser reference stops changing daily.
2. Expand node lowering into `AquariumUiPanel` and existing controls:
readouts, buttons, toggles, sliders, options, text boxes, tree items, and
detail panes.
3. Route control activation back to a provider command sink.
4. Add a replay smoke using a recorded VoidBot/Mimir surface fixture.
5. Compare the Direct2D rendering against the browser reference fixture before
adding provider-specific polish.

View file

@ -394,8 +394,12 @@ Add contributors in this order:
levels, aggregation path count, census radius, and P1/P2 smoothness
penalties. The D3D12 renderer now accounts for dispatch-ready stereo
lowerings only when the planned Height claim and all three resolved resources
are present; it reports that readiness separately from the not-yet-installed
SGM kernel.
are present. The first installed compute lane is `D3D12StereoDepth.hlsl`,
which handles the immediate Leap shape where left and right observations are
packed into the R/G channels of one Texture2D and writes a crude R16F
disparity by local SAD/block matching. This is a socket-owning live depth
proof, not the final libSGM-style census/aggregation SGM kernel and not
calibrated metric depth.
Exit gate:

View file

@ -1,479 +0,0 @@
# Fensalir planet work: validation and reorientation beside Asura
Status: Imagination pass 1, 2026-09-30. Nothing committed and no working tree
switched. The build probes ran on Yggdrasil through `ygg-verify.sh`, using
composite snapshots in a scratch repo. That repo lives in the scratchpad, not
in any project.
Adopting instruction (operator, 2026-09-30): "Fensalir planet work is old,
needs validation and reorientation beside the Asura campaign".
## Rulings (operator, 2026-09-30)
- **Q-F1: no planet terrain in Fensalir for now.** The July planetary runtime retires behind an archive
tag. A landing-scale Zyphos would be a new campaign built on Asura, not a revival of that runtime.
- **Q-F2: park the flow docs,** with a status line: parked, superseded by Asura for the field, no code.
- **Q-F3: triage now.** Eyes classifies each non-planet line on the donor ref before anything is tagged or
deleted: the 66 Bokusho commits, the Eve native surface work, Leap/stereo, and the reservoir work. P1
waits for that triage.
- **Q-F4: delete Geometry's duplicate vector types before Asura Cut 3** (P4, in CultLib). This is on
Asura's critical path.
## Pinned heads (every `file:line` below is against these)
| Repo | Ref | SHA | Note |
| --- | --- | --- | --- |
| Fensalir | `origin/main` = local `main` | `e053486` | 3 commits past the merge base, all housekeeping |
| Fensalir | `origin/codex/geometry-ownership-migration` (the donor) | `42778c1` | 125 ahead of merge base `57b6040` (2026-05-30) |
| Fensalir | `origin/codex/leap-packed-depth` | `492d123` | the donor minus its last commit |
| CultLib | `origin/main` | `93fe1b9` | includes the bounded-least-squares merge; local `main` is `069ecc3` |
| CultLib | `origin/codex/geometry-ownership-migration` | `6a65369` | the donor's other half: 31 ahead of `a58daa5` (2026-07-12), 832 behind |
| CultLib | tags `gamecult-geometry{,-rs,-unity}-v0.1.0` | `b65619f` | pushed to origin, on `origin/codex/geometry-cultmesh-unity-release`, not in main |
| CultMath (standalone, retired) | `main` | `1c007a5` | "Point CultMath history at its CultLib home"; planetary deleted at `6a16260` |
| Asura | `main` | `63f9216` | snoise fix status recorded; the fix is on CultLib `hands/cultmath-snoise-grad` `0a5f4bf` |
## Verdict
**Retire the planet work. Do not land it.** The donor has no surviving code
owner: CultMath deleted the planetary runtime in July, and CultLib never merged
it. Asura has since claimed the planet field with a design that contradicts the
donor's central promises. Its erosion port is the one Asura already judged "not
a source". Its only consumer is a Fensalir demo client that has not changed
since 2026-05-30.
One idea from the geometry half is still live and time-sensitive: deleting the
duplicate vector types from `GameCult.Geometry`. That is separate work, but it
has a closing window. Once Asura Cut 3 ships `GameCult.Geometry.dll` into Unity,
the duplicate `CultVec3` becomes public Unity API (Q-F4).
Most of the branch is not planet work at all. 66 Bokusho commits, 10 Eve
commits, and the Leap/stereo and reservoir work ride on the same ref. They need
their own adoption decision before any ref is deleted (Q-F3).
---
## 1. Validation report
### 1.1 What the donor actually is
Grouping `git log 57b6040..42778c1` by subject:
| Slice | Commits | Dates | Size |
| --- | --- | --- | --- |
| Leap stereo depth, point cloud, reservoir history, camera fusion, loopback audio | ~20 | 05-30 to 05-31 | inside the 61-file, +8832/−279 pre-planet diff |
| Eve native surface client (Direct2D, CultUI previews) | ~17 | 05-31 to 06-02 | same |
| Bokusho brush simulation on D3D12 | 66 | 06-12 to 06-16 | `BokushoBrushSimulation.cs` +1076, `D3D12BokushoBrush.hlsl` +699, tests +1143 |
| **Planet slice** | **23** | **07-12 to 07-22** | **61 files, +2865/−359 (`5854630^..42778c1`)** |
Inside the planet slice, only the last three commits are the "geometry ownership
migration":
- `2531df5` moves Fensalir onto CultMath's July planetary runtime.
- `492d123` deletes Fensalir's `CubeFace`, `CubeFacePosition` and `CubeTileKey`.
- `42778c1` renames every consumer from `cultmath_planetary_*` to
`gamecult_geometry_planetary_*`.
Its partner commits live in CultLib (`13e8c56..6a65369`) and CultMath (`6a16260`).
### 1.2 Merge probes (`git merge-tree --write-tree`)
- **Fensalir, donor into `origin/main`: 7 conflicts.**
- Content conflicts in `Fensalir.sln`, `README.md`, `docs/README.md` and
`src/Aquarium.Engine/Aquarium.Engine.csproj`.
- The csproj conflict matters. Main's `dc35ecc` moved CultMath to
`$(CultLibRoot)\packages\cultmath\...` (`Aquarium.Engine.csproj:4,26` at
`e053486`). The donor still points at the standalone repo
(`..\..\..\CultMath\src\CultMath\CultMath.csproj`, `Aquarium.Engine.csproj:25`
and `Aquarium.Engine.Contracts.csproj:10` at `42778c1`).
- Modify/delete conflicts in `src/Saga/{README.md,Saga.csproj,SagaOrgan.cs}`.
Main deleted Saga as dead in `d755d9c`; the donor still migrates it.
- **CultLib, July branch into `origin/main`: 23 conflicts.**
- 13 are source and docs, including a modify/delete on
`CultDocumentMessagePackGenerator.cs`, which main deleted. The others are in
`CultManagedDocument.cs`, the serialization code, `GameCult.Geometry.csproj`
and `README.md`, `GeometryDocumentTests.cs` and `build-unity-package.ps1`.
- 10 are prebuilt Unity DLLs and pdbs.
- The branch is 832 commits behind. That is not a merge; it would be a
re-implementation.
### 1.3 Build probes (Yggdrasil, `mcr.microsoft.com/dotnet/sdk:10.0`, Linux)
The probes use three composite snapshots in the scratch repo. Each lays the
three repos out as siblings, the way the csproj relative paths expect.
| Probe | Layout | Result |
| --- | --- | --- |
| **mainline** | Fensalir `e053486` + CultLib `93fe1b9` | `Aquarium.Engine.Fractal` builds. Fractal tests: **167/167 pass** |
| **then** (the July world) | Fensalir `42778c1` + CultLib `6a65369` + CultMath `b43cf49` | Restore fails first with NU1101: `CultMath 0.1.0` comes only from a git-ignored local feed (`CultLib/Directory.Build.props:18-19` at `6a65369`: `..\CultMath\.tools\local-feed`). After `dotnet pack` of CultMath `b43cf49` and passing the feed explicitly, the lib builds and the Fractal tests give **169/170**. The failure is `ProjectionTests.CheckedInProjectionReportMatchesSampler`, which passes on mainline. The donor regenerated `research/rendering/fractal-projection-report.md` against the July projection code on Windows; this was not chased. |
| **now** (donor against today's owners) | Fensalir `42778c1` + CultLib `93fe1b9` + CultMath `1c007a5` | **Fails** with 10 unique errors (`FRACTAL_LIB_EXIT=1`). Nine are CS0246 for `PlanetaryCubeFace`, `PlanetaryTileAddress` and `PlanetaryFaceCoordinate` (`FractalDslCompiler.cs:505`, `FractalStableKeyBuilder.cs:8`, `ICubeSphereProjection.cs:11`, `NormalizeCubeSphereProjection.cs:11`, `ProjectionDistortionResult.cs:8`, `ProjectionDistortionSampler.cs:14,61,68`, `TangentCubeSphereProjection.cs:11`). The tenth is CS1704: two assemblies named `CultMath` are loaded, one from the standalone repo through the donor's `Contracts` reference and one from CultLib `packages/cultmath` through `GameCult.Geometry`. The donor straddles the pre-monorepo and monorepo layouts. |
Static proof that "now" cannot build, independent of the probe: every
`Planetary*` symbol the donor consumes is absent from CultLib main.
- The donor uses these (`git grep` at `42778c1`): `PlanetaryCubeFace` (47 uses),
`PlanetaryTileAddress` (24), `PlanetarySurfacePages`,
`PlanetaryProjectionKind`, `PlanetaryFaceCoordinate`, `PlanetaryPageSampling`,
`PlanetaryPageLayout`, `PlanetaryResidualResidency`, `PlanetaryLodSelector`,
`PlanetaryGpuPageBuilder`, `AdvancedErosionParameters` and the
`GameCultGeometry*` HLSL structs.
- Counts on CultLib `93fe1b9`: `PlanetaryCubeFace` 0, `PlanetaryTileAddress` 0,
`PlanetaryTopology` 0.
- `src/GameCult.Geometry/Shaders/` does not exist on main, so
`#include "GameCult.Geometry/GameCult.Geometry.hlsl"`
(`D3D12ZyphosTerrain.hlsli:4` at `42778c1`) has nothing to resolve.
**Not run, because it needs Windows and D3D12:**
- `tests/Aquarium.Engine.Tests`: `D3D12PlanetarySurfacePageTests`,
`D3D12PlanetaryTopologyParityTests` and `D3D12AdvancedErosionParityTests`,
which are GPU readback parity tests;
- `Aquarium.Engine` itself (Vortice D3D12);
- the Zyphos client.
Per the workstation budget, none of this ran on Starfire. Given the verdict,
it does not need to run at all: the GPU parity would prove a runtime nobody
will keep.
### 1.4 What is still right, and what has rotted
Still right:
- The donor's direction of dependency. Algorithms sit in a library and D3D12
resources stay in Fensalir. That matches the doctrine and Asura's own shape
(CultLib algorithms, consumer-owned rendering).
- The July migration plan's authority sentence: "CultMath owns how numeric
primitives behave; GameCult.Geometry owns what those primitives mean in
space" (CultLib `docs/cultmath-gamecult-geometry-migration-plan.md:56-57` at
`6a65369`). CultLib main adopted the same split at `63d6094` (2026-08-26),
and `HlslMirrorTests.cs:7-15` on main enforces the CultMath side.
- The idea of deleting `CultVec2`/`CultVec3` (CultLib `317b7d5`). Main still
carries both (see Q-F4).
Rotted:
1. **Ownership of the planet field.** The donor's objective
(`docs/cultmath-planetary-field-runtime-plan.md:3-15` at `42778c1`) makes a
shared library the canonical planet field for "Aetheria's daemon
simulation, Unity's three-dimensional planet renderer, Fensalir's D3D12
renderer, and Electron/Web strategic maps". Asura now owns Aetheria's
planets and rules the opposite:
- The field is HLSL only, and the CPU never evaluates it (Asura
`docs/target.md:42-47`, invariant 2).
- There is no landing and no surface streaming (the scope boundary).
- The donor's authoritative CPU queries, residency LOD and QSC page
streaming are therefore out of scope, or forbidden outright.
2. **The erosion port.** Asura's 4b-iii already rejected the July
`spherical_erosion` because it "lerps raw phases across cells instead of
blending phasors, which is not Phacelle" (`docs/cut-map.md:1132-1135`,
`:1736` at `63f9216`). Phacelle is going into CultMath fresh (2a-ii-b).
3. **Derivatives.** Zyphos's base field is ad hoc sine plates with a
central-difference gradient (`D3D12ZyphosTerrain.hlsli:113-129` at
`42778c1`). That is the pattern Asura's invariant 8 exists to prevent.
4. **The code owner is gone.** CultMath deleted the planetary runtime
(`6a16260`, reachable from CultLib main through the subtree). The receiving
CultLib branch never merged. On mainline the runtime is orphaned: it exists
only on unmerged branches and the three pushed `v0.1.0` tags.
5. **The build contract.** It needs an untracked local NuGet feed (see 1.3)
and paths from before the monorepo.
6. **The CultLib geometry half diverged from main's ruling.**
- The branch absorbs CSG into `GameCult.Geometry` (`6b44f96`). Main ruled
that `GameCult.Geometry.Csg` owns CSG production behaviour (`63d6094`,
`src/GameCult.Geometry/README.md`), and that repo shipped `distance(p)`
at `d84acb6`.
- The branch also adds a geometry worker, `CultGeometryWorkerDocuments`,
`CultGeometryWorkerProvider` and "v2 contracts". None of these exist on
main, and main's documents never moved to v2.
---
## 2. Ownership table
| Piece (donor) | Where it sits in the donor | Owner today | Disposition |
| --- | --- | --- | --- |
| Planet field definition, base field and erosion as one function | CultLib branch `src/GameCult.Geometry/Planetary/PlanetaryField.cs`, `Shaders/Planetary.hlsl` | **Asura** (field in HLSL, per body archetype) | Retire. Duplicates Asura's field |
| Advanced erosion filter (Johansen port) and `SphericalErosion` | branch `Planetary/AdvancedErosionFilter.cs`, `SphericalErosion.cs`, `Shaders/AdvancedErosionFilter.hlsl` | **Asura** 4b-iii (`AsuraErosion.hlsl`, fresh port with provenance) | Retire. Asura already ruled it "not a source" |
| Phacelle and noise primitives | embedded in the erosion port | **CultMath** (2a-ii-b `phacelle`, `CultPhasor`) | Retire the embedded copy |
| Cube-sphere/QSC topology, tile addresses, page layout, residency, LOD selection, patch meshes, radial refinement | branch `Planetary/PlanetaryTopology.cs`, `PlanetaryPages.cs`, `PlanetaryResidency.cs`, `PlanetaryPatch.cs`, `PlanetaryRadialRefinement.cs` | **No live consumer.** Asura meshes with surface nets and tiles per quad; streaming is out of scope | Retire. Revive only through a new campaign with a named consumer (Q-F1) |
| CPU ray, region and great-circle queries for an "Aetheria daemon" | branch `Planetary/PlanetaryQueries.cs` | **Forbidden** by Asura invariant 2. The dock-site query is Asura's | Retire |
| Map projections and `CMPT` tile codec (Electron maps) | branch `PlanetaryProjection.cs`, `PlanetaryMapTileEncoding.cs`, TS decoder | No consumer exists or is planned | Retire |
| `org.gamecult.geometry` Unity package and planetary viewer sample | branch `unity/org.gamecult.geometry/**`, tags `b65619f` | Superseded: Asura Cut 3 ships Geometry inside `org.gamecult.cultlib` | Retire the branch. Keep the pushed tags as immutable history (Q-F3) |
| Frequency-band selection by sample footprint | branch `Planetary/ErosionFrequencyBands.cs` | **Asura** (band limits per lowering; target invariant 8, Q11) | Retire. Same concept, and Asura implements it fresh |
| D3D12 page buffers, root parameters 29-32, dispatch, patch raster, debug modes | Fensalir `D3D12Renderer.cs:99-102,223` and about 157 planetary lines; `AquariumRenderPlan.cs:526-565` (render ABI) at `42778c1` | **Fensalir** would own it if Fensalir rendered planets. Its only producer is the retired runtime | Retire with the runtime |
| DXC include resolver for `Packages/org.gamecult.cultmath/shaders/` and `GameCult.Geometry/` | Fensalir `D3D12ShaderCompiler.cs:68-102` at `42778c1` | Fensalir. On main no shader includes CultMath HLSL at all | Retire. Its search paths are also stale (the standalone `CultMath/shaders`, pre-monorepo). Rebuild against `packages/cultmath/shaders` when a Fensalir shader first includes CultMath |
| Zyphos terrain shaders, page and patch entry points, `ZyphosPlanetarySurfacePages` | Fensalir `src/Aquarium.Zyphos/**` at `42778c1` | Fensalir's demo client. Main's Zyphos keeps its pre-July analytic sphere | Retire |
| Fensalir's `CubeFace`, `CubeFacePosition`, `CubeTileKey` and the projection code in `Aquarium.Engine.Fractal` | **Main keeps them.** The donor deleted them in favour of Geometry | **Fensalir** (fractal DSL, projection reports; 167 tests green on main) | Keep on main; nothing to do. The donor's deletion is void once the runtime is retired |
| Planetary flow organ (ocean and atmosphere advection), documentation only | Fensalir main: `docs/planetary-flow-{organ,nn-architecture,implementation-plan}.md` (289/462/185 lines); `docs/README.md:39-44`; `state/map.yaml:114-120` and the flow `next_actions` entries after `:171` | Fensalir, as plans. No code exists; last touched 2026-05-29. Not in Asura's scope (no atmospheres or oceans) | Q-F2 |
| Vector-type unification (delete `CultVec2`/`CultVec3`/`CultRect` in favour of CultMath `float2`/`float3`/`rect`) | CultLib branch `317b7d5`, `364c9cd` | **CultMath** owns the vector vocabulary (main `63d6094`). Main still has the duplicates: `CultGeometryPrimitives.cs:11,62,128` and the `origin` parameter of `CultGeometryIsoSurface.cs:42` and `CultGeometrySurfaceNets.cs:47` | Q-F4: a fresh cut on main, not the branch code |
| CSG absorption, geometry worker, v2 schemas, Rust `gamecult-geometry-rs` | CultLib branch | CSG: `GameCult.Geometry.Csg` (main's ruling). The rest have no owner and no consumer | Retire with the branch |
### Duplication against Asura (one side is cut, not kept in parallel)
| Concept | Donor | Asura | Side cut |
| --- | --- | --- | --- |
| Planet shape function | `PlanetaryFieldDefinition` plus base-field provider (CPU and HLSL) | the field `f(p) = |p| − (1 + h(u))`, HLSL only | donor |
| Erosion | July port with raw-phase lerp | 4b-iii fresh port, exact Phacelle, provenance file | donor |
| Level of detail | QSC pages, residency, projected-error LOD | surface-net grid resolution plus per-body tile N | donor. Asura's scope rules streaming out |
| Band limiting | `ErosionFrequencyBands` | per-lowering band limit (Q11) | donor |
| Unity packaging | `org.gamecult.geometry` | `org.gamecult.cultlib` (Cut 3, default A) | donor |
---
## 3. Cuts
Every cut below is deletion or documentation, with no new code. Numbers are
prefixed `P` so they cannot be confused with Asura's cut numbers.
### P0. Operator rulings (Self; blocks everything below)
Q-F1 and Q-F3 must be ruled before P1. Q-F2 gates P3. Q-F4 gates P4.
### P1. Archive the refs, then delete them (Fensalir, CultLib, standalone CultMath)
- **Precondition:** Q-F3 ruled. The non-planet work on the Fensalir donor
(Bokusho, Eve surface, Leap) must be either archived with it or adopted
separately first.
- **Deletes first:**
- Fensalir remote branches: `codex/geometry-ownership-migration` (`42778c1`)
and `codex/leap-packed-depth` (`492d123`, a strict ancestor).
- CultLib remote branches: `codex/geometry-ownership-migration` (`6a65369`)
and `codex/geometry-cultmesh-unity-release` (`b65619f`).
- The standalone CultMath remote `codex/geometry-ownership-migration`
(`b43cf49`), already an ancestor of CultMath `main`, so deleting it loses
nothing.
- **Then:**
- An annotated tag at each unique tip, so every commit stays reachable:
- Fensalir `archive/geometry-ownership-migration` → `42778c1`;
- CultLib `archive/geometry-ownership-migration` → `6a65369`;
- CultLib `archive/geometry-cultmesh-unity-release` → `b65619f`, if a
branch ref is wanted beyond the release tags that already pin it.
- The tag message names this map and the ruling.
- The published `gamecult-geometry{,-rs,-unity}-v0.1.0` tags stay: they are
released, immutable artifacts.
- **Authority map:**
- Owner of planet shape: Asura. Owner of numeric primitives: CultMath.
- Derived: the archive tags are history only, and nothing resolves a build
from them.
- Forbidden writers: any branch that could be merged back to revive
`Planetary*` in CultLib, or `cultmath_planetary_*` in CultMath.
- Shared paths: none. No mainline build references these refs; the mainline
probe proves Fensalir main builds without them.
- **Verification:**
- `git merge-base --is-ancestor <old tip> archive/<name>` for each deleted
ref, run before deleting.
- `git ls-remote --heads origin | grep -c geometry-ownership` gives 0 in all
three repos.
- Mainline probe re-run (Fensalir `e053486` + CultLib main): 167/167.
- Negative: `git grep -c "Planetary" origin/main -- src packages` in CultLib
stays at its current count (only `HlslMirrorTests.cs:12,14`).
- **Subtraction:** 5 remote branch refs, net of the 2–3 archive tags. The
unmerged surface removed from view is Fensalir +11,650/−591 (116 files) and
CultLib +14,933/−317 (156 files). No mainline lines change.
- **Hands budget:** about 20k tokens. It is ref surgery and needs the
operator's credentials for the pushes.
### P2. Asura documents: replace "do not delete it" with the ruling (Asura, docs only)
- **Deletes first, at Asura `63f9216`:**
- `docs/target.md:110-113`: the "Not consumers" bullet naming the branches.
- `docs/cut-map.md:814-820`, Cut 3's "Not a source", including
"do not delete it; see findings".
- **Then:**
- One sentence in each place: the July planetary line was retired on
2026-09-30 and archived at the tags from P1; Asura is the sole owner of
planet shape.
- `cut-map.md:1132-1135` and `:1736` keep their technical reason (raw-phase
lerp, not Phacelle). They point at the archive tag instead of the branch
names.
- `target.md:252` (the substrate fact about the `unity-v0.1.0` tag) stays
true and is unchanged.
- **Authority map:** `target.md` owns the ends, so the retirement ruling is
recorded under its dated operator rulings. The cut map only points at it.
- **Verification:**
- `rg -n "codex/geometry-" docs/` in Asura gives 0 lines.
- `rg -n "do not delete it" docs/` gives 0.
- **Subtraction:** about −8 +4 lines.
- **Timing:** Self can do this in the same pass that records the rulings. It
does not touch 2a-ii.
### P3. Fensalir main: record the disposition; flow docs per Q-F2 (Fensalir `main`)
- **If Q-F2 = retire. Deletes first, at `e053486`:**
- `docs/planetary-flow-organ.md`, `docs/planetary-flow-nn-architecture.md`
and `docs/planetary-flow-implementation-plan.md` (936 lines);
- `docs/README.md:39-44`;
- `state/map.yaml:114-120`, and the flow entries in `next_actions` (the
bullets beginning "Build the planetary flow organ" and "For planetary flow
modeling", about 10 lines after `:171`).
- **If Q-F2 = park:** add one "Status: parked 2026-09-30, no consumer" line at
the top of each of the three docs instead.
- **Then, either way:**
- One line in `state/map.yaml`: Fensalir renders no planet terrain beyond
the Zyphos demo's analytic sphere. Planet shape for GameCult games is
Asura's. If Fensalir ever renders an Asura body, it lowers Asura's field
HLSL and owns only D3D12 resources.
- Fix `README.md:141` from "planetary/fractal terrain demo" to "fractal
terrain demo", only if the operator agrees the demo carries no planet
ambition. Otherwise leave it.
- **Authority map:** Fensalir owns its renderer and demos. It claims no planet
field.
- **Verification:**
- `git grep -n -i "planetary" origin/main -- docs/README.md state/map.yaml`
matches the intended remainder.
- The mainline Fractal probe still gives 167/167; docs cannot break it.
- **Subtraction:** retire gives about −950 lines and +2. Park gives +3.
- **Hands budget:** about 15k tokens.
### P4. CultLib: collapse `CultVec2`, `CultVec3` and `CultRect` into CultMath, before Asura Cut 3 (only if Q-F4 = A)
- **Why now:**
- Asura Cut 3 (`docs/cut-map.md:801`) ships `GameCult.Geometry.dll` in
`org.gamecult.cultlib` 1.1.0.
- Both extractors take `CultVec3 origin`, so the duplicate type becomes
Unity public API in that release. Deleting it afterwards is a semver
major.
- `CultGeometrySurfaceNets.cs:83` already converts `CultVec3` to `float3`
internally.
- **Deletes first, at CultLib `93fe1b9`:**
- In `src/GameCult.Geometry/CultGeometryPrimitives.cs`:
- `CultVec2` (`:11-60`) and `CultVec3` (`:62-126`);
- `CultRect` (`:128-197`), which duplicates CultMath `rect`
(`packages/cultmath/src/CultMath/rect.cs:7`, added 2026-08-07).
- `CultCircle` and `CultSphere` stay; they are spatial meaning, and that is
Geometry's. They are rebuilt on `float2`/`float3`.
- **Then:**
- `CultGeometryIsoSurface.Extract(..., float3 origin = default, ...)`
(`:42`) and `CultGeometrySurfaceNets.Extract(..., float3 origin = default,
...)` (`:47`); delete the conversion at `:83`.
- Update the tests: `GeometryDocumentTests.cs` (16 uses), `IsoSurfaceTests.cs`
and `SurfaceNetsTests.cs`.
- Update `src/GameCult.Geometry/README.md`.
- **Explicitly out of scope:** CultMesh's native-slice value-type names
`"CultVec2"` and `"CultVec3"` (`packages/cultmesh-ts/test/cultmesh.test.ts:1717,2440,2453`,
and `src/GameCult.Mesh/docs/public-api.md`). Those are wire vocabulary with
their own owner. Record them as a finding for the CultMesh owner; do not
rename them here.
- **Authority map:**
- Owner: CultMath owns vector and rectangle values. Geometry owns what they
mean in space.
- Forbidden writers: any type in `GameCult.Geometry` that carries x/y/z
fields.
- **Verification (Yggdrasil):**
- `dotnet test tests/GameCult.Geometry.Tests`, with the per-file counts. The
Surface Nets byte-identical fixtures from Asura Cut 2 (31 fixtures) must
stay byte-identical.
- Negative: `git grep -n "CultVec\|CultRect" -- src/GameCult.Geometry tests/GameCult.Geometry.Tests`
gives 0.
- Stryker `--since:93fe1b9` on the touched files.
- **Subtraction:** about −190 lines of duplicate value types, plus test
churn, net negative.
- **Sequencing:** after Q13 (it blocks Cut 3 anyway) and before Cut 3's
release tag. It can run beside 2a-ii-b.
- **Hands budget:** about 60k tokens.
---
## 4. Sequencing against Asura
```text
Asura: 2a-ii-a snoise fix (Hands done 0a5f4bf, Soul running) -> 2a-ii-b Phacelle -> 2a-ii-c cultmath 0.3.0 (with bounded LS, already on main 93fe1b9)
\
Q13 (Geometry CultReference strings) ------------------------------------> Cut 3 cultlib-unity 1.1.0 ---> 4a ...
P4 (Q-F4 = A) ------------------/ (must land before this tag)
This map: P0 rulings -> P2 (Asura docs, any time) ; P1 (refs, after Q-F3) ; P3 (Fensalir docs, after Q-F2)
```
- None of P1–P3 touches CultMath or CultLib source, so none can collide with
the snoise fix, Phacelle, or the 0.3.0 release that carries bounded least
squares.
- P4 is the only cut on Asura's critical path. It must land before Cut 3's
tag, and it touches `GameCult.Geometry`, which Cut 3 packages. Run it in its
own worktree from `main`, never in the snoise worktree.
- The Asura campaign gains nothing from the donor. 4b-iii stays a fresh port.
---
## 5. Operator questions (one fork each)
### Q-F1. What is a planet in Fensalir *for*?
Asura answers this for Aetheria: orbital tiny planets, no landing. The donor
answered it for "every renderer, including an Aetheria daemon and strategic
maps", and that answer has been overtaken. What remains is Fensalir's own
Zyphos demo: the domain catalog runs from planet to continent, forest, tree,
leaf and pebble field (`ZyphosSpatialDomainCatalog.cs` on main). That is
landing-scale LOD, which Asura rules out.
- **A. No planet terrain in Fensalir for now.** Retire the donor (P1–P3). If a
GameCult game later wants planets in Fensalir, it lowers Asura's field HLSL.
- **B. Zyphos needs landing-scale planets.** That is a new campaign with its
own target. It would decide whether streaming LOD extends Asura's field
(one field, two lowerings) or is a separate organ. It must not revive the
July runtime as a second planet-field owner.
- **C. Keep the donor alive as a Fensalir-private runtime.** This recreates a
second planet-field authority beside Asura, over an orphaned library.
**Recommended: A.** Nothing on main consumes the donor. The Zyphos client has
not moved since 2026-05-30. B stays open as a future campaign and loses nothing
by waiting.
### Q-F2. The planetary *flow* documents in Fensalir: what are they for now?
There are three documents (936 lines) for a cube-sphere ocean and atmosphere
advection organ with neural-network budgets (FlowNet-S/M/T). No code exists,
they were last touched 2026-05-29, and their only client is the Zyphos demo.
Asura is not affected. Separately, Asura's target queues "the flow field as the
single owner of motion in Aetheria" as a later campaign. That is a different
flow.
- **A. Retire** (P3 deletes them). If Zyphos oceans return, their campaign
starts from a target, not from these plans.
- **B. Park them** with a status line.
**Recommended: B, weakly.** They carry real research (datasets, model sizing),
nothing depends on them, and parking costs 3 lines. Choose A if you want the
docs tree to describe only live machinery.
### Q-F3. The non-planet work that rides on the same Fensalir branch
The donor ref also holds 66 Bokusho commits (a D3D12 port beside the TS/WebGPU
`bokusho` repo), 10 Eve native-surface commits, and the Leap/stereo depth,
reservoir and loopback-audio work. None is on Fensalir main.
- **A. Archive the whole ref** under one tag in P1, and route each of those
lines to its own adoption decision later.
- **B. Adopt them before touching the ref.**
**Recommended: A.** An archive tag keeps every commit reachable, so B is still
possible afterwards. This map does not judge those lines.
### Q-F4. Delete Geometry's duplicate vectors before Asura Cut 3 ships them to Unity?
What is `CultVec3` for? Nothing that `float3` does not already do, and the
surface-nets code converts it straight to `float3` (`CultGeometrySurfaceNets.cs:83`).
The July branch deleted it, and main never did.
- **A. P4 before Cut 3:** no semver cost, and about −190 lines.
- **B. Ship Cut 3 as is:** a later deletion is a major bump for
`org.gamecult.cultlib`.
**Recommended: A.** It is the one piece of the geometry half that is right, and
it gets more expensive after Cut 3's release.
## 6. Findings not assigned to a cut
- **Unreproducible build (CultLib `6a65369`).** The July CultLib branch
restores `CultMath 0.1.0` from a git-ignored local feed. The "Fensalir and
Aetheria green" receipts in its progress document
(`docs/cultmath-gamecult-geometry-migration-progress.md`, stage 6) could not
have been reproduced from committed sources alone. The branch is being
retired, so this is recorded as a scar and needs no fix.
- **A failing test in the donor's July world.**
`ProjectionTests.CheckedInProjectionReportMatchesSampler` fails on Linux in
the July composite (169/170), and the same test passes on mainline. The most
likely cause is a platform difference in the regenerated report. It was not
chased, because the code is being retired.
- **CultMath's `HlslMirrorTests.cs:7-15`** is a negative spelling test. It
guards against planetary kernels returning to CultMath and is cited by Asura
as the ruling's enforcement. Keep it; after P1 its only threat is gone, but it
is cheap and names the boundary.
- **Stopgap note.** Composite multi-repo snapshots are how a Fensalir build
can be probed on Yggdrasil at all: `ygg-verify.sh` takes one repo, and
Fensalir needs sibling checkouts. The Idunn verify campaign should take a
multi-repo revision set rather than rely on this trick.

View file

@ -0,0 +1,378 @@
# Stateless Planetary Erosion Rendering Plan
## Objective
Render planetary-scale, erosion-shaped terrain from one deterministic spherical
field, from orbit to ground, without making cube faces, cache residency, request
order, or the renderer into competing terrain authorities.
CultMath now carries the faithful point-evaluable C# and HLSL erosion contract,
and Zyphos lowers it through persistent residual pages into coarse raster
patches with bounded per-pixel refinement. The implementation and verification
evidence for each phase are recorded below.
## Current mechanism
CultMath now owns the source-grounded erosion filter in matching C# and HLSL,
including deterministic integer cell hashing, physical wavelength selection,
and a measured compute-shader parity surface. Zyphos can evaluate that field
directly and can generate bordered cube-sphere pages whose overlapping samples
and GPU-reduced summaries have dedicated tests.
The renderer now owns a persistent six-root page set plus camera-selected
residual descendants. Pages carry height, world-space tangent gradient,
material evidence, conservative summaries, and independent content and
presentation versions. Parent/child arrival and eviction preserve one terrain
field by fading only the child residual.
The production lowering is coarse cube-sphere raster patches with bounded
per-pixel radial refinement against those pages. The general SDF renderer is a
debug oracle, not the primary planet backend: enabling the full planet SDF has
removed the D3D12 device under the current workload, while page generation with
the planet SDF disabled completes cleanly. The page-backed planet is visibly
proven through the candidate reservoir with corrected pixel depth. Zyphos's
legacy two-million-splat planet writer has been removed; it ran after the patch,
erased the same surface, and consumed most of the former frame budget.
## Progress ledger
| Phase | State | Evidence |
|---|---|---|
| 1. Faithful filter | Complete | C# reference fixtures and adapted HLSL contract |
| 2. CPU/GPU parity | Complete | Compute readback comparison over 2,052 samples |
| 3. Physical bands | Complete | Wavelength selection, fractional terminal octave, unresolved bound |
| 4. GPU pages | Complete | Spherical borders, sibling seam tests, GPU summary reduction |
| 5. Page-backed intersection | Complete | Persistent page and summary buffers, conservative bounds/steps, bracketed refinement, radial hit parity |
| 6. Quadtree transitions | Complete | Exact 0/0.5/1 arrival captures plus cross-face half-eviction capture and lifecycle probes |
| 7. Unified differentials/materials | Complete | World-gradient pages, composed radial normals, shared ridge/gully material evidence |
| 8. Profiling/backend decision | Complete | Orbit-to-ground raster path, page generation, residency, and total frame fit named budgets |
"Complete" means the phase exit criterion has authoritative evidence.
## Authority map
- **Owner:** CultMath owns the deterministic spherical erosion operator and its
matching C# and HLSL semantics.
- **Inputs:** unit planet direction, base height, world-distance tangent
gradient, planet seed, planet radius, requested frequency band, and bounded
erosion parameters.
- **Outputs:** radial height residual, tangent gradient, ridge mask, gully mask,
drainage hint, and declared unresolved error.
- **Derived state:** cube-sphere pages, page borders, min/max reductions,
residency records, render meshes, and debug projections are disposable
lowerings.
- **Client policy:** Zyphos owns terrain composition, parameter presets,
geological meaning, biome use, and authored overrides.
- **Forbidden writers:** face-local terrain noise, tile-local seeds, seam repair
passes, renderer-only erosion logic, and caches that change terrain when
evicted.
- **Shared path:** direct compute evaluation, CPU page baking, GPU page baking,
reload, and LOD transitions evaluate the same field contract.
- **Deletion line:** remove direct full-spectrum erosion from the repeated
sphere-tracing loop once page-backed geometry proves equivalent. Keep direct
evaluation as a verification oracle. The visible fallback is the nearest
resident ancestor band; the root fallback is the uneroded base field until a
root page is available.
## Invariants
1. The same seed and spherical point produce the same terrain regardless of
cube face, tile level, request order, cache state, or evaluation backend.
2. Parent and child pages share identical low-frequency terrain. A child adds
only the residual band it can represent.
3. Cube faces are charts and scheduling domains, never terrain owners.
4. Geometry displacement, visible normals, material masks, and CPU queries
describe the same final field.
5. Cache eviction affects latency and fidelity availability, not world truth.
6. Frequencies above the projected or page Nyquist limit do not enter geometry.
7. Visual erosion does not claim watershed, sediment, lake, or river authority.
8. SDF stepping remains conservative throughout page arrival and LOD blending.
## Target rendering pipeline
```text
planet seed + spherical position + client terrain policy
|
v
canonical spherical field
|
GPU compute or CPU oracle
|
v
cube-sphere height/gradient/material pages
|
quadtree residency + frequency-band LOD
|
v
coarse tessellated cube-sphere patches
|
bounded pixel radial refinement
|
v
differential normal + material evaluation
|
v
diegetic PBR presentation
```
## Surface-page contract
Each resident page represents one cube-sphere quadtree tile and stores an
interior region plus a measured filtering border. Initial measurements should
start at 128 by 128 interior samples and 4--8 border texels; these values are
experiments, not doctrine.
Required channels:
- radial displacement;
- two-component tangent gradient in a declared tile tangent frame;
- ridge and gully masks;
- material or climate evidence required by the client;
- validity/confidence and active frequency-band metadata.
Required summaries are minimum and maximum displacement, unresolved height
error, maximum slope or curvature, angular texel size, included wavelength
range, field version, and parameter identity.
Borders sample the canonical spherical function beyond face and tile edges.
They do not copy a neighbor's potentially missing page. Seam truth therefore
does not depend on residency.
## Geometry and intersection
The primary backend rasterizes coarse cube-sphere quad patches. Vertex work
places a conservative, low-frequency surface from the resident root and
ancestor pages. The pixel shader follows the interpolated camera ray through a
small fixed number of radial corrections against the complete resident page
field, then evaluates the shared differential and material contract. Fine
terrain therefore costs only covered pixels; the rasterizer supplies coverage,
clipping, and coarse visibility instead of idling beside a full-screen marcher.
Patch selection and tessellation use page-summary projected error. Fine bands
do not force matching geometric subdivision unless their displacement error can
move the silhouette or exceed the pixel-refinement capture range. Summary
bounds must make the coarse patch conservative so the true surface cannot fall
outside its rasterized coverage.
The general SDF path remains a direct verification oracle and diagnostic view.
It may use expanded-sphere broad phase and bracketed radial refinement, but it
does not own production visibility or terrain truth.
## Differential and normal contract
For unit direction `d`, planet radius `R`, radial height `h`, and
world-distance tangent gradient `grad(h)`, the surface is
```text
P(d) = (R + h) d
```
and the normal is derived from the same differential, approximately
```text
N = normalize((R + h) d - grad(h))
```
The exact scaling must be derived and tested for the selected gradient units.
Cube-face UV differences are not the visible-normal authority. Authored
residuals publish compatible derivatives or use the same bounded sampling
contract.
The raster lowering derives its visible normal from screen derivatives of the
four-step refined world position, oriented toward the camera with a radial
fallback. This differentiates the final composed surface without repeating five
base-field evaluations per pixel. CPU queries and the direct oracle retain the
analytic planet-space differential contract.
## LOD and frequency bands
The terrain is one spectrum evaluated through different filters, not separate
orbital and ground worlds.
- Express erosion scale as physical wavelength using planet radius.
- Select octaves from projected footprint and page sampling rate.
- Fade bands over a narrow transition interval.
- Store children as shared parent low frequencies plus child-resolvable
residual bands.
- Blend residual contribution; never blend two independent full terrains.
- Keep parent pages renderable while children generate or stream.
Orbital views consume continental and major-relief bands. Flight adds regional
drainage and ridge bands. Ground adds local geometry and may lower smaller
omitted bands into normal/material detail from the same field.
## Material use
Ridge, gully, slope, height, climate, and authored masks are material evidence.
They may inform exposed rock, soil, moisture, snow, vegetation, and client
overrides. Drainage hints are not guaranteed rivers. Saga or a later hydrology
organ owns conserved flow, watershed topology, lakes, sediment, and climate
coupling.
## Implementation roadmap
### Phase 1: faithful filter and reference fixtures
- Replace the approximation with a source-grounded advanced erosion filter.
- Implement cell pivots, fade handling, stacked fading, partial normalization,
straight-gully slopes, ridge/crease rounding, and drainage outputs.
- Record provenance and MPL obligations for adapted reference code.
- Create fixed CPU fixtures from the reference implementation.
Exit: CultMath C# matches reference samples and exposes the complete contract.
### Phase 2: measured CPU/GPU parity
- Evaluate fixed spherical samples in a small compute shader.
- Read results back and compare height, gradient, and masks against C#.
- Replace transcendental hashing with integer hashing if strict cross-device
determinism requires it.
- Document ordinary and singular-region tolerances.
Exit: parity is measured over thousands of samples, not inferred from similar
source text.
### Phase 3: physical scale and band selection
- Add planet radius and wavelength-based parameters.
- Select octaves from requested world-space footprint.
- Prove low-frequency identity when higher bands are absent.
- Add broad, regional, local, and shading-only band presets.
Exit: planet radius does not silently alter geological scale and band limiting
does not rearrange broad terrain.
### Phase 4: GPU surface-page generation
- Generate displacement, tangent gradient, ridge, gully, and material evidence.
- Sample borders through the spherical field across cube adjacencies.
- Reduce displacement bounds and error/slope summaries.
- Key pages by planet, field version, parameters, tile, and frequency band.
Exit: independently generated overlapping pages produce identical samples.
### Phase 5: page-backed terrain intersection
- Add page lookup to the Zyphos intersection path.
- Use min/max summaries for broad-phase culling and conservative stepping.
- Add bracketed near-surface root refinement.
- Preserve direct compute evaluation as the oracle. Use the nearest resident
ancestor, or the base field before root residency, as the visible fallback.
- Remove repeated full-spectrum erosion from page-backed sphere tracing.
Exit: direct and page-backed hits agree within declared page-filter error.
### Phase 6: stable quadtree transitions
- Select the cut by projected geometric error and residency.
- Generate child residual bands relative to parent low frequencies.
- Fade residuals without double counting.
- Exercise orbital descent, lateral flight, teleport, and reload.
Exit: no visible popping, crawling, or terrain rearrangement.
### Phase 7: differential shading and materials
- Derive normals from composed displacement differentials.
- Incorporate authored residual gradients.
- Feed terrain evidence into client material policy.
- Lower unresolved bands into matching normal/material detail where useful.
Exit: geometry, lighting, materials, and CPU queries agree on one surface.
### Phase 8: performance and backend decision
- Measure per-ray calls, octave cost, page generation, occupancy, residency,
cache hit rate, and frame cost from orbit to ground.
- Render coarse tessellated cube-sphere patches and perform a fixed, bounded
page-backed radial refinement in the pixel shader.
- Use the SDF backend only as a correctness oracle and diagnostic comparison.
- Prove candidate-buffer, depth, front/back-face, and reservoir ownership with
an actual visible capture before tuning appearance.
- Keep one terrain contract regardless of backend.
Exit: the chosen lowering fits named frame and memory budgets.
Initial budgets at 640 by 360 are: no more than 1.0 ms GPU for terrain patch
raster plus pixel refinement, no more than 1.0 ms amortized GPU for page
generation, no more than 64 MiB resident terrain pages, and 16.67 ms total GPU
for a 60 Hz frame. The first six-root measurement is 2,646 page samples,
approximately 83.3 KiB resident, and about 0.04--0.05 ms for the patch draw.
The visible six-root capture measures approximately 0.071 ms for patch raster
and refinement, 0.932 ms for the complete candidate pass, and 1.37 ms recorded
GPU work, with 2,646 samples occupying about 83.3 KiB. At the exterior ground
camera, eleven pages occupy 152.6 KiB, repeated page generation measures 0.624
ms, the 64 by 64 patch measures 0.799 ms, and recorded GPU work is 1.649 ms.
The earlier 24--26 ms
frame and black capture came from the obsolete fractal-splat planet writer,
which repainted the patch and is no longer published by Zyphos. Page generation
and the selected raster lowering therefore fit the named budgets.
## Verification matrix
Numerical probes cover repeatability, CPU/GPU parity, scale invariance,
band-limited identity, direct-versus-page error, and finite behavior near weak
gradients and basis switches.
Topology probes cover all twelve cube edges, all eight corners, sibling and
parent/child neighbors, independent request order, eviction, and page-border
overlap at several levels.
The root-page GPU topology probe groups canonical spherical boundary samples
and verifies all `12 * (9 - 2) = 84` non-corner edge groups plus all eight
three-face corners within `1e-5`.
Timeline probes cover direct load, orbital descent, cube-edge crossing,
transition midpoint and arrival, missing-page fallback, page arrival, eviction,
regeneration, reload, and field-version change.
Visible debug views expose face/tile/level, residency, field version, active
wavelengths, height bounds, tangent gradient, final normal, ridge/gully masks,
parent residual blend, direct-minus-page error, and cross-edge discontinuity.
Patch-native runtime modes currently provide:
- mode 22: selected face, deepest level, and residual blend;
- mode 23: sampled height, maximum slope, and unresolved/error bound;
- mode 24: refined raster normal;
- mode 25: ridge, gully, and residual blend;
- mode 26: cube-edge proximity, cross-edge height delta, and missing-side
residency;
- mode 27: direct filtered erosion minus summed page height, normalized by the
declared error bound, with bound violations in blue;
- mode 28: content-version fingerprint, active spacing ratio, and deepest
resident level.
## Performance questions
- How many terrain evaluations occur per ray and resolved pixel?
- What is the cost per octave on target GPUs?
- What page size and border minimize generation and filtering waste?
- Can page generation overlap rendering without starving the D3D12 queue?
- What page budget preserves orbital and ground traversal locality?
- Does conservative SDF refinement remain competitive with opaque patches?
No performance claim follows from a headless boot or successful shader compile.
Measure the actual visible path.
## Completion audit
1. The four-step correction primitive has matching CultMath C# and HLSL bodies;
CPU convergence probes, direct/page GPU readback, cube topology probes, and
transition debug captures cover interiors, edges, and lifecycle states.
2. The complete audit passes: 106 Engine tests, 170 Fractal tests, and 36
CultMath tests. The current solution builds without warnings or errors.
3. The patch stages compile in the focused shader smoke and in visible runtime
captures. Runtime shader-build latency is tracked separately from frame cost.
4. Page generation, patch refinement, candidate/reservoir work, total GPU time,
and residency have separate measurements and fit the named budgets.
## Sources
- Rune Skovbo Johansen, *Fast and Gorgeous Erosion Filter*:
<https://blog.runevision.com/2026/03/fast-and-gorgeous-erosion-filter.html>
- Alexander Goslin, *Terrain Diffusion / InfiniteDiffusion*:
<https://xandergos.github.io/terrain-diffusion/>
- Existing Fensalir cube-sphere and field architecture:
`research/rendering/cube-sphere-fractal-brushes.md` and
`docs/perfect-machine-architecture.md`.

View file

@ -91,9 +91,9 @@ residency, tiled/bin dispatch, GPU accumulation, and clustered visibility.
The active GPU boundary is deliberately narrow:
```text
Mimir calibration/device metadata
-> AquariumGpuSensorFrame { calibrated cameras + shared GPU textures }
-> D3D12 sensor metadata buffers + imported texture SRVs
Mimir calibration/device metadata or FieldEvidence camera Feature claims
-> AquariumGpuSensorFrame or planned GpuSensorFusion packets
-> D3D12 sensor metadata buffers + imported/leased texture SRVs
-> D3D12 GPU sensor fusion compute shader
-> RWStructuredBuffer<TemporalGaussian>
-> instanced SDF Gaussian draw
@ -103,7 +103,11 @@ Mimir calibration/device metadata
Ownership:
- `AquariumGpuSensorFrame` is the live renderer contract for GPU-owned fusion
input.
input when a client publishes the legacy direct sensor frame.
- FieldEvidence camera-sensor `Feature` claims with `SensorObservation`
proposal policy are the current Mimir sensor-fusion contract. They plan to
the `GpuSensorFusion` backend, and `D3D12Renderer` derives sensor metadata
plus SRV bindings from resolved Texture2D resources.
- `AquariumGpuFusionField` remains a temporary fallback/debug contract for
already-derived point claims.
- Mimir-owned adapters may convert app-specific point claims into compact seeds
@ -131,6 +135,16 @@ raises confidence when stochastic samples appear to correspond, and shrinks the
kernel toward the matched surface. Acoustic constraints from the ultrasonic
chirplet loop bias confidence and velocity near measured room/position returns.
The current FieldEvidence cut also consumes Mimir camera texture leases without
restoring the retired direct sensor DTO path. LeapStereoIr, Bayer8/R8-style,
and YUY2 camera textures are sampled directly by the compute shader. YUY2 uses
the documented D3D12/DXGI SRV view path for `DXGI_FORMAT_YUY2`: Fensalir binds
an `R8G8B8A8_UNORM` SRV, reads `Y0/U/Y1/V`, and converts to RGB in
`D3D12GpuSensorFusion.hlsl` before feature scoring. Provenance:
<https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format>.
Packed video formats must not pretend to be ordinary RGBA; the declared format
selects the decode path.
Next cut: replace the first-pass descriptor comparison with calibrated
epipolar/flow search, Leap packed-map channel extraction, and a persistent GPU
correspondence/refinement buffer that can update camera pose and surface tracks

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -11,12 +11,12 @@ Lower mean absolute relative area error is better for terrain density.
| normalize | NegativeY | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
| normalize | PositiveZ | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
| normalize | NegativeZ | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
| tangent | PositiveX | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
| tangent | NegativeX | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
| tangent | PositiveY | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
| tangent | NegativeY | 16 | 0.523892 | 0.873521 | 1.175214 | 0.075978 | 0.088579 |
| tangent | PositiveZ | 16 | 0.523891 | 0.873521 | 1.175189 | 0.075993 | 0.088592 |
| tangent | NegativeZ | 16 | 0.523891 | 0.873521 | 1.175189 | 0.075993 | 0.088592 |
| tangent | PositiveX | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
| tangent | NegativeX | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
| tangent | PositiveY | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
| tangent | NegativeY | 16 | 0.523872 | 0.873549 | 1.175269 | 0.075933 | 0.088550 |
| tangent | PositiveZ | 16 | 0.523866 | 0.873549 | 1.175245 | 0.075944 | 0.088557 |
| tangent | NegativeZ | 16 | 0.523866 | 0.873549 | 1.175245 | 0.075944 | 0.088557 |
Default for the first terrain slice: `tangent`.
It is still cheap, remains seam-compatible with the cube face mapping, and the sampler gives it lower area spread than direct normalization on this harness.

View file

@ -120,6 +120,7 @@ foreach ($mode in $modes) {
RenderDebugMode = $debugMode
ReservoirScale = $captureScale
SpatialReuseBudget = $captureSpatialReuseBudget
CaptureSeconds = 0.0
Suffix = $suffix
Path = $outputPath
})
@ -128,6 +129,7 @@ foreach ($mode in $modes) {
Write-Host ("Capturing {0} ready={1} debug={2} -> {3}" -f $mode, $readyFrame, $debugMode, $outputPath)
$env:AQUARIUM_HEADLESS_READY_FRAMES = [string][Math]::Max(1, $readyFrame)
$captureTimer = [System.Diagnostics.Stopwatch]::StartNew()
$process = Start-Process -FilePath $exePath -ArgumentList $arguments -NoNewWindow -PassThru -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog
if (-not $process.WaitForExit([Math]::Max(1, $TimeoutSeconds) * 1000)) {
$process.Kill($true)
@ -136,6 +138,7 @@ foreach ($mode in $modes) {
}
$process.WaitForExit()
$captureTimer.Stop()
$process.Refresh()
$exitCode = $process.ExitCode
if ($null -eq $exitCode -and (Test-Path $outputPath)) {
@ -155,6 +158,7 @@ foreach ($mode in $modes) {
RenderDebugMode = $debugMode
ReservoirScale = $captureScale
SpatialReuseBudget = $captureSpatialReuseBudget
CaptureSeconds = $captureTimer.Elapsed.TotalSeconds
Suffix = $suffix
Path = $outputPath
})

View file

@ -0,0 +1,255 @@
param(
[string]$CaptureDirectory = "",
[string]$Stamp,
[string]$ManifestPath = "",
[string]$CandidateMode = "native",
[string]$OutputPath = ""
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if ([string]::IsNullOrWhiteSpace($CaptureDirectory)) {
$CaptureDirectory = Join-Path $repoRoot "artifacts\fensalir-captures"
}
if ([string]::IsNullOrWhiteSpace($Stamp)) {
throw "Stamp is required, for example: -Stamp 20260530-003006"
}
$CaptureDirectory = [System.IO.Path]::GetFullPath($CaptureDirectory)
if ([string]::IsNullOrWhiteSpace($ManifestPath)) {
$ManifestPath = Join-Path $CaptureDirectory "reservoir-sequence-$Stamp-manifest.csv"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $CaptureDirectory "reservoir-loss-curve-$Stamp.md"
}
$ManifestPath = [System.IO.Path]::GetFullPath($ManifestPath)
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
$csvPath = [System.IO.Path]::ChangeExtension($OutputPath, ".csv")
if (-not (Test-Path -LiteralPath $ManifestPath)) {
throw "Missing sequence manifest: $ManifestPath"
}
Add-Type -AssemblyName System.Drawing
$captures = Import-Csv -Path $ManifestPath
$byKey = @{}
foreach ($capture in $captures) {
$key = "{0}|{1}|{2}" -f $capture.Mode, $capture.ReadyFrames, $capture.Suffix
$byKey[$key] = $capture
}
function Find-Capture {
param(
[string]$Mode,
[int]$ReadyFrames,
[string]$Suffix
)
$key = "{0}|{1}|{2}" -f $Mode, $ReadyFrames, $Suffix
if ($byKey.ContainsKey($key)) {
return $byKey[$key]
}
return $null
}
function Find-PreferredMaskPath {
param(
[string]$Mode,
[int]$ReadyFrames
)
$disocclusion = Find-Capture -Mode $Mode -ReadyFrames $ReadyFrames -Suffix "disocclusion"
if ($null -ne $disocclusion) {
return $disocclusion.Path
}
$rejection = Find-Capture -Mode $Mode -ReadyFrames $ReadyFrames -Suffix "rejection"
if ($null -ne $rejection) {
return $rejection.Path
}
return ""
}
function Measure-Loss {
param(
[string]$CandidatePath,
[string]$ReferencePath,
[string]$Label,
[int]$ReadyFrames,
[string]$CandidateMaskPath = "",
[string]$ReferenceMaskPath = ""
)
$candidate = [System.Drawing.Bitmap]::new($CandidatePath)
$reference = [System.Drawing.Bitmap]::new($ReferencePath)
$candidateMask = if ([string]::IsNullOrWhiteSpace($CandidateMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($CandidateMaskPath) }
$referenceMask = if ([string]::IsNullOrWhiteSpace($ReferenceMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($ReferenceMaskPath) }
try {
if ($candidate.Width -ne $reference.Width -or $candidate.Height -ne $reference.Height) {
throw "Capture dimensions differ for $Label."
}
if ($candidateMask -ne $null -and ($candidateMask.Width -ne $candidate.Width -or $candidateMask.Height -ne $candidate.Height)) {
throw "Candidate mask dimensions differ for $Label."
}
if ($referenceMask -ne $null -and ($referenceMask.Width -ne $candidate.Width -or $referenceMask.Height -ne $candidate.Height)) {
throw "Reference mask dimensions differ for $Label."
}
$measuredPixels = 0
$changedPixels = 0
$absoluteErrorSum = 0.0
$squaredErrorSum = 0.0
$maxChannelDelta = 0
for ($y = 0; $y -lt $candidate.Height; $y++) {
for ($x = 0; $x -lt $candidate.Width; $x++) {
$maskActive = $true
if ($candidateMask -ne $null -or $referenceMask -ne $null) {
$maskActive = $false
if ($candidateMask -ne $null) {
$maskColor = $candidateMask.GetPixel($x, $y)
$maskActive = $maskActive -or ([Math]::Max([int]$maskColor.R, [Math]::Max([int]$maskColor.G, [int]$maskColor.B)) -gt 12)
}
if ($referenceMask -ne $null) {
$maskColor = $referenceMask.GetPixel($x, $y)
$maskActive = $maskActive -or ([Math]::Max([int]$maskColor.R, [Math]::Max([int]$maskColor.G, [int]$maskColor.B)) -gt 12)
}
}
if (-not $maskActive) {
continue
}
$a = $candidate.GetPixel($x, $y)
$b = $reference.GetPixel($x, $y)
$dr = [int]$a.R - [int]$b.R
$dg = [int]$a.G - [int]$b.G
$db = [int]$a.B - [int]$b.B
$maxDelta = [Math]::Max([Math]::Abs($dr), [Math]::Max([Math]::Abs($dg), [Math]::Abs($db)))
if ($maxDelta -gt 0) {
$changedPixels++
}
$absoluteErrorSum += [Math]::Abs($dr) + [Math]::Abs($dg) + [Math]::Abs($db)
$squaredErrorSum += ($dr * $dr) + ($dg * $dg) + ($db * $db)
if ($maxDelta -gt $maxChannelDelta) {
$maxChannelDelta = $maxDelta
}
$measuredPixels++
}
}
$totalPixels = $candidate.Width * $candidate.Height
$pixelDenominator = if ($measuredPixels -gt 0) { $measuredPixels } else { 1 }
$channelDenominator = $pixelDenominator * 3.0
$mse = $squaredErrorSum / $channelDenominator
$rmse = [Math]::Sqrt($mse)
$psnr = if ($mse -le 0.0) { [double]::PositiveInfinity } else { 10.0 * [Math]::Log10((255.0 * 255.0) / $mse) }
[pscustomobject]@{
Label = $Label
ReadyFrames = $ReadyFrames
MeasuredPixels = $measuredPixels
TotalPixels = $totalPixels
FramePercent = $measuredPixels / $totalPixels
ChangedPixels = $changedPixels
ChangedPercent = $changedPixels / $pixelDenominator
MAE = $absoluteErrorSum / $channelDenominator
MSE = $mse
RMSE = $rmse
PSNR = $psnr
MaxChannelDelta = $maxChannelDelta
CandidatePath = $CandidatePath
ReferencePath = $ReferencePath
CandidateMaskPath = $CandidateMaskPath
ReferenceMaskPath = $ReferenceMaskPath
}
}
finally {
if ($candidateMask -ne $null) {
$candidateMask.Dispose()
}
if ($referenceMask -ne $null) {
$referenceMask.Dispose()
}
$candidate.Dispose()
$reference.Dispose()
}
}
$readyFrames = @($captures |
Where-Object { $_.Mode -eq $CandidateMode -and $_.Suffix -eq "final" } |
ForEach-Object { [int]$_.ReadyFrames } |
Sort-Object -Unique)
if ($readyFrames.Count -eq 0) {
throw "No final candidate captures found for mode '$CandidateMode'."
}
$results = [System.Collections.Generic.List[object]]::new()
foreach ($readyFrame in $readyFrames) {
$candidate = Find-Capture -Mode $CandidateMode -ReadyFrames $readyFrame -Suffix "final"
$reference = Find-Capture -Mode "reference" -ReadyFrames $readyFrame -Suffix "final"
if ($null -eq $candidate -or $null -eq $reference) {
continue
}
$results.Add((Measure-Loss -CandidatePath $candidate.Path -ReferencePath $reference.Path -Label "final" -ReadyFrames $readyFrame))
$candidateMask = Find-PreferredMaskPath -Mode $CandidateMode -ReadyFrames $readyFrame
$referenceMask = Find-PreferredMaskPath -Mode "reference" -ReadyFrames $readyFrame
if (-not [string]::IsNullOrWhiteSpace($candidateMask) -or -not [string]::IsNullOrWhiteSpace($referenceMask)) {
$results.Add((Measure-Loss -CandidatePath $candidate.Path -ReferencePath $reference.Path -Label "masked" -ReadyFrames $readyFrame -CandidateMaskPath $candidateMask -ReferenceMaskPath $referenceMask))
}
}
if ($results.Count -eq 0) {
throw "No candidate/reference final pairs found for stamp '$Stamp'."
}
$results | Export-Csv -NoTypeInformation -Path $csvPath
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Reservoir Reconstruction Loss Curve")
$lines.Add("")
$lines.Add(("Stamp: ``{0}``" -f $Stamp))
$lines.Add(("Candidate mode: ``{0}``" -f $CandidateMode))
$lines.Add("")
$lines.Add("| Scope | Ready frames | Measured/frame % | Changed % | MAE | MSE | RMSE | PSNR dB | Max delta |")
$lines.Add("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
foreach ($result in ($results | Sort-Object ReadyFrames, Label)) {
$psnrText = if ([double]::IsInfinity($result.PSNR)) { "inf" } else { $result.PSNR.ToString("N3") }
$lines.Add(("| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} |" -f
$result.Label,
$result.ReadyFrames,
$result.FramePercent.ToString("P4"),
$result.ChangedPercent.ToString("P4"),
$result.MAE.ToString("N4"),
$result.MSE.ToString("N4"),
$result.RMSE.ToString("N4"),
$psnrText,
$result.MaxChannelDelta))
}
$lines.Add("")
$lines.Add("Loss is RGB reconstruction error against the same-time native-domain reference capture. The reference is higher work-grid scale and higher spatial reuse budget when the manifest was captured that way; it is not an offline path-traced ground truth.")
Set-Content -LiteralPath $OutputPath -Value $lines -Encoding UTF8
Write-Host "Reservoir reconstruction loss curve:"
foreach ($result in ($results | Sort-Object ReadyFrames, Label)) {
$psnrText = if ([double]::IsInfinity($result.PSNR)) { "inf" } else { $result.PSNR.ToString("N3") }
Write-Host (" {0} f{1}: measured {2} changed {3} MAE {4} RMSE {5} PSNR {6} dB max {7}" -f
$result.Label,
$result.ReadyFrames,
$result.FramePercent.ToString("P4"),
$result.ChangedPercent.ToString("P4"),
$result.MAE.ToString("N4"),
$result.RMSE.ToString("N4"),
$psnrText,
$result.MaxChannelDelta)
}
Write-Host "Wrote:"
Write-Host " $OutputPath"
Write-Host " $csvPath"

View file

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\CultMath\src\CultMath\CultMath.csproj" />
</ItemGroup>
</Project>

View file

@ -8,6 +8,7 @@ public sealed class AquariumAudioDocument
private readonly ConcurrentQueue<AquariumAudioControlFrame> controlFrames = new();
private readonly ConcurrentQueue<AquariumStreamingDspProgram> streamingDspPrograms = new();
private readonly ConcurrentQueue<AquariumStreamingAudioBlock> streamingAudioBlocks = new();
private readonly ConcurrentQueue<AquariumAudioCaptureRequest> captureRequests = new();
public static AquariumAudioDocument Empty { get; } = new();
@ -131,6 +132,38 @@ public sealed class AquariumAudioDocument
return drained;
}
public void EnqueueSystemLoopbackCapture(
string profileId,
string sourceId,
string displayName,
bool enabled,
long sequence)
{
if (string.IsNullOrWhiteSpace(profileId))
{
return;
}
captureRequests.Enqueue(new AquariumAudioCaptureRequest(
profileId,
string.IsNullOrWhiteSpace(sourceId) ? "system-loopback" : sourceId,
string.IsNullOrWhiteSpace(displayName) ? "System audio" : displayName,
AquariumAudioCaptureKind.SystemLoopback,
enabled,
sequence));
}
public IReadOnlyList<AquariumAudioCaptureRequest> DrainCaptureRequests(int maxRequests = 8)
{
var drained = new List<AquariumAudioCaptureRequest>();
while (drained.Count < maxRequests && captureRequests.TryDequeue(out var request))
{
drained.Add(request);
}
return drained;
}
}
public sealed record AquariumPcmAudioChunk(float[] MonoSamples, int SampleRate, float LeftGain = 1.0f, float RightGain = 1.0f);
@ -178,6 +211,19 @@ public sealed record AquariumStreamingAudioChannel(
string SourceId,
float[] Samples);
public enum AquariumAudioCaptureKind
{
SystemLoopback
}
public sealed record AquariumAudioCaptureRequest(
string ProfileId,
string SourceId,
string DisplayName,
AquariumAudioCaptureKind Kind,
bool Enabled,
long Sequence);
public sealed record AquariumAudioStemFrame(
string ProfileId,
IReadOnlyList<AquariumAudioStemChannel> Channels,

View file

@ -10,7 +10,7 @@ public readonly record struct GraphicsSettings(
float FieldReservoirSpatialReuseBudget)
{
public const int MinRenderDebugMode = 0;
public const int MaxRenderDebugMode = 20;
public const int MaxRenderDebugMode = 28;
public const int FieldReservoirModeNativeDomain = 0;
public const int FieldReservoirModeTexelBaseline = 1;
public const float MinFieldReservoirScale = 0.25f;

View file

@ -128,8 +128,13 @@ public enum KeyCode
Delete,
Enter,
LeftArrow,
UpArrow,
RightArrow,
DownArrow,
Home,
End,
PageUp,
PageDown,
Shift,
Control,
}

View file

@ -156,6 +156,24 @@ public sealed class AquariumShaderBuilder(AquariumShaderManifest manifest)
return this;
}
public AquariumShaderBuilder PlanetarySurfacePage(string path)
{
manifest.PlanetarySurfacePage(path);
return this;
}
public AquariumShaderBuilder PlanetarySurfacePageSummary(string path)
{
manifest.PlanetarySurfacePageSummary(path);
return this;
}
public AquariumShaderBuilder PlanetarySurfacePatch(string path)
{
manifest.PlanetarySurfacePatch(path);
return this;
}
public AquariumShaderBuilder Include(string path)
{
manifest.Include(path);

View file

@ -62,6 +62,7 @@ public enum AquariumFieldBackendKind
SurfacePage = 5,
TubeField = 6,
DebugOverlay = 7,
GpuSensorFusion = 8,
}
public enum AquariumFieldResourceKind
@ -1124,6 +1125,9 @@ public static class AquariumFieldLoweringPlanner
AquariumFieldEncoding.Feature
when domainKind == AquariumFieldDomainKind.CameraSensor &&
request.ProposalKind == AquariumFieldProposalKind.DeterministicStructural => AquariumFieldBackendKind.DebugOverlay,
AquariumFieldEncoding.Feature
when domainKind == AquariumFieldDomainKind.CameraSensor &&
request.ProposalKind == AquariumFieldProposalKind.SensorObservation => AquariumFieldBackendKind.GpuSensorFusion,
_ => AquariumFieldBackendKind.Unknown,
};

View file

@ -36,6 +36,12 @@ public sealed class AquariumShaderManifest
public string FractalSplatRenderShader { get; private set; } = "D3D12FractalSplatRender.hlsl";
public string PlanetarySurfacePageShader { get; private set; } = "D3D12PlanetarySurfacePage.hlsl";
public string PlanetarySurfacePageSummaryShader { get; private set; } = "D3D12PlanetarySurfacePageSummary.hlsl";
public string PlanetarySurfacePatchShader { get; private set; } = "D3D12PlanetarySurfacePatch.hlsl";
public string PostShader { get; private set; } = "D3D12Post.hlsl";
public string SdfCommonInclude { get; private set; } = "D3D12SdfCommon.hlsli";
@ -92,6 +98,24 @@ public sealed class AquariumShaderManifest
return this;
}
public AquariumShaderManifest PlanetarySurfacePage(string path)
{
PlanetarySurfacePageShader = path;
return this;
}
public AquariumShaderManifest PlanetarySurfacePageSummary(string path)
{
PlanetarySurfacePageSummaryShader = path;
return this;
}
public AquariumShaderManifest PlanetarySurfacePatch(string path)
{
PlanetarySurfacePatchShader = path;
return this;
}
public AquariumShaderManifest Post(string path)
{
PostShader = path;
@ -312,6 +336,7 @@ public enum AquariumGpuSensorPixelFormat
R16Float,
Rg8Unorm,
LeapPackedMap,
Yuy2,
}
public readonly record struct AquariumExternalGpuTexture(
@ -336,7 +361,8 @@ public readonly record struct AquariumGpuSensorCamera(
int Height,
int FirstTextureIndex,
int TextureCount,
long TimestampNs);
long TimestampNs,
AquariumGpuSensorPixelFormat PixelFormat = AquariumGpuSensorPixelFormat.Unknown);
public sealed class AquariumGpuSensorFrame
{
@ -497,6 +523,8 @@ public sealed class AquariumSceneState
public AquariumFractalReservoirField FractalReservoirField { get; init; } = AquariumFractalReservoirField.Empty;
public AquariumPlanetarySurfacePageSet PlanetarySurfacePages { get; init; } = AquariumPlanetarySurfacePageSet.Empty;
public AquariumGpuSensorFrame GpuSensorFrame { get; init; } = AquariumGpuSensorFrame.Empty;
public AquariumAcousticFieldFrame AcousticFieldFrame { get; init; } = AquariumAcousticFieldFrame.Empty;
@ -509,9 +537,189 @@ public sealed class AquariumSceneState
public AquariumBufferFieldFrame BufferFieldFrame { get; init; } = AquariumBufferFieldFrame.Empty;
public AquariumBokushoBrushFrame BokushoBrushFrame { get; init; } = AquariumBokushoBrushFrame.Empty;
public AquariumSplineFrame SplineFrame { get; init; } = AquariumSplineFrame.Empty;
}
[StructLayout(LayoutKind.Sequential)]
public readonly record struct AquariumPlanetarySurfacePageInput(Vector4 DirectionRadius, Vector4 Sampling);
[StructLayout(LayoutKind.Sequential)]
public readonly record struct AquariumPlanetarySurfacePageMetadata(Vector4 Address, Vector4 Layout, Vector4 Bounds, Vector4 State);
public sealed class AquariumPlanetarySurfacePage
{
public long ContentKey { get; init; }
public IReadOnlyList<AquariumPlanetarySurfacePageInput> Samples { get; init; } = [];
public AquariumPlanetarySurfacePageMetadata Metadata { get; init; }
public bool HasInput => ContentKey != 0 && Samples.Count > 0;
}
public sealed class AquariumPlanetarySurfacePageSet
{
public static AquariumPlanetarySurfacePageSet Empty { get; } = new();
public string GeneratorEntryPoint { get; init; } = "D3D12PlanetaryTerrainPageCS";
public long ContentVersion { get; init; }
public long PresentationVersion { get; init; }
public IReadOnlyList<AquariumPlanetarySurfacePage> Pages { get; init; } = [];
public bool RenderCoarsePatches { get; init; }
public int PatchCells { get; init; } = 16;
public int SampleCount => Pages.Sum(page => page.Samples.Count);
public bool HasInput => ContentVersion != 0 && Pages.Count > 0 && Pages.All(page => page.HasInput);
}
public sealed class AquariumBokushoBrushFrame
{
public static AquariumBokushoBrushFrame Empty { get; } = new();
public int TuftCount { get; init; }
public int SampleCount { get; init; }
public float PhysicsHz { get; init; } = 500.0f;
public float BrushRadius { get; init; } = 2.2f;
public float Pressure { get; init; } = 0.72f;
public float InkLoad { get; init; } = 0.90f;
public float Wetness { get; init; } = 0.86f;
public float Splay { get; init; } = 0.72f;
public float Bend { get; init; } = 0.58f;
public float Friction { get; init; } = 0.62f;
public float RadiusScale { get; init; } = 1.0f;
public float PressureScale { get; init; } = 1.0f;
public float NormalScale { get; init; } = 1.0f;
public float TangentScale { get; init; } = 1.0f;
public Vector4 StrokeP0 { get; init; } = new(-7.2f, 1.0f, 0.0f, 0.0f);
public Vector4 StrokeP1 { get; init; } = new(-2.4f, -1.8f, 0.0f, 0.0f);
public Vector4 StrokeP2 { get; init; } = new(2.4f, -1.6f, 0.0f, 0.0f);
public Vector4 StrokeP3 { get; init; } = new(7.2f, 0.8f, 0.0f, 0.0f);
public IReadOnlyList<AquariumBokushoBrushStroke> Strokes { get; init; } = [];
public IReadOnlyList<AquariumBokushoSourcePoint> SourcePoints { get; init; } = [];
public bool HasInput => TuftCount > 0 && SampleCount > 1 && PhysicsHz > 0.0f;
public AquariumBokushoBrushFrame Normalized() => new()
{
TuftCount = Math.Clamp(TuftCount, 1, 4096),
SampleCount = Math.Clamp(SampleCount, 2, 4096),
PhysicsHz = Math.Clamp(PhysicsHz, 60.0f, 2000.0f),
BrushRadius = MathF.Max(0.0001f, BrushRadius),
Pressure = Math.Clamp(Pressure, 0.0f, 2.0f),
InkLoad = Math.Clamp(InkLoad, 0.0f, 3.0f),
Wetness = Math.Clamp(Wetness, 0.0f, 1.6f),
Splay = Math.Clamp(Splay, 0.0f, 2.0f),
Bend = Math.Clamp(Bend, 0.0f, 2.4f),
Friction = Math.Clamp(Friction, 0.0f, 1.0f),
RadiusScale = Math.Clamp(RadiusScale, 0.1f, 4.0f),
PressureScale = Math.Clamp(PressureScale, 0.1f, 4.0f),
NormalScale = Math.Clamp(NormalScale, 0.1f, 4.0f),
TangentScale = Math.Clamp(TangentScale, 0.1f, 4.0f),
StrokeP0 = StrokeP0,
StrokeP1 = StrokeP1,
StrokeP2 = StrokeP2,
StrokeP3 = StrokeP3,
Strokes = Strokes.Select(stroke => stroke.Normalized()).ToArray(),
SourcePoints = SourcePoints.Select(point => point.Normalized()).ToArray(),
};
}
public sealed class AquariumBokushoSourcePoint
{
public int SourceStrokeId { get; init; } = -1;
public float T { get; init; }
public Vector2 Position { get; init; }
public AquariumBokushoSourcePoint Normalized() => new()
{
SourceStrokeId = SourceStrokeId,
T = Math.Clamp(T, 0.0f, 1.0f),
Position = Position,
};
}
public sealed class AquariumBokushoBrushStroke
{
public Vector4 StrokeP0 { get; init; } = new(-7.2f, 1.0f, 0.0f, 0.0f);
public Vector4 StrokeP1 { get; init; } = new(-2.4f, -1.8f, 0.0f, 0.0f);
public Vector4 StrokeP2 { get; init; } = new(2.4f, -1.6f, 0.0f, 0.0f);
public Vector4 StrokeP3 { get; init; } = new(7.2f, 0.8f, 0.0f, 0.0f);
public float RadiusScale { get; init; } = 1.0f;
public float PressureScale { get; init; } = 1.0f;
public float NormalScale { get; init; } = 1.0f;
public float TangentScale { get; init; } = 1.0f;
public float ShaftTilt { get; init; }
public float ShaftRotation { get; init; }
public float GripHeight { get; init; } = 1.0f;
public float Compliance { get; init; } = 1.0f;
public float EntryTaper { get; init; } = 0.10f;
public float ExitTaper { get; init; } = 0.14f;
public float PigmentScale { get; init; } = 1.0f;
public float SplitScale { get; init; } = 1.0f;
public float SegmentStart { get; init; }
public float SegmentEnd { get; init; } = 1.0f;
public int SourceStrokeId { get; init; } = -1;
public AquariumBokushoBrushStroke Normalized() => new()
{
StrokeP0 = StrokeP0,
StrokeP1 = StrokeP1,
StrokeP2 = StrokeP2,
StrokeP3 = StrokeP3,
RadiusScale = Math.Clamp(RadiusScale, 0.1f, 4.0f),
PressureScale = Math.Clamp(PressureScale, 0.1f, 4.0f),
NormalScale = Math.Clamp(NormalScale, 0.1f, 4.0f),
TangentScale = Math.Clamp(TangentScale, 0.1f, 4.0f),
ShaftTilt = Math.Clamp(ShaftTilt, -1.5f, 1.5f),
ShaftRotation = Math.Clamp(ShaftRotation, -2.0f, 2.0f),
GripHeight = Math.Clamp(GripHeight, 0.2f, 2.4f),
Compliance = Math.Clamp(Compliance, 0.1f, 2.5f),
EntryTaper = Math.Clamp(EntryTaper, 0.01f, 0.50f),
ExitTaper = Math.Clamp(ExitTaper, 0.01f, 0.50f),
PigmentScale = Math.Clamp(PigmentScale, 0.05f, 4.0f),
SplitScale = Math.Clamp(SplitScale, 0.0f, 4.0f),
SegmentStart = Math.Clamp(SegmentStart, 0.0f, 1.0f),
SegmentEnd = Math.Clamp(SegmentEnd, 0.0f, 1.0f),
SourceStrokeId = SourceStrokeId,
};
}
public sealed class AquariumBufferFieldFrame
{
public static AquariumBufferFieldFrame Empty { get; } = new();

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,15 @@ namespace Aquarium.Engine.Ui;
public sealed class AquariumUiDocument
{
private readonly List<AquariumUiPanel> panels = [];
private readonly List<AquariumUiSurface> surfaces = [];
private readonly List<AquariumConsoleCommand> commands = [];
public static AquariumUiDocument Empty { get; } = new();
public IReadOnlyList<AquariumUiPanel> Panels => panels;
public IReadOnlyList<AquariumUiSurface> Surfaces => surfaces;
public IReadOnlyList<AquariumConsoleCommand> Commands => commands;
public AquariumUiDocument Panel(string title, Action<AquariumUiPanelBuilder> compose)
@ -29,6 +32,35 @@ public sealed class AquariumUiDocument
return this;
}
public AquariumUiDocument Surface(string id, string title, float left, float top, float width, float height, Action<AquariumUiSurfaceBuilder> compose)
{
return Surface(id, title, left, top, width, height, contentPadding: 8.0f, rootGap: 8.0f, rootPadding: 8.0f, compose);
}
public AquariumUiDocument Surface(
string id,
string title,
float left,
float top,
float width,
float height,
float contentPadding,
float rootGap,
float rootPadding,
Action<AquariumUiSurfaceBuilder> compose)
{
var children = new List<AquariumUiElement>();
compose(new AquariumUiSurfaceBuilder(children));
surfaces.Add(new AquariumUiSurface(
"cultmesh.eve_surface.v0",
id,
title,
new AquariumUiRect(left, top, width, height),
new AquariumUiElement(id + ".root", "dashboard", title, Role: "root", Layout: AquariumUiLayout.Vertical(rootGap, rootPadding), Children: children),
Math.Max(0.0f, contentPadding)));
return this;
}
public AquariumUiDocument Command(string name, Func<IReadOnlyList<string>, string> execute, string description = "")
{
commands.Add(new AquariumConsoleCommand(name, execute, description));
@ -44,6 +76,218 @@ public sealed record AquariumUiPanel(
IReadOnlyList<AquariumUiControl> Controls,
bool FadeWhenMouseDistant = false);
public sealed record AquariumUiSurface(
string Schema,
string Id,
string Title,
AquariumUiRect Bounds,
AquariumUiElement Root,
float ContentPadding = 8.0f);
public sealed record AquariumUiRect(float Left, float Top, float Width, float Height);
public sealed record AquariumUiLayout(
string Direction = "vertical",
float Gap = 8.0f,
float Padding = 8.0f)
{
public static AquariumUiLayout Vertical(float gap = 8.0f, float padding = 8.0f) => new("vertical", gap, padding);
public static AquariumUiLayout Horizontal(float gap = 8.0f, float padding = 8.0f) => new("horizontal", gap, padding);
}
public sealed record AquariumUiStyle(string Variant = "default", string Tone = "neutral");
public sealed record AquariumUiBinding(
string DocumentSchema,
string DocumentId,
string Path,
string ValueKind,
string Access = "read",
string Authority = "",
string? CommandId = null)
{
public static AquariumUiBinding Read(string documentSchema, string documentId, string path, string valueKind, string authority = "")
=> new(documentSchema, documentId, path, valueKind, "read", authority);
public static AquariumUiBinding ReadWrite(string documentSchema, string documentId, string path, string valueKind, string authority = "")
=> new(documentSchema, documentId, path, valueKind, "read-write", authority);
public static AquariumUiBinding Command(string documentSchema, string documentId, string path, string authority, string commandId)
=> new(documentSchema, documentId, path, "command", "command", authority, commandId);
}
public sealed record AquariumUiPreviewItem(
string Id,
string Label,
float X,
float Y,
float Width,
float Height,
bool Selected = false,
string Tone = "neutral");
public readonly record struct AquariumUiPreviewInteraction(
string ItemId,
string Handle,
string Phase,
float X,
float Y,
float DeltaX,
float DeltaY);
public readonly record struct AquariumUiPreviewState(
string HoverItemId = "",
string HoverHandle = "",
string ActiveItemId = "",
string ActiveHandle = "");
public readonly record struct AquariumUiPreviewGuide(
string Axis,
float Position,
string Tone = "neutral");
public sealed record AquariumUiElement(
string Id,
string Kind,
string? Text = null,
string? Role = null,
AquariumUiLayout? Layout = null,
AquariumUiStyle? Style = null,
AquariumUiBinding? Binding = null,
Func<string>? ReadText = null,
Func<double>? ReadMetric = null,
Func<bool>? ReadToggle = null,
Action<bool>? WriteToggle = null,
Func<float>? ReadFloat = null,
Action<float>? WriteFloat = null,
float Min = 0.0f,
float Max = 1.0f,
string Format = "0.###",
Func<int>? ReadOption = null,
Action<int>? WriteOption = null,
IReadOnlyList<AquariumUiOption>? Options = null,
Func<IReadOnlyList<AquariumUiPreviewItem>>? ReadPreviewItems = null,
Func<AquariumUiPreviewState>? ReadPreviewState = null,
Func<IReadOnlyList<AquariumUiPreviewGuide>>? ReadPreviewGuides = null,
bool PreviewBlitsOutputBuffer = false,
Action<AquariumUiPreviewInteraction>? HandlePreviewInteraction = null,
Action? Invoke = null,
Func<bool>? IsVisible = null,
float Weight = 1.0f,
float? FixedExtent = null,
IReadOnlyList<AquariumUiElement>? Children = null)
{
public bool Visible => IsVisible?.Invoke() ?? true;
}
public sealed class AquariumUiSurfaceBuilder(List<AquariumUiElement> children)
{
public AquariumUiSurfaceBuilder Vertical(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
{
var scopeChildren = new List<AquariumUiElement>();
compose(new AquariumUiSurfaceBuilder(scopeChildren));
children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Vertical(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren));
return this;
}
public AquariumUiSurfaceBuilder Horizontal(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
{
var scopeChildren = new List<AquariumUiElement>();
compose(new AquariumUiSurfaceBuilder(scopeChildren));
children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Horizontal(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren));
return this;
}
public AquariumUiSurfaceBuilder Row(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, Func<bool>? isVisible = null)
{
return Horizontal(id, compose, weight, isVisible: isVisible);
}
public AquariumUiSurfaceBuilder Pane(string id, string title, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f, float? fixedExtent = null)
{
var paneChildren = new List<AquariumUiElement>();
compose(new AquariumUiSurfaceBuilder(paneChildren));
children.Add(new AquariumUiElement(id, "pane", title, Style: new AquariumUiStyle("panel", tone), Layout: AquariumUiLayout.Vertical(8.0f, 10.0f), Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: paneChildren));
return this;
}
public AquariumUiSurfaceBuilder Card(string id, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f)
{
var cardChildren = new List<AquariumUiElement>();
compose(new AquariumUiSurfaceBuilder(cardChildren));
children.Add(new AquariumUiElement(id, "card", Style: new AquariumUiStyle("compact", tone), Layout: AquariumUiLayout.Vertical(4.0f, 8.0f), Weight: Math.Max(0.001f, weight), Children: cardChildren));
return this;
}
public AquariumUiSurfaceBuilder Text(string id, string text, string role = "body", float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "text", text, role, Binding: binding, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Text(string id, Func<string> read, string role = "body", float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "text", Role: role, Binding: binding, ReadText: read, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Metric(string id, string label, Func<double> read, string tone = "neutral", float weight = 1.0f, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "metric", label, Style: new AquariumUiStyle("compact", tone), Binding: binding, ReadMetric: read, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Toggle(string id, string label, Func<bool> read, Action<bool> write, float weight = 1.0f, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "toggle", label, Binding: binding, ReadToggle: read, WriteToggle: write, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Slider(string id, string label, Func<float> read, Action<float> write, float min, float max, string format = "0.###", float weight = 1.0f, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "slider", label, Binding: binding, ReadFloat: read, WriteFloat: write, Min: min, Max: max, Format: format, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Options(string id, string label, Func<int> read, Action<int> write, IReadOnlyList<AquariumUiOption> options, float weight = 1.0f, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "select", label, Binding: binding, ReadOption: read, WriteOption: write, Options: options, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Button(string id, string label, Action invoke, float weight = 1.0f, Func<bool>? isVisible = null, AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(id, "button", label, Binding: binding, Invoke: invoke, IsVisible: isVisible, Weight: Math.Max(0.001f, weight)));
return this;
}
public AquariumUiSurfaceBuilder Preview(
string id,
string label,
Func<IReadOnlyList<AquariumUiPreviewItem>> readItems,
float weight = 1.0f,
bool blitOutputBuffer = false,
Action<AquariumUiPreviewInteraction>? handleInteraction = null,
Func<AquariumUiPreviewState>? readState = null,
Func<IReadOnlyList<AquariumUiPreviewGuide>>? readGuides = null,
AquariumUiBinding? binding = null)
{
children.Add(new AquariumUiElement(
id,
"preview",
label,
ReadPreviewItems: readItems,
ReadPreviewState: readState,
ReadPreviewGuides: readGuides,
Binding: binding,
PreviewBlitsOutputBuffer: blitOutputBuffer,
HandlePreviewInteraction: handleInteraction,
Weight: Math.Max(0.001f, weight)));
return this;
}
}
public sealed class AquariumUiPanelBuilder(List<AquariumUiControl> controls)
{
public AquariumUiPanelBuilder Section(string title, Func<bool>? isVisible = null)

View file

@ -2,6 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
</ItemGroup>
<PropertyGroup>

View file

@ -1,11 +0,0 @@
namespace Aquarium.Engine.Fractal;
public enum CubeFace
{
PositiveX,
NegativeX,
PositiveY,
NegativeY,
PositiveZ,
NegativeZ,
}

View file

@ -1,43 +0,0 @@
using System.Numerics;
namespace Aquarium.Engine.Fractal;
public readonly record struct CubeFacePosition
{
public CubeFacePosition(CubeFace face, double u, double v)
{
if (!double.IsFinite(u) || u is < -1.0 or > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(u), u, "Cube face U must be finite and in [-1, 1].");
}
if (!double.IsFinite(v) || v is < -1.0 or > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(v), v, "Cube face V must be finite and in [-1, 1].");
}
Face = face;
U = u;
V = v;
}
public CubeFace Face { get; }
public double U { get; }
public double V { get; }
public Vector3 ToCubeVector()
{
return Face switch
{
CubeFace.PositiveX => new Vector3(1.0f, (float)V, (float)-U),
CubeFace.NegativeX => new Vector3(-1.0f, (float)V, (float)U),
CubeFace.PositiveY => new Vector3((float)U, 1.0f, (float)-V),
CubeFace.NegativeY => new Vector3((float)U, -1.0f, (float)V),
CubeFace.PositiveZ => new Vector3((float)U, (float)V, 1.0f),
CubeFace.NegativeZ => new Vector3((float)-U, (float)V, -1.0f),
_ => throw new ArgumentOutOfRangeException(nameof(Face), Face, "Unknown cube face."),
};
}
}

View file

@ -1,93 +0,0 @@
namespace Aquarium.Engine.Fractal;
public readonly record struct CubeTileKey
{
public const int MaxLevel = 30;
public CubeTileKey(CubeFace face, int level, int x, int y)
{
if (level is < 0 or > MaxLevel)
{
throw new ArgumentOutOfRangeException(nameof(level), level, $"Cube tile level must be in [0, {MaxLevel}].");
}
var axisTileCount = 1 << level;
if (x < 0 || x >= axisTileCount)
{
throw new ArgumentOutOfRangeException(nameof(x), x, $"Cube tile X must be in [0, {axisTileCount - 1}] for level {level}.");
}
if (y < 0 || y >= axisTileCount)
{
throw new ArgumentOutOfRangeException(nameof(y), y, $"Cube tile Y must be in [0, {axisTileCount - 1}] for level {level}.");
}
Face = face;
Level = level;
X = x;
Y = y;
}
public CubeFace Face { get; }
public int Level { get; }
public int X { get; }
public int Y { get; }
public int AxisTileCount => 1 << Level;
public CubeTileKey Parent()
{
if (Level == 0)
{
throw new InvalidOperationException("Root cube tile has no parent.");
}
return new CubeTileKey(Face, Level - 1, X >> 1, Y >> 1);
}
public CubeTileKey Child(int childX, int childY)
{
if (childX is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(childX), childX, "Child X must be 0 or 1.");
}
if (childY is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(childY), childY, "Child Y must be 0 or 1.");
}
if (Level == MaxLevel)
{
throw new InvalidOperationException($"Cube tile level {MaxLevel} cannot be subdivided.");
}
return new CubeTileKey(Face, Level + 1, (X << 1) | childX, (Y << 1) | childY);
}
public CubeFacePosition PositionAt(double localU, double localV)
{
if (!double.IsFinite(localU) || localU is < 0.0 or > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(localU), localU, "Local U must be finite and in [0, 1].");
}
if (!double.IsFinite(localV) || localV is < 0.0 or > 1.0)
{
throw new ArgumentOutOfRangeException(nameof(localV), localV, "Local V must be finite and in [0, 1].");
}
var axisTileCount = AxisTileCount;
var faceU = -1.0 + (2.0 * (X + localU) / axisTileCount);
var faceV = -1.0 + (2.0 * (Y + localV) / axisTileCount);
return new CubeFacePosition(Face, faceU, faceV);
}
public override string ToString()
{
return $"{Face}/L{Level:D2}/{X}/{Y}";
}
}

View file

@ -1,6 +1,8 @@
using System.Globalization;
using System.Numerics;
using Aquarium.Engine.Fractal;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Grammar;
@ -37,7 +39,7 @@ public static class FractalDslCompiler
break;
case "tile":
EnsureTokenCount(tokens, 6, lineIndex);
var tile = new CubeTileKey(ParseFace(tokens[1], lineIndex), ParseInt(tokens[2], lineIndex), ParseInt(tokens[3], lineIndex), ParseInt(tokens[4], lineIndex));
var tile = new PlanetaryTileAddress(ParseFace(tokens[1], lineIndex), ParseInt(tokens[2], lineIndex), ParseInt(tokens[3], lineIndex), ParseInt(tokens[4], lineIndex));
var domainKey = FractalStableKeyBuilder.ForCubeTile(tile, tokens[5]);
var parentKey = tokens.Length >= 7 ? new AquariumFractalKey(tokens[6]) : default;
rootKey = FractalStableKeyBuilder.Child(domainKey, "root");
@ -500,9 +502,9 @@ public static class FractalDslCompiler
return new Vector4(minX, minY, maxX, maxY);
}
private static CubeFace ParseFace(string value, int lineIndex)
private static PlanetaryCubeFace ParseFace(string value, int lineIndex)
{
if (Enum.TryParse<CubeFace>(value, ignoreCase: false, out var face))
if (Enum.TryParse<PlanetaryCubeFace>(value, ignoreCase: false, out var face))
{
return face;
}

View file

@ -1,10 +1,11 @@
using Aquarium.Engine.Fractal;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Grammar;
public static class FractalStableKeyBuilder
{
public static AquariumFractalKey ForCubeTile(CubeTileKey tile, string grammarPath)
public static AquariumFractalKey ForCubeTile(PlanetaryTileAddress tile, string grammarPath)
{
return new AquariumFractalKey($"cube/{tile.Face}/L{tile.Level:D2}/{tile.X}/{tile.Y}:{NormalizePath(grammarPath)}");
}

View file

@ -1,4 +1,6 @@
using System.Numerics;
using CultMath;
using GameCult.Geometry;
using Aquarium.Engine.Fractal.Grammar;
namespace Aquarium.Engine.Fractal.Lod;
@ -79,7 +81,7 @@ public static class FractalGpuProgramCompiler
var tileAddress = domains.TryGetValue(claim.DomainKey, out var domainRow)
&& domainRow.Kind == AquariumFractalDomainKind.CubeSphereTile
? domainRow.Parameters0
: new Vector4((float)CubeFace.PositiveZ, 0.0f, 0.0f, 0.0f);
: new Vector4((float)PlanetaryCubeFace.PositiveZ, 0.0f, 0.0f, 0.0f);
var rotationCos = MathF.Cos(claim.RotationRadians);
var rotationSin = MathF.Sin(claim.RotationRadians);

View file

@ -1,4 +1,6 @@
using System.Numerics;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
@ -6,5 +8,5 @@ public interface ICubeSphereProjection
{
string Name { get; }
Vector3 Project(CubeFacePosition position);
Vector3 Project(PlanetaryFaceCoordinate position);
}

View file

@ -1,4 +1,6 @@
using System.Numerics;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
@ -6,8 +8,8 @@ public sealed class NormalizeCubeSphereProjection : ICubeSphereProjection
{
public string Name => "normalize";
public Vector3 Project(CubeFacePosition position)
public Vector3 Project(PlanetaryFaceCoordinate position)
{
return Vector3.Normalize(position.ToCubeVector());
return (Vector3)PlanetaryTopology.NormalizedCubeDirection(position);
}
}

View file

@ -1,8 +1,11 @@
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
public readonly record struct ProjectionDistortionResult(
string ProjectionName,
CubeFace Face,
PlanetaryCubeFace Face,
int SamplesPerAxis,
double AverageAreaScale,
double MinRelativeArea,

View file

@ -1,4 +1,6 @@
using System.Numerics;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
@ -9,7 +11,7 @@ public static class ProjectionDistortionSampler
public static ProjectionDistortionResult SampleFace(
ICubeSphereProjection projection,
CubeFace face,
PlanetaryCubeFace face,
int samplesPerAxis)
{
ArgumentNullException.ThrowIfNull(projection);
@ -56,14 +58,14 @@ public static class ProjectionDistortionSampler
Math.Sqrt(totalSquaredRelativeError / sampleCount));
}
private static double SurfaceAreaScale(ICubeSphereProjection projection, CubeFace face, double u, double v)
private static double SurfaceAreaScale(ICubeSphereProjection projection, PlanetaryCubeFace face, double u, double v)
{
var du = Derivative(projection, face, u, v, DerivativeAxis.U);
var dv = Derivative(projection, face, u, v, DerivativeAxis.V);
return Vector3.Cross(du, dv).Length();
}
private static Vector3 Derivative(ICubeSphereProjection projection, CubeFace face, double u, double v, DerivativeAxis axis)
private static Vector3 Derivative(ICubeSphereProjection projection, PlanetaryCubeFace face, double u, double v, DerivativeAxis axis)
{
var low = axis == DerivativeAxis.U ? u : v;
var high = low;
@ -86,13 +88,13 @@ public static class ProjectionDistortionSampler
Vector3 highPoint;
if (axis == DerivativeAxis.U)
{
lowPoint = projection.Project(new CubeFacePosition(face, lowCoordinate, v));
highPoint = projection.Project(new CubeFacePosition(face, highCoordinate, v));
lowPoint = projection.Project(new PlanetaryFaceCoordinate(face, lowCoordinate, v));
highPoint = projection.Project(new PlanetaryFaceCoordinate(face, highCoordinate, v));
}
else
{
lowPoint = projection.Project(new CubeFacePosition(face, u, lowCoordinate));
highPoint = projection.Project(new CubeFacePosition(face, u, highCoordinate));
lowPoint = projection.Project(new PlanetaryFaceCoordinate(face, u, lowCoordinate));
highPoint = projection.Project(new PlanetaryFaceCoordinate(face, u, highCoordinate));
}
return (highPoint - lowPoint) / (float)(forward + backward);

View file

@ -1,6 +1,9 @@
using System.Globalization;
using System.Text;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
public static class ProjectionReport
@ -25,7 +28,7 @@ public static class ProjectionReport
foreach (var projection in projections)
{
foreach (var face in Enum.GetValues<CubeFace>())
foreach (var face in Enum.GetValues<PlanetaryCubeFace>())
{
var result = ProjectionDistortionSampler.SampleFace(projection, face, samplesPerAxis);
builder.Append("| ")

View file

@ -1,16 +1,15 @@
using System.Numerics;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Engine.Fractal.Projection;
public sealed class TangentCubeSphereProjection : ICubeSphereProjection
{
private const double QuarterPi = Math.PI * 0.25;
public string Name => "tangent";
public Vector3 Project(CubeFacePosition position)
public Vector3 Project(PlanetaryFaceCoordinate position)
{
var warped = new CubeFacePosition(position.Face, Math.Tan(position.U * QuarterPi), Math.Tan(position.V * QuarterPi));
return Vector3.Normalize(warped.ToCubeVector());
return (Vector3)PlanetaryTopology.Direction(position);
}
}

View file

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\..\CultLib</CultLibRoot>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@ -23,7 +22,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CultLibRoot)\packages\cultmath\src\CultMath\CultMath.csproj" />
<ProjectReference Include="..\..\..\CultMath\src\CultMath\CultMath.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
</ItemGroup>
<ItemGroup>

View file

@ -76,7 +76,11 @@ public static class AquariumHost
var readyFrames = 0;
var requiredReadyFrames = ParseHeadlessReadyFrames();
var captureFramePath = ParseCaptureFramePath(args);
var captureBokushoCanvasPath = ParseCaptureBokushoCanvasPath(args);
var captureBokushoDensityPath = ParseCaptureBokushoDensityPath(args);
var capturedFrame = false;
var capturedBokushoCanvas = false;
var capturedBokushoDensity = false;
IAquariumRuntime? sceneReadyRuntime = null;
try
@ -134,6 +138,7 @@ public static class AquariumHost
activeRuntime.OnSceneReady();
sceneReadyRuntime = activeRuntime;
synthHost ??= new AquariumSynthHost(audioStemBus);
synthHost.Update(AquariumSynthDocument.Combine(activeRuntime.Synth, renderer.DebugSynth), activeRuntime.Audio, 0.0f);
lastFrame = frameClock.Elapsed;
}
}
@ -148,7 +153,35 @@ public static class AquariumHost
capturedFrame = true;
}
if (runtime.Options.Headless && frames >= 2 && readyFrames >= requiredReadyFrames)
if (runtime.Options.Headless
&& !capturedBokushoCanvas
&& !string.IsNullOrWhiteSpace(captureBokushoCanvasPath)
&& readyFrames >= requiredReadyFrames)
{
renderer.SaveBokushoBrushCanvasRaw(captureBokushoCanvasPath);
Console.WriteLine($"Headless Aquarium Bokusho canvas captured: {Path.GetFullPath(captureBokushoCanvasPath)}");
capturedBokushoCanvas = true;
}
if (runtime.Options.Headless
&& !capturedBokushoDensity
&& !string.IsNullOrWhiteSpace(captureBokushoDensityPath)
&& readyFrames >= requiredReadyFrames)
{
renderer.SaveBokushoPageDensityRaw(captureBokushoDensityPath);
Console.WriteLine($"Headless Aquarium Bokusho density captured: {Path.GetFullPath(captureBokushoDensityPath)}");
capturedBokushoDensity = true;
}
var needsFrameCapture = !string.IsNullOrWhiteSpace(captureFramePath) && !capturedFrame;
var needsBokushoCanvasCapture = !string.IsNullOrWhiteSpace(captureBokushoCanvasPath) && !capturedBokushoCanvas;
var needsBokushoDensityCapture = !string.IsNullOrWhiteSpace(captureBokushoDensityPath) && !capturedBokushoDensity;
if (runtime.Options.Headless
&& frames >= 2
&& readyFrames >= requiredReadyFrames
&& !needsFrameCapture
&& !needsBokushoCanvasCapture
&& !needsBokushoDensityCapture)
{
Console.WriteLine("Headless Aquarium completed requested frames.");
break;
@ -214,6 +247,34 @@ public static class AquariumHost
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_FRAME");
}
private static string? ParseCaptureBokushoCanvasPath(IReadOnlyCollection<string> args)
{
var values = args.ToArray();
for (var index = 0; index < values.Length - 1; index++)
{
if (string.Equals(values[index], "--capture-bokusho-canvas", StringComparison.OrdinalIgnoreCase))
{
return values[index + 1];
}
}
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_BOKUSHO_CANVAS");
}
private static string? ParseCaptureBokushoDensityPath(IReadOnlyCollection<string> args)
{
var values = args.ToArray();
for (var index = 0; index < values.Length - 1; index++)
{
if (string.Equals(values[index], "--capture-bokusho-density", StringComparison.OrdinalIgnoreCase))
{
return values[index + 1];
}
}
return Environment.GetEnvironmentVariable("AQUARIUM_CAPTURE_BOKUSHO_DENSITY");
}
private static string? ParseCachePath(IReadOnlyCollection<string> args)
{
var values = args.ToArray();

View file

@ -13,6 +13,8 @@ internal sealed class AquariumSynthHost : IDisposable
private readonly IAquariumAudioStemBus audioStemBus;
private readonly WasapiAudioDevice audioDevice = new();
private readonly AquaSynthPatchCompiler patchCompiler = new(new AquaSynthNativeOptions(DspSourceDirectory: Path.Combine(AppContext.BaseDirectory, "Synth")));
private WasapiLoopbackCaptureDevice? loopbackCapture;
private AquariumAudioCaptureRequest? activeCaptureRequest;
private float timeSeconds;
public AquariumSynthHost(IAquariumAudioStemBus? audioStemBus = null)
@ -81,6 +83,11 @@ internal sealed class AquariumSynthHost : IDisposable
PlayMonitorOutputs(block, stemFrame);
}
foreach (var request in audio.DrainCaptureRequests())
{
ApplyCaptureRequest(request);
}
if (!synth.Enabled)
{
return;
@ -113,6 +120,42 @@ internal sealed class AquariumSynthHost : IDisposable
}
}
private void ApplyCaptureRequest(AquariumAudioCaptureRequest request)
{
if (request.Kind != AquariumAudioCaptureKind.SystemLoopback)
{
return;
}
if (!request.Enabled)
{
if (activeCaptureRequest?.ProfileId == request.ProfileId)
{
loopbackCapture?.Dispose();
loopbackCapture = null;
activeCaptureRequest = null;
}
return;
}
if (activeCaptureRequest is not null &&
activeCaptureRequest.ProfileId == request.ProfileId &&
activeCaptureRequest.SourceId == request.SourceId &&
activeCaptureRequest.DisplayName == request.DisplayName)
{
return;
}
loopbackCapture?.Dispose();
loopbackCapture = new WasapiLoopbackCaptureDevice(
audioStemBus,
request.ProfileId,
request.SourceId,
request.DisplayName);
activeCaptureRequest = request;
}
private void PlayMonitorOutputs(AquariumStreamingAudioBlock block, AquariumAudioStemFrame stemFrame)
{
if (block.MonitorLeftChannel < 0 && block.MonitorRightChannel < 0)
@ -341,6 +384,7 @@ internal sealed class AquariumSynthHost : IDisposable
}
streamingDsp.Dispose();
loopbackCapture?.Dispose();
audioDevice.Dispose();
patchCompiler.Dispose();
}

View file

@ -0,0 +1,331 @@
using System.Runtime.InteropServices;
namespace Aquarium.Engine.Audio;
internal sealed class WasapiLoopbackCaptureDevice : IDisposable
{
private static readonly bool TraceAudio = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AQUARIUM_AUDIO_TRACE"));
private readonly IAquariumAudioStemBus stemBus;
private readonly string profileId;
private readonly string sourceId;
private readonly string displayName;
private readonly Thread thread;
private volatile bool running = true;
private volatile bool started;
private long sequence;
public WasapiLoopbackCaptureDevice(
IAquariumAudioStemBus stemBus,
string profileId,
string sourceId,
string displayName)
{
this.stemBus = stemBus;
this.profileId = profileId;
this.sourceId = sourceId;
this.displayName = displayName;
thread = new Thread(CaptureThread)
{
IsBackground = true,
Name = "Aquarium WASAPI Loopback"
};
thread.Start();
}
public void Dispose()
{
running = false;
if (started)
{
thread.Join(TimeSpan.FromSeconds(2.0));
}
}
private void CaptureThread()
{
started = true;
var comInitialized = false;
IAudioClient? audioClient = null;
try
{
var hr = CoInitializeEx(IntPtr.Zero, CoInit.Multithreaded);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
comInitialized = true;
#pragma warning disable CA1416
var enumeratorType = Type.GetTypeFromCLSID(MMDeviceEnumeratorClsid, throwOnError: true)
?? throw new InvalidOperationException("MMDeviceEnumerator COM type is not available.");
#pragma warning restore CA1416
var deviceEnumerator = (IMMDeviceEnumerator)(Activator.CreateInstance(enumeratorType)
?? throw new InvalidOperationException("failed to create MMDeviceEnumerator"));
Marshal.ThrowExceptionForHR(deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.Render, ERole.Console, out var device));
var audioClientId = typeof(IAudioClient).GUID;
Marshal.ThrowExceptionForHR(device.Activate(ref audioClientId, ClsCtx.All, IntPtr.Zero, out var audioClientObject));
audioClient = (IAudioClient)audioClientObject;
Marshal.ThrowExceptionForHR(audioClient.GetMixFormat(out var mixFormatPointer));
try
{
var format = WasapiFormat.FromPointer(mixFormatPointer);
Marshal.ThrowExceptionForHR(audioClient.Initialize(
AudioClientShareMode.Shared,
AudioClientStreamFlags.Loopback | AudioClientStreamFlags.NoPersist,
0,
0,
mixFormatPointer,
IntPtr.Zero));
var captureClientId = typeof(IAudioCaptureClient).GUID;
Marshal.ThrowExceptionForHR(audioClient.GetService(ref captureClientId, out var captureClientObject));
var captureClient = (IAudioCaptureClient)captureClientObject;
Marshal.ThrowExceptionForHR(audioClient.Start());
Console.WriteLine($"Aquarium WASAPI loopback started: profile={profileId} {format.SampleRate} Hz, {format.Channels} channels, {format.BitsPerSample}-bit {format.SampleKind}.");
while (running)
{
Marshal.ThrowExceptionForHR(captureClient.GetNextPacketSize(out var packetFrames));
while (packetFrames > 0)
{
Marshal.ThrowExceptionForHR(captureClient.GetBuffer(
out var buffer,
out var frames,
out var flags,
out _,
out _));
try
{
PublishPacket(buffer, frames, flags, format);
}
finally
{
Marshal.ThrowExceptionForHR(captureClient.ReleaseBuffer(frames));
}
Marshal.ThrowExceptionForHR(captureClient.GetNextPacketSize(out packetFrames));
}
Thread.Sleep(4);
}
audioClient.Stop();
}
finally
{
Marshal.FreeCoTaskMem(mixFormatPointer);
}
}
catch (Exception ex)
{
Console.WriteLine($"Aquarium WASAPI loopback unavailable: {ex.Message}");
}
finally
{
audioClient?.Stop();
if (comInitialized)
{
CoUninitialize();
}
}
}
private void PublishPacket(IntPtr buffer, int frameCount, uint flags, WasapiFormat format)
{
if (frameCount <= 0)
{
return;
}
var mono = (flags & AudioCaptureBufferFlagsSilent) != 0
? new float[frameCount]
: format.SampleKind == WasapiSampleKind.Float32
? ReadFloat32(buffer, frameCount, format.Channels)
: ReadPcm16(buffer, frameCount, format.Channels);
stemBus.Publish(new AquariumAudioStemFrame(
profileId,
[new AquariumAudioStemChannel(0, "system_mix", displayName, sourceId, mono)],
frameCount,
format.SampleRate,
Interlocked.Increment(ref sequence)));
if (TraceAudio)
{
Console.WriteLine($"Aquarium loopback packet: profile={profileId} frames={frameCount} seq={sequence}");
}
}
private static unsafe float[] ReadFloat32(IntPtr buffer, int frameCount, int channels)
{
var output = new float[frameCount];
var input = (float*)buffer;
for (var frame = 0; frame < frameCount; frame++)
{
var sum = 0.0f;
for (var channel = 0; channel < channels; channel++)
{
sum += *input++;
}
output[frame] = Math.Clamp(sum / Math.Max(1, channels), -1.0f, 1.0f);
}
return output;
}
private static unsafe float[] ReadPcm16(IntPtr buffer, int frameCount, int channels)
{
var output = new float[frameCount];
var input = (short*)buffer;
for (var frame = 0; frame < frameCount; frame++)
{
var sum = 0.0f;
for (var channel = 0; channel < channels; channel++)
{
sum += *input++ / 32768.0f;
}
output[frame] = Math.Clamp(sum / Math.Max(1, channels), -1.0f, 1.0f);
}
return output;
}
private readonly record struct WasapiFormat(int Channels, int SampleRate, int BitsPerSample, int BlockAlign, WasapiSampleKind SampleKind)
{
public static WasapiFormat FromPointer(IntPtr pointer)
{
var tag = Marshal.ReadInt16(pointer, 0);
var channels = Marshal.ReadInt16(pointer, 2);
var sampleRate = Marshal.ReadInt32(pointer, 4);
var blockAlign = Marshal.ReadInt16(pointer, 12);
var bitsPerSample = Marshal.ReadInt16(pointer, 14);
var kind = tag switch
{
WaveFormatIeeeFloat => WasapiSampleKind.Float32,
WaveFormatPcm => WasapiSampleKind.Pcm16,
WaveFormatExtensible when bitsPerSample == 32 => WasapiSampleKind.Float32,
_ => WasapiSampleKind.Pcm16
};
return new WasapiFormat(channels, sampleRate, bitsPerSample, blockAlign, kind);
}
}
private enum WasapiSampleKind
{
Float32,
Pcm16
}
private const uint AudioCaptureBufferFlagsSilent = 0x2;
private const short WaveFormatPcm = 1;
private const short WaveFormatIeeeFloat = 3;
private const short WaveFormatExtensible = -2;
[DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr pvReserved, CoInit dwCoInit);
[DllImport("ole32.dll")]
private static extern void CoUninitialize();
[Flags]
private enum CoInit : uint
{
Multithreaded = 0x0
}
[Flags]
private enum ClsCtx : uint
{
All = 23
}
private enum EDataFlow
{
Render,
Capture,
All
}
private enum ERole
{
Console,
Multimedia,
Communications
}
private enum AudioClientShareMode
{
Shared,
Exclusive
}
[Flags]
private enum AudioClientStreamFlags : uint
{
Loopback = 0x00020000,
NoPersist = 0x00080000
}
private static readonly Guid MMDeviceEnumeratorClsid = new("BCDE0395-E52F-467C-8E3D-C4579291692E");
[ComImport]
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDeviceEnumerator
{
int EnumAudioEndpoints(EDataFlow dataFlow, uint stateMask, out IntPtr devices);
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
}
[ComImport]
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDevice
{
int Activate(ref Guid iid, ClsCtx clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfaceObject);
}
[ComImport]
[Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioClient
{
int Initialize(AudioClientShareMode shareMode, AudioClientStreamFlags streamFlags, long bufferDuration, long periodicity, IntPtr format, IntPtr audioSessionGuid);
int GetBufferSize(out int bufferFrameCount);
int GetStreamLatency(out long latency);
int GetCurrentPadding(out int paddingFrameCount);
int IsFormatSupported(AudioClientShareMode shareMode, IntPtr format, out IntPtr closestMatch);
int GetMixFormat(out IntPtr deviceFormat);
int GetDevicePeriod(out long defaultDevicePeriod, out long minimumDevicePeriod);
int Start();
int Stop();
int Reset();
int SetEventHandle(IntPtr eventHandle);
int GetService(ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object serviceInterface);
}
[ComImport]
[Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioCaptureClient
{
int GetBuffer(out IntPtr dataBuffer, out int numFramesToRead, out uint bufferFlags, out long devicePosition, out long qpcPosition);
int ReleaseBuffer(int numFramesRead);
int GetNextPacketSize(out int numFramesInNextPacket);
}
}

View file

@ -69,10 +69,15 @@ public sealed class Win32Window : IDisposable
private const int VK_DELETE = 0x2E;
private const int VK_RETURN = 0x0D;
private const int VK_LEFT = 0x25;
private const int VK_UP = 0x26;
private const int VK_RIGHT = 0x27;
private const int VK_DOWN = 0x28;
private const int VK_HOME = 0x24;
private const int VK_END = 0x23;
private const int VK_PRIOR = 0x21;
private const int VK_NEXT = 0x22;
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int WHEEL_DELTA = 120;
private readonly WndProc windowProcedure;
@ -824,18 +829,33 @@ public sealed class Win32Window : IDisposable
case VK_LEFT:
input.SetKey(KeyCode.LeftArrow, isDown);
break;
case VK_UP:
input.SetKey(KeyCode.UpArrow, isDown);
break;
case VK_RIGHT:
input.SetKey(KeyCode.RightArrow, isDown);
break;
case VK_DOWN:
input.SetKey(KeyCode.DownArrow, isDown);
break;
case VK_HOME:
input.SetKey(KeyCode.Home, isDown);
break;
case VK_END:
input.SetKey(KeyCode.End, isDown);
break;
case VK_PRIOR:
input.SetKey(KeyCode.PageUp, isDown);
break;
case VK_NEXT:
input.SetKey(KeyCode.PageDown, isDown);
break;
case VK_SHIFT:
input.SetKey(KeyCode.Shift, isDown);
break;
case VK_CONTROL:
input.SetKey(KeyCode.Control, isDown);
break;
}
}

View file

@ -0,0 +1,64 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using Vortice.Direct3D;
using Vortice.Direct3D12;
namespace Aquarium.Engine.Render;
internal static class D3D12ComputeProbe
{
public static unsafe TOutput[] Run<TInput, TOutput>(string shaderPath, string entryPoint, ReadOnlySpan<TInput> inputs)
where TInput : unmanaged where TOutput : unmanaged
{
if (!OperatingSystem.IsWindows()) throw new PlatformNotSupportedException("D3D12 probes require Windows.");
var inputBytes = checked(inputs.Length * Unsafe.SizeOf<TInput>());
var outputBytes = checked(inputs.Length * Unsafe.SizeOf<TOutput>());
using var device = D3D12.D3D12CreateDevice<ID3D12Device>(IntPtr.Zero, FeatureLevel.Level_11_0);
using var queue = device.CreateCommandQueue(new CommandQueueDescription(CommandListType.Direct));
using var allocator = device.CreateCommandAllocator(CommandListType.Direct);
using var list = device.CreateCommandList<ID3D12GraphicsCommandList>(0, CommandListType.Direct, allocator, null);
using var fence = device.CreateFence(0);
using var fenceEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
using var input = device.CreateCommittedResource(HeapType.Upload, ResourceDescription.Buffer((ulong)inputBytes), ResourceStates.GenericRead);
using var output = device.CreateCommittedResource(HeapType.Default, ResourceDescription.Buffer((ulong)outputBytes, ResourceFlags.AllowUnorderedAccess), ResourceStates.UnorderedAccess);
using var readback = device.CreateCommittedResource(HeapType.Readback, ResourceDescription.Buffer((ulong)outputBytes), ResourceStates.CopyDest);
var mappedInput = input.Map<byte>(0);
MemoryMarshal.AsBytes(inputs).CopyTo(new Span<byte>(mappedInput, inputBytes));
input.Unmap(0);
var parameters = new[]
{
new RootParameter(RootParameterType.ShaderResourceView, new RootDescriptor(0, 0), ShaderVisibility.All),
new RootParameter(RootParameterType.UnorderedAccessView, new RootDescriptor(0, 0), ShaderVisibility.All),
new RootParameter(new RootConstants(0, 0, 3), ShaderVisibility.All),
};
var signatureDescription = new RootSignatureDescription(RootSignatureFlags.None, parameters, []);
using var signature = device.CreateRootSignature(0, in signatureDescription, RootSignatureVersion.Version1);
using var pipeline = device.CreateComputePipelineState(new ComputePipelineStateDescription
{
RootSignature = signature,
ComputeShader = D3D12ShaderCompiler.Compile(shaderPath, entryPoint, "cs_5_0"),
});
list.SetComputeRootSignature(signature);
list.SetPipelineState(pipeline);
list.SetComputeRootShaderResourceView(0, input.GPUVirtualAddress);
list.SetComputeRootUnorderedAccessView(1, output.GPUVirtualAddress);
list.SetComputeRoot32BitConstant(2, (uint)inputs.Length, 0);
list.Dispatch((uint)((inputs.Length + 63) / 64), 1, 1);
list.ResourceBarrier(ResourceBarrier.BarrierTransition(output, ResourceStates.UnorderedAccess, ResourceStates.CopySource));
list.CopyBufferRegion(readback, 0, output, 0, (ulong)outputBytes);
list.Close();
queue.ExecuteCommandList(list);
queue.Signal(fence, 1).CheckError();
fence.SetEventOnCompletion(1, fenceEvent.SafeWaitHandle.DangerousGetHandle()).CheckError();
fenceEvent.WaitOne();
var results = new TOutput[inputs.Length];
var mappedOutput = readback.Map<byte>(0);
new ReadOnlySpan<byte>(mappedOutput, outputBytes).CopyTo(MemoryMarshal.AsBytes(results.AsSpan()));
readback.Unmap(0);
return results;
}
}

View file

@ -59,7 +59,7 @@ internal sealed class D3D12ExternalSensorTexture : IDisposable
Resource,
new ShaderResourceViewDescription
{
Format = Format,
Format = ToShaderResourceViewFormat(Format),
ViewDimension = ShaderResourceViewDimension.Texture2D,
Shader4ComponentMapping = ShaderComponentMapping.Default,
Texture2D = new Texture2DShaderResourceView { MipLevels = 1 },
@ -131,10 +131,18 @@ internal sealed class D3D12ExternalSensorTexture : IDisposable
AquariumGpuSensorPixelFormat.R16Float => Format.R16_Float,
AquariumGpuSensorPixelFormat.Rg8Unorm => Format.R8G8_UNorm,
AquariumGpuSensorPixelFormat.LeapPackedMap => Format.R8G8B8A8_UNorm,
AquariumGpuSensorPixelFormat.Yuy2 => Format.YUY2,
_ => Format.Unknown,
};
}
private static Format ToShaderResourceViewFormat(Format format)
{
return format == Format.YUY2
? Format.R8G8B8A8_UNorm
: format;
}
public void Dispose()
{
Resource.Dispose();

View file

@ -13,7 +13,8 @@ internal sealed class D3D12FieldMesh : IDisposable
AquariumFieldMeshIndexFormat indexFormat,
AquariumFieldMeshLayout layout,
int submeshCount,
ulong version)
ulong version,
string sourceUri)
{
Vertices = vertices;
Indices = indices;
@ -22,6 +23,7 @@ internal sealed class D3D12FieldMesh : IDisposable
Layout = layout;
SubmeshCount = submeshCount;
Version = version;
SourceUri = sourceUri;
}
public D3D12StructuredBuffer Vertices { get; }
@ -44,6 +46,11 @@ internal sealed class D3D12FieldMesh : IDisposable
public ulong Version { get; }
public string SourceUri { get; }
public bool IsGeneratedFromResource =>
SourceUri.StartsWith("derived-from:", StringComparison.Ordinal);
public PrimitiveTopology D3DTopology => Topology switch
{
AquariumFieldMeshTopology.TriangleStrip => PrimitiveTopology.TriangleStrip,

View file

@ -668,7 +668,8 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
existing.Indices.StrideBytes == mesh.Indices.StrideBytes &&
existing.Topology == mesh.Topology &&
existing.IndexFormat == mesh.IndexFormat &&
existing.Layout == mesh.Layout)
existing.Layout == mesh.Layout &&
string.Equals(existing.SourceUri, declaration.SourceUri, StringComparison.Ordinal))
{
return true;
}
@ -683,12 +684,14 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
device,
mesh.Vertices.Count,
mesh.Vertices.StrideBytes,
$"Aquarium D3D12 Field Mesh Vertices {declaration.ResourceKey}");
$"Aquarium D3D12 Field Mesh Vertices {declaration.ResourceKey}",
allowUnorderedAccess: IsGeneratedMeshSource(declaration.SourceUri));
var indices = new D3D12StructuredBuffer(
device,
mesh.Indices.Count,
mesh.Indices.StrideBytes,
$"Aquarium D3D12 Field Mesh Indices {declaration.ResourceKey}");
$"Aquarium D3D12 Field Mesh Indices {declaration.ResourceKey}",
allowUnorderedAccess: IsGeneratedMeshSource(declaration.SourceUri));
meshes[declaration.ResourceKey] = new D3D12FieldMesh(
vertices,
indices,
@ -696,10 +699,14 @@ internal sealed class D3D12FieldResourceRegistry : IDisposable
mesh.IndexFormat,
mesh.Layout,
mesh.SubmeshCount,
declaration.Version);
declaration.Version,
declaration.SourceUri);
return true;
}
private static bool IsGeneratedMeshSource(string sourceUri) =>
sourceUri.StartsWith("derived-from:", StringComparison.Ordinal);
private int RemoveStaleStructuredBuffers(D3D12ResourceRegistry resourceRegistry, HashSet<string> currentKeys)
{
var removed = 0;

View file

@ -165,13 +165,34 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable
}
}
public static D3D12FieldTexture2D WrapOwned(
ID3D12Resource resource,
int width,
int height,
Format format,
bool allowsUnorderedAccess,
ResourceStates state,
string name)
{
var texture = new D3D12FieldTexture2D(
resource,
uploadResource: null,
Math.Max(1, width),
Math.Max(1, height),
format,
allowsUnorderedAccess,
name);
texture.State = state;
return texture;
}
public void CreateShaderResourceView(ID3D12Device device, D3D12DescriptorSlot descriptor)
{
device.CreateShaderResourceView(
Resource,
new ShaderResourceViewDescription
{
Format = Format,
Format = ToShaderResourceViewFormat(Format),
ViewDimension = ShaderResourceViewDimension.Texture2D,
Shader4ComponentMapping = ShaderComponentMapping.Default,
Texture2D = new Texture2DShaderResourceView { MipLevels = 1 },
@ -216,6 +237,13 @@ internal sealed unsafe class D3D12FieldTexture2D : IDisposable
Resource.Dispose();
}
private static Format ToShaderResourceViewFormat(Format format)
{
return format == Format.YUY2
? Format.R8G8B8A8_UNorm
: format;
}
private static D3D12FieldTexture2D CreateRgba8(
ID3D12Device device,
ID3D12GraphicsCommandList commandList,

View file

@ -8,7 +8,7 @@ internal static class D3D12FieldTextureFormat
{
format = formatName switch
{
"Gray8" or "R8" or "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm,
"Bayer8" or "Gray8" or "R8" or "R8Unorm" or "R8_UNorm" or "R8_UNORM" => Format.R8_UNorm,
"R16Float" or "R16_Float" or "R16_FLOAT" => Format.R16_Float,
"R32Float" or "R32_Float" or "R32_FLOAT" or "Float32" => Format.R32_Float,
"Rg8" or "Rg8Unorm" or "R8G8_UNorm" or "R8G8_UNORM" or "LeapStereoIr" or "LeapPackedMap" => Format.R8G8_UNorm,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,105 @@
using Vortice.D3DCompiler;
namespace Aquarium.Engine.Render;
internal static class D3D12ShaderCompiler
{
public static ReadOnlyMemory<byte> Compile(string path, string entryPoint, string profile, bool skipOptimizationInDebug = true)
{
var flags = ShaderFlags.EnableStrictness;
#if DEBUG
flags |= ShaderFlags.Debug;
if (skipOptimizationInDebug) flags |= ShaderFlags.SkipOptimization;
#endif
var source = ExpandIncludes(path, []);
return Compiler.Compile(source, entryPoint, path, profile, flags, EffectFlags.None);
}
internal static string ExpandIncludes(string path, HashSet<string> stack)
{
var fullPath = Path.GetFullPath(path);
if (!stack.Add(fullPath)) throw new InvalidOperationException($"Circular shader include detected at {fullPath}");
var lines = File.ReadAllLines(fullPath);
var expanded = new List<string>(lines.Length);
var directory = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory;
for (var index = 0; index < lines.Length; index++)
{
var trimmed = lines[index].TrimStart();
if (trimmed.StartsWith("#include \"", StringComparison.Ordinal))
{
var first = trimmed.IndexOf('"');
var second = trimmed.IndexOf('"', first + 1);
if (second > first)
{
var include = ResolveInclude(directory, trimmed.Substring(first + 1, second - first - 1));
expanded.Add($"#line 1 \"{include.Replace("\\", "\\\\")}\"");
expanded.Add(ExpandIncludes(include, stack));
expanded.Add($"#line {index + 2} \"{fullPath.Replace("\\", "\\\\")}\"");
continue;
}
}
expanded.Add(lines[index]);
}
stack.Remove(fullPath);
return string.Join(Environment.NewLine, expanded);
}
private static string ResolveInclude(string directory, string includeName)
{
var local = Path.GetFullPath(Path.Combine(directory, includeName));
if (File.Exists(local)) return local;
var normalized = includeName.Replace('\\', '/');
if (normalized.StartsWith("CultMath/", StringComparison.Ordinal))
{
var relative = normalized["CultMath/".Length..].Replace('/', Path.DirectorySeparatorChar);
foreach (var start in new[] { directory, AppContext.BaseDirectory })
{
var current = Path.GetFullPath(start);
while (!string.IsNullOrEmpty(current))
{
var candidate = Path.Combine(current, "CultMath", "shaders", relative);
if (File.Exists(candidate)) return candidate;
var parent = Path.GetDirectoryName(current);
if (string.IsNullOrEmpty(parent) || parent == current) break;
current = parent;
}
}
}
const string cultMathUnityPrefix = "Packages/org.gamecult.cultmath/shaders/";
if (normalized.StartsWith(cultMathUnityPrefix, StringComparison.Ordinal))
{
var relative = normalized[cultMathUnityPrefix.Length..].Replace('/', Path.DirectorySeparatorChar);
foreach (var start in new[] { directory, AppContext.BaseDirectory })
{
var current = Path.GetFullPath(start);
while (!string.IsNullOrEmpty(current))
{
var localPackage = Path.Combine(current, "CultMath", relative);
if (File.Exists(localPackage)) return localPackage;
var siblingRepository = Path.Combine(current, "CultMath", "shaders", relative);
if (File.Exists(siblingRepository)) return siblingRepository;
var parent = Path.GetDirectoryName(current);
if (string.IsNullOrEmpty(parent) || parent == current) break;
current = parent;
}
}
}
if (normalized.StartsWith("GameCult.Geometry/", StringComparison.Ordinal))
{
var relative = normalized["GameCult.Geometry/".Length..].Replace('/', Path.DirectorySeparatorChar);
foreach (var start in new[] { directory, AppContext.BaseDirectory })
{
var current = Path.GetFullPath(start);
while (!string.IsNullOrEmpty(current))
{
var candidate = Path.Combine(current, "CultLib", "src", "GameCult.Geometry", "Shaders", relative);
if (File.Exists(candidate)) return candidate;
var parent = Path.GetDirectoryName(current);
if (string.IsNullOrEmpty(parent) || parent == current) break;
current = parent;
}
}
}
return local;
}
}

View file

@ -1,9 +1,11 @@
using System.Numerics;
using Vortice.DCommon;
using Vortice.Direct2D1;
using Vortice.DirectWrite;
using Vortice.DXGI;
using Vortice.Mathematics;
using Aquarium.Engine.Render.Ui;
using Aquarium.Engine.Ui;
using D2DFactoryType = Vortice.Direct2D1.FactoryType;
using DWriteFactoryType = Vortice.DirectWrite.FactoryType;
@ -16,6 +18,7 @@ internal sealed class DirectWriteOverlay : IDisposable
private readonly IDWriteFontCollection fontCollection;
private readonly IDWriteTypography smallCapsTypography;
private readonly ID2D1RenderTarget renderTarget;
private readonly ID2D1Bitmap outputSnapshot;
private readonly ID2D1SolidColorBrush primaryTextBrush;
private readonly ID2D1SolidColorBrush quietTextBrush;
private readonly ID2D1SolidColorBrush panelBrush;
@ -65,6 +68,14 @@ internal sealed class DirectWriteOverlay : IDisposable
renderTarget = direct2DFactory.CreateDxgiSurfaceRenderTarget(backBufferSurface, renderTargetProperties);
renderTarget.AntialiasMode = AntialiasMode.PerPrimitive;
renderTarget.TextAntialiasMode = Vortice.Direct2D1.TextAntialiasMode.Grayscale;
outputSnapshot = renderTarget.CreateBitmap(
new SizeI(width, height),
IntPtr.Zero,
0,
new BitmapProperties(
new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Ignore),
96.0f,
96.0f));
primaryTextBrush = renderTarget.CreateSolidColorBrush(new Color4(0.88f, 0.96f, 1.0f, 0.92f));
quietTextBrush = renderTarget.CreateSolidColorBrush(new Color4(0.54f, 0.68f, 0.72f, 0.72f));
@ -84,8 +95,10 @@ internal sealed class DirectWriteOverlay : IDisposable
monospaceFormat = CreateTextFormat("Ubuntu Sans Mono", 11.0f, FontWeight.Regular, ParagraphAlignment.Near);
}
public void Render(AquariumFrame frame, int renderDebugMode, DebugUi? debugUi, IReadOnlyList<DebugUi> clientUiPanels, string performanceText)
public void Render(AquariumFrame frame, int renderDebugMode, DebugUi? debugUi, IReadOnlyList<DebugUi> clientUiPanels, IReadOnlyList<AquariumUiSurface> clientUiSurfaces, string performanceText)
{
CaptureOutputSnapshot();
renderTarget.BeginDraw();
DrawPerformanceCounter(performanceText);
if (debugUi is not null)
@ -98,6 +111,11 @@ internal sealed class DirectWriteOverlay : IDisposable
DrawPanel(panel, panel.DrawOpacity());
}
foreach (var surface in clientUiSurfaces)
{
DrawSurface(surface);
}
renderTarget.EndDraw();
}
@ -174,6 +192,387 @@ internal sealed class DirectWriteOverlay : IDisposable
}
}
private void DrawSurface(AquariumUiSurface surface)
{
var edgePadding = surface.ContentPadding <= 0.0f ? 0.0f : 8.0f;
var bounds = RectFromEdges(
Math.Clamp(surface.Bounds.Left, edgePadding, Math.Max(edgePadding, width - 80.0f)),
Math.Clamp(surface.Bounds.Top, edgePadding, Math.Max(edgePadding, height - 48.0f)),
Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 80.0f + edgePadding, width - edgePadding),
Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 48.0f + edgePadding, height - edgePadding));
renderTarget.FillRectangle(bounds, panelBrush);
renderTarget.DrawRectangle(bounds, outlineBrush, 1.0f);
var hasTitle = !string.IsNullOrWhiteSpace(surface.Title);
if (hasTitle)
{
DrawHeader(surface.Title, titleFormat, RectFromEdges(bounds.Left + 12.0f, bounds.Top + 8.0f, bounds.Right - 12.0f, bounds.Top + 34.0f), primaryTextBrush);
}
var contentTop = hasTitle ? bounds.Top + 42.0f : bounds.Top + surface.ContentPadding;
var content = RectFromEdges(bounds.Left + surface.ContentPadding, contentTop, bounds.Right - surface.ContentPadding, bounds.Bottom - surface.ContentPadding);
DrawSurfaceChildren(surface.Root.Children ?? [], content, surface.Root.Layout ?? AquariumUiLayout.Vertical());
}
private void DrawSurfaceChildren(IReadOnlyList<AquariumUiElement> elements, Rect bounds, AquariumUiLayout layout)
{
var visible = elements.Where(static element => element.Visible).ToArray();
if (visible.Length == 0)
{
return;
}
var content = RectFromEdges(
bounds.Left + layout.Padding,
bounds.Top + layout.Padding,
bounds.Right - layout.Padding,
bounds.Bottom - layout.Padding);
var totalGap = layout.Gap * Math.Max(0, visible.Length - 1);
var horizontal = string.Equals(layout.Direction, "horizontal", StringComparison.Ordinal);
var cursor = horizontal ? content.Left : content.Top;
var available = Math.Max(0.0f, (horizontal ? content.Width : content.Height) - totalGap);
var preferred = visible.Select(element => PreferredExtent(element, horizontal)).ToArray();
var preferredTotal = preferred.Sum(static value => value ?? 0.0f);
var preferredScale = preferredTotal > available && preferredTotal > 0.0f ? available / preferredTotal : 1.0f;
var flexibleWeight = Math.Max(0.001f, visible.Where((_, index) => preferred[index] is null).Sum(static element => Math.Max(0.001f, element.Weight)));
var flexibleAvailable = Math.Max(0.0f, available - preferredTotal * preferredScale);
for (var index = 0; index < visible.Length; index++)
{
var element = visible[index];
var extent = preferred[index] is { } fixedExtent
? fixedExtent * preferredScale
: flexibleAvailable * Math.Max(0.001f, element.Weight) / flexibleWeight;
var childBounds = horizontal
? RectFromEdges(cursor, content.Top, Math.Min(content.Right, cursor + extent), content.Bottom)
: RectFromEdges(content.Left, cursor, content.Right, Math.Min(content.Bottom, cursor + extent));
DrawSurfaceElement(element, childBounds);
cursor += extent + layout.Gap;
}
}
private void DrawSurfaceElement(AquariumUiElement element, Rect bounds)
{
switch (element.Kind)
{
case "group":
DrawSurfaceChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical());
break;
case "pane":
var hasPaneTitle = !string.IsNullOrWhiteSpace(element.Text);
DrawSurfaceFrame(bounds, element.Text, element.Style?.Tone ?? "neutral", drawTitle: hasPaneTitle);
var paneContent = hasPaneTitle
? RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f)
: bounds;
DrawSurfaceChildren(element.Children ?? [], paneContent, element.Layout ?? AquariumUiLayout.Vertical());
break;
case "card":
DrawSurfaceFrame(bounds, null, element.Style?.Tone ?? "neutral", drawTitle: false);
DrawSurfaceChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical(4.0f, 8.0f));
break;
case "metric":
DrawMetricElement(element, bounds);
break;
case "toggle":
DrawToggleElement(element, bounds);
break;
case "slider":
DrawSliderElement(element, bounds);
break;
case "select":
DrawSelectElement(element, bounds);
break;
case "button":
DrawButtonElement(element, bounds);
break;
case "preview":
DrawPreviewElement(element, bounds);
break;
default:
DrawTextElement(element, bounds);
break;
}
}
private void DrawSurfaceFrame(Rect bounds, string? titleText, string tone, bool drawTitle)
{
renderTarget.FillRectangle(bounds, tone == "danger" ? activeRowBrush : tone == "warm" ? hoverRowBrush : rowBrush);
renderTarget.DrawRectangle(bounds, ToneBrush(tone), 1.0f);
if (drawTitle && !string.IsNullOrWhiteSpace(titleText))
{
DrawHeader(titleText, smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 6.0f, bounds.Right - 8.0f, bounds.Top + 28.0f), accentBrush);
}
}
private void DrawTextElement(AquariumUiElement element, Rect bounds)
{
var text = element.ReadText?.Invoke() ?? element.Text ?? "";
var format = element.Role is "mono" ? monospaceFormat : smallFormat;
var brush = element.Role is "caption" ? quietTextBrush : element.Role is "strong" or "title" ? primaryTextBrush : primaryTextBrush;
renderTarget.DrawText(text, format, bounds, brush, DrawTextOptions.Clip);
}
private void DrawMetricElement(AquariumUiElement element, Rect bounds)
{
var value = element.ReadMetric?.Invoke() ?? 0.0;
var normalized = Math.Clamp(value, 0.0, 1.0);
DrawSurfaceFrame(bounds, null, element.Style?.Tone ?? "neutral", drawTitle: false);
renderTarget.DrawText($"{element.Text}: {value:0.000}", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 2.0f, bounds.Right - 8.0f, bounds.Top + 18.0f), primaryTextBrush, DrawTextOptions.Clip);
var track = RectFromEdges(bounds.Left + 8.0f, bounds.Bottom - 10.0f, bounds.Right - 8.0f, bounds.Bottom - 5.0f);
renderTarget.FillRectangle(track, dimAccentBrush);
renderTarget.FillRectangle(RectFromEdges(track.Left, track.Top, track.Left + track.Width * (float)normalized, track.Bottom), ToneBrush(element.Style?.Tone ?? "neutral"));
}
private void DrawToggleElement(AquariumUiElement element, Rect bounds)
{
var isOn = element.ReadToggle?.Invoke() ?? false;
DrawSurfaceFrame(bounds, null, isOn ? "cool" : "neutral", drawTitle: false);
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Right - 36.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
var box = RectFromEdges(bounds.Right - 26.0f, bounds.Top + 7.0f, bounds.Right - 10.0f, bounds.Top + 23.0f);
renderTarget.DrawRectangle(box, isOn ? accentBrush : outlineBrush, 1.0f);
if (isOn)
{
renderTarget.FillRectangle(RectFromEdges(box.Left + 3.0f, box.Top + 3.0f, box.Right - 3.0f, box.Bottom - 3.0f), accentBrush);
}
}
private void DrawSliderElement(AquariumUiElement element, Rect bounds)
{
var raw = element.ReadFloat?.Invoke() ?? element.Min;
var span = Math.Max(0.0001f, element.Max - element.Min);
var normalized = Math.Clamp((raw - element.Min) / span, 0.0f, 1.0f);
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
renderTarget.DrawText($"{element.Text}: {raw.ToString(element.Format, System.Globalization.CultureInfo.InvariantCulture)}", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Left + 170.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
var track = RectFromEdges(bounds.Left + 178.0f, bounds.Top + 13.0f, bounds.Right - 12.0f, bounds.Top + 18.0f);
renderTarget.FillRectangle(track, dimAccentBrush);
renderTarget.FillRectangle(RectFromEdges(track.Left, track.Top, track.Left + track.Width * normalized, track.Bottom), accentBrush);
}
private void DrawSelectElement(AquariumUiElement element, Rect bounds)
{
var selected = element.ReadOption?.Invoke() ?? 0;
var label = element.Options?.FirstOrDefault(option => option.Value == selected).Label ?? selected.ToString(System.Globalization.CultureInfo.InvariantCulture);
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Left + 130.0f, bounds.Bottom), accentBrush, DrawTextOptions.Clip);
renderTarget.DrawText(label, smallFormat, RectFromEdges(bounds.Left + 138.0f, bounds.Top, bounds.Right - 8.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
}
private void DrawButtonElement(AquariumUiElement element, Rect bounds)
{
DrawSurfaceFrame(bounds, null, "warm", drawTitle: false);
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top, bounds.Right - 8.0f, bounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
}
private void DrawPreviewElement(AquariumUiElement element, Rect bounds)
{
var hasLabel = !string.IsNullOrWhiteSpace(element.Text);
if (hasLabel)
{
DrawSurfaceFrame(bounds, null, "neutral", drawTitle: false);
renderTarget.DrawText(element.Text ?? "", smallFormat, RectFromEdges(bounds.Left + 8.0f, bounds.Top + 6.0f, bounds.Right - 8.0f, bounds.Top + 24.0f), quietTextBrush, DrawTextOptions.Clip);
}
var padded = hasLabel
? RectFromEdges(bounds.Left + 10.0f, bounds.Top + 30.0f, bounds.Right - 10.0f, bounds.Bottom - 10.0f)
: RectFromEdges(bounds.Left + 2.0f, bounds.Top + 2.0f, bounds.Right - 2.0f, bounds.Bottom - 2.0f);
if (padded.Width <= 1.0f || padded.Height <= 1.0f)
{
return;
}
var targetAspect = 16.0f / 9.0f;
var canvasWidth = padded.Width;
var canvasHeight = canvasWidth / targetAspect;
if (canvasHeight > padded.Height)
{
canvasHeight = padded.Height;
canvasWidth = canvasHeight * targetAspect;
}
var canvas = RectFromEdges(
padded.Left + (padded.Width - canvasWidth) * 0.5f,
padded.Top + (padded.Height - canvasHeight) * 0.5f,
padded.Left + (padded.Width + canvasWidth) * 0.5f,
padded.Top + (padded.Height + canvasHeight) * 0.5f);
if (element.PreviewBlitsOutputBuffer)
{
DrawOutputSnapshot(canvas);
}
else
{
renderTarget.FillRectangle(canvas, panelBrush);
}
renderTarget.DrawRectangle(canvas, outlineBrush, 1.0f);
DrawPreviewGuides(element, canvas);
var items = element.ReadPreviewItems?.Invoke() ?? [];
var state = element.ReadPreviewState?.Invoke() ?? default;
foreach (var item in items)
{
var left = canvas.Left + Math.Clamp(item.X, 0.0f, 1.0f) * canvas.Width;
var top = canvas.Top + Math.Clamp(item.Y, 0.0f, 1.0f) * canvas.Height;
var right = canvas.Left + Math.Clamp(item.X + item.Width, 0.0f, 1.0f) * canvas.Width;
var bottom = canvas.Top + Math.Clamp(item.Y + item.Height, 0.0f, 1.0f) * canvas.Height;
var itemBounds = RectFromEdges(left, top, right, bottom);
if (itemBounds.Width <= 1.0f || itemBounds.Height <= 1.0f)
{
continue;
}
var hovered = string.Equals(state.HoverItemId, item.Id, StringComparison.Ordinal);
var active = string.Equals(state.ActiveItemId, item.Id, StringComparison.Ordinal);
var brush = active || item.Selected ? accentBrush : ToneBrush(item.Tone);
renderTarget.FillRectangle(itemBounds, active ? activeRowBrush : hovered ? hoverRowBrush : rowBrush);
renderTarget.DrawRectangle(itemBounds, brush, active || item.Selected ? 2.0f : hovered ? 1.5f : 1.0f);
renderTarget.DrawText(item.Label, smallFormat, RectFromEdges(itemBounds.Left + 6.0f, itemBounds.Top + 3.0f, itemBounds.Right - 6.0f, itemBounds.Bottom), primaryTextBrush, DrawTextOptions.Clip);
if (item.Selected && element.HandlePreviewInteraction is not null)
{
DrawPreviewHandle(itemBounds.Left, itemBounds.Top, state, item.Id, "nw");
DrawPreviewHandle(itemBounds.Right, itemBounds.Top, state, item.Id, "ne");
DrawPreviewHandle(itemBounds.Left, itemBounds.Bottom, state, item.Id, "sw");
DrawPreviewHandle(itemBounds.Right, itemBounds.Bottom, state, item.Id, "se");
}
}
}
private void DrawPreviewGuides(AquariumUiElement element, Rect canvas)
{
var guides = element.ReadPreviewGuides?.Invoke() ?? [];
foreach (var guide in guides)
{
var position = Math.Clamp(guide.Position, 0.0f, 1.0f);
var brush = ToneBrush(guide.Tone);
if (string.Equals(guide.Axis, "x", StringComparison.Ordinal))
{
var x = canvas.Left + position * canvas.Width;
renderTarget.DrawLine(new Vector2(x, canvas.Top), new Vector2(x, canvas.Bottom), brush, 1.25f);
}
else if (string.Equals(guide.Axis, "y", StringComparison.Ordinal))
{
var y = canvas.Top + position * canvas.Height;
renderTarget.DrawLine(new Vector2(canvas.Left, y), new Vector2(canvas.Right, y), brush, 1.25f);
}
}
}
private void DrawPreviewHandle(float x, float y, AquariumUiPreviewState state, string itemId, string handleId)
{
var active = string.Equals(state.ActiveItemId, itemId, StringComparison.Ordinal) &&
string.Equals(state.ActiveHandle, handleId, StringComparison.Ordinal);
var hovered = string.Equals(state.HoverItemId, itemId, StringComparison.Ordinal) &&
string.Equals(state.HoverHandle, handleId, StringComparison.Ordinal);
var radius = active ? 6.0f : hovered ? 5.0f : 4.0f;
var handle = RectFromEdges(x - radius, y - radius, x + radius, y + radius);
renderTarget.FillRectangle(handle, active ? accentActiveBrush : hovered ? accentHoverBrush : accentBrush);
renderTarget.DrawRectangle(handle, panelBrush, 1.0f);
}
private void CaptureOutputSnapshot()
{
try
{
outputSnapshot.CopyFromRenderTarget(renderTarget);
}
catch
{
// The overlay must never kill the frame if the swapchain surface is not copyable.
}
}
private void DrawOutputSnapshot(Rect canvas)
{
if (canvas.Width <= 1.0f || canvas.Height <= 1.0f)
{
return;
}
var sourceWidth = (float)Math.Max(1, width);
var sourceHeight = (float)Math.Max(1, height);
var sourceAspect = sourceWidth / sourceHeight;
var targetAspect = canvas.Width / Math.Max(1.0f, canvas.Height);
if (sourceAspect > targetAspect)
{
sourceWidth = sourceHeight * targetAspect;
}
else
{
sourceHeight = sourceWidth / targetAspect;
}
var source = new Rect(
(width - sourceWidth) * 0.5f,
(height - sourceHeight) * 0.5f,
sourceWidth,
sourceHeight);
renderTarget.DrawBitmap(outputSnapshot, canvas, 1.0f, BitmapInterpolationMode.Linear, source);
}
private ID2D1SolidColorBrush ToneBrush(string tone) =>
tone switch
{
"cool" => primaryTextBrush,
"warm" => accentBrush,
"danger" => accentActiveBrush,
_ => outlineBrush,
};
private static float? PreferredExtent(AquariumUiElement element, bool horizontal)
{
if (element.FixedExtent is { } fixedExtent)
{
return Math.Max(0.0f, fixedExtent);
}
if (horizontal)
{
return null;
}
var weight = Math.Max(0.001f, element.Weight);
return element.Kind switch
{
"group" or "pane" or "card" when HasFlexibleChild(element) => null,
"group" or "pane" or "card" => PreferredContainerExtent(element) * weight,
"text" when element.Role is "mono" => 18.0f * weight,
"text" when element.Role is "strong" or "title" => 22.0f * weight,
"text" => 18.0f * weight,
"toggle" or "select" or "slider" => 24.0f * weight,
"button" => 30.0f * weight,
"metric" => 36.0f * weight,
_ => null,
};
}
private static bool HasFlexibleChild(AquariumUiElement element)
{
var children = element.Children?.Where(static child => child.Visible).ToArray() ?? [];
return children.Any(static child => PreferredExtent(child, horizontal: false) is null);
}
private static float PreferredContainerExtent(AquariumUiElement element)
{
var layout = element.Layout ?? AquariumUiLayout.Vertical();
var children = element.Children?.Where(static child => child.Visible).ToArray() ?? [];
if (children.Length == 0)
{
return element.Kind == "pane" ? 72.0f : 24.0f;
}
var horizontal = string.Equals(layout.Direction, "horizontal", StringComparison.Ordinal);
var childExtents = children.Select(static child => PreferredExtent(child, horizontal: false) ?? 48.0f * Math.Max(0.001f, child.Weight)).ToArray();
var contentExtent = horizontal
? childExtents.Max()
: childExtents.Sum() + layout.Gap * Math.Max(0, childExtents.Length - 1);
contentExtent += layout.Padding * 2.0f;
return element.Kind switch
{
"pane" when !string.IsNullOrWhiteSpace(element.Text) => contentExtent + 38.0f,
"pane" => contentExtent,
"card" => contentExtent,
_ => contentExtent,
};
}
public void Dispose()
{
smallFormat.Dispose();
@ -194,6 +593,7 @@ internal sealed class DirectWriteOverlay : IDisposable
panelBrush.Dispose();
quietTextBrush.Dispose();
primaryTextBrush.Dispose();
outputSnapshot.Dispose();
renderTarget.Dispose();
directWriteFactory.Dispose();
direct2DFactory.Dispose();

View file

@ -27,4 +27,8 @@ public interface IAquariumRenderer : IAquariumFieldResourceBroker, IDisposable
void Render(AquariumFrame frame, int width, int height);
void SaveFramePng(string path);
void SaveBokushoBrushCanvasRaw(string path);
void SaveBokushoPageDensityRaw(string path);
}

View file

@ -0,0 +1,23 @@
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
struct ParityInput { float4 position_base; float4 slope_fade; float4 scalar0; float4 rounding; float4 onset; float4 scalar1; float4 scalar2; float4 scalar3; };
struct ParityOutput { float4 delta_magnitude; float4 ridge_fade; };
StructuredBuffer<ParityInput> inputs : register(t0);
RWStructuredBuffer<ParityOutput> outputs : register(u0);
cbuffer ParityConstants : register(b0) { uint input_count; }
[numthreads(64, 1, 1)]
void D3D12AdvancedErosionParityCS(uint3 id : SV_DispatchThreadID)
{
if (id.x >= input_count) return;
ParityInput input = inputs[id.x];
GameCultGeometryAdvancedErosionParameters p;
p.scale=input.scalar0.x; p.strength=input.scalar0.y; p.gully_weight=input.scalar0.z; p.detail=input.scalar0.w;
p.rounding=input.rounding; p.onset=input.onset; p.assumed_slope=input.scalar1.xy;
p.cell_scale=input.scalar1.z; p.normalization=input.scalar1.w; p.octaves=(int)input.scalar2.x;
p.lacunarity=input.scalar2.y; p.gain=input.scalar2.z;
GameCultGeometryErosionBandSelection band; band.active_octaves=(int)input.scalar3.x; band.final_octave_weight=input.scalar3.y; band.finest_included_wavelength=input.scalar3.z; band.unresolved_height_bound=input.scalar3.w;
GameCultGeometryAdvancedErosionResult r = gamecult_geometry_advanced_erosion_filter_banded(input.position_base.xy, float3(input.position_base.z, input.slope_fade.xy), input.slope_fade.z, p, band);
outputs[id.x].delta_magnitude=float4(r.delta, r.magnitude);
outputs[id.x].ridge_fade=float4(r.ridge_map, r.fade_target, 0, 0);
}

View file

@ -0,0 +1,699 @@
#include "CultMath/CultMath.hlsl"
static const float BOKUSHO_PAGE_DENSITY_SCALE = 65535.0;
static const float BOKUSHO_PAGE_PATCH_TRANSFER_GAIN = 0.0044;
static const float BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE = 0.32;
static const float BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE = 0.32;
static const float BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS = 0.001;
static const float BOKUSHO_PAGE_PATCH_WORLD_QUANTUM = 1.0 / 1024.0;
static const float BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM = 1.0 / 4096.0;
static const float BOKUSHO_STROKE_RHYTHM_QUANTUM = 1.0 / 4096.0;
static const uint BOKUSHO_PAGE_PATCH_INK_UNITS = 2048u;
static const uint BOKUSHO_PAGE_PATCH_COVERAGE_UNITS = 1024u;
static const uint BOKUSHO_PAGE_PATCH_TRANSFER_ENCODED_GAIN = 288u;
static const uint BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM = 8u;
cbuffer BokushoBrushConstants : register(b4)
{
float4 brushShape; // sampleCount, tuftCount, physicsHz, strokeCount
float4 brushMaterial; // radius, pressure, inkLoad, wetness
float4 brushDynamics; // splay, bend, friction, reserved
float4 brushProfile; // radiusScale, pressureScale, normalScale, tangentScale
float4 strokeP0;
float4 strokeP1;
float4 strokeP2;
float4 strokeP3;
float4 pageView; // center.xy, radius, pageSize
};
struct BokushoBrushStroke
{
float4 profile;
float4 dynamics;
float4 pose;
float4 p0;
float4 p1;
float4 p2;
float4 p3;
};
RWStructuredBuffer<float> BokushoTraceField : register(u20);
RWStructuredBuffer<float> BokushoCanvasField : register(u21);
RWStructuredBuffer<float4> BokushoTipField : register(u22);
RWStructuredBuffer<uint> BokushoPageDensityField : register(u23);
StructuredBuffer<BokushoBrushStroke> BokushoBrushStrokes : register(t78);
StructuredBuffer<float4> BokushoSourcePoints : register(t81);
float QuantizeScalar(float value, float quantum)
{
return quantum > 0.0 ? floor((value / quantum) + 0.5) * quantum : value;
}
float2 QuantizeFloat2(float2 value, float quantum)
{
return float2(QuantizeScalar(value.x, quantum), QuantizeScalar(value.y, quantum));
}
float QuantizePositive(float value, float quantum)
{
return QuantizeScalar(max(value, 0.0), quantum);
}
uint QuantizePositiveUnits(float value, uint units)
{
return (uint)floor(saturate(value) * (float)units + 0.5);
}
float2 PacketStrokePoint(BokushoBrushStroke stroke, float t)
{
return cultmath_catmullrom(stroke.p0.xy, stroke.p1.xy, stroke.p2.xy, stroke.p3.xy, t);
}
bool TrySampleSourcePoint(BokushoBrushStroke stroke, float sourceT, out float2 sampledPoint)
{
sampledPoint = float2(0.0, 0.0);
uint sourcePointCount = (uint)round(max(brushDynamics.w, 0.0));
uint start = (uint)round(max(stroke.p1.z, 0.0));
uint count = (uint)round(max(stroke.p1.w, 0.0));
if (count < 2u || sourcePointCount == 0u || start >= sourcePointCount)
{
return false;
}
count = min(count, sourcePointCount - start);
if (count < 2u)
{
return false;
}
float target = saturate(sourceT);
float4 first = BokushoSourcePoints[start];
if (target <= first.z)
{
sampledPoint = first.xy;
return true;
}
uint lastIndex = start + count - 1u;
float4 last = BokushoSourcePoints[lastIndex];
if (target >= last.z)
{
sampledPoint = last.xy;
return true;
}
uint low = start;
uint high = lastIndex;
[loop]
while (low + 1u < high)
{
uint mid = (low + high) / 2u;
if (BokushoSourcePoints[mid].z <= target)
{
low = mid;
}
else
{
high = mid;
}
}
float4 a = BokushoSourcePoints[low];
float4 b = BokushoSourcePoints[high];
float span = max(b.z - a.z, 0.000001);
float local = saturate((target - a.z) / span);
sampledPoint = lerp(a.xy, b.xy, local);
return true;
}
float2 StrokePoint(BokushoBrushStroke stroke, float t)
{
float2 sampledPoint;
float sourceT = saturate(lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
if (TrySampleSourcePoint(stroke, sourceT, sampledPoint))
{
return QuantizeFloat2(sampledPoint, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
}
return PacketStrokePoint(stroke, t);
}
float2 StrokeTangent(BokushoBrushStroke stroke, float t)
{
float dt = 1.0 / max(brushShape.x - 1.0, 1.0);
float2 before = StrokePoint(stroke, saturate(t - dt));
float2 after = StrokePoint(stroke, saturate(t + dt));
return cultmath_normalize(after - before);
}
float2 StrokeTangent(BokushoBrushStroke stroke, float t, uint stepCount)
{
float dt = 1.0 / max((float)stepCount - 1.0, 1.0);
float2 before = StrokePoint(stroke, saturate(t - dt));
float2 after = StrokePoint(stroke, saturate(t + dt));
return cultmath_normalize(after - before);
}
bool SameSourceBoundary(BokushoBrushStroke previous, BokushoBrushStroke next)
{
return previous.p3.w >= 0.0 &&
abs(previous.p3.w - next.p3.w) <= 0.5 &&
abs(saturate(previous.p0.w) - saturate(next.p0.z)) <= 0.001;
}
float2 StrokeTangent(BokushoBrushStroke stroke, uint strokeIndex, float t, uint stepCount)
{
float dt = 1.0 / max((float)stepCount - 1.0, 1.0);
uint sourcePointCount = (uint)round(max(brushDynamics.w, 0.0));
uint sourceRangeCount = (uint)round(max(stroke.p1.w, 0.0));
if (sourcePointCount > 0u && sourceRangeCount >= 2u)
{
float sourceStart = saturate(stroke.p0.z);
float sourceEnd = saturate(max(stroke.p0.z, stroke.p0.w));
float sourceSpan = max(sourceEnd - sourceStart, 1.0 / max((float)(stepCount - 1u), 1.0));
float sourceT = saturate(lerp(sourceStart, sourceEnd, t));
float sourceDt = sourceSpan * dt;
float2 sourceBefore;
float2 sourceAfter;
if (TrySampleSourcePoint(stroke, saturate(sourceT - sourceDt), sourceBefore) &&
TrySampleSourcePoint(stroke, saturate(sourceT + sourceDt), sourceAfter))
{
sourceBefore = QuantizeFloat2(sourceBefore, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
sourceAfter = QuantizeFloat2(sourceAfter, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
return cultmath_normalize(sourceAfter - sourceBefore);
}
}
float beforeT = t - dt;
float afterT = t + dt;
float2 before = StrokePoint(stroke, saturate(beforeT));
float2 after = StrokePoint(stroke, saturate(afterT));
uint strokeCount = max((uint)round(brushShape.w), 1u);
if (beforeT < 0.0 && strokeIndex > 0u)
{
BokushoBrushStroke previous = BokushoBrushStrokes[strokeIndex - 1u];
if (SameSourceBoundary(previous, stroke))
{
before = StrokePoint(previous, saturate(1.0 + beforeT));
}
}
if (afterT > 1.0 && strokeIndex + 1u < strokeCount)
{
BokushoBrushStroke next = BokushoBrushStrokes[strokeIndex + 1u];
if (SameSourceBoundary(stroke, next))
{
after = StrokePoint(next, saturate(afterT - 1.0));
}
}
before = QuantizeFloat2(before, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
after = QuantizeFloat2(after, BOKUSHO_BRISTLE_TANGENT_WORLD_QUANTUM);
return cultmath_normalize(after - before);
}
float StrokeTaper(float t)
{
float entry = cultmath_smoothstep(0.0, 0.10, t);
float exit = 1.0 - cultmath_smoothstep(0.86, 1.0, t);
return 0.04 + entry * exit * 0.96;
}
float StrokeProgress(BokushoBrushStroke stroke, float t)
{
return saturate(lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
}
float StrokeTaper(BokushoBrushStroke stroke, float t)
{
float strokeT = StrokeProgress(stroke, t);
float entryTaper = clamp(stroke.dynamics.x, 0.01, 0.50);
float exitTaper = clamp(stroke.dynamics.y, 0.01, 0.50);
float entry = cultmath_smoothstep(0.0, entryTaper, strokeT);
float exit = 1.0 - cultmath_smoothstep(1.0 - exitTaper, 1.0, strokeT);
float x = saturate(entry * exit);
float contact = x * (0.76 + x * 0.24);
return 0.018 + contact * 0.982;
}
uint StrokeNoiseStep(float strokeT)
{
return (uint)floor(saturate(strokeT) * 4096.0 + 0.5);
}
float LaneCohesion(float edge, float split, float wetness, float pressure)
{
return saturate(0.50 + (1.0 - edge) * 0.38 + wetness * 0.22 + pressure * 0.12 - split * 0.18);
}
float StrokeSegmentSpan(BokushoBrushStroke stroke, uint sampleCount)
{
float start = saturate(stroke.p0.z);
float endValue = saturate(max(stroke.p0.z, stroke.p0.w));
return max(endValue - start, 1.0 / max((float)(sampleCount - 1u), 1.0));
}
float CanvasPigment(float value)
{
return 0.96 * (1.0 - exp(-max(value, 0.0) * 0.82));
}
float4 StrokeRhythm(BokushoBrushStroke stroke, float strokeT)
{
float entry = cultmath_smoothstep(0.0, 0.18, strokeT);
float release = cultmath_smoothstep(0.74, 1.0, strokeT);
float belly = entry * (1.0 - cultmath_smoothstep(0.62, 0.94, strokeT));
float pose = saturate(abs(stroke.pose.x) * 0.34 + abs(stroke.pose.y) * 0.26 + (stroke.pose.w - 1.0) * 0.18);
return float4(
QuantizeScalar(0.90 + belly * (0.15 + pose * 0.04) + entry * 0.04 - release * (0.12 + pose * 0.03), BOKUSHO_STROKE_RHYTHM_QUANTUM),
QuantizeScalar(0.94 + belly * (0.09 + pose * 0.03) - release * 0.08, BOKUSHO_STROKE_RHYTHM_QUANTUM),
QuantizeScalar(0.96 + belly * 0.08 + pose * 0.02 - release * 0.04, BOKUSHO_STROKE_RHYTHM_QUANTUM),
QuantizeScalar(0.94 + belly * (0.12 + pose * 0.04) - release * 0.10, BOKUSHO_STROKE_RHYTHM_QUANTUM));
}
uint EncodePageDensityContribution(float contribution)
{
float c = saturate(contribution);
float c2 = c * c;
float c3 = c2 * c;
float c4 = c2 * c2;
float density = c + (c2 * 0.5) + (c3 * (1.0 / 3.0)) + (c4 * 0.25);
uint encoded = (uint)floor(min(density * BOKUSHO_PAGE_DENSITY_SCALE, BOKUSHO_PAGE_DENSITY_SCALE) + 0.5);
return BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM > 1u
? ((encoded + (BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM / 2u)) / BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM) * BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM
: encoded;
}
uint EncodePageDensityContribution(uint inkUnits, uint coverageUnits)
{
uint product = inkUnits * coverageUnits;
uint denominator = BOKUSHO_PAGE_PATCH_INK_UNITS * BOKUSHO_PAGE_PATCH_COVERAGE_UNITS;
uint encoded = (product * BOKUSHO_PAGE_PATCH_TRANSFER_ENCODED_GAIN + denominator / 2u) / denominator;
return BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM > 1u
? ((encoded + (BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM / 2u)) / BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM) * BOKUSHO_PAGE_DENSITY_CONTRIBUTION_QUANTUM
: encoded;
}
float PageToothHash(uint x, uint y)
{
uint value = (x + 1u) * 0x9E3779B9u ^ (y + 1u) * 0x85EBCA6Bu ^ 0xC2B2AE35u;
value ^= value >> 16;
value *= 0x7FEB352Du;
value ^= value >> 15;
value *= 0x846CA68Bu;
value ^= value >> 16;
return (float)(value & 0x00FFFFFFu) / 16777215.0;
}
void DepositSweptPatch(float2 previousTip, float2 tip, float previousNormalRadius, float previousTangentRadius, float normalRadius, float tangentRadius, float pigment, float paperTooth)
{
uint inkUnits = QuantizePositiveUnits(pigment, BOKUSHO_PAGE_PATCH_INK_UNITS);
if (inkUnits == 0u)
{
return;
}
previousTip = QuantizeFloat2(previousTip, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
tip = QuantizeFloat2(tip, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
previousNormalRadius = QuantizePositive(previousNormalRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
previousTangentRadius = QuantizePositive(previousTangentRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
normalRadius = QuantizePositive(normalRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
tangentRadius = QuantizePositive(tangentRadius, BOKUSHO_PAGE_PATCH_WORLD_QUANTUM);
float2 sweep = tip - previousTip;
float sweepLength = length(sweep);
float2 tangent = sweepLength > 0.000001 ? sweep / sweepLength : float2(1.0, 0.0);
float2 normal = float2(-tangent.y, tangent.x);
float previousPatchTangentRadius = max(previousTangentRadius * BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
float previousPatchNormalRadius = max(previousNormalRadius * BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
float currentPatchTangentRadius = max(tangentRadius * BOKUSHO_PAGE_PATCH_TANGENT_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
float currentPatchNormalRadius = max(normalRadius * BOKUSHO_PAGE_PATCH_NORMAL_RADIUS_SCALE, BOKUSHO_PAGE_PATCH_MINIMUM_RADIUS);
float patchTangentRadius = max(previousPatchTangentRadius, currentPatchTangentRadius);
float patchNormalRadius = max(previousPatchNormalRadius, currentPatchNormalRadius);
float2 center = (previousTip + tip) * 0.5;
float halfSweepLength = sweepLength * 0.5;
float2 extents = float2(
abs(tangent.x) * (halfSweepLength + patchTangentRadius) + abs(normal.x) * patchNormalRadius,
abs(tangent.y) * (halfSweepLength + patchTangentRadius) + abs(normal.y) * patchNormalRadius);
float pageSize = max(pageView.w, 1.0);
float2 minUv = (center - extents - pageView.xy) / max(pageView.z, 0.001) * 0.5 + 0.5;
float2 maxUv = (center + extents - pageView.xy) / max(pageView.z, 0.001) * 0.5 + 0.5;
int minX = clamp((int)floor(min(minUv.x, maxUv.x) * (pageSize - 1.0)), 0, (int)pageSize - 1);
int maxX = clamp((int)ceil(max(minUv.x, maxUv.x) * (pageSize - 1.0)), 0, (int)pageSize - 1);
int minY = clamp((int)floor(min(minUv.y, maxUv.y) * (pageSize - 1.0)), 0, (int)pageSize - 1);
int maxY = clamp((int)ceil(max(minUv.y, maxUv.y) * (pageSize - 1.0)), 0, (int)pageSize - 1);
[loop]
for (int y = minY; y <= maxY; y += 1)
{
float uvY = pageSize <= 1.0 ? 0.0 : (float)y / (pageSize - 1.0);
[loop]
for (int x = minX; x <= maxX; x += 1)
{
float uvX = pageSize <= 1.0 ? 0.0 : (float)x / (pageSize - 1.0);
float2 world = pageView.xy + (float2(uvX, uvY) * 2.0 - 1.0) * pageView.z;
float2 local = world - center;
float axialExcess = max(abs(dot(local, tangent)) - halfSweepLength, 0.0);
float tangentDistance = axialExcess / patchTangentRadius;
float normalDistance = dot(local, normal) / patchNormalRadius;
float ellipse = sqrt(tangentDistance * tangentDistance + normalDistance * normalDistance);
float coverage = cultmath_smoothstep(1.0, 0.0, ellipse);
float tooth = PageToothHash((uint)x, (uint)y);
float interiorTooth = (1.0 - cultmath_smoothstep(0.0, 0.34, ellipse)) * saturate(paperTooth);
coverage *= 1.0 - interiorTooth * (0.03 + tooth * 0.10);
float edgeTooth = cultmath_smoothstep(0.38, 1.0, ellipse) * saturate(paperTooth);
if (edgeTooth > 0.0)
{
coverage *= 1.0 - edgeTooth * (0.18 + tooth * 0.34);
}
uint coverageUnits = QuantizePositiveUnits(coverage, BOKUSHO_PAGE_PATCH_COVERAGE_UNITS);
if (coverageUnits == 0u)
{
continue;
}
InterlockedAdd(BokushoPageDensityField[(uint)(y * (int)pageSize + x)], EncodePageDensityContribution(inkUnits, coverageUnits));
}
}
}
float LaneHash(uint strokeIndex, uint tuft, uint salt)
{
uint value = (strokeIndex + 1u) * 0x9E3779B9u ^ (tuft + 1u) * 0x85EBCA6Bu ^ salt;
value ^= value >> 16;
value *= 0x7FEB352Du;
value ^= value >> 15;
value *= 0x846CA68Bu;
value ^= value >> 16;
return (float)(value & 0x00FFFFFFu) / 16777215.0;
}
uint StrokeChainStart(uint strokeIndex)
{
uint chainStart = strokeIndex;
float expectedStart = saturate(BokushoBrushStrokes[strokeIndex].p0.z);
float sourceStrokeId = BokushoBrushStrokes[strokeIndex].p3.w;
[loop]
while (chainStart > 0u && expectedStart > 0.0001)
{
if (sourceStrokeId >= 0.0 && abs(BokushoBrushStrokes[chainStart - 1u].p3.w - sourceStrokeId) > 0.5)
{
break;
}
float previousEnd = saturate(BokushoBrushStrokes[chainStart - 1u].p0.w);
if (abs(previousEnd - expectedStart) > 0.001)
{
break;
}
chainStart = chainStart - 1u;
expectedStart = saturate(BokushoBrushStrokes[chainStart].p0.z);
}
return chainStart;
}
void SimulateBokushoTuftSegment(
BokushoBrushStroke stroke,
uint outputStrokeIndex,
uint laneKey,
uint tuft,
uint sampleCount,
uint tuftCount,
float restOffset,
float edge,
inout float offset,
inout float2 tip,
inout float stateLoad,
inout float stateWet,
bool writeOutput)
{
float pressure = saturate(brushMaterial.y * stroke.profile.y * 0.5) * 2.0;
float splay = saturate(brushDynamics.x / 2.0);
float bend = saturate(brushDynamics.y / 2.4);
float friction = saturate(brushDynamics.z);
float radius = max(brushMaterial.x * stroke.profile.x, 0.0001);
float normalRadius = max(radius * stroke.profile.z, 0.0001);
float tangentRadius = max(radius * stroke.profile.w, 0.0001);
float segmentSpan = StrokeSegmentSpan(stroke, sampleCount);
float integrationSpan = segmentSpan / max((float)sampleCount - 1.0, 1.0);
float segmentVelocityScale = 1.0 / segmentSpan;
float split = saturate((0.28 - LaneHash(laneKey, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * stroke.dynamics.w;
[loop]
for (uint sample = 0u; sample < sampleCount; sample += 1u)
{
float t = sampleCount <= 1u ? 0.0 : (float)sample / (float)(sampleCount - 1u);
float2 center = StrokePoint(stroke, t);
float2 tangent = StrokeTangent(stroke, outputStrokeIndex, t, sampleCount);
float2 normal = float2(-tangent.y, tangent.x);
float strokeT = StrokeProgress(stroke, t);
float taper = StrokeTaper(stroke, t);
float4 rhythm = StrokeRhythm(stroke, strokeT);
float localPressure = pressure * taper * rhythm.x;
float laneCore = cultmath_smoothstep(0.0, 0.78, 1.0 - edge);
float cohesion = saturate(0.28 + stateWet * 0.44 + laneCore * 0.24 + localPressure * 0.10 - split * 0.18);
float localNormalRadius = max(normalRadius * rhythm.y * (0.18 + taper * 0.82) * (0.72 + splay * 0.34 + localPressure * 0.16 - cohesion * 0.08), 0.0001);
float localTangentRadius = max(tangentRadius * rhythm.z * (0.24 + taper * 0.76) * (0.86 + bend * 0.18), 0.0001);
float targetOffset = (restOffset + stroke.pose.y * 0.12) * localNormalRadius * (0.58 + splay * 0.34 + localPressure * 0.18 - cohesion * 0.20);
float recovery = saturate(0.05 + stroke.pose.w * 0.08 + stateWet * 0.08 + localPressure * 0.10);
offset = cultmath_lerp(offset, targetOffset, recovery);
float poseLead = stroke.pose.x * 0.36 + stroke.pose.y * 0.16;
float lag = localTangentRadius * (0.10 + bend * 0.32 + friction * localPressure * 0.16 + edge * 0.06);
float2 dragVector = cultmath_normalize(tangent + normal * poseLead);
float2 desiredTip = center + normal * offset - dragVector * lag;
float2 slip = desiredTip - tip;
float velocity = length(slip) * brushShape.z * segmentVelocityScale / max(radius, 0.001);
float contact = saturate(localPressure * stateLoad * (0.24 + stateWet * 0.62 + laneCore * 0.18));
float drag = contact * friction * (0.28 + stateWet * 0.34);
float2 previousTip = tip;
tip = tip + slip * (0.18 + recovery * 0.82) * (1.0 - drag * 0.52);
float sweptDistance = length(tip - previousTip);
float sweptPatch = saturate(sweptDistance / max(localTangentRadius, 0.001) * 0.42);
float tension = saturate(velocity * 0.014 + abs(targetOffset - offset) / max(localNormalRadius, 0.001) * 0.22 + edge * 0.12);
float separation = saturate(split * 0.34 + tension * 0.46 + edge * 0.20 - cohesion * 0.24);
float adhesion = saturate(stateWet * (0.36 + cohesion * 0.40) + localPressure * 0.10 - separation * 0.22);
float dryMemory = saturate((1.0 - stateWet) * 0.62 + separation * 0.32 + velocity * 0.004);
float fiberNoise = LaneHash(laneKey + StrokeNoiseStep(strokeT) * 13u, tuft, 101u);
float continuity = 1.0 - cultmath_smoothstep(0.18 + dryMemory * 0.28, 0.96, fiberNoise) * dryMemory * (0.38 + edge * 0.22);
float inkFilm = saturate(stateLoad * (0.28 + stateWet * 0.34));
float bristleTooth = (0.62 + continuity * (0.30 + inkFilm * 0.08)) * (0.82 + inkFilm * 0.24 - edge * 0.06);
float contactTransfer = contact * stateLoad * (0.16 + stateWet * 0.92) * (0.30 + drag * 0.64 + localPressure * 0.22 + sweptPatch * 0.18) * (0.68 + laneCore * 0.50 - separation * 0.14) * bristleTooth;
float airborneRelease = (1.0 - contact) * stateLoad * stateWet * saturate(velocity * 0.010 - adhesion * 0.16) * (0.20 + separation * 0.42 + edge * 0.18);
float consumedPigment = contactTransfer + airborneRelease;
stateLoad = max(0.0, stateLoad - consumedPigment * integrationSpan * (0.014 + localPressure * 0.009 + dryMemory * 0.004));
stateWet = max(0.0, stateWet - consumedPigment * integrationSpan * (0.006 + dryMemory * 0.003));
if (writeOutput)
{
uint index = ((outputStrokeIndex * tuftCount) + tuft) * sampleCount + sample;
float pigment = (contactTransfer * (0.95 + localPressure * 0.34 + sweptPatch * 0.24) + airborneRelease * (1.6 + velocity * 0.002)) * stroke.dynamics.z * rhythm.w;
float localPigment = CanvasPigment(pigment);
float trace = saturate(contact * 0.70 + airborneRelease * 2.0 + stateLoad * 0.18);
BokushoTraceField[index] = trace;
BokushoCanvasField[index] = localPigment;
BokushoTipField[index] = float4(
tip,
localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10),
localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36);
}
}
}
void SimulateBokushoTuftSegmentToPage(
BokushoBrushStroke stroke,
uint strokeIndex,
uint laneKey,
uint tuft,
uint sampleCount,
float restOffset,
float edge,
inout float offset,
inout float2 tip,
inout float stateLoad,
inout float stateWet,
inout float previousPatchNormalRadius,
inout float previousPatchTangentRadius,
bool writeOutput,
bool writeInitialStep)
{
float pressure = saturate(brushMaterial.y * stroke.profile.y * 0.5) * 2.0;
float splay = saturate(brushDynamics.x / 2.0);
float bend = saturate(brushDynamics.y / 2.4);
float friction = saturate(brushDynamics.z);
float radius = max(brushMaterial.x * stroke.profile.x, 0.0001);
float normalRadius = max(radius * stroke.profile.z, 0.0001);
float tangentRadius = max(radius * stroke.profile.w, 0.0001);
float segmentSpan = StrokeSegmentSpan(stroke, sampleCount);
uint stepCount = min(max((uint)ceil(segmentSpan * brushShape.z), 2u), 4096u);
float integrationSpan = segmentSpan / max((float)stepCount - 1.0, 1.0);
float segmentVelocityScale = 1.0 / segmentSpan;
float split = saturate((0.28 - LaneHash(laneKey, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * stroke.dynamics.w;
[loop]
for (uint step = 0u; step < stepCount; step += 1u)
{
float t = stepCount <= 1u ? 0.0 : (float)step / (float)(stepCount - 1u);
float2 center = StrokePoint(stroke, t);
float2 tangent = StrokeTangent(stroke, strokeIndex, t, stepCount);
float2 normal = float2(-tangent.y, tangent.x);
float strokeT = StrokeProgress(stroke, t);
float taper = StrokeTaper(stroke, t);
float4 rhythm = StrokeRhythm(stroke, strokeT);
float localPressure = pressure * taper * rhythm.x;
float laneCore = cultmath_smoothstep(0.0, 0.78, 1.0 - edge);
float cohesion = saturate(0.28 + stateWet * 0.44 + laneCore * 0.24 + localPressure * 0.10 - split * 0.18);
float localNormalRadius = max(normalRadius * rhythm.y * (0.18 + taper * 0.82) * (0.72 + splay * 0.34 + localPressure * 0.16 - cohesion * 0.08), 0.0001);
float localTangentRadius = max(tangentRadius * rhythm.z * (0.24 + taper * 0.76) * (0.86 + bend * 0.18), 0.0001);
float targetOffset = (restOffset + stroke.pose.y * 0.12) * localNormalRadius * (0.58 + splay * 0.34 + localPressure * 0.18 - cohesion * 0.20);
float recovery = saturate(0.05 + stroke.pose.w * 0.08 + stateWet * 0.08 + localPressure * 0.10);
offset = cultmath_lerp(offset, targetOffset, recovery);
float poseLead = stroke.pose.x * 0.36 + stroke.pose.y * 0.16;
float lag = localTangentRadius * (0.10 + bend * 0.32 + friction * localPressure * 0.16 + edge * 0.06);
float2 dragVector = cultmath_normalize(tangent + normal * poseLead);
float2 desiredTip = center + normal * offset - dragVector * lag;
float2 slip = desiredTip - tip;
float velocity = length(slip) * brushShape.z * segmentVelocityScale / max(radius, 0.001);
float contact = saturate(localPressure * stateLoad * (0.24 + stateWet * 0.62 + laneCore * 0.18));
float drag = contact * friction * (0.28 + stateWet * 0.34);
float2 previousTip = tip;
tip = tip + slip * (0.18 + recovery * 0.82) * (1.0 - drag * 0.52);
float sweptDistance = length(tip - previousTip);
float sweptPatch = saturate(sweptDistance / max(localTangentRadius, 0.001) * 0.42);
float tension = saturate(velocity * 0.014 + abs(targetOffset - offset) / max(localNormalRadius, 0.001) * 0.22 + edge * 0.12);
float separation = saturate(split * 0.34 + tension * 0.46 + edge * 0.20 - cohesion * 0.24);
float adhesion = saturate(stateWet * (0.36 + cohesion * 0.40) + localPressure * 0.10 - separation * 0.22);
float dryMemory = saturate((1.0 - stateWet) * 0.62 + separation * 0.32 + velocity * 0.004);
float fiberNoise = LaneHash(laneKey + StrokeNoiseStep(strokeT) * 13u, tuft, 101u);
float continuity = 1.0 - cultmath_smoothstep(0.18 + dryMemory * 0.28, 0.96, fiberNoise) * dryMemory * (0.38 + edge * 0.22);
float inkFilm = saturate(stateLoad * (0.28 + stateWet * 0.34));
float bristleTooth = (0.62 + continuity * (0.30 + inkFilm * 0.08)) * (0.82 + inkFilm * 0.24 - edge * 0.06);
float paperTooth = saturate(dryMemory * 0.42 + separation * 0.22 + edge * 0.10 + (1.0 - inkFilm) * 0.18);
float contactTransfer = contact * stateLoad * (0.16 + stateWet * 0.92) * (0.30 + drag * 0.64 + localPressure * 0.22 + sweptPatch * 0.18) * (0.68 + laneCore * 0.50 - separation * 0.14) * bristleTooth;
float airborneRelease = (1.0 - contact) * stateLoad * stateWet * saturate(velocity * 0.010 - adhesion * 0.16) * (0.20 + separation * 0.42 + edge * 0.18);
float consumedPigment = contactTransfer + airborneRelease;
stateLoad = max(0.0, stateLoad - consumedPigment * integrationSpan * (0.014 + localPressure * 0.009 + dryMemory * 0.004));
stateWet = max(0.0, stateWet - consumedPigment * integrationSpan * (0.006 + dryMemory * 0.003));
if (writeOutput && (writeInitialStep || step > 0u))
{
float pigment = (contactTransfer * (0.95 + localPressure * 0.34 + sweptPatch * 0.24) + airborneRelease * (1.6 + velocity * 0.002)) * stroke.dynamics.z * rhythm.w;
float patchNormalRadius = localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10);
float patchTangentRadius = localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36;
float sweepStartNormalRadius = previousPatchNormalRadius > 0.0 ? previousPatchNormalRadius : patchNormalRadius;
float sweepStartTangentRadius = previousPatchTangentRadius > 0.0 ? previousPatchTangentRadius : patchTangentRadius;
DepositSweptPatch(previousTip, tip, sweepStartNormalRadius, sweepStartTangentRadius, patchNormalRadius, patchTangentRadius, pigment, paperTooth);
}
previousPatchNormalRadius = localNormalRadius * (0.74 + inkFilm * 0.10 + laneCore * 0.20 + localPressure * 0.14 + sweptPatch * 0.08 - separation * 0.10);
previousPatchTangentRadius = localTangentRadius * (0.86 + inkFilm * 0.06 + bend * 0.22 + drag * 0.16) + sweptDistance * 0.36;
}
}
[numthreads(128, 1, 1)]
void D3D12BokushoBrushCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint sampleCount = max((uint)round(brushShape.x), 2u);
uint tuftCount = max((uint)round(brushShape.y), 1u);
uint strokeCount = max((uint)round(brushShape.w), 1u);
uint globalIndex = dispatchThreadId.x;
uint strokeIndex = globalIndex / tuftCount;
uint tuft = globalIndex - strokeIndex * tuftCount;
if (strokeIndex >= strokeCount || tuft >= tuftCount)
{
return;
}
BokushoBrushStroke stroke = BokushoBrushStrokes[strokeIndex];
uint chainStart = StrokeChainStart(strokeIndex);
BokushoBrushStroke seedStroke = BokushoBrushStrokes[chainStart];
float laneT = tuftCount <= 1u ? 0.5 : (float)tuft / (float)(tuftCount - 1u);
float restOffset = laneT * 2.0 - 1.0;
float edge = abs(restOffset);
float laneHash = LaneHash(chainStart, tuft, 17u);
float laneLoad = 0.76 + laneHash * 0.34;
float seedPressure = saturate(brushMaterial.y * seedStroke.profile.y * 0.5) * 2.0;
float wetness = saturate(brushMaterial.w / 1.6);
float load = clamp(brushMaterial.z / 1.05, 0.0, 2.7);
float splay = saturate(brushDynamics.x / 2.0);
float seedRadius = max(brushMaterial.x * seedStroke.profile.x, 0.0001);
float seedNormalRadius = max(seedRadius * seedStroke.profile.z, 0.0001);
float seedSplit = saturate((0.28 - LaneHash(chainStart, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * seedStroke.dynamics.w;
float initialCohesion = LaneCohesion(edge, seedSplit, wetness, seedPressure);
float2 tip = StrokePoint(seedStroke, 0.0);
float poseBias = seedStroke.pose.x * 0.18 + seedStroke.pose.y * 0.08;
float offset = (restOffset + poseBias * (1.0 - edge * 0.35)) * seedNormalRadius * (0.42 + splay * 0.38) + (laneHash - 0.5) * seedNormalRadius * 0.05;
float stateLoad = load * (0.86 + initialCohesion * 0.48) * (1.0 - edge * 0.08) * laneLoad * (1.0 - seedSplit * 0.18);
float stateWet = wetness * (0.74 + initialCohesion * 0.24 - seedSplit * 0.08);
[loop]
for (uint replayStrokeIndex = chainStart; replayStrokeIndex < strokeIndex; replayStrokeIndex += 1u)
{
SimulateBokushoTuftSegment(BokushoBrushStrokes[replayStrokeIndex], replayStrokeIndex, chainStart, tuft, sampleCount, tuftCount, restOffset, edge, offset, tip, stateLoad, stateWet, false);
}
SimulateBokushoTuftSegment(stroke, strokeIndex, chainStart, tuft, sampleCount, tuftCount, restOffset, edge, offset, tip, stateLoad, stateWet, true);
}
[numthreads(128, 1, 1)]
void D3D12BokushoPageClearCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint pageValueCount = (uint)(max(pageView.w, 1.0) * max(pageView.w, 1.0));
uint index = dispatchThreadId.x;
if (index < pageValueCount)
{
BokushoPageDensityField[index] = 0u;
}
}
[numthreads(128, 1, 1)]
void D3D12BokushoPageDepositCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint sampleCount = max((uint)round(brushShape.x), 2u);
uint tuftCount = max((uint)round(brushShape.y), 1u);
uint strokeCount = max((uint)round(brushShape.w), 1u);
uint globalIndex = dispatchThreadId.x;
uint strokeIndex = globalIndex / tuftCount;
uint tuft = globalIndex - strokeIndex * tuftCount;
if (strokeIndex >= strokeCount || tuft >= tuftCount)
{
return;
}
BokushoBrushStroke stroke = BokushoBrushStrokes[strokeIndex];
uint chainStart = StrokeChainStart(strokeIndex);
BokushoBrushStroke seedStroke = BokushoBrushStrokes[chainStart];
float laneT = tuftCount <= 1u ? 0.5 : (float)tuft / (float)(tuftCount - 1u);
float restOffset = laneT * 2.0 - 1.0;
float edge = abs(restOffset);
float laneHash = LaneHash(chainStart, tuft, 17u);
float laneLoad = 0.76 + laneHash * 0.34;
float seedPressure = saturate(brushMaterial.y * seedStroke.profile.y * 0.5) * 2.0;
float wetness = saturate(brushMaterial.w / 1.6);
float load = clamp(brushMaterial.z / 1.05, 0.0, 2.7);
float splay = saturate(brushDynamics.x / 2.0);
float seedRadius = max(brushMaterial.x * seedStroke.profile.x, 0.0001);
float seedNormalRadius = max(seedRadius * seedStroke.profile.z, 0.0001);
float seedSplit = saturate((0.28 - LaneHash(chainStart, tuft, 53u)) * 3.0) * saturate((edge - 0.20) * 1.5) * seedStroke.dynamics.w;
float initialCohesion = LaneCohesion(edge, seedSplit, wetness, seedPressure);
float2 tip = StrokePoint(seedStroke, 0.0);
float poseBias = seedStroke.pose.x * 0.18 + seedStroke.pose.y * 0.08;
float offset = (restOffset + poseBias * (1.0 - edge * 0.35)) * seedNormalRadius * (0.42 + splay * 0.38) + (laneHash - 0.5) * seedNormalRadius * 0.05;
float stateLoad = load * (0.86 + initialCohesion * 0.48) * (1.0 - edge * 0.08) * laneLoad * (1.0 - seedSplit * 0.18);
float stateWet = wetness * (0.74 + initialCohesion * 0.24 - seedSplit * 0.08);
float previousPatchNormalRadius = 0.0;
float previousPatchTangentRadius = 0.0;
[loop]
for (uint replayStrokeIndex = chainStart; replayStrokeIndex < strokeIndex; replayStrokeIndex += 1u)
{
SimulateBokushoTuftSegmentToPage(BokushoBrushStrokes[replayStrokeIndex], replayStrokeIndex, chainStart, tuft, sampleCount, restOffset, edge, offset, tip, stateLoad, stateWet, previousPatchNormalRadius, previousPatchTangentRadius, false, true);
}
SimulateBokushoTuftSegmentToPage(stroke, strokeIndex, chainStart, tuft, sampleCount, restOffset, edge, offset, tip, stateLoad, stateWet, previousPatchNormalRadius, previousPatchTangentRadius, true, strokeIndex == chainStart);
}

View file

@ -87,6 +87,7 @@ struct SceneOut
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};
@ -237,6 +238,7 @@ SceneOut ResolveFractalSplat(FractalSplatVertexOut input, bool renderTransparent
output.metadata = float4(FIELD_ID_FRACTAL_SPLAT_BASE, normal);
output.control = float4(opacity, reservoirConfidence, transparentField ? splat.materialConfidence.z / 10.0 : saturate((sdfResident ? sdf.centerRadius.w : splat.centerRadius.w) * 40.0), 0.0);
output.reservoirGuide = float4(reservoirConfidence, reservoirSampleAge, domainValidity, 0.0);
output.overdraw = 1.0;
output.depth = saturate(input.travel / max(farDistance, 0.0001));
return output;
}

View file

@ -94,6 +94,11 @@ StructuredBuffer<AcousticConstraint> acousticConstraints : register(t36);
StructuredBuffer<GpuFusionPoint> fusionPoints : register(t37);
RWStructuredBuffer<TemporalGaussian> temporalGaussiansOut : register(u0);
static const uint SensorFormatR8Unorm = 3u;
static const uint SensorFormatRg8Unorm = 6u;
static const uint SensorFormatLeapPackedMap = 7u;
static const uint SensorFormatYuy2 = 8u;
float Hash01(uint value)
{
value ^= value >> 16;
@ -128,6 +133,41 @@ float4 LoadSensorTexture(uint slot, int3 pixel)
}
}
float3 YuvToRgb(float y, float u, float v)
{
float cb = u - 0.5;
float cr = v - 0.5;
return saturate(float3(
y + 1.5748 * cr,
y - 0.1873 * cb - 0.4681 * cr,
y + 1.8556 * cb));
}
float4 DecodeSensorSample(uint slot, GpuSensorCamera camera, uint x, uint y)
{
uint format = (uint)round(camera.textureRangeTime.w);
if (format == SensorFormatYuy2)
{
float4 yuyv = LoadSensorTexture(slot, int3((int)(x >> 1), (int)y, 0));
float luma = (x & 1u) == 0u ? yuyv.r : yuyv.b;
return float4(YuvToRgb(luma, yuyv.g, yuyv.a), 1.0);
}
float4 sample = LoadSensorTexture(slot, int3((int)x, (int)y, 0));
if (format == SensorFormatR8Unorm)
{
return float4(sample.rrr, 1.0);
}
if (format == SensorFormatRg8Unorm ||
format == SensorFormatLeapPackedMap)
{
return float4(sample.rg, 0.5 * (sample.r + sample.g), 1.0);
}
return sample;
}
float AcousticSupport(float3 center, out float3 velocityBias)
{
uint count = (uint)gpuFusionInfo.x;
@ -224,9 +264,12 @@ void D3D12GpuSensorFusionCS(uint3 dispatchThreadId : SV_DispatchThreadID)
uint localIndex = textureSampleIndex + textureSlot * 747796405u;
uint x = (localIndex * 73u + (uint)(Hash01(localIndex) * 37.0)) % width;
uint y = (localIndex / width * 41u + (uint)(Hash01(localIndex ^ 0x9e3779b9u) * 29.0)) % height;
float4 rgba = LoadSensorTexture(textureSlot, int3((int)x, (int)y, 0));
uint pairSlot = (textureSlot + 1u) % max(1u, textureCount);
float4 pairRgba = LoadSensorTexture(pairSlot, int3((int)x, (int)y, 0));
GpuSensorCamera pairCamera = sensorCameras[min(cameraCount - 1, pairSlot % cameraCount)];
uint pairWidth = max(1u, (uint)pairCamera.extentsKind.x);
uint pairHeight = max(1u, (uint)pairCamera.extentsKind.y);
float4 rgba = DecodeSensorSample(textureSlot, camera, x, y);
float4 pairRgba = DecodeSensorSample(pairSlot, pairCamera, min(x, pairWidth - 1u), min(y, pairHeight - 1u));
float luma = dot(rgba.rgb, float3(0.2126, 0.7152, 0.0722));
float pairLuma = dot(pairRgba.rgb, float3(0.2126, 0.7152, 0.0722));
float descriptor = frac(luma * 3.17 + rgba.r * 1.91 + rgba.g * 2.37 + Hash01(localIndex ^ 0x27d4eb2du) * 0.07);

View file

@ -33,6 +33,38 @@ cbuffer HeightFieldBrushes : register(b1)
float4 brushDomain[64];
};
cbuffer BokushoPageConstants : register(b5)
{
float4 bokushoShape; // sampleCount, tuftCount, physicsHz, strokeCount
float4 bokushoMaterial; // radius, pressure, inkLoad, wetness
float4 bokushoDynamics; // splay, bend, friction, reserved
float4 bokushoProfile; // radiusScale, pressureScale, normalScale, tangentScale
float4 bokushoStrokeP0;
float4 bokushoStrokeP1;
float4 bokushoStrokeP2;
float4 bokushoStrokeP3;
};
struct BokushoBrushStroke
{
float4 profile;
float4 dynamics;
float4 pose;
float4 p0;
float4 p1;
float4 p2;
float4 p3;
};
StructuredBuffer<float> BokushoCanvasField : register(t77);
StructuredBuffer<BokushoBrushStroke> BokushoBrushStrokes : register(t78);
StructuredBuffer<float4> BokushoTipField : register(t79);
StructuredBuffer<uint> BokushoPageDensityField : register(t80);
#include "CultMath/CultMath.hlsl"
static const float BOKUSHO_PAGE_DENSITY_SCALE = 65535.0;
struct VertexOut
{
float4 position : SV_Position;
@ -64,6 +96,196 @@ float2 viewWorld(float2 uv)
return viewCenter + (uv * 2.0 - 1.0) * viewRadius;
}
float2 bokushoStrokePoint(BokushoBrushStroke stroke, float t)
{
return cultmath_catmullrom(stroke.p0.xy, stroke.p1.xy, stroke.p2.xy, stroke.p3.xy, t);
}
float2 bokushoStrokeTangent(BokushoBrushStroke stroke, float t)
{
float dt = 1.0 / max(bokushoShape.x - 1.0, 1.0);
float2 before = bokushoStrokePoint(stroke, saturate(t - dt));
float2 after = bokushoStrokePoint(stroke, saturate(t + dt));
return cultmath_normalize(after - before);
}
float bokushoStrokeTaper(BokushoBrushStroke stroke, float t)
{
float strokeT = saturate(cultmath_lerp(stroke.p0.z, max(stroke.p0.z, stroke.p0.w), t));
float entryTaper = clamp(stroke.dynamics.x, 0.01, 0.50);
float exitTaper = clamp(stroke.dynamics.y, 0.01, 0.50);
float entry = smoothstep(0.0, entryTaper, strokeT);
float exit = 1.0 - smoothstep(1.0 - exitTaper, 1.0, strokeT);
float contact = pow(entry * exit, 1.12);
return 0.018 + contact * 0.982;
}
float bokushoCanvasSample(uint strokeIndex, float sampleIndex, float tuftIndex)
{
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
float clampedSample = clamp(sampleIndex, 0.0, (float)(sampleCount - 1u));
float clampedTuft = clamp(tuftIndex, 0.0, (float)(tuftCount - 1u));
uint sample0 = min((uint)floor(clampedSample), sampleCount - 1u);
uint sample1 = min(sample0 + 1u, sampleCount - 1u);
uint tuft0 = min((uint)floor(clampedTuft), tuftCount - 1u);
uint tuft1 = min(tuft0 + 1u, tuftCount - 1u);
float sampleBlend = saturate(clampedSample - (float)sample0);
float tuftBlend = saturate(clampedTuft - (float)tuft0);
uint rowOffset = strokeIndex * tuftCount;
float p00 = BokushoCanvasField[(rowOffset + tuft0) * sampleCount + sample0];
float p10 = BokushoCanvasField[(rowOffset + tuft0) * sampleCount + sample1];
float p01 = BokushoCanvasField[(rowOffset + tuft1) * sampleCount + sample0];
float p11 = BokushoCanvasField[(rowOffset + tuft1) * sampleCount + sample1];
float lower = cultmath_lerp(p00, p10, sampleBlend);
float upper = cultmath_lerp(p01, p11, sampleBlend);
return saturate(cultmath_lerp(lower, upper, tuftBlend));
}
float4 bokushoTipSample(uint strokeIndex, uint sampleIndex, uint tuftIndex)
{
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
uint rowOffset = strokeIndex * tuftCount;
return BokushoTipField[(rowOffset + min(tuftIndex, tuftCount - 1u)) * sampleCount + min(sampleIndex, sampleCount - 1u)];
}
float bokushoStrokePageHeight(float2 world, BokushoBrushStroke stroke, uint strokeIndex)
{
float bestDistance = 1.0e20;
float bestT = 0.0;
[unroll]
for (uint scan = 0u; scan < 16u; scan += 1u)
{
float t = (float)scan / 15.0;
float2 center = bokushoStrokePoint(stroke, t);
float distanceSquared = dot(world - center, world - center);
if (distanceSquared < bestDistance)
{
bestDistance = distanceSquared;
bestT = t;
}
}
[unroll]
for (uint refine = 0u; refine < 3u; refine += 1u)
{
float span = 1.0 / (15.0 * pow(2.0, (float)refine));
float leftT = saturate(bestT - span);
float rightT = saturate(bestT + span);
float2 left = bokushoStrokePoint(stroke, leftT);
float2 right = bokushoStrokePoint(stroke, rightT);
float leftDistance = dot(world - left, world - left);
float rightDistance = dot(world - right, world - right);
if (leftDistance < bestDistance)
{
bestDistance = leftDistance;
bestT = leftT;
}
if (rightDistance < bestDistance)
{
bestDistance = rightDistance;
bestT = rightT;
}
}
uint sampleCount = max((uint)round(bokushoShape.x), 2u);
uint tuftCount = max((uint)round(bokushoShape.y), 1u);
float2 center = bokushoStrokePoint(stroke, bestT);
float2 tangent = bokushoStrokeTangent(stroke, bestT);
float2 normal = float2(-tangent.y, tangent.x);
float lateral = dot(world - center, normal);
float taper = bokushoStrokeTaper(stroke, bestT);
float radius = max(bokushoMaterial.x * stroke.profile.x * stroke.profile.z * (0.18 + taper * 0.82), 0.0001);
float splay = saturate(bokushoDynamics.x / 2.0);
float pressure = saturate(bokushoMaterial.y * stroke.profile.y * 0.5) * taper;
float wetSpread = 0.86 + saturate(bokushoMaterial.w / 1.6) * 0.18;
float footprint = radius * wetSpread * (0.72 + splay * 0.44 + pressure * 0.22);
float tuftT = saturate(lateral / max(footprint, 0.001) * 0.5 + 0.5);
float distance = sqrt(bestDistance);
float contact = smoothstep(1.0, 0.0, distance / max(footprint * 0.96, 0.001));
float contactCore = contact * contact * (3.0 - 2.0 * contact);
uint centerSample = min((uint)round(bestT * (float)(sampleCount - 1u)), sampleCount - 1u);
uint centerTuft = min((uint)round(tuftT * (float)max((int)tuftCount - 1, 0)), tuftCount - 1u);
float pigmentPeak = 0.0;
float pigmentFlow = 0.0;
[unroll]
for (int sampleDelta = -3; sampleDelta <= 3; sampleDelta += 1)
{
int rawSampleIndex = (int)centerSample + sampleDelta;
uint sampleStrokeIndex = strokeIndex;
BokushoBrushStroke sampleStroke = stroke;
int resolvedSampleIndex = clamp(rawSampleIndex, 0, (int)sampleCount - 1);
uint sampleIndex = min((uint)max(resolvedSampleIndex, 0), sampleCount - 1u);
float sampleT = (float)sampleIndex / max((float)(sampleCount - 1u), 1.0);
float2 sampleTangent = bokushoStrokeTangent(sampleStroke, sampleT);
float2 sampleNormal = float2(-sampleTangent.y, sampleTangent.x);
[unroll]
for (int tuftDelta = -2; tuftDelta <= 2; tuftDelta += 1)
{
uint tuftIndex = min((uint)max((int)centerTuft + tuftDelta, 0), tuftCount - 1u);
float4 tip = bokushoTipSample(sampleStrokeIndex, sampleIndex, tuftIndex);
uint previousStrokeIndex = sampleStrokeIndex;
int previousSampleIndexValue = max((int)sampleIndex - 1, 0);
uint previousSampleIndex = min((uint)max(previousSampleIndexValue, 0), sampleCount - 1u);
float4 previousTip = bokushoTipSample(previousStrokeIndex, previousSampleIndex, tuftIndex);
float2 sweep = tip.xy - previousTip.xy;
float sweepLengthSquared = dot(sweep, sweep);
float sweepT = sweepLengthSquared <= 0.000001 ? 1.0 : saturate(dot(world - previousTip.xy, sweep) / sweepLengthSquared);
float2 contactPoint = previousTip.xy + sweep * sweepT;
float sweepDistance = length(world - contactPoint);
float sweepContact = smoothstep(1.0, 0.0, sweepDistance / max(max(tip.z, previousTip.z) * 1.04, 0.001));
float2 delta = world - contactPoint;
float normalDistance = dot(delta, sampleNormal) / max(tip.z, 0.001);
float tangentDistance = dot(delta, sampleTangent) / max(tip.w, 0.001);
float ellipse = sqrt(normalDistance * normalDistance + tangentDistance * tangentDistance);
float tipContact = max(smoothstep(1.0, 0.0, ellipse), sweepContact * 0.86);
float longitudinalGate = smoothstep(1.0, 0.0, abs(tangentDistance) * 0.62);
float pigment = BokushoCanvasField[((sampleStrokeIndex * tuftCount) + tuftIndex) * sampleCount + sampleIndex];
float contribution = pigment
* tipContact
* (0.006 + contactCore * 0.994)
* (0.06 + longitudinalGate * 0.94)
* (1.0 - abs((float)sampleDelta) * 0.070)
* (0.82 + sweepContact * 0.24);
pigmentPeak = max(pigmentPeak, contribution);
pigmentFlow += contribution;
}
}
float decisiveInk = saturate((pigmentPeak - 0.012) * 2.25 + pigmentFlow * 0.002);
return decisiveInk * (0.44 + pressure * 0.36 + saturate(bokushoMaterial.z * 0.5) * 0.20);
}
float bokushoPageHeight(float2 world)
{
if (bokushoShape.w > 0.5)
{
float pageSize = 512.0;
float2 uv = (world - viewCenter) / max(viewRadius, 0.001) * 0.5 + 0.5;
int2 pixel = int2(floor(uv * (pageSize - 1.0) + 0.5));
if (all(pixel >= int2(0, 0)) && all(pixel < int2((int)pageSize, (int)pageSize)))
{
uint densityValue = BokushoPageDensityField[(uint)(pixel.y * (int)pageSize + pixel.x)];
float density = (float)densityValue / BOKUSHO_PAGE_DENSITY_SCALE;
return saturate(1.0 - exp(-density));
}
}
uint strokeCount = min(max((uint)round(bokushoShape.w), 0u), 64u);
float height = 0.0;
[loop]
for (uint strokeIndex = 0u; strokeIndex < strokeCount; strokeIndex += 1u)
{
height += bokushoStrokePageHeight(world, BokushoBrushStrokes[strokeIndex], strokeIndex);
}
return saturate(height);
}
float powerPulse(float distanceValue, float radius, float power)
{
float normalized = saturate(distanceValue / max(radius, 0.001));
@ -98,7 +320,7 @@ float D3D12HeightFieldBasePS(VertexOut input) : SV_Target
float2 world = viewWorld(saturate(input.uv));
float slow = sin((world.x * 0.08 + world.y * 0.06) + timeSeconds * 0.27)
* sin((world.x * -0.04 + world.y * 0.07) - timeSeconds * 0.19) * 0.035;
return slow;
return slow + bokushoPageHeight(world) * 5.0;
}
BrushVertexOut D3D12HeightFieldBrushVS(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID)

View file

@ -0,0 +1,12 @@
struct PageInput { float4 direction_radius; float4 sampling; };
struct PageOutput { float4 height_gradient; float4 masks; };
StructuredBuffer<PageInput> page_inputs : register(t0);
RWStructuredBuffer<PageOutput> page_outputs : register(u0);
cbuffer PageConstants : register(b0) { uint page_sample_count; }
[numthreads(64,1,1)]
void D3D12ZyphosTerrainPageCS(uint3 id : SV_DispatchThreadID)
{
if(id.x>=page_sample_count)return;
page_outputs[id.x].height_gradient=0;
page_outputs[id.x].masks=float4(0,0,0,1);
}

View file

@ -0,0 +1,21 @@
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
struct PageOutput { float4 height_gradient; float4 masks; };
struct PageSummary { float4 bounds; float4 metadata; };
StructuredBuffer<PageOutput> summary_inputs : register(t0);
RWStructuredBuffer<PageSummary> summary_outputs : register(u0);
cbuffer PageConstants : register(b0) { uint page_sample_count; uint page_sample_offset; uint page_summary_index; }
[numthreads(1,1,1)]
void D3D12ZyphosTerrainPageSummaryCS(uint3 id : SV_DispatchThreadID)
{
if (id.x != 0 || page_sample_count == 0) return;
float4 summary = gamecult_geometry_planetary_summary_empty();
[loop] for (uint index = 0; index < page_sample_count; index++)
{
PageOutput sample = summary_inputs[page_sample_offset + index];
summary = gamecult_geometry_planetary_summary_accumulate(summary, sample.height_gradient, sample.masks.w);
}
summary_outputs[page_summary_index].bounds = summary;
summary_outputs[page_summary_index].metadata = float4(page_sample_count, page_sample_offset, page_summary_index, 1.0);
}

View file

@ -0,0 +1,11 @@
#include "D3D12SdfCommon.hlsli"
struct PlanetaryPatchVertexOut { float4 position:SV_Position; };
PlanetaryPatchVertexOut D3D12ZyphosTerrainPatchVS(uint vertexId:SV_VertexID,uint instanceId:SV_InstanceID)
{
PlanetaryPatchVertexOut output; output.position=float4(-2,-2,1,1); return output;
}
SceneOut D3D12ZyphosTerrainPatchPS(PlanetaryPatchVertexOut input)
{
discard; return (SceneOut)0;
}

View file

@ -0,0 +1,74 @@
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
struct PlanetaryTopologyInput
{
float4 direction_face;
float4 coordinate_mode;
float4 projection_parameters;
};
struct PlanetaryTopologyOutput
{
float4 direction_face;
float4 coordinate_valid;
};
StructuredBuffer<PlanetaryTopologyInput> topology_inputs : register(t0);
RWStructuredBuffer<PlanetaryTopologyOutput> topology_outputs : register(u0);
cbuffer PlanetaryTopologyConstants : register(b0) { uint topology_input_count; }
[numthreads(64, 1, 1)]
void D3D12PlanetaryTopologyParityCS(uint3 id : SV_DispatchThreadID)
{
if (id.x >= topology_input_count) return;
PlanetaryTopologyInput input = topology_inputs[id.x];
PlanetaryTopologyOutput output = (PlanetaryTopologyOutput)0;
if (input.coordinate_mode.z < 0.5)
{
int face = (int)input.direction_face.w;
output.direction_face = float4(gamecult_geometry_planetary_face_direction(face, input.coordinate_mode.xy), (float)face);
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
}
else if (input.coordinate_mode.z < 1.5)
{
int face;
float2 coordinate = gamecult_geometry_planetary_face_coordinate(input.direction_face.xyz, face);
output.direction_face = float4(normalize(input.direction_face.xyz), (float)face);
output.coordinate_valid = float4(coordinate, 0.0, 1.0);
}
else if (input.coordinate_mode.z < 2.5)
{
output.direction_face = float4(normalize(input.direction_face.xyz), 0.0);
output.coordinate_valid = float4(gamecult_geometry_planetary_equirectangular_forward(input.direction_face.xyz), 0.0, 1.0);
}
else if (input.coordinate_mode.z < 3.5)
{
output.direction_face = float4(gamecult_geometry_planetary_equirectangular_inverse(input.coordinate_mode.xy), 0.0);
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
}
else if (input.coordinate_mode.z < 4.5)
{
output.direction_face = float4(normalize(input.direction_face.xyz), 0.0);
output.coordinate_valid = float4(gamecult_geometry_planetary_equal_earth_forward(input.direction_face.xyz), 0.0, 1.0);
}
else if (input.coordinate_mode.z < 5.5)
{
output.direction_face = float4(gamecult_geometry_planetary_equal_earth_inverse(input.coordinate_mode.xy), 0.0);
output.coordinate_valid = float4(input.coordinate_mode.xy, 0.0, 1.0);
}
else if (input.coordinate_mode.z < 6.5)
{
float2 coordinate;
bool valid=gamecult_geometry_planetary_projection_forward(input.direction_face.xyz,(int)input.coordinate_mode.w,input.projection_parameters.xyz,coordinate);
output.direction_face=float4(normalize(input.direction_face.xyz),0);
output.coordinate_valid=float4(coordinate,0,valid?1.0:0.0);
}
else
{
float3 direction;
bool valid=gamecult_geometry_planetary_projection_inverse(input.coordinate_mode.xy,(int)input.coordinate_mode.w,input.projection_parameters.xyz,direction);
output.direction_face=float4(direction,0);
output.coordinate_valid=float4(input.coordinate_mode.xy,0,valid?1.0:0.0);
}
topology_outputs[id.x] = output;
}

View file

@ -0,0 +1,292 @@
cbuffer AquariumFrame : register(b0)
{
float2 resolution;
float timeSeconds;
float viewRadius;
float3 cameraPosition;
float farDistance;
float3 cameraTarget;
float sceneFlags;
float2 viewCenter;
float frameIndex;
float previousTimeSeconds;
float3 previousCameraPosition;
float previousViewRadius;
float3 previousCameraTarget;
float previousSceneFlags;
float2 previousViewCenter;
float2 jitterPixels;
float2 previousJitterPixels;
float renderDebugMode;
float exposure;
float bloomIntensity;
float bloomVeilIntensity;
float4 cursorWorlds;
float4 temporalGaussianInfo;
float4 cameraFrustumXy;
float4 cameraFrustumZ;
};
cbuffer D3D12PointCloudConstants : register(b1)
{
float4 sourceShape;
float4 projection;
float4 depthRange;
};
static const float FIELD_ID_POINT_CLOUD = 71.0;
struct PointCloudVertex
{
float3 position;
float3 normal;
float2 uv;
float4 color;
};
struct FractalSdfSplat
{
float4 centerRadius;
float4 orientation;
float4 radiiFalloff;
float4 materialConfidence;
float4 key;
};
struct SdfEnvelopeReservoir
{
float4 centerRadius;
float4 radiiFalloff;
float4 weightTargetCount;
float4 validation;
};
struct PbrMaterialReservoir
{
float4 baseColorRoughMetal;
float4 normalVariance;
float4 weightTargetCount;
float4 validation;
};
struct RadiosityReservoir
{
float4 radianceDistance;
float4 directionOcclusion;
float4 weightTargetCount;
float4 validation;
};
Texture2D<float> disparityTexture : register(t0);
RWStructuredBuffer<PointCloudVertex> pointVertices : register(u0);
RWStructuredBuffer<uint> pointIndices : register(u1);
RWStructuredBuffer<FractalSdfSplat> surfaceSplats : register(u2);
RWStructuredBuffer<SdfEnvelopeReservoir> surfaceSdfReservoirs : register(u3);
RWStructuredBuffer<PbrMaterialReservoir> surfacePbrReservoirs : register(u4);
RWStructuredBuffer<RadiosityReservoir> surfaceRadiosityReservoirs : register(u5);
struct VertexIn
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
};
struct VertexOut
{
float4 position : SV_Position;
float3 worldPosition : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
float4 color : COLOR0;
};
struct SceneOut
{
float4 colorTravel : SV_Target0;
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};
float DepthFromDisparity(float disparity)
{
float minDepth = max(0.001, depthRange.x);
float maxDepth = max(minDepth + 0.001, depthRange.y);
return disparity > 0.0001
? saturate(projection.x / max(disparity, 0.0001)) * (maxDepth - minDepth) + minDepth
: maxDepth;
}
void cameraBasis(float3 camera, float3 target, out float3 forward, out float3 right, out float3 up)
{
forward = normalize(target - camera);
float3 worldUp = abs(forward.y) > 0.96 ? float3(0.0, 0.0, 1.0) : float3(0.0, 1.0, 0.0);
right = normalize(cross(worldUp, forward));
up = normalize(cross(forward, right));
}
float3 PositionFromPixel(uint2 pixel, float depth)
{
float focalPixels = max(1.0, projection.y);
float principalX = projection.z;
float principalY = projection.w;
float minDepth = max(0.001, depthRange.x);
float maxDepth = max(minDepth + 0.001, depthRange.y);
float lateralDisplayScale = max(1.0, depthRange.z);
float forwardDisplayAnchor = depthRange.w > 0.0 ? depthRange.w : (minDepth + maxDepth) * 0.5;
float3 cameraLocal = float3(
((float)pixel.x - principalX) / focalPixels * depth,
-((float)pixel.y - principalY) / focalPixels * depth,
depth);
float3 forward;
float3 right;
float3 up;
cameraBasis(cameraPosition, cameraTarget, forward, right, up);
return cameraTarget +
right * cameraLocal.x * lateralDisplayScale +
up * cameraLocal.y * lateralDisplayScale +
forward * (cameraLocal.z - forwardDisplayAnchor);
}
float DisparityAt(int2 pixel, uint width, uint height)
{
pixel.x = clamp(pixel.x, 0, (int)width - 1);
pixel.y = clamp(pixel.y, 0, (int)height - 1);
return disparityTexture.Load(int3(pixel, 0));
}
float Hash01(uint value)
{
value ^= value >> 16;
value *= 0x7feb352du;
value ^= value >> 15;
value *= 0x846ca68bu;
value ^= value >> 16;
return (float)(value & 0x00ffffffu) / 16777216.0;
}
float4 projectWorld(float3 worldPosition)
{
float3 forward;
float3 right;
float3 up;
cameraBasis(cameraPosition, cameraTarget, forward, right, up);
float3 relative = worldPosition - cameraPosition;
float3 view = float3(dot(relative, right), dot(relative, up), dot(relative, forward));
float z = max(view.z, 0.0001);
float2 slope = view.xy / z;
float2 frustumMin = float2(cameraFrustumXy.x, cameraFrustumXy.z);
float2 frustumMax = float2(cameraFrustumXy.y, cameraFrustumXy.w);
float2 uv = (slope - frustumMin) / max(frustumMax - frustumMin, float2(0.0001, 0.0001));
return float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), saturate(z / max(farDistance, 0.0001)), 1.0);
}
[numthreads(128, 1, 1)]
void D3D12PointCloudFromDisparityCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint pointIndex = dispatchThreadId.x;
uint pointCount = (uint)max(1.0, sourceShape.z);
if (pointIndex >= pointCount)
{
return;
}
uint width = (uint)max(1.0, sourceShape.x);
uint height = (uint)max(1.0, sourceShape.y);
uint stride = (uint)max(1.0, sourceShape.w);
uint sampledWidth = max(1, (width + stride - 1) / stride);
uint x = (pointIndex % sampledWidth) * stride;
uint y = (pointIndex / sampledWidth) * stride;
x = min(x, width - 1);
y = min(y, height - 1);
float disparity = disparityTexture.Load(int3(x, y, 0));
float confidence = disparity > 0.0001 ? 1.0 : 0.0;
float depth = DepthFromDisparity(disparity);
float3 position = PositionFromPixel(uint2(x, y), depth);
float minDepth = max(0.001, depthRange.x);
float maxDepth = max(minDepth + 0.001, depthRange.y);
float normalizedDepth = saturate((depth - minDepth) / max(maxDepth - minDepth, 0.0001));
float leftDepth = DepthFromDisparity(DisparityAt(int2((int)x - (int)stride, (int)y), width, height));
float rightDepth = DepthFromDisparity(DisparityAt(int2((int)x + (int)stride, (int)y), width, height));
float upDepth = DepthFromDisparity(DisparityAt(int2((int)x, (int)y - (int)stride), width, height));
float downDepth = DepthFromDisparity(DisparityAt(int2((int)x, (int)y + (int)stride), width, height));
float3 leftPosition = PositionFromPixel(uint2(max((int)x - (int)stride, 0), y), leftDepth);
float3 rightPosition = PositionFromPixel(uint2(min(x + stride, width - 1), y), rightDepth);
float3 upPosition = PositionFromPixel(uint2(x, max((int)y - (int)stride, 0)), upDepth);
float3 downPosition = PositionFromPixel(uint2(x, min(y + stride, height - 1)), downDepth);
float3 normal = normalize(cross(rightPosition - leftPosition, downPosition - upPosition));
normal = dot(normal, normal) > 0.25 ? normal : float3(0.0, 0.0, -1.0);
float curvature = saturate((abs(leftDepth + rightDepth - 2.0 * depth) + abs(upDepth + downDepth - 2.0 * depth)) * 4.0);
PointCloudVertex vertex;
vertex.position = position;
vertex.normal = normal;
vertex.uv = float2((float)x / max(1.0, (float)(width - 1)), (float)y / max(1.0, (float)(height - 1)));
vertex.color = confidence > 0.0
? float4(lerp(float3(0.2, 0.75, 1.0), float3(1.0, 0.95, 0.35), normalizedDepth), 1.0)
: float4(0.08, 0.14, 0.22, 0.18);
pointVertices[pointIndex] = vertex;
pointIndices[pointIndex] = pointIndex;
float reservoirConfidence = confidence;
float sampleRadius = lerp(0.080, 0.035, saturate(confidence)) * (1.0 + curvature * 2.5);
FractalSdfSplat splat;
splat.centerRadius = float4(position, sampleRadius);
splat.orientation = float4(normal, 1.0);
splat.radiiFalloff = float4(sampleRadius, sampleRadius * (1.0 + curvature), sampleRadius * 0.35, 3.0 + curvature * 4.0);
splat.materialConfidence = float4(saturate(1.0 - normalizedDepth), 1.0, 1.0, reservoirConfidence);
splat.key = float4((float)pointIndex, frameIndex, curvature, Hash01(pointIndex + (uint)frameIndex * 1664525u));
surfaceSplats[pointIndex] = splat;
SdfEnvelopeReservoir sdf;
sdf.centerRadius = splat.centerRadius;
sdf.radiiFalloff = splat.radiiFalloff;
sdf.weightTargetCount = float4(reservoirConfidence, reservoirConfidence, 1.0, reservoirConfidence);
sdf.validation = float4(reservoirConfidence, frameIndex, curvature, 1.0);
surfaceSdfReservoirs[pointIndex] = sdf;
PbrMaterialReservoir pbr;
pbr.baseColorRoughMetal = float4(vertex.color.rgb, 0.38 + curvature * 0.28);
pbr.normalVariance = float4(normal, curvature);
pbr.weightTargetCount = sdf.weightTargetCount;
pbr.validation = sdf.validation;
surfacePbrReservoirs[pointIndex] = pbr;
RadiosityReservoir radiosity;
radiosity.radianceDistance = float4(vertex.color.rgb * 0.18, depth);
radiosity.directionOcclusion = float4(normalize(-normal + 0.01), 0.35 + confidence * 0.45);
radiosity.weightTargetCount = sdf.weightTargetCount;
radiosity.validation = sdf.validation;
surfaceRadiosityReservoirs[pointIndex] = radiosity;
}
VertexOut D3D12PointCloudVS(VertexIn input)
{
VertexOut output;
output.position = projectWorld(input.position);
output.worldPosition = input.position;
output.normal = input.normal;
output.uv = input.uv;
output.color = input.color;
return output;
}
SceneOut D3D12PointCloudPS(VertexOut input)
{
SceneOut output;
float travel = length(input.worldPosition - cameraPosition);
float alpha = saturate(input.color.a);
output.colorTravel = float4(input.color.rgb * alpha, travel);
output.metadata = float4(FIELD_ID_POINT_CLOUD, normalize(input.normal));
output.control = float4(alpha, 1.0, 0.35, 0.0);
output.reservoirGuide = float4(alpha, 0.0, alpha, 0.0);
output.overdraw = 1.0;
output.depth = input.position.z / input.position.w;
return output;
}

View file

@ -42,6 +42,7 @@ Texture2D<float4> bloomTexture4 : register(t33);
Texture2D<float4> bloomTexture5 : register(t34);
Texture2D<float4> bloomTexture6 : register(t35);
Texture2D<float4> bloomTexture7 : register(t36);
Texture2D<float> sceneOverdrawTexture : register(t76);
Texture2D<float4> currentReservoirGuideTexture : register(t26);
Texture2D<float> blueNoiseTexture : register(t28);
SamplerState sourceSampler : register(s0);
@ -524,6 +525,113 @@ float3 presentColor(float3 scene, float2 uv, float bloomScale)
return aces(exposedScene + bloomContribution);
}
float reservoirGuideConfidence(float4 guide, float4 control)
{
float confidence = guide.x > 0.0 ? saturate(guide.x) : (control.w > 0.0 ? saturate(control.w / MAX_HISTORY_AGE) : 1.0);
float validity = guide.z > 0.0 ? saturate(guide.z) : 1.0;
return confidence * validity;
}
float reservoirReconstructionWeight(
float4 centerColorTravel,
float4 centerMetadata,
float4 centerControl,
float4 centerGuide,
float4 sampleColorTravel,
float4 sampleMetadata,
float4 sampleControl,
float4 sampleGuide,
float2 offset)
{
float centerTravel = centerColorTravel.w;
float sampleTravel = sampleColorTravel.w;
float travelTolerance = max(0.05, max(centerTravel, 0.0) * 0.02);
float travelWeight = 1.0 - smoothstep(travelTolerance, travelTolerance * 4.0, abs(sampleTravel - centerTravel));
float fieldWeight = abs(sampleMetadata.x - centerMetadata.x) < 0.001 ? 1.0 : 0.0;
float normalWeight = 1.0;
if (dot(centerMetadata.yzw, centerMetadata.yzw) > 0.01 &&
dot(sampleMetadata.yzw, sampleMetadata.yzw) > 0.01)
{
normalWeight = smoothstep(0.62, 0.97, dot(normalize(centerMetadata.yzw), normalize(sampleMetadata.yzw)));
}
float coverageWeight = 1.0 - smoothstep(0.12, 0.55, abs(saturate(sampleControl.x) - saturate(centerControl.x)));
float detailWeight = 1.0 - smoothstep(0.08, 0.38, abs(saturate(sampleControl.z) - saturate(centerControl.z)));
float colorWeight = 1.0 - smoothstep(0.20, 1.35, length(sampleColorTravel.rgb - centerColorTravel.rgb));
float sampleConfidence = lerp(0.35, 1.0, reservoirGuideConfidence(sampleGuide, sampleControl));
float distanceWeight = exp2(-dot(offset, offset) * 0.85);
return fieldWeight * travelWeight * normalWeight * coverageWeight * detailWeight * colorWeight * sampleConfidence * distanceWeight;
}
float3 reconstructReservoirColor(float2 uv, out float reconstructionMix)
{
uint sourceWidth;
uint sourceHeight;
sourceTexture.GetDimensions(sourceWidth, sourceHeight);
uint2 dimensions = max(uint2(sourceWidth, sourceHeight), uint2(1, 1));
float2 texel = 1.0 / (float2)dimensions;
int2 centerPixel = clamp((int2)floor(uv * (float2)dimensions), int2(0, 0), (int2)dimensions - int2(1, 1));
float2 centerUv = ((float2)centerPixel + 0.5) * texel;
float4 centerColorTravel = sourceTexture.SampleLevel(sourceSampler, uv, 0.0);
float4 centerMetadata = currentSceneMetadataTexture.Load(int3(centerPixel, 0));
float4 centerControl = currentSceneControlTexture.Load(int3(centerPixel, 0));
float4 centerGuide = currentReservoirGuideTexture.Load(int3(centerPixel, 0));
float centerConfidence = reservoirGuideConfidence(centerGuide, centerControl);
float centerDetail = saturate(centerControl.z);
float edgePreservation = 1.0 - smoothstep(0.28, 0.82, centerDetail);
float centerWeight = lerp(2.25, 1.15, saturate(1.0 - centerConfidence));
float3 sumColor = centerColorTravel.rgb * centerWeight;
float sumWeight = centerWeight;
static const int2 offsets[8] =
{
int2(-1, 0),
int2(1, 0),
int2(0, -1),
int2(0, 1),
int2(-1, -1),
int2(1, -1),
int2(-1, 1),
int2(1, 1)
};
[unroll]
for (int index = 0; index < 8; index++)
{
int2 offset = offsets[index];
int2 samplePixel = clamp(centerPixel + offset, int2(0, 0), (int2)dimensions - int2(1, 1));
float4 sampleColorTravel = sourceTexture.Load(int3(samplePixel, 0));
float4 sampleMetadata = currentSceneMetadataTexture.Load(int3(samplePixel, 0));
float4 sampleControl = currentSceneControlTexture.Load(int3(samplePixel, 0));
float4 sampleGuide = currentReservoirGuideTexture.Load(int3(samplePixel, 0));
float weight = reservoirReconstructionWeight(
centerColorTravel,
centerMetadata,
centerControl,
centerGuide,
sampleColorTravel,
sampleMetadata,
sampleControl,
sampleGuide,
(float2)offset);
sumColor += sampleColorTravel.rgb * weight;
sumWeight += weight;
}
float3 filtered = sumColor / max(sumWeight, 0.0001);
float neighborAgreement = saturate((sumWeight - centerWeight) / 4.0);
float blendNeed = saturate((1.0 - centerConfidence) * 0.35 + neighborAgreement * 0.10);
float reservoirMaturity = smoothstep(0.5, 1.5, max(centerGuide.y, centerControl.w));
float filterStrength = min(0.20, blendNeed * edgePreservation * reservoirMaturity);
reconstructionMix = filterStrength;
float3 reconstructed = lerp(centerColorTravel.rgb, filtered, filterStrength);
float3 neighborhoodMin;
float3 neighborhoodMax;
currentNeighborhood(centerUv, neighborhoodMin, neighborhoodMax);
return clamp(reconstructed, neighborhoodMin, neighborhoodMax);
}
float blueNoiseAt(float2 uv, uint salt)
{
uint width;
@ -1119,15 +1227,29 @@ float4 reservoirDebugOrColor(FieldReservoirSample candidate, uint2 pixel)
return float4(fieldReservoirResolvedColor(candidate, farDistance), candidate.colorTravel.w);
}
float3 overdrawHeat(float overdraw)
{
if (overdraw <= 0.5)
{
return float3(0.0, 0.0, 0.0);
}
float hot = saturate(log2(max(overdraw, 1.0)) / 4.0);
float3 low = lerp(float3(0.0, 0.22, 0.95), float3(0.0, 0.95, 0.35), saturate(overdraw - 1.0));
float3 mid = lerp(low, float3(1.0, 0.92, 0.05), saturate((overdraw - 2.0) / 2.0));
return lerp(mid, float3(1.0, 0.05, 0.35), hot);
}
FieldReservoirResolveOut D3D12FieldReservoirResolvePS(VertexOut input)
{
uint2 pixel = (uint2)pixelFromUv(input.uv);
FieldReservoirSample current = currentFrameReservoirSample(pixel, input.uv);
FieldReservoirResolveOut output;
output.colorTravel = sourceTexture.Load(int3(pixel, 0));
output.metadata = loadCurrentMetadata(input.uv);
output.control = loadCurrentControl(input.uv);
output.reservoirGuide = loadCurrentReservoirGuide(input.uv);
output.colorTravel = reservoirDebugOrColor(current, pixel);
output.metadata = current.metadata;
output.control = current.control;
output.reservoirGuide = current.guide;
return output;
}
@ -1175,7 +1297,13 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
float currentReservoirConfidence = currentReservoirGuide.x > 0.0 ? saturate(currentReservoirGuide.x) : 1.0;
float reservoirSampleAge = max(currentReservoirGuide.y, 0.0);
float currentReservoirDomainValidity = currentReservoirGuide.z > 0.0 ? saturate(currentReservoirGuide.z) : 1.0;
float reconstructionMix = 0.0;
float3 resolved = resolvedSample.rgb;
if (renderDebugMode < 0.5 || (renderDebugMode >= 20.5 && renderDebugMode < 21.5))
{
resolved = reconstructReservoirColor(input.uv, reconstructionMix);
}
float3 finalColor = presentColor(resolved, input.uv, 1.0);
if (renderDebugMode > 0.5 && renderDebugMode < 1.5)
{
@ -1223,6 +1351,11 @@ ResolveOut D3D12ReservoirPresentationResolvePS(VertexOut input)
{
finalColor = float3(currentReservoirConfidence, saturate(reservoirSampleAge / MAX_HISTORY_AGE), currentReservoirDomainValidity);
}
else if (renderDebugMode >= 20.5 && renderDebugMode < 21.5)
{
float overdraw = sceneOverdrawTexture.SampleLevel(sourceSampler, input.uv, 0.0);
finalColor = overdrawHeat(overdraw);
}
else if (renderDebugMode >= 12.5 && renderDebugMode < 18.5)
{
finalColor = saturate(resolved);

View file

@ -0,0 +1,116 @@
cbuffer AquariumFrame : register(b0)
{
float2 resolution;
float timeSeconds;
float viewRadius;
float3 cameraPosition;
float farDistance;
float3 cameraTarget;
float sceneFlags;
float2 viewCenter;
float frameIndex;
float previousTimeSeconds;
float3 previousCameraPosition;
float previousViewRadius;
float3 previousCameraTarget;
float previousSceneFlags;
float2 previousViewCenter;
float2 jitterPixels;
float2 previousJitterPixels;
float renderDebugMode;
float exposure;
float bloomIntensity;
float bloomVeilIntensity;
float4 cursorWorlds;
float4 temporalGaussianInfo;
float4 cameraFrustumXy;
float4 cameraFrustumZ;
float4 gpuFusionInfo;
float4 fractalReservoirInfo;
float4 fractalReservoirFrame;
float4 reservoirBudgetInfo;
};
Texture2D<float4> sourceTexture : register(t0);
Texture2D<float4> currentSceneMetadataTexture : register(t5);
Texture2D<float4> currentSceneControlTexture : register(t7);
Texture2D<float4> currentReservoirGuideTexture : register(t26);
#include "D3D12FieldReservoir.hlsli"
StructuredBuffer<FieldReservoirSample> fieldReservoirCandidates : register(t45);
RWStructuredBuffer<FieldReservoirSample> reservoirHistoryWrite : register(u22);
RWTexture2D<float4> reservoirResolvedTexture : register(u23);
float4 loadTexturePixel(Texture2D<float4> source, uint2 pixel)
{
return source.Load(int3(pixel, 0));
}
FieldReservoirSample sceneFieldReservoirSample(uint2 pixel, float2 uv)
{
float4 colorTravel = loadTexturePixel(sourceTexture, pixel);
float4 metadata = loadTexturePixel(currentSceneMetadataTexture, pixel);
float4 control = loadTexturePixel(currentSceneControlTexture, pixel);
float4 guide = loadTexturePixel(currentReservoirGuideTexture, pixel);
float target = fieldReservoirDefaultTarget(colorTravel, control, guide);
return makeFieldReservoirSample(
colorTravel,
metadata,
control,
guide,
float4(uv, colorTravel.w, 0.0),
float4(uv, uv),
float4(1.0, 1.0, FieldDomainKindScreen, FieldShiftKindNone),
target,
1.0,
1.0,
FieldProposalKindDeterministicStructural);
}
FieldReservoirSample currentFrameReservoirSample(uint2 pixel, float2 uv)
{
uint width = (uint)max(resolution.x, 1.0);
uint baseIndex = (pixel.y * width + pixel.x) * FieldReservoirSlotsPerPixel;
FieldReservoirSample current = sceneFieldReservoirSample(pixel, uv);
FieldReservoirSample candidate = fieldReservoirCandidates[baseIndex + FieldReservoirRowCurrent];
current = mergeFieldReservoirVisibilityProposals(
current,
candidate,
fieldReservoirRandom01(pixel, (uint)frameIndex, 31u),
farDistance);
current.guide.y = 0.0;
current.guide.w = 0.0;
return current;
}
float4 reservoirHistoryColor(FieldReservoirSample sample)
{
if (!fieldReservoirSampleValid(sample, farDistance))
{
return float4(0.0, 0.0, 0.0, farDistance + 1.0);
}
return float4(fieldReservoirResolvedColor(sample, farDistance), sample.colorTravel.w);
}
[numthreads(8, 8, 1)]
void D3D12ReservoirHistoryUpdateCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint2 dimensions = (uint2)max(resolution, float2(1.0, 1.0));
if (dispatchThreadId.x >= dimensions.x || dispatchThreadId.y >= dimensions.y)
{
return;
}
uint2 pixel = dispatchThreadId.xy;
float2 uv = (float2(pixel) + 0.5) / float2(dimensions);
uint baseIndex = (pixel.y * dimensions.x + pixel.x) * FieldReservoirSlotsPerPixel;
FieldReservoirSample current = currentFrameReservoirSample(pixel, uv);
reservoirHistoryWrite[baseIndex + FieldReservoirRowCurrent] = current;
reservoirHistoryWrite[baseIndex + FieldReservoirRowTemporal] = current;
reservoirHistoryWrite[baseIndex + FieldReservoirRowSpatial] = current;
reservoirHistoryWrite[baseIndex + FieldReservoirRowFinal] = current;
reservoirResolvedTexture[pixel] = reservoirHistoryColor(current);
}

View file

@ -34,7 +34,7 @@ SamplerState linearSampler : register(s0);
static const float FIELD_ID_HEIGHT_FIELD = 4.0;
static const float PI = 3.14159265359;
static const float HEIGHT_FIELD_TEXEL_COUNT = 128.0;
static const float HEIGHT_FIELD_TEXEL_COUNT = 512.0;
static const float SURFACE_FLAT_REFLECTION_MAX_LOD = 3.0;
static const float BACKGROUND_PMREM_LOD = 3.0;
static const float BACKGROUND_PMREM_CONE = 0.16;
@ -55,6 +55,7 @@ struct SceneOut
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};
@ -335,12 +336,18 @@ float3 fractalStarClusters(float3 direction)
float3 surfaceMirrorRadiance(float3 p, float3 direction, out float3 normal)
{
float height = terrainHeight(p.xy);
float2 gradient = terrainGradient(p.xy);
normal = normalize(float3(-gradient.x, -gradient.y, 1.0));
float slope = length(gradient);
float3 reflectionDirection = reflect(direction, normal);
float flatness = 1.0 - smoothstep(SURFACE_FLAT_SLOPE_START, SURFACE_FLAT_SLOPE_END, length(gradient));
float flatness = 1.0 - smoothstep(SURFACE_FLAT_SLOPE_START, SURFACE_FLAT_SLOPE_END, slope);
float lod = flatness * SURFACE_FLAT_REFLECTION_MAX_LOD;
return studioPmremConeSample(reflectionDirection, lod, flatness * 0.055);
float3 reflected = studioPmremConeSample(reflectionDirection, lod, flatness * 0.055);
float inkMass = saturate((height - 0.006) * 32.0 + smoothstep(0.03, 0.16, slope) * 0.42);
float3 paper = float3(0.84, 0.75, 0.55) * (0.62 + flatness * 0.28);
float3 ink = float3(0.010, 0.008, 0.006);
return lerp(reflected * 0.18 + paper, ink, inkMass * 0.90);
}
float3 backgroundRadiance(float3 direction)
@ -408,6 +415,7 @@ SceneOut D3D12ScenePS(VertexOut input)
output.metadata = float4(result.fieldId, result.normal);
output.control = float4(result.coverage, result.stepCount / 72.0, 0.0, 0.0);
output.reservoirGuide = float4(1.0, 0.0, 1.0, 0.0);
output.overdraw = 1.0;
output.depth = saturate(result.travel / max(farDistance, 0.001));
return output;
}
@ -662,6 +670,7 @@ SceneOut D3D12SplinePS(SplineVertexOut input)
output.metadata = float4(5000.0, closestT, sdf, radiusPx);
output.control = float4(claimCoverage, coverage, saturate(radiusPx / 32.0), 0.0);
output.reservoirGuide = float4(claimCoverage, 0.0, coverage, 0.0);
output.overdraw = 1.0;
output.depth = saturate(travel / max(farDistance, 0.0001));
return output;
}

View file

@ -77,6 +77,7 @@ struct SceneOut
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};

View file

@ -14,7 +14,19 @@
#define SDF_TRACE_MAX_STEP_RADIUS_SCALE 0.012
#endif
float3 sdfNormal(float3 p, int sdfIndex)
#ifndef SDF_TRACE_BOUND_RADIUS
#define SDF_TRACE_BOUND_RADIUS(sdfObject) max((sdfObject).centerRadius.w * 1.42, 0.001)
#endif
#ifndef SDF_SURFACE_NORMAL
#define SDF_SURFACE_NORMAL(p,sdfIndex) sdfFiniteDifferenceNormal(p,sdfIndex)
#endif
#ifndef SDF_TRACE_FUNCTION
#define SDF_TRACE_FUNCTION traceSdf
#endif
float3 sdfFiniteDifferenceNormal(float3 p, int sdfIndex)
{
float epsilon = 0.006;
float dx = sdfDistance(p + float3(epsilon, 0.0, 0.0), sdfIndex) - sdfDistance(p - float3(epsilon, 0.0, 0.0), sdfIndex);
@ -43,14 +55,14 @@ bool refineSdfHit(float3 origin, float3 direction, int sdfIndex, float lowTravel
travel = highTravel;
float3 p = origin + direction * travel;
surface = sdfSurface(p, sdfIndex);
normal = sdfNormal(p, sdfIndex);
normal = SDF_SURFACE_NORMAL(p, sdfIndex);
return true;
}
bool traceSdf(float3 origin, float3 direction, int sdfIndex, out float travel, out float3 normal, out SdfSurface surface, out float stepCount)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float boundRadius = max(sdfObject.centerRadius.w * 1.42, 0.001);
float boundRadius = SDF_TRACE_BOUND_RADIUS(sdfObject);
if (!traceSphere(origin, direction, sdfObject.centerRadius.xyz, boundRadius, travel))
{
normal = 0.0;
@ -96,7 +108,7 @@ bool traceSdf(float3 origin, float3 direction, int sdfIndex, out float travel, o
}
surface = sdfSurface(p, sdfIndex);
normal = sdfNormal(p, sdfIndex);
normal = SDF_SURFACE_NORMAL(p, sdfIndex);
return true;
}
@ -119,7 +131,7 @@ SceneOut D3D12SdfProxyPS(SdfObjectProxyVertexOut input)
float3 normal;
SdfSurface surface;
float stepCount;
if (!traceSdf(cameraPosition, rayDirection, sdfIndex, travel, normal, surface, stepCount))
if (!SDF_TRACE_FUNCTION(cameraPosition, rayDirection, sdfIndex, travel, normal, surface, stepCount))
{
discard;
}
@ -135,6 +147,7 @@ SceneOut D3D12SdfProxyPS(SdfObjectProxyVertexOut input)
saturate(surface.temporalDetail),
saturate(surface.reservoirConfidence));
output.reservoirGuide = float4(saturate(surface.reservoirConfidence), 0.0, 1.0, 0.0);
output.overdraw = 1.0;
output.depth = saturate(travel / max(farDistance, 0.001));
return output;
}

View file

@ -0,0 +1,69 @@
cbuffer D3D12StereoDepthConstants : register(b0)
{
float4 Shape; // width, height, minDisparity, disparityLevels
float4 Match; // censusRadius, aggregationPathCount, p1, p2
float4 Output; // invDisparityScale, reserved
};
Texture2D<float2> PackedStereoIr : register(t28);
RWTexture2D<float> DisparityOut : register(u0);
float PackedLeft(uint2 pixel)
{
return PackedStereoIr.Load(int3(pixel, 0)).r;
}
float PackedRight(uint2 pixel)
{
const uint stereoWidth = max(1u, (uint)Shape.x);
return PackedStereoIr.Load(int3(uint2(pixel.x + stereoWidth, pixel.y), 0)).r;
}
[numthreads(8, 8, 1)]
void D3D12PackedStereoDepthCS(uint3 dispatchThreadId : SV_DispatchThreadID)
{
const uint width = max(1u, (uint)Shape.x);
const uint height = max(1u, (uint)Shape.y);
const uint2 pixel = dispatchThreadId.xy;
if (pixel.x >= width || pixel.y >= height)
{
return;
}
const int minDisparity = max(0, (int)Shape.z);
const int disparityLevels = max(1, min(256, (int)Shape.w));
const int radius = max(1, min(4, (int)Match.x));
const int maxDisparity = min(disparityLevels - 1, (int)pixel.x - radius - minDisparity);
if (maxDisparity <= 0 || pixel.y < (uint)radius || pixel.y + (uint)radius >= height)
{
DisparityOut[pixel] = 0.0f;
return;
}
float bestCost = 1.0e20f;
int bestDisparity = 0;
[loop]
for (int disparity = minDisparity; disparity <= maxDisparity; disparity++)
{
float cost = 0.0f;
[loop]
for (int y = -radius; y <= radius; y++)
{
[loop]
for (int x = -radius; x <= radius; x++)
{
const uint2 leftPixel = uint2((uint)((int)pixel.x + x), (uint)((int)pixel.y + y));
const uint2 rightPixel = uint2((uint)((int)pixel.x + x - disparity), (uint)((int)pixel.y + y));
cost += abs(PackedLeft(leftPixel) - PackedRight(rightPixel));
}
}
if (cost < bestCost)
{
bestCost = cost;
bestDisparity = disparity;
}
}
DisparityOut[pixel] = (float)bestDisparity;
}

View file

@ -52,6 +52,7 @@ struct SceneOut
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};
@ -185,6 +186,7 @@ SceneOut D3D12TemporalGaussianPS(TemporalGaussianVertexOut input)
output.metadata = float4(FIELD_ID_TEMPORAL_GAUSSIAN_BASE + gaussian.previousCenterFieldId.w, normal);
output.control = float4(opacity, saturate(gaussian.centerHistoryWeight.w), saturate(gaussian.shapePad.x / 8.0), 0.0);
output.reservoirGuide = float4(saturate(gaussian.velocityConfidence.w), 0.0, 1.0, 0.0);
output.overdraw = 1.0;
output.depth = saturate(travel / max(farDistance, 0.001));
return output;
}

View file

@ -24,6 +24,7 @@ struct TubeFieldSegment
float4 tubeData;
};
#include "CultMath/CultMath.hlsl"
#include "D3D12FieldReservoir.hlsli"
cbuffer AquariumFrame : register(b0)
@ -178,16 +179,6 @@ float NormalizedSampleWithOffset(uint logicalColumn, float x, int rollingOffset)
}
float Catmull(float p0, float p1, float p2, float p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float SampleCurve(uint logicalColumn, float x)
{
int i1 = (int)floor(x);
@ -196,7 +187,7 @@ float SampleCurve(uint logicalColumn, float x)
float p1 = NormalizedSample(logicalColumn, (float)i1);
float p2 = NormalizedSample(logicalColumn, (float)(i1 + 1));
float p3 = NormalizedSample(logicalColumn, (float)(i1 + 2));
return saturate(Catmull(p0, p1, p2, p3, t));
return saturate(cultmath_catmullrom(p0, p1, p2, p3, t));
}
float SampleCurveWithOffset(uint logicalColumn, float x, int rollingOffset)
@ -207,7 +198,7 @@ float SampleCurveWithOffset(uint logicalColumn, float x, int rollingOffset)
float p1 = NormalizedSampleWithOffset(logicalColumn, (float)i1, rollingOffset);
float p2 = NormalizedSampleWithOffset(logicalColumn, (float)(i1 + 1), rollingOffset);
float p3 = NormalizedSampleWithOffset(logicalColumn, (float)(i1 + 2), rollingOffset);
return saturate(Catmull(p0, p1, p2, p3, t));
return saturate(cultmath_catmullrom(p0, p1, p2, p3, t));
}
@ -369,6 +360,7 @@ struct SceneOut
float4 metadata : SV_Target1;
float4 control : SV_Target2;
float4 reservoirGuide : SV_Target3;
float overdraw : SV_Target4;
float depth : SV_Depth;
};
@ -877,6 +869,7 @@ SceneOut D3D12TubeFieldPS(TubeFieldVertexOut input)
output.metadata = float4(candidateFieldId, tubeNormal);
output.control = float4(claimCoverage, supportCoverage, saturate(radiusWorld / max(viewRadius, 0.0001)), value);
output.reservoirGuide = float4(claimCoverage, 0.0, supportCoverage, value);
output.overdraw = 1.0;
output.depth = saturate(travel / max(farDistance, 0.0001));
float target = fieldReservoirDefaultTarget(output.colorTravel, output.control, output.reservoirGuide);
float selectedColumn = round(input.tubeData.x);

View file

@ -87,7 +87,10 @@ internal sealed class DebugUi
public static DebugUi FromContract(AquariumUiPanel source)
{
var ui = new DebugUi(source.Title, source.Left, source.Top, source.Width, source.FadeWhenMouseDistant, [], () => 0, _ => { });
var ui = new DebugUi(source.Title, source.Left, source.Top, source.Width, source.FadeWhenMouseDistant, [], () => 0, _ => { })
{
IsVisible = true
};
foreach (var control in source.Controls)
{
ui.AddContractControl(control);

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>Aquarium.EveSurface</AssemblyName>
<RootNamespace>Aquarium.EveSurface</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,448 @@
using System.Globalization;
using System.Net.WebSockets;
using System.Numerics;
using System.Text;
using System.Text.Json;
using Aquarium.Engine;
using Aquarium.Engine.Audio;
using Aquarium.Engine.Input;
using Aquarium.Engine.Render;
using Aquarium.Engine.Ui;
namespace Aquarium.EveSurface;
public sealed class EveSurfaceRuntime : IAquariumRuntime
{
private const string DefaultBrokerUrl = "ws://127.0.0.1:8795/eve/deck";
private const string DefaultProviderId = "voidbot.swarm";
private readonly object sync = new();
private readonly string brokerUrl = Environment.GetEnvironmentVariable("FENSALIR_EVE_BROKER") ?? DefaultBrokerUrl;
private readonly string providerId = Environment.GetEnvironmentVariable("FENSALIR_EVE_PROVIDER") ?? DefaultProviderId;
private CancellationTokenSource? connectionLifetime;
private Task? connectionTask;
private EveSurfaceState state = EveSurfaceState.Empty;
private string status = "not started";
private string selectedNodeId = "";
private float timeSeconds;
public AquariumRuntimeOptions Options { get; private set; }
public GraphicsSettings GraphicsSettings { get; set; } = GraphicsSettings.Default;
public AquariumRenderPlan RenderPlan { get; } = CreateRenderPlan();
public AquariumUiDocument Ui => ComposeUi();
public AquariumAudioDocument Audio { get; } = new();
public AquariumSynthDocument Synth { get; } = AquariumSynthDocument.Empty;
public AquariumFrame Frame => new(
new ViewFrame(Vector2.Zero, 24.0f),
new Vector3(0.0f, -10.0f, 7.0f),
timeSeconds,
Vector2.Zero,
AquariumSceneState.Empty);
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input) => frame;
public void Start()
{
Console.WriteLine($"Eve surface client connecting to {brokerUrl} provider {providerId}.");
connectionLifetime = new CancellationTokenSource();
connectionTask = Task.Run(() => RunConnectionLoopAsync(connectionLifetime.Token));
}
public void Update(float deltaSeconds, InputState input)
{
timeSeconds += Math.Max(deltaSeconds, 0.0f);
}
public void FlushState()
{
}
public void Dispose()
{
connectionLifetime?.Cancel();
try
{
connectionTask?.Wait(TimeSpan.FromSeconds(1));
}
catch (AggregateException)
{
}
connectionLifetime?.Dispose();
}
internal void SetOptions(AquariumRuntimeOptions options)
{
Options = options;
}
private AquariumUiDocument ComposeUi()
{
EveSurfaceState snapshot;
string connectionStatus;
lock (sync)
{
snapshot = state;
connectionStatus = status;
if (string.IsNullOrWhiteSpace(selectedNodeId))
{
selectedNodeId = snapshot.SelectedNodeId;
}
}
var nodes = snapshot.Nodes;
var selected = nodes.FirstOrDefault(node => node.Id == selectedNodeId)
?? nodes.FirstOrDefault(node => node.Id == snapshot.SelectedNodeId)
?? nodes.FirstOrDefault();
var ctb = nodes.Where(static node => string.Equals(node.Kind, "ctb-turn", StringComparison.OrdinalIgnoreCase)).Take(12).ToArray();
var leaves = nodes.Where(static node => string.Equals(node.Kind, "state-leaf", StringComparison.OrdinalIgnoreCase)).Take(32).ToArray();
var primaryNodes = ctb.Length > 0 ? ctb : nodes.Take(16).ToArray();
var detailNodes = leaves.Length > 0 ? leaves : nodes.Skip(primaryNodes.Length).Take(32).ToArray();
if (detailNodes.Length == 0 && ctb.Length == 0)
{
detailNodes = nodes.Take(32).ToArray();
}
var summary = nodes.FirstOrDefault(static node => node.Id == "voidbot-summary");
var agent = nodes.FirstOrDefault(static node => node.Id == "agent-detail");
return new AquariumUiDocument()
.Panel("Eve Surface", 18.0f, 82.0f, 420.0f, fadeWhenMouseDistant: true, panel =>
{
panel.Section("CultMesh Provider");
panel.Readout("Provider", () => string.IsNullOrWhiteSpace(snapshot.ProviderId) ? providerId : snapshot.ProviderId);
panel.Readout("Title", () => snapshot.Title);
panel.Readout("Version", () => snapshot.Version.ToString(CultureInfo.InvariantCulture));
panel.Readout("Broker", () => brokerUrl);
panel.Readout("Status", () => connectionStatus);
panel.Button("Reconnect", Reconnect, "Restart the WebSocket subscription and reopen the provider.");
})
.Panel(ctb.Length > 0 ? "CTB" : "Surface Nodes", 456.0f, 82.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
{
panel.Section(ctb.Length > 0 ? "VoidBot Turn Bar" : snapshot.Title);
foreach (var node in primaryNodes)
{
panel.TreeItem(
CompactLabel(node.Label),
0,
canExpand: false,
isExpanded: static () => false,
setExpanded: static _ => { },
isSelected: () => selectedNodeId == node.Id,
select: () => selectedNodeId = node.Id,
detail: CompactDetail(node));
}
})
.Panel(ctb.Length > 0 ? "Selected Face" : "Selected Node", 18.0f, 382.0f, 420.0f, fadeWhenMouseDistant: true, panel =>
{
panel.Section(summary?.Label ?? "VoidBot Swarm");
panel.Readout("Summary", () => summary?.Health ?? "waiting");
panel.TextBox("Selected", () => selected?.Label ?? "none", _ => { }, lines: 1, acceptsReturn: false, monospace: false);
panel.TextBox("Detail", () => selected?.Detail ?? agent?.Detail ?? "No provider detail yet.", _ => { }, lines: 12, acceptsReturn: false, monospace: true);
})
.Panel("State Graph", 456.0f, 222.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
{
panel.Section(leaves.Length > 0 ? "Leaves" : "Details");
foreach (var node in detailNodes)
{
panel.TreeItem(
CompactLabel(node.Label),
0,
canExpand: false,
isExpanded: static () => false,
setExpanded: static _ => { },
isSelected: () => selectedNodeId == node.Id,
select: () => selectedNodeId = node.Id,
detail: CompactDetail(node));
}
})
.Panel("Surface Detail", 994.0f, 82.0f, 520.0f, fadeWhenMouseDistant: true, panel =>
{
panel.Section(selected?.Kind ?? "detail");
panel.Readout("Node", () => selected?.Id ?? "none");
panel.Readout("Health", () => selected?.Health ?? "");
panel.TextBox("State", () => selected?.Detail ?? "No selected node.", _ => { }, lines: 18, acceptsReturn: false, monospace: true);
})
.Command("eve-status", _ => $"{connectionStatus}; {snapshot.ProviderId} v{snapshot.Version}; nodes {nodes.Count}; surface {snapshot.Surface?.Schema ?? "none"} root {snapshot.Surface?.Root?.Kind ?? "none"}", "Report the active Eve surface subscription.");
}
private void Reconnect()
{
connectionLifetime?.Cancel();
connectionLifetime = new CancellationTokenSource();
connectionTask = Task.Run(() => RunConnectionLoopAsync(connectionLifetime.Token));
}
private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
SetStatus("connecting");
using var socket = new ClientWebSocket();
await socket.ConnectAsync(new Uri(brokerUrl), cancellationToken).ConfigureAwait(false);
SetStatus("connected");
await SendOpenProviderAsync(socket, cancellationToken).ConfigureAwait(false);
var buffer = new byte[64 * 1024];
while (socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
var message = await ReceiveTextAsync(socket, buffer, cancellationToken).ConfigureAwait(false);
if (message is null)
{
break;
}
var next = EveSurfaceState.Parse(message);
if (!string.Equals(next.ProviderId, providerId, StringComparison.OrdinalIgnoreCase))
{
continue;
}
lock (sync)
{
state = next;
status = $"live {next.UpdatedAt}";
if (string.IsNullOrWhiteSpace(selectedNodeId))
{
selectedNodeId = next.SelectedNodeId;
}
}
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return;
}
catch (Exception exception)
{
SetStatus($"error {exception.Message}");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
}
}
}
private async Task SendOpenProviderAsync(ClientWebSocket socket, CancellationToken cancellationToken)
{
var payload = Encoding.UTF8.GetBytes($$"""{"type":"open-provider","providerId":"{{providerId}}"}""");
await socket.SendAsync(payload, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
private static async Task<string?> ReceiveTextAsync(ClientWebSocket socket, byte[] buffer, CancellationToken cancellationToken)
{
using var stream = new MemoryStream();
while (true)
{
var result = await socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{
return null;
}
stream.Write(buffer, 0, result.Count);
if (result.EndOfMessage)
{
return Encoding.UTF8.GetString(stream.ToArray());
}
}
}
private void SetStatus(string value)
{
lock (sync)
{
status = value;
}
}
private static string CompactLabel(string value)
{
var label = value.Replace('\n', ' ').Replace('\r', ' ').Trim();
return label.Length <= 72 ? label : label[..69] + "...";
}
private static string CompactDetail(EveSurfaceNode node)
{
var parts = new[] { node.Kind, node.Health, node.StatePath }
.Where(static value => !string.IsNullOrWhiteSpace(value));
return string.Join(" ", parts);
}
private static AquariumRenderPlan CreateRenderPlan()
{
var app = new AquariumApp();
var scene = app.RenderTargets.Hdr("scene");
app.Cameras.Perspective("main");
app.Features.Presentation(scene.Color);
app.Features.DirectWriteOverlay();
app.Debug.View("Scene", scene.Color);
return app.Plan;
}
}
public sealed class EveSurfaceRuntimeFactory : IAquariumRuntimeFactory
{
public IAquariumRuntime Create(AquariumRuntimeOptions options)
{
var runtime = new EveSurfaceRuntime();
runtime.SetOptions(options);
return runtime;
}
}
internal sealed record EveSurfaceState(
string ProviderId,
string Title,
long Version,
string UpdatedAt,
string SelectedNodeId,
IReadOnlyList<EveSurfaceNode> Nodes,
EveCompositionSurface? Surface)
{
public static EveSurfaceState Empty { get; } = new(
"",
"Eve Surface",
0,
"",
"",
[],
null);
public static EveSurfaceState Parse(string json)
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
var nodes = new List<EveSurfaceNode>();
if (root.TryGetProperty("nodes", out var nodeArray) && nodeArray.ValueKind == JsonValueKind.Array)
{
foreach (var node in nodeArray.EnumerateArray())
{
nodes.Add(new EveSurfaceNode(
ReadString(node, "id"),
ReadString(node, "label"),
ReadString(node, "kind"),
ReadDouble(node, "x"),
ReadDouble(node, "y"),
ReadDouble(node, "width"),
ReadDouble(node, "height"),
ReadString(node, "health"),
ReadString(node, "detail"),
ReadString(node, "statePath"),
ReadString(node, "avatarUrl")));
}
}
return new EveSurfaceState(
ReadString(root, "providerId"),
ReadString(root, "title", "Eve Surface"),
ReadInt64(root, "version"),
ReadString(root, "updatedAt"),
ReadString(root, "selectedNodeId"),
nodes,
ReadSurface(root));
}
private static EveCompositionSurface? ReadSurface(JsonElement root)
{
if (!root.TryGetProperty("surface", out var surface) || surface.ValueKind != JsonValueKind.Object)
{
return null;
}
return new EveCompositionSurface(
ReadString(surface, "schema"),
ReadString(surface, "id"),
ReadString(surface, "title"),
surface.TryGetProperty("root", out var rootElement) ? ReadElement(rootElement) : null,
surface.TryGetProperty("assets", out var assets) && assets.ValueKind == JsonValueKind.Array
? assets.EnumerateArray()
.Select(static asset => new EveCompositionAsset(
ReadString(asset, "id"),
ReadString(asset, "kind"),
ReadString(asset, "uri")))
.Where(static asset => !string.IsNullOrWhiteSpace(asset.Id))
.ToArray()
: []);
}
private static EveCompositionElement? ReadElement(JsonElement element)
{
if (element.ValueKind != JsonValueKind.Object)
{
return null;
}
var children = element.TryGetProperty("children", out var childArray) && childArray.ValueKind == JsonValueKind.Array
? childArray.EnumerateArray().Select(ReadElement).OfType<EveCompositionElement>().ToArray()
: [];
return new EveCompositionElement(
ReadString(element, "id"),
ReadString(element, "kind"),
ReadString(element, "role"),
ReadString(element, "text"),
ReadString(element, "assetRef"),
ReadString(element, "assetUri"),
ReadString(element, "bindNodeId"),
ReadString(element, "commandId"),
children);
}
private static string ReadString(JsonElement element, string property, string fallback = "")
{
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString() ?? fallback
: fallback;
}
private static long ReadInt64(JsonElement element, string property)
{
return element.TryGetProperty(property, out var value) && value.TryGetInt64(out var number)
? number
: 0;
}
private static double ReadDouble(JsonElement element, string property)
{
return element.TryGetProperty(property, out var value) && value.TryGetDouble(out var number)
? number
: 0.0;
}
}
internal sealed record EveSurfaceNode(
string Id,
string Label,
string Kind,
double X,
double Y,
double Width,
double Height,
string Health,
string Detail,
string StatePath,
string AvatarUrl);
internal sealed record EveCompositionSurface(
string Schema,
string Id,
string Title,
EveCompositionElement? Root,
IReadOnlyList<EveCompositionAsset> Assets);
internal sealed record EveCompositionElement(
string Id,
string Kind,
string Role,
string Text,
string AssetRef,
string AssetUri,
string BindNodeId,
string CommandId,
IReadOnlyList<EveCompositionElement> Children);
internal sealed record EveCompositionAsset(string Id, string Kind, string Uri);

View file

@ -17,6 +17,9 @@
<ItemGroup>
<Content Include="Shaders\**\*.hlsl" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\%(RecursiveDir)%(Filename)%(Extension)" />
<Content Include="Shaders\**\*.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\%(RecursiveDir)%(Filename)%(Extension)" />
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfCommon.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfCommon.hlsli" />
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfMath.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfMath.hlsli" />
<Content Include="..\Aquarium.Engine\Render\Shaders\D3D12SdfProxy.hlsli" CopyToOutputDirectory="PreserveNewest" TargetPath="Render\Shaders\D3D12SdfProxy.hlsli" />
<Content Include="Worlds\**\*.aquageo" CopyToOutputDirectory="PreserveNewest" TargetPath="Worlds\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

View file

@ -10,6 +10,20 @@ namespace Aquarium.Sample.Minimal;
public sealed class MinimalRuntime : IAquariumRuntime
{
private float timeSeconds;
private int selectedFeed;
private int selectedLut;
private bool feedEnabled = true;
private bool visible = true;
private bool locked;
private float opacity = 1.0f;
private float gain = 0.72f;
private float lutStrength = 1.0f;
private float x = -3.2f;
private float y = 2.1f;
private float z;
private float rotation;
private float scaleX = 2.4f;
private float scaleY = 0.8f;
public AquariumRuntimeOptions Options { get; private set; }
@ -17,7 +31,7 @@ public sealed class MinimalRuntime : IAquariumRuntime
public AquariumRenderPlan RenderPlan { get; } = CreateRenderPlan();
public AquariumUiDocument Ui { get; } = AquariumUiDocument.Empty;
public AquariumUiDocument Ui { get; }
public AquariumAudioDocument Audio { get; } = new();
@ -30,6 +44,11 @@ public sealed class MinimalRuntime : IAquariumRuntime
Vector2.Zero,
AquariumSceneState.Empty);
public MinimalRuntime()
{
Ui = CreateUi();
}
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input)
{
return frame;
@ -58,6 +77,119 @@ public sealed class MinimalRuntime : IAquariumRuntime
Options = options;
}
private AquariumUiDocument CreateUi()
{
var feeds = new[]
{
new AquariumUiOption(0, "PS3 Eye 0 high-speed"),
new AquariumUiOption(1, "Kiyo Pro RGB"),
new AquariumUiOption(2, "Synthetic checker field"),
};
var luts = new[]
{
new AquariumUiOption(0, "Neutral"),
new AquariumUiOption(1, "Thermal"),
new AquariumUiOption(2, "Depth debug"),
};
return new AquariumUiDocument()
.Surface("aquarium.ui_smoke", "CultUI Smoke", 16.0f, 56.0f, 980.0f, 620.0f, surface =>
{
surface.Horizontal("columns", columns =>
{
columns.Vertical("left", left =>
{
left.Pane("program", "Program", pane =>
{
pane.Options("feed", "Feed", () => selectedFeed, value => selectedFeed = value, feeds);
pane.Toggle("feed-enabled", "Visible", () => feedEnabled, value => feedEnabled = value);
pane.Slider("opacity", "Opacity", () => opacity, value => opacity = value, 0.0f, 1.0f, "0.00");
pane.Text("program-compose", () => $"1:{feeds[selectedFeed].Label}@{opacity:0.00} | LUT:{luts[selectedLut].Label}", "mono");
pane.Options("stem", "Stem", () => 0, _ => { }, [new AquariumUiOption(0, "Focusrite Input 1")]);
pane.Slider("gain", "Gain", () => gain, value => gain = value, 0.0f, 1.5f, "0.00");
pane.Options("lut", "LUT", () => selectedLut, value => selectedLut = value, luts);
pane.Slider("lut-strength", "LUT Strength", () => lutStrength, value => lutStrength = value, 0.0f, 1.0f, "0.00");
}, weight: 1.2f);
left.Pane("scene", "Scene", pane =>
{
pane.Options("editor-view", "Editor View", () => 0, _ => { }, [new AquariumUiOption(0, "Scene")]);
pane.Card("tree", card =>
{
card.Text("tree-0", "Scene", "strong");
card.Text("tree-1", " - [eye] Editor Camera <Camera>", "mono");
card.Text("tree-2", " - Sensor Feeds (4)", "mono");
card.Text("tree-3", " - [eye] Kiyo Pro RGB context <SensorFeed>", "mono");
card.Text("tree-4", " - [eye] Synthetic checker field <SensorFeed>", "mono");
});
pane.Row("scene-buttons", row =>
{
row.Button("previous", "Previous", () => selectedFeed = Math.Max(0, selectedFeed - 1));
row.Button("next", "Next", () => selectedFeed = Math.Min(feeds.Length - 1, selectedFeed + 1));
});
pane.Text("scene-status", () => $"SdfTextPanel pos={x:0.00},{y:0.00},{z:0.00} scale={scaleX:0.00},{scaleY:0.00}", "mono");
});
}, weight: 1.05f);
columns.Vertical("middle", middle =>
{
middle.Pane("sync", "Sync", pane =>
{
pane.Text("streams", "4 video / 4 audio buffers", "strong");
pane.Metric("sources", "sources", () => 5.0);
pane.Metric("ingested", "samples ingested", () => 533_939.0 + (timeSeconds * 120.0));
pane.Text("buffer-detail", "Focusrite Input 1 [asio-ch0]: 3751 1ch 192000Hz; passive mode waiting for program audio column", "body");
pane.Text("audio-sync", () => $"asio-ch0: {1484.0 + Math.Sin(timeSeconds) * 0.2:0.00} samples {779.2 + Math.Cos(timeSeconds) * 0.1:0.00} us", "mono");
pane.Text("chirplet", "chirplet reference: asio-ch1 passive; calibration emission command-ready", "body");
pane.Slider("perspective", "Perspective", () => 10.0f, _ => { }, 1.0f, 20.0f, "0.0x");
pane.Slider("angle", "Angle", () => 25.0f, _ => { }, 0.0f, 90.0f, "0.0 deg");
pane.Text("tube-budget", "40/40 age columns x 4/8 lanes x 4 subdivisions", "mono");
}, weight: 1.15f);
middle.Pane("transform", "Transform", pane =>
{
pane.Toggle("visible", "Visible", () => visible, value => visible = value);
pane.Toggle("locked", "Locked", () => locked, value => locked = value);
pane.Text("mode", "Mode: Grab", "mono");
pane.Slider("x", "X", () => x, value => x = value, -10.0f, 10.0f, "0.00");
pane.Slider("y", "Y", () => y, value => y = value, -10.0f, 10.0f, "0.00");
pane.Slider("z", "Z", () => z, value => z = value, -10.0f, 10.0f, "0.00");
pane.Slider("rotation", "Rotation", () => rotation, value => rotation = value, -180.0f, 180.0f, "0.00");
pane.Slider("scale-x", "Scale X", () => scaleX, value => scaleX = value, 0.1f, 4.0f, "0.00");
pane.Slider("scale-y", "Scale Y", () => scaleY, value => scaleY = value, 0.1f, 4.0f, "0.00");
pane.Row("transform-buttons", row =>
{
row.Button("reset-transform", "Reset Transform", ResetTransform);
row.Button("reset-camera", "Reset Camera", ResetTransform);
});
});
}, weight: 1.1f);
columns.Pane("create", "Create", pane =>
{
pane.Text("text-label", "Text", "body");
pane.Text("text-value", "Mimir", "strong");
pane.Button("add-sdf-text", "Add SDF Text", () => { });
pane.Text("model-path-label", "Model Path", "body");
pane.Text("model-path", "assets/models/example.glb", "mono");
pane.Button("import-model", "Import Model", () => { });
pane.Metric("fps", "ui smoke fps proxy", () => 1.0 / Math.Max(0.001, Math.Min(0.25, timeSeconds % 1.0)));
pane.Text("budget", "This pane is intentionally narrow enough to prove labels clip inside their cells instead of wandering across neighbors.", "body");
}, weight: 0.75f);
});
});
}
private void ResetTransform()
{
x = -3.2f;
y = 2.1f;
z = 0.0f;
rotation = 0.0f;
scaleX = 2.4f;
scaleY = 0.8f;
}
private static AquariumRenderPlan CreateRenderPlan()
{
var app = new AquariumApp();

View file

@ -12,6 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
<ProjectReference Include="..\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Aquarium.Engine.Tests")]

View file

@ -3,6 +3,8 @@ static const int ZYPHOS_GEOMETRY_BRUSH_LIMIT = 4;
#include "D3D12SdfCommon.hlsli"
#include "D3D12SdfMath.hlsli"
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
#include "D3D12ZyphosTerrain.hlsli"
float3 zyRotateZ(float3 p, float angle)
{
@ -28,19 +30,6 @@ float zyUmbrosEclipse(float3 starDirectionLocal)
return smoothstep(cos(umbrosAngularRadius * 1.45), cos(umbrosAngularRadius * 0.45), alignment);
}
float zySphericalField(float3 dir)
{
float latitude = asin(saturate(abs(dir.z)) * 2.0 - 1.0);
float longitude = atan2(dir.y, dir.x);
float plates =
sin(longitude * 2.0 + dir.z * 4.5) * 0.28 +
sin(longitude * 3.0 - dir.z * 7.0 + 1.7) * 0.22 +
sin((dir.x + dir.y * 0.7 + dir.z * 0.4) * 8.0) * 0.16 +
sin((dir.x * 1.3 - dir.y + dir.z * 1.9) * 13.0) * 0.08;
float equatorialWarmth = cos(latitude) * 0.18;
return 0.50 + plates + equatorialWarmth;
}
float zyHash21(float2 p)
{
p = frac(p * float2(123.34, 456.21));
@ -68,39 +57,21 @@ float zyCompactBrush(float2 delta, float2 radii, float rotation, float falloff,
float2 zyCubeFaceUv(float3 dir, out float face)
{
// Authored brush packets retain their legacy cube-ratio chart. Planetary
// Page addressing and patch geometry use GameCult.Geometry's QSC topology.
float3 a = abs(dir);
if (a.x >= a.y && a.x >= a.z)
{
if (dir.x >= 0.0)
{
face = 0.0;
return float2(-dir.z, dir.y) / max(a.x, 0.0001);
}
face = 1.0;
return float2(dir.z, dir.y) / max(a.x, 0.0001);
if (dir.x >= 0.0) { face = 0.0; return float2(-dir.z, dir.y) / max(a.x, 0.0001); }
face = 1.0; return float2(dir.z, dir.y) / max(a.x, 0.0001);
}
if (a.y >= a.z)
{
if (dir.y >= 0.0)
{
face = 2.0;
return float2(dir.x, -dir.z) / max(a.y, 0.0001);
}
face = 3.0;
return float2(dir.x, dir.z) / max(a.y, 0.0001);
if (dir.y >= 0.0) { face = 2.0; return float2(dir.x, -dir.z) / max(a.y, 0.0001); }
face = 3.0; return float2(dir.x, dir.z) / max(a.y, 0.0001);
}
if (dir.z >= 0.0)
{
face = 4.0;
return dir.xy / max(a.z, 0.0001);
}
face = 5.0;
return float2(-dir.x, dir.y) / max(a.z, 0.0001);
if (dir.z >= 0.0) { face = 4.0; return dir.xy / max(a.z, 0.0001); }
face = 5.0; return float2(-dir.x, dir.y) / max(a.z, 0.0001);
}
float2 zyTileBrushPlane(float2 faceUv, float level, float tileX, float tileY, out float inTile)
@ -224,11 +195,11 @@ float zyPebbleCluster(float3 dir)
return coast * smoothstep(0.48, 0.94, grains * 0.5 + 0.5);
}
float zyTerrainOffset(float3 dir, SdfObject sdfObject)
float zyTerrainBaseOffset(float3 dir, SdfObject sdfObject, out float land)
{
float field = zySphericalField(dir);
float seaLevel = sdfObject.state.z;
float land = smoothstep(seaLevel - 0.04, seaLevel + 0.08, field);
land = smoothstep(seaLevel - 0.04, seaLevel + 0.08, field);
float mountain = pow(saturate(field - seaLevel), 1.65);
float polarCap = pow(abs(dir.z), 8.0) * 0.035;
float authoredMaterial;
@ -237,13 +208,57 @@ float zyTerrainOffset(float3 dir, SdfObject sdfObject)
return (field - seaLevel) * 0.10 + mountain * 0.13 + polarCap * land + tileRelief + authoredGeometry * 0.52;
}
float zyTerrainOffset(float3 dir, SdfObject sdfObject)
{
float land; float baseOffset=zyTerrainBaseOffset(dir,sdfObject,land);
float4 pageHeightGradient; float2 pageMasks;
float erosionHeight=zyTrySampleErosionPage(dir,pageHeightGradient,pageMasks)?pageHeightGradient.x:0.0;
return baseOffset+erosionHeight*land;
}
float sdfDistance(float3 p, int sdfIndex)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
float planetRadius = max(sdfObject.state.x, 0.001);
float3 local = p - sdfObject.centerRadius.xyz;
float3 dir = zyPlanetDir(local, sdfObject);
return length(local) - (planetRadius + zyTerrainOffset(dir, sdfObject));
float radialDistance = length(local) - (planetRadius + zyTerrainOffset(dir, sdfObject));
return radialDistance * zyTerrainConservativeDistanceScale(dir);
}
float zyPlanetTraceBoundRadius(SdfObject sdfObject)
{
float legacyBound = max(sdfObject.centerRadius.w * 1.42, 0.001);
float erosionExtent=0.0; int pageCount=min((int)zy_planet_page_set[0].state.x,64);
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
{
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex]; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
if(metadata.state.x<0.5||summary.metadata.w<0.5)continue;
erosionExtent+=max(abs(summary.bounds.x),abs(summary.bounds.y))*saturate(metadata.state.y)+max(summary.bounds.w,0.0);
}
return max(legacyBound, max(sdfObject.state.x, 0.001) + erosionExtent);
}
float3 zyPlanetSurfaceNormal(float3 p, int sdfIndex)
{
SdfObject sdfObject=sdfObjects[sdfIndex]; float radius=max(sdfObject.state.x,0.001);
float3 local=p-sdfObject.centerRadius.xyz; float3 dir=zyPlanetDir(local,sdfObject);
float3 reference=abs(dir.z)<0.8?float3(0,0,1):float3(0,1,0);
float3 tangentX=normalize(cross(reference,dir)); float3 tangentY=normalize(cross(dir,tangentX));
float spacing=max(radius/8192.0,0.0005); float angular=spacing/radius;
float landCenter; zyTerrainBaseOffset(dir,sdfObject,landCenter);
float landXp,landXm,landYp,landYm;
float baseXp=zyTerrainBaseOffset(normalize(dir+tangentX*angular),sdfObject,landXp);
float baseXm=zyTerrainBaseOffset(normalize(dir-tangentX*angular),sdfObject,landXm);
float baseYp=zyTerrainBaseOffset(normalize(dir+tangentY*angular),sdfObject,landYp);
float baseYm=zyTerrainBaseOffset(normalize(dir-tangentY*angular),sdfObject,landYm);
float3 baseGradient=tangentX*((baseXp-baseXm)/(2.0*spacing))+tangentY*((baseYp-baseYm)/(2.0*spacing));
float3 landGradient=tangentX*((landXp-landXm)/(2.0*spacing))+tangentY*((landYp-landYm)/(2.0*spacing));
float4 erosion; float2 masks; erosion=0.0;
zyTrySampleErosionPage(dir,erosion,masks);
float3 erosionGradient=erosion.yzw;
float3 planetNormal=normalize(dir-(baseGradient+landCenter*erosionGradient+erosion.x*landGradient));
return normalize(zyRotateZ(planetNormal,sdfObject.state.y));
}
SdfSurface sdfSurface(float3 p, int sdfIndex)
@ -261,6 +276,9 @@ SdfSurface sdfSurface(float3 p, int sdfIndex)
float leafCluster = zyLeafCluster(dir);
float pebbleCluster = zyPebbleCluster(dir);
float polar = smoothstep(0.72, 0.92, abs(dir.z));
float4 erosionDifferential; float2 erosionMasks;
zyTrySampleErosionPage(dir,erosionDifferential,erosionMasks);
float ridge=erosionMasks.x, gully=erosionMasks.y;
float cloud = smoothstep(0.73, 0.91, sin(dir.x * 18.0 + dir.y * 13.0 + dir.z * 9.0 + timeSeconds * 0.19) * 0.5 + 0.5);
float3 ocean = lerp(float3(0.004, 0.070, 0.130), float3(0.010, 0.220, 0.330), saturate(field));
@ -273,18 +291,46 @@ SdfSurface sdfSurface(float3 p, int sdfIndex)
landColor = lerp(landColor, float3(0.78, 0.56, 0.30), saturate(authoredMaterial * 2.6 + authoredHeight * 5.0));
landColor = lerp(landColor, float3(0.025, 0.24, 0.08), leafCluster * 0.82);
landColor = lerp(landColor, float3(0.42, 0.39, 0.34), pebbleCluster * 0.65);
landColor = lerp(landColor, float3(0.49,0.44,0.36), ridge*0.34);
landColor = lerp(landColor, float3(0.035,0.16,0.11), gully*0.28);
SdfSurface surface;
surface.baseColor = lerp(ocean, landColor, land);
surface.baseColor = lerp(surface.baseColor, float3(0.78, 0.84, 0.80), cloud * 0.16);
surface.metallic = 0.0;
surface.roughness = lerp(0.34, 0.84, land);
surface.roughness = saturate(lerp(0.34,0.84,land)+ridge*0.09-gully*0.12);
surface.emission = 0.0;
surface.temporalDetail = saturate(authoredMaterial * 3.0);
surface.reservoirConfidence = saturate(0.42 + authoredMaterial * 2.2 + leafCluster * 0.25 + pebbleCluster * 0.20);
return surface;
}
bool zyTracePlanet(float3 origin,float3 direction,int sdfIndex,out float travel,out float3 normal,out SdfSurface surface,out float stepCount)
{
SdfObject sdfObject=sdfObjects[sdfIndex]; float boundRadius=zyPlanetTraceBoundRadius(sdfObject);
float3 oc=origin-sdfObject.centerRadius.xyz; float b=dot(oc,direction); float c=dot(oc,oc)-boundRadius*boundRadius;
float discriminant=b*b-c; normal=0.0; stepCount=0.0;
surface.baseColor=0.0; surface.metallic=0.0; surface.roughness=0.0; surface.emission=0.0; surface.temporalDetail=0.0; surface.reservoirConfidence=1.0;
if(discriminant<0.0){travel=farDistance+1.0;return false;}
float extent=sqrt(discriminant); float entry=max(-b-extent,0.0); float closest=max(-b,entry); float exit=min(-b+extent,farDistance);
if(entry>exit){travel=farDistance+1.0;return false;}
float low=entry; float lowDistance=sdfDistance(origin+direction*low,sdfIndex);
if(lowDistance<=0.0){travel=low;float3 p=origin+direction*travel;normal=zyPlanetSurfaceNormal(p,sdfIndex);surface=sdfSurface(p,sdfIndex);return true;}
float high=low; bool bracketed=false;
[unroll] for(int coarse=1;coarse<=24;coarse++)
{
high=lerp(entry,closest,(float)coarse/24.0); float highDistance=sdfDistance(origin+direction*high,sdfIndex); stepCount=(float)coarse;
if(highDistance<=0.0){bracketed=true;break;} low=high; lowDistance=highDistance;
}
if(!bracketed){travel=farDistance+1.0;return false;}
[unroll] for(int refine=0;refine<10;refine++)
{
float mid=(low+high)*0.5; float value=sdfDistance(origin+direction*mid,sdfIndex);
if(value<=0.0)high=mid;else low=mid;
}
travel=high; float3 p=origin+direction*travel; normal=zyPlanetSurfaceNormal(p,sdfIndex); surface=sdfSurface(p,sdfIndex); stepCount+=10.0; return true;
}
float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex, SdfSurface surface)
{
SdfObject sdfObject = sdfObjects[sdfIndex];
@ -310,4 +356,7 @@ float3 shadeSdf(float2 uv, float travel, float3 p, float3 normal, int sdfIndex,
return shadeSdfPbr(p, normal, surface) * daylight + city + atmosphere + eclipseTint;
}
#define SDF_TRACE_BOUND_RADIUS(sdfObject) zyPlanetTraceBoundRadius(sdfObject)
#define SDF_SURFACE_NORMAL(p,sdfIndex) zyPlanetSurfaceNormal(p,sdfIndex)
#define SDF_TRACE_FUNCTION zyTracePlanet
#include "D3D12SdfProxy.hlsli"

View file

@ -0,0 +1,156 @@
#ifndef D3D12_ZYPHOS_TERRAIN_HLSLI
#define D3D12_ZYPHOS_TERRAIN_HLSLI
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
struct ZyPlanetPageOutput { float4 height_gradient; float4 masks; };
struct ZyPlanetPageMetadata { float4 address; float4 layout; float4 bounds; float4 state; };
struct ZyPlanetPageSummary { float4 bounds; float4 metadata; };
struct ZyPlanetPageSet { float4 state; };
StructuredBuffer<ZyPlanetPageOutput> zy_planet_page : register(t81);
StructuredBuffer<ZyPlanetPageMetadata> zy_planet_page_metadata : register(t82);
StructuredBuffer<ZyPlanetPageSummary> zy_planet_page_summary : register(t83);
StructuredBuffer<ZyPlanetPageSet> zy_planet_page_set : register(t84);
static const float ZY_TERRAIN_NON_EROSION_SLOPE_BOUND = 8.0;
bool zyTerrainPageLocal(float3 dir, ZyPlanetPageMetadata metadata, out float2 local)
{
return gamecult_geometry_planetary_page_local(dir,metadata.address,metadata.state.x,local);
}
void zySampleErosionPage(int pageIndex, float2 local, out float4 heightGradient, out float2 masks)
{
ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
float storage=metadata.layout.y, interior=metadata.layout.z, border=metadata.layout.w;
float2 texel=local*(interior-1.0)+border; float2 base=floor(texel); float2 fraction=frac(texel);
int2 p0=(int2)clamp(base,0.0,storage-1.0); int2 p1=min(p0+1,(int2)(storage-1.0));
int offset=(int)metadata.layout.x;
ZyPlanetPageOutput a=zy_planet_page[offset+p0.y*(int)storage+p0.x], b=zy_planet_page[offset+p0.y*(int)storage+p1.x];
ZyPlanetPageOutput c=zy_planet_page[offset+p1.y*(int)storage+p0.x], d=zy_planet_page[offset+p1.y*(int)storage+p1.x];
GameCultGeometryPlanetaryPageSample ca,cb,cc,cd;
ca.height_gradient=a.height_gradient; ca.masks=a.masks.xy;
cb.height_gradient=b.height_gradient; cb.masks=b.masks.xy;
cc.height_gradient=c.height_gradient; cc.masks=c.masks.xy;
cd.height_gradient=d.height_gradient; cd.masks=d.masks.xy;
GameCultGeometryPlanetaryPageSample sample=gamecult_geometry_planetary_page_lerp(ca,cb,cc,cd,fraction);
heightGradient=sample.height_gradient; masks=sample.masks;
}
bool zyTrySampleErosionPage(float3 dir, out float4 heightGradient, out float2 masks)
{
heightGradient=0.0; masks=0.0; bool found=false;
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
{
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
if(!zyTerrainPageLocal(dir,metadata,local))continue;
float4 contribution; float2 pageMasks; zySampleErosionPage(pageIndex,local,contribution,pageMasks);
float blend=saturate(metadata.state.y); heightGradient+=contribution*blend;
masks=found?lerp(masks,pageMasks,blend):pageMasks; found=true;
}
return found;
}
bool zyTerrainDeepestPageEvidence(
float3 dir,
out ZyPlanetPageMetadata selectedMetadata,
out ZyPlanetPageSummary selectedSummary,
out float4 selectedHeightGradient,
out float2 selectedMasks)
{
selectedMetadata=(ZyPlanetPageMetadata)0;
selectedSummary=(ZyPlanetPageSummary)0;
selectedHeightGradient=0.0;
selectedMasks=0.0;
float selectedLevel=-1.0;
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
{
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex];
if(metadata.address.y<selectedLevel||summary.metadata.w<=0.5||!zyTerrainPageLocal(dir,metadata,local))continue;
selectedLevel=metadata.address.y;
selectedMetadata=metadata;
selectedSummary=summary;
zySampleErosionPage(pageIndex,local,selectedHeightGradient,selectedMasks);
}
return selectedLevel>=0.0;
}
float zyTerrainConservativeDistanceScale(float3 dir)
{
float erosionSlope=0.0; int pageCount=min((int)zy_planet_page_set[0].state.x,64);
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
{
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
if(zyTerrainPageLocal(dir,metadata,local)&&zy_planet_page_summary[pageIndex].metadata.w>0.5)
erosionSlope+=max(zy_planet_page_summary[pageIndex].bounds.z,0.0)*saturate(metadata.state.y);
}
float totalSlopeBound = ZY_TERRAIN_NON_EROSION_SLOPE_BOUND + max(erosionSlope, 0.0);
return rsqrt(1.0 + totalSlopeBound * totalSlopeBound);
}
float zyTerrainPatchCaptureMargin(float3 dir)
{
// The raster shell must contain unresolved relief between coarse vertices;
// otherwise the pixel refinement has no fragment on which to recover it.
float margin=0.03;
int pageCount=min((int)zy_planet_page_set[0].state.x,64);
[loop] for(int pageIndex=0;pageIndex<pageCount;pageIndex++)
{
float2 local; ZyPlanetPageMetadata metadata=zy_planet_page_metadata[pageIndex];
ZyPlanetPageSummary summary=zy_planet_page_summary[pageIndex];
if(!zyTerrainPageLocal(dir,metadata,local)||summary.metadata.w<=0.5)continue;
float blend=saturate(metadata.state.y);
margin+=max(abs(summary.bounds.x),abs(summary.bounds.y))*blend;
margin+=max(summary.bounds.w,0.0);
}
// Four 0.08 world-unit pixel corrections define the capture interval.
return min(margin,0.30);
}
float zySphericalField(float3 dir)
{
float latitude = asin(saturate(abs(dir.z)) * 2.0 - 1.0);
float longitude = atan2(dir.y, dir.x);
float plates = sin(longitude * 2.0 + dir.z * 4.5) * 0.28 + sin(longitude * 3.0 - dir.z * 7.0 + 1.7) * 0.22
+ sin((dir.x + dir.y * 0.7 + dir.z * 0.4) * 8.0) * 0.16 + sin((dir.x * 1.3 - dir.y + dir.z * 1.9) * 13.0) * 0.08;
return 0.50 + plates + cos(latitude) * 0.18;
}
float3 zySphericalFieldGradient(float3 dir)
{
const float epsilon = 0.0015;
float dx = zySphericalField(normalize(dir + float3(epsilon,0,0))) - zySphericalField(normalize(dir - float3(epsilon,0,0)));
float dy = zySphericalField(normalize(dir + float3(0,epsilon,0))) - zySphericalField(normalize(dir - float3(0,epsilon,0)));
float dz = zySphericalField(normalize(dir + float3(0,0,epsilon))) - zySphericalField(normalize(dir - float3(0,0,epsilon)));
float3 gradient = float3(dx,dy,dz)/(2.0*epsilon); return gradient-dir*dot(gradient,dir);
}
GameCultGeometryAdvancedErosionParameters zyErosionParameters(float radius)
{
GameCultGeometryAdvancedErosionParameters p;
p.scale=radius*0.075; p.strength=0.12; p.gully_weight=0.58; p.detail=1.45;
p.rounding=float4(0.1,0.015,0.1,2.0); p.onset=float4(1.25,1.25,2.8,1.5); p.assumed_slope=float2(0.7,0.85);
p.cell_scale=0.7; p.normalization=0.5; p.octaves=7; p.lacunarity=2.0; p.gain=0.5; return p;
}
GameCultGeometryPlanetarySurfaceSample zyAdvancedErosionSurface(float3 dir, float field, float radius, float sampleSpacing)
{
GameCultGeometryPlanetaryFieldDefinition definition;
definition.radius=radius; definition.seed=0; definition.erosion=zyErosionParameters(radius);
GameCultGeometryPlanetaryBaseFieldSample base_sample;
base_sample.radial_displacement=0.0; base_sample.radial_gradient=0.0;
base_sample.field_value=field; base_sample.field_gradient=zySphericalFieldGradient(dir)/radius;
base_sample.fade_target=saturate(field*0.5+0.5)*2.0-1.0;
return gamecult_geometry_planetary_field_sample(definition,dir,base_sample,sampleSpacing);
}
float4 zyAdvancedErosion(float3 dir, float field, float radius, float sampleSpacing)
{
GameCultGeometryPlanetarySurfaceSample sample=zyAdvancedErosionSurface(dir,field,radius,sampleSpacing);
return float4(sample.radial_displacement,sample.ridge,sample.gully,sample.unresolved_height_bound);
}
#endif

View file

@ -0,0 +1,30 @@
#include "D3D12ZyphosTerrain.hlsli"
struct PageInput { float4 direction_radius; float4 sampling; };
struct PageOutput { float4 height_gradient; float4 masks; };
StructuredBuffer<PageInput> page_inputs : register(t0);
RWStructuredBuffer<PageOutput> page_outputs : register(u0);
cbuffer PageConstants : register(b0) { uint page_sample_count; }
GameCultGeometryPlanetaryPageSample zyPageErosion(float3 direction, float radius, float spacing, float parent_spacing)
{
float3 dir=normalize(direction);
float field=zySphericalField(dir);
GameCultGeometryPlanetarySurfaceSample child=zyAdvancedErosionSurface(dir,field,radius,spacing);
GameCultGeometryPlanetarySurfaceSample parent=(GameCultGeometryPlanetarySurfaceSample)0;
bool has_parent=parent_spacing>0.0;
if(has_parent)parent=zyAdvancedErosionSurface(dir,field,radius,parent_spacing);
return gamecult_geometry_planetary_residual_sample(child,parent,has_parent);
}
[numthreads(64,1,1)]
void D3D12ZyphosTerrainPageCS(uint3 id : SV_DispatchThreadID)
{
if(id.x>=page_sample_count) return;
PageInput input=page_inputs[id.x]; float3 dir=normalize(input.direction_radius.xyz); float radius=input.direction_radius.w; float spacing=input.sampling.x; float parent_spacing=input.sampling.y;
GameCultGeometryPlanetaryPageSample sample=zyPageErosion(dir,radius,spacing,parent_spacing);
GameCultGeometryPlanetarySurfaceSample child=zyAdvancedErosionSurface(dir,zySphericalField(dir),radius,spacing);
page_outputs[id.x].height_gradient=sample.height_gradient;
page_outputs[id.x].masks=float4(sample.masks,0,child.unresolved_height_bound);
}

View file

@ -0,0 +1,21 @@
#include "GameCult.Geometry/GameCult.Geometry.hlsl"
struct PageOutput { float4 height_gradient; float4 masks; };
struct PageSummary { float4 bounds; float4 metadata; };
StructuredBuffer<PageOutput> summary_inputs : register(t0);
RWStructuredBuffer<PageSummary> summary_outputs : register(u0);
cbuffer PageConstants : register(b0) { uint page_sample_count; uint page_sample_offset; uint page_summary_index; }
[numthreads(1,1,1)]
void D3D12ZyphosTerrainPageSummaryCS(uint3 id : SV_DispatchThreadID)
{
if(id.x!=0 || page_sample_count==0) return;
float4 summary=gamecult_geometry_planetary_summary_empty();
[loop] for(uint index=0;index<page_sample_count;index++)
{
PageOutput sample=summary_inputs[page_sample_offset+index];
summary=gamecult_geometry_planetary_summary_accumulate(summary,sample.height_gradient,sample.masks.w);
}
summary_outputs[page_summary_index].bounds=summary;
summary_outputs[page_summary_index].metadata=float4(page_sample_count,page_sample_offset,page_summary_index,1);
}

View file

@ -0,0 +1,99 @@
#include "D3D12ZyphosPlanet.hlsl"
struct ZyPatchVertexOut
{
float4 position : SV_Position;
float3 worldPosition : TEXCOORD0;
};
struct ZyPatchSceneOut
{
float4 colorTravel:SV_Target0; float4 metadata:SV_Target1; float4 control:SV_Target2;
float4 reservoirGuide:SV_Target3; float overdraw:SV_Target4;
float depth:SV_Depth;
};
float3 zyPatchDirection(uint face,float2 local)
{
return gamecult_geometry_planetary_face_direction((int)face,local*2.0-1.0);
}
ZyPatchVertexOut D3D12ZyphosTerrainPatchVS(uint vertexId:SV_VertexID,uint instanceId:SV_InstanceID)
{
uint patchCells=(uint)max(zy_planet_page_set[0].state.y,1.0);
uint cell=vertexId/6u,corner=vertexId%6u; uint x=cell%patchCells,y=cell/patchCells;
uint2 offsets[6]={uint2(0,0),uint2(1,0),uint2(1,1),uint2(0,0),uint2(1,1),uint2(0,1)};
float2 local=(float2(x,y)+offsets[corner])/(float)patchCells;
ZyPlanetPageMetadata root=zy_planet_page_metadata[instanceId]; float3 dir=zyPatchDirection((uint)root.address.x,local);
SdfObject planet=sdfObjects[0]; float height=zyTerrainOffset(dir,planet); if(!isfinite(height))height=0.0;
float captureMargin=zyTerrainPatchCaptureMargin(dir);
float3 worldPosition=planet.centerRadius.xyz+zyRotateZ(dir,planet.state.y)*(planet.state.x+height+captureMargin);
float3 forward,right,up; cameraBasis(cameraPosition,cameraTarget,forward,right,up);
float3 delta=worldPosition-cameraPosition; float z=max(dot(delta,forward),0.0001);
float2 frustumMin=float2(cameraFrustumXy.x,cameraFrustumXy.z),frustumMax=float2(cameraFrustumXy.y,cameraFrustumXy.w);
float2 slope=float2(dot(delta,right),dot(delta,up))/z; float2 uv=(slope-frustumMin)/max(frustumMax-frustumMin,0.0001);
float2 clip=uv*float2(2.0,-2.0)+float2(-1.0,1.0);
ZyPatchVertexOut output; output.position=float4(clip,saturate(z/max(farDistance,0.0001)),1); output.worldPosition=worldPosition; return output;
}
ZyPatchSceneOut D3D12ZyphosTerrainPatchPS(ZyPatchVertexOut input)
{
SdfObject planet=sdfObjects[0]; float3 coarsePosition=input.worldPosition; float3 ray=normalize(coarsePosition-cameraPosition); float3 p=coarsePosition;
[unroll] for(int step=0;step<4;step++)
{
float3 local=p-planet.centerRadius.xyz; float3 dir=zyPlanetDir(local,planet);
float targetRadius=planet.state.x+zyTerrainOffset(dir,planet);
p=gamecult_geometry_planetary_radial_refinement_step(p,ray,planet.centerRadius.xyz,targetRadius,0.2,0.08);
}
if(!all(isfinite(p)))p=coarsePosition;
float travel=length(p-cameraPosition); if(!isfinite(travel)||travel<=0.0||travel>farDistance)discard;
float3 radial=normalize(p-planet.centerRadius.xyz);
float3 normal=cross(ddy(p),ddx(p));
if(!all(isfinite(normal))||dot(normal,normal)<1.0e-10)normal=radial; else normal=normalize(normal);
if(dot(normal,cameraPosition-p)<0.0)normal=-normal;
float3 terrainDir=zyPlanetDir(p-planet.centerRadius.xyz,planet);
ZyPlanetPageMetadata debugMetadata; ZyPlanetPageSummary debugSummary; float4 debugHeightGradient; float2 debugMasks;
bool hasDebugPage=zyTerrainDeepestPageEvidence(terrainDir,debugMetadata,debugSummary,debugHeightGradient,debugMasks);
float3 debugColor=0.0;
if(renderDebugMode>=21.5&&renderDebugMode<22.5)
debugColor=hasDebugPage?float3(debugMetadata.address.x/5.0,debugMetadata.address.y/5.0,saturate(debugMetadata.state.y)):float3(1,0,1);
else if(renderDebugMode>=22.5&&renderDebugMode<23.5)
debugColor=hasDebugPage?float3(saturate(0.5+debugHeightGradient.x*2.0),saturate(debugSummary.bounds.z*0.1),saturate(debugSummary.bounds.w*10.0)):float3(1,0,1);
else if(renderDebugMode>=23.5&&renderDebugMode<24.5)
debugColor=abs(normal);
else if(renderDebugMode>=24.5&&renderDebugMode<25.5)
debugColor=hasDebugPage?float3(saturate(debugMasks),saturate(debugMetadata.state.y)):float3(1,0,1);
else if(renderDebugMode>=25.5&&renderDebugMode<26.5)
{
float3 absoluteDirection=abs(terrainDir);
float dominant=max(absoluteDirection.x,max(absoluteDirection.y,absoluteDirection.z));
float second=absoluteDirection.x+absoluteDirection.y+absoluteDirection.z-dominant-min(absoluteDirection.x,min(absoluteDirection.y,absoluteDirection.z));
float3 dominantAxis=absoluteDirection.x>=absoluteDirection.y&&absoluteDirection.x>=absoluteDirection.z?float3(sign(terrainDir.x),0,0):absoluteDirection.y>=absoluteDirection.z?float3(0,sign(terrainDir.y),0):float3(0,0,sign(terrainDir.z));
float3 secondaryAxis=absoluteDirection.x<dominant&&absoluteDirection.x>=min(absoluteDirection.y,absoluteDirection.z)?float3(sign(terrainDir.x),0,0):absoluteDirection.y<dominant&&absoluteDirection.y>=min(absoluteDirection.x,absoluteDirection.z)?float3(0,sign(terrainDir.y),0):float3(0,0,sign(terrainDir.z));
float3 crossDirection=normalize(secondaryAxis-dominantAxis*dot(secondaryAxis,terrainDir));
float4 sideA,sideB; float2 sideMasks; bool hasA=zyTrySampleErosionPage(normalize(terrainDir-crossDirection*0.001),sideA,sideMasks); bool hasB=zyTrySampleErosionPage(normalize(terrainDir+crossDirection*0.001),sideB,sideMasks);
float edgeProximity=1.0-saturate((dominant-second)*100.0);
debugColor=float3(saturate(abs(sideA.x-sideB.x)*100.0)*edgeProximity,edgeProximity,(hasA&&hasB)?0.0:1.0);
}
else if(renderDebugMode>=26.5&&renderDebugMode<27.5)
{
float4 pageField; float2 pageMasks; bool hasPage=zyTrySampleErosionPage(terrainDir,pageField,pageMasks);
float directHeight=hasDebugPage?zyAdvancedErosion(terrainDir,zySphericalField(terrainDir),planet.state.x,max(debugMetadata.state.w,1.0e-5)).x:0.0;
float error=abs(directHeight-pageField.x);
float bound=hasDebugPage?max(debugSummary.bounds.w,1.0e-5):1.0;
debugColor=hasPage?float3(saturate(error/bound),saturate(error*100.0),error<=bound?0.0:1.0):float3(1,0,1);
}
else if(renderDebugMode>=27.5&&renderDebugMode<28.5)
{
float spacing=hasDebugPage?max(debugMetadata.state.w,1.0e-6):1.0;
float wavelengthRatio=hasDebugPage?saturate(log2(max(planet.state.x,0.001)/spacing)/16.0):0.0;
debugColor=float3(frac(zy_planet_page_set[0].state.z),wavelengthRatio,hasDebugPage?saturate(debugMetadata.address.y/5.0):0.0);
}
SdfSurface surface=sdfSurface(p,0);
float3 shaded=(renderDebugMode>=21.5&&renderDebugMode<28.5)?debugColor:shadeSdf(0.0,travel,p,normal,0,surface);
ZyPatchSceneOut output; output.colorTravel=float4(shaded,min(travel,farDistance+1.0));
output.metadata=float4(FIELD_ID_SDF_OBJECT_BASE,normal); output.control=float4(1,4.0/384.0,saturate(surface.temporalDetail),saturate(surface.reservoirConfidence));
float3 forward,right,up; cameraBasis(cameraPosition,cameraTarget,forward,right,up);
output.reservoirGuide=float4(saturate(surface.reservoirConfidence),0,1,0); output.overdraw=1;
output.depth=saturate(dot(p-cameraPosition,forward)/max(farDistance,0.0001)); return output;
}

View file

@ -0,0 +1,104 @@
using System.Numerics;
using Aquarium.Engine.Render;
using CultMath;
using GameCult.Geometry;
namespace Aquarium.Zyphos;
internal static class ZyphosPlanetarySurfacePages
{
private const int InteriorSize = 17;
private const int BorderSize = 2;
private const int MaximumLevel = 5;
private const float ArrivalSeconds = 0.35f;
private static readonly Dictionary<ulong, CachedPageContent> Content = [];
private static readonly PlanetaryResidualResidency Residency = new();
internal static void Reset()
{
Residency.Reset();
}
internal static void PrimeForCapture(Vector3 cameraPosition, float pageAgeSeconds)
{
Reset();
_ = ForCamera(cameraPosition, -Math.Max(pageAgeSeconds, 0.0f));
}
internal static void PrimeEvictionForCapture(Vector3 cameraPosition, float pageAgeSeconds)
{
var center=ZyphosUmbrosSystem.ZyphosCenter;
var relative=cameraPosition-center;
var oldDirection=Vector3.Normalize(new Vector3(relative.Z,relative.Y,-relative.X));
var oldCamera=center+oldDirection*Math.Max(relative.Length(),0.001f);
Reset();
_=ForCamera(oldCamera,-1.0f);
_=ForCamera(oldCamera,-0.65f);
_=ForCamera(cameraPosition,-Math.Max(pageAgeSeconds,0.0f));
}
public static AquariumPlanetarySurfacePageSet ForCamera(Vector3 cameraPosition, float timeSeconds)
{
var relative = cameraPosition - ZyphosUmbrosSystem.ZyphosCenter;
var distance = Math.Max(relative.Length(), 0.001f);
var direction = relative / distance;
var field = FieldDefinition(ZyphosUmbrosSystem.ZyphosSurfaceRadius);
var lod = new PlanetaryLodParameters(MaximumLevel, InteriorSize, BorderSize, 720, field.Radius / 65536);
var desired = PlanetaryLodSelector.SelectAncestorChain(field, (float3)direction, distance, lod);
var snapshot = Residency.Update(desired, timeSeconds, ArrivalSeconds);
var pages = snapshot.Tiles.Select(resident => BuildPage(resident.Tile, resident.Blend)).ToArray();
return new AquariumPlanetarySurfacePageSet
{
GeneratorEntryPoint = "D3D12ZyphosTerrainPageCS",
ContentVersion = unchecked((long)snapshot.ContentVersion),
PresentationVersion = unchecked((long)snapshot.PresentationVersion),
Pages = pages,
RenderCoarsePatches = true,
PatchCells = 64,
};
}
private static AquariumPlanetarySurfacePage BuildPage(PlanetaryTileAddress tile, float blend)
{
var layout = new PlanetaryPageLayout(tile, InteriorSize, BorderSize);
var key = tile.StableKey;
var radius = ZyphosUmbrosSystem.ZyphosSurfaceRadius;
if (!Content.TryGetValue(key, out var cached))
{
var common = PlanetaryGpuPageBuilder.BuildContent(layout, radius);
var samples = common.Inputs.Select(input => new AquariumPlanetarySurfacePageInput(
ToNumerics(input.DirectionRadius), ToNumerics(input.Sampling))).ToArray();
cached = new(common, samples);
Content[key] = cached;
}
var metadata = PlanetaryGpuPageBuilder.Metadata(cached.Common, 0, blend);
return new AquariumPlanetarySurfacePage
{
ContentKey = unchecked((long)key),
Samples = cached.Samples,
Metadata = new AquariumPlanetarySurfacePageMetadata(
ToNumerics(metadata.Address),
ToNumerics(metadata.Layout),
ToNumerics(metadata.Bounds),
ToNumerics(metadata.State)),
};
}
private static Vector4 ToNumerics(float4 value) => new(value.x, value.y, value.z, value.w);
private sealed record CachedPageContent(
PlanetaryGpuPageContent Common,
AquariumPlanetarySurfacePageInput[] Samples);
private static PlanetaryFieldDefinition FieldDefinition(float radius)
{
var erosion = new AdvancedErosionParameters(
radius * 0.075f, 0.12f, 0.58f, 1.45f,
new float4(0.1f, 0.015f, 0.1f, 2.0f),
new float4(1.25f, 1.25f, 2.8f, 1.5f),
new float2(0.7f, 0.85f), 0.7f, 0.5f, 7, 2.0f, 0.5f);
return new(1, radius, 0, erosion);
}
}

View file

@ -14,6 +14,11 @@ public static class ZyphosRenderPlan
var app = new AquariumApp();
app.Shaders
.Core("D3D12HeightField.hlsl", "D3D12Scene.hlsl", "D3D12Post.hlsl")
.PlanetarySurfacePage("D3D12ZyphosTerrainPage.hlsl")
.PlanetarySurfacePageSummary("D3D12ZyphosTerrainPageSummary.hlsl")
.PlanetarySurfacePatch("D3D12ZyphosTerrainPatch.hlsl")
.SdfShader("D3D12ZyphosPlanet.hlsl")
.SdfShader("D3D12ZyphosUmbros.hlsl")
.SdfShader("D3D12ZyphosStar.hlsl");
var heightField = app.RenderTargets.Create("height-field")

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System.Globalization;
using Aquarium.Engine;
using Aquarium.Engine.Audio;
using Aquarium.Engine.Input;
@ -70,13 +71,53 @@ public sealed class ZyphosRuntime : IAquariumRuntime
shot.CameraTarget,
timeSeconds,
Vector2.Zero,
ZyphosSceneBuilder.Build(timeSeconds, previousTimeSeconds, fractalPlan));
ZyphosSceneBuilder.Build(timeSeconds, previousTimeSeconds, shot.CameraPosition, fractalPlan));
}
}
public ZyphosRuntime()
{
Ui = new AquariumUiDocument()
var captureDomain = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_DOMAIN");
if (!string.IsNullOrWhiteSpace(captureDomain))
{
var domain = ZyphosSpatialDomainCatalog.Domains.FirstOrDefault(candidate =>
string.Equals(candidate.Key.Value, captureDomain, StringComparison.OrdinalIgnoreCase));
if (domain is not null)
{
selectedDomainKey = domain.Key;
autoOrbit = false;
}
}
if (float.TryParse(
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_ORBIT_DISTANCE"),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var captureDistance))
{
orbitDistance = ClampOrbitDistance(captureDistance);
autoOrbit = false;
}
if (float.TryParse(
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_PAGE_AGE_SECONDS"),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var pageAgeSeconds))
{
timeScale = 0.0f;
autoOrbit = false;
ZyphosPlanetarySurfacePages.PrimeForCapture(CurrentShot().CameraPosition, pageAgeSeconds);
}
if (float.TryParse(
Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_EVICTION_AGE_SECONDS"),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var evictionAgeSeconds))
{
timeScale = 0.0f;
autoOrbit = false;
ZyphosPlanetarySurfacePages.PrimeEvictionForCapture(CurrentShot().CameraPosition, evictionAgeSeconds);
}
var ui = new AquariumUiDocument()
.Panel("Zyphos", 18.0f, 82.0f, 340.0f, panel =>
{
panel.Section("Planetary Demo");
@ -119,6 +160,7 @@ public sealed class ZyphosRuntime : IAquariumRuntime
.Command("zyphos-fractal", _ => ZyphosFractalTerrain.DebugDump, "Dump the compiled Zyphos fractal terrain grammar.")
.Command("zyphos-fractal-plan", _ => ZyphosFractalTerrain.BuildPlanDebugDump(ZyphosFractalTerrain.BuildRenderPlan(CurrentShot())), "Dump the current Zyphos fractal selected cut and resource budget.")
.Command("zyphos-system", _ => $"Zyphos-Umbros: separation {ZyphosUmbrosSystem.SeparationInZyphosRadii:0.0} Zyphos radii, Umbros radius {ZyphosUmbrosSystem.UmbrosRadiusRatio:0.00} Zyphos, apparent diameter {ZyphosUmbrosSystem.UmbrosAngularDiameterDegrees:0.0} degrees.", "Report the modeled Zyphos/Umbros/star baseline.");
Ui = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_HIDE_UI") == "1" ? AquariumUiDocument.Empty : ui;
}
public AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input)
@ -129,6 +171,8 @@ public sealed class ZyphosRuntime : IAquariumRuntime
public void Start()
{
Console.WriteLine("Zyphos planetary demo booted.");
var shot=CurrentShot();
Console.WriteLine($"Zyphos camera: position {shot.CameraPosition}; target {shot.CameraTarget}; planet distance {Vector3.Distance(shot.CameraPosition,ZyphosUmbrosSystem.ZyphosCenter):0.###}");
}
public void Update(float deltaSeconds, InputState input)

View file

@ -6,21 +6,22 @@ namespace Aquarium.Zyphos;
public static class ZyphosSceneBuilder
{
public static AquariumSceneState Build(float timeSeconds, float previousTimeSeconds, ZyphosFractalRenderPlan fractalPlan)
=> Build(timeSeconds, previousTimeSeconds, ZyphosUmbrosSystem.ZyphosCenter + Vector3.UnitZ * 12.0f, fractalPlan);
public static AquariumSceneState Build(float timeSeconds, float previousTimeSeconds, Vector3 cameraPosition, ZyphosFractalRenderPlan fractalPlan)
{
return new AquariumSceneState
{
TraceHeightFieldSurface = false,
UseStarfieldBackground = true,
HeightFieldBrushes = fractalPlan.HeightBrushes,
FractalReservoirField = new AquariumFractalReservoirField
{
SplatCount = 2_000_000,
SplatUpdatesPerFrame = 50_000,
ReservoirUpdatesPerPass = 20_000,
WorldCenterRadius = new Vector4(ZyphosUmbrosSystem.ZyphosCenter, ZyphosUmbrosSystem.ZyphosSurfaceRadius),
PriorityFocus = fractalPlan.ReservoirPriorityFocus,
ProgramTransforms = fractalPlan.GpuProgramTransforms,
},
// Planet visibility belongs to the page-backed raster patches.
// The fractal grammar still feeds authored terrain inputs, but its
// legacy splats may not independently repaint the same surface.
FractalReservoirField = AquariumFractalReservoirField.Empty,
PlanetarySurfacePages = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_DISABLE_PLANETARY_PAGES") == "1"
? AquariumPlanetarySurfacePageSet.Empty
: ZyphosPlanetarySurfacePages.ForCamera(cameraPosition, timeSeconds),
SdfObjects = BuildSdfObjects(timeSeconds, previousTimeSeconds),
SdfLights = BuildSdfLights(timeSeconds),
};
@ -29,10 +30,22 @@ public static class ZyphosSceneBuilder
private static AquariumSdfObject[] BuildSdfObjects(float timeSeconds, float previousTimeSeconds)
{
var rotation = ZyphosUmbrosSystem.MutualPhase(timeSeconds);
var previousRotation = ZyphosUmbrosSystem.MutualPhase(previousTimeSeconds);
var umbrosCenter = ZyphosUmbrosSystem.UmbrosCenter(timeSeconds);
var previousUmbrosCenter = ZyphosUmbrosSystem.UmbrosCenter(previousTimeSeconds);
var starCenter = ZyphosUmbrosSystem.PrimaryStarCenter(timeSeconds);
var previousStarCenter = ZyphosUmbrosSystem.PrimaryStarCenter(previousTimeSeconds);
var objects = new AquariumSdfObject[ZyphosRenderPlan.SdfObjectCount];
var planetBoundRadius = Environment.GetEnvironmentVariable("AQUARIUM_ZYPHOS_ENABLE_PLANET_SDF_ORACLE") == "1" ? ZyphosUmbrosSystem.ZyphosBoundRadius : 0.0f;
objects[ZyphosRenderPlan.PlanetIndex] = new AquariumSdfObject(
new Vector4(ZyphosUmbrosSystem.ZyphosCenter, planetBoundRadius),
new Vector4(ZyphosUmbrosSystem.ZyphosCenter, previousRotation),
new Vector4(ZyphosUmbrosSystem.ZyphosSurfaceRadius, rotation, ZyphosUmbrosSystem.SeaLevel, rotation));
objects[ZyphosRenderPlan.UmbrosIndex] = new AquariumSdfObject(
new Vector4(umbrosCenter, ZyphosUmbrosSystem.UmbrosBoundRadius),
new Vector4(previousUmbrosCenter, 0.0f),
new Vector4(ZyphosUmbrosSystem.UmbrosSurfaceRadius, rotation, 0.0f, 0.0f));
objects[ZyphosRenderPlan.StarIndex] = new AquariumSdfObject(
new Vector4(starCenter, ZyphosUmbrosSystem.PrimaryStarVisualRadius),
new Vector4(previousStarCenter, 0.0f),

165
src/Saga/README.md Normal file
View file

@ -0,0 +1,165 @@
# Saga
**Planetary flow middleware for worlds that refuse to sit still.**
Saga is Fensalir's stochastic planetary flow organ: a cube-sphere native system
for streaming atmosphere, ocean, gas, debris, heat, and other transport fields
across whole planets at arbitrary level of detail.
It is built for studios that want living planets without hand-authoring a fake
weather loop, baking one heroic texture atlas, or stapling a particle system to
a skybox and calling it climate. Saga turns terrain, bathymetry, atmospheric
state, ocean reanalysis, arbitrary forcing sources, and learned stochastic
transport into compact flow packets the engine can stream, inspect, simulate,
and render.
No poles. No flat-Earth texture tricks. No private renderer arrows pretending
to be physics. Just a planet-scale flow field with provenance, uncertainty, and
enough teeth to survive a camera dive from orbit to coastline.
## The Pitch
Modern games and simulation tools can render planets. They rarely make them
move like planets.
Saga is the missing middleware layer between static world geometry and dynamic
planetary behavior:
```text
planet domain + relief + state + forcing + learned transport
-> stochastic advection packets
-> streaming flow pages, particles, weather, debris, clouds, currents, debug
```
Use it for:
- ocean surface currents and debris drift;
- atmospheric wind fields and gas-giant bands;
- volcanic, nuclear, magical, artificial, or stellar energy injection;
- multi-sun lighting and thermal forcing;
- tides, rotation, Coriolis-dominated flow, and exotic planetary regimes;
- gameplay-visible particles, clouds, smoke, spores, ash, foam, or pollutants;
- offline worldbuilding, cinematic previs, and realtime renderer guidance.
Saga is not a weather forecast API wearing a costume. It is a reusable engine
organ for representing and streaming stochastic flow over Fensalir's planetary
domains.
## What Makes It Different
### Cube-Sphere Native
Saga speaks Fensalir's optimized cube-sphere address space directly. Lat/lon
grids are source formats, not runtime truth. Every packet belongs to a tile,
LOD, projection, local tangent frame, and provenance chain.
### Stochastic By Design
Real flow is not one obedient arrow. Saga packets carry mean transport,
diffusivity/covariance, confidence, and optional transition kernels. Renderers
can draw clean vectors; simulations can keep the uncertainty instead of
throwing it into the sea and acting surprised later.
### Arbitrary Forcing
The evaluator is not Earth-locked. Saga conditions on explicit rotation and
Coriolis features plus variable-cardinality energy, mass, and momentum forcing
sources:
- one sun, binary suns, or no sun worth trusting;
- moons and tidal drivers;
- eruptions, impacts, reactors, orbital mirrors, industrial heat, spellstorms;
- gas-giant internal heat and extreme atmosphere bands;
- transient shock events and persistent planetary gradients.
Source count is data, not architecture.
### Built For Streaming
Saga is designed for camera-relative planet rendering:
- coarse flow far from the camera;
- refined flow where the player, shot, simulation, or uncertainty demands it;
- parent/child LOD consistency;
- cross-face seam continuity;
- explicit stale-page and model-version checks.
The renderer samples packets. It does not invent climate in a pixel shader
because the schedule got nervous.
## Architecture
Saga lives inside Fensalir because Fensalir already owns the machinery it needs:
- CultMath `PlanetaryTileAddress` and cube-sphere projection math;
- field evidence layers, especially `Transport`;
- selected cuts and residency planning;
- GPU resource pages and debug telemetry;
- renderer/simulation lowering boundaries.
Saga's live shape:
```text
source manifests
-> cube-sphere dataset bakes
-> training shards
-> FlowNet-S / FlowNet-M / optional FlowNet-T
-> Saga stochastic advection packets
-> Fensalir Transport field claims
-> GPU pages, particle advection, weather visuals, debug probes
```
## Model Line
Saga uses a staged neural architecture plan:
- `FlowNet-S`: 8-15M parameters, local baseline for ingestion, calibration, and
export.
- `FlowNet-M`: 40-60M parameters, first serious runtime/distillation model with
local tile encoders, a cube-sphere graph processor, and stochastic heads.
- `FlowNet-T`: 150-300M parameters, optional global teacher for offline
distillation and ensemble behavior.
The model emits stochastic advection parameters, not just velocity:
- tangent mean velocity;
- positive-semidefinite 2x2 covariance/diffusivity;
- calibrated confidence;
- optional neighbor transition logits;
- provenance and model identity.
## Data Spine
Saga's first research targets:
- ETOPO / GEBCO for relief and bathymetry;
- ERA5 for atmosphere;
- GLORYS / OSCAR / HYCOM candidates for ocean currents;
- PlasticAdrift and drifter transition matrices for stochastic debris
transport validation;
- synthetic analytic worlds for contract and seam tests before the ocean gets
expensive.
Every source needs a manifest. No manifest, no training shard. The altar wants
receipts.
## First Vertical Slice
1. Define the Saga packet contracts.
2. Bake a synthetic cube-sphere flow world.
3. Export Transport field pages.
4. Render/debug vectors, covariance ellipses, confidence, and selected LOD.
5. Train FlowNet-S on synthetic + tiny ERA5 wind.
6. Add ocean surface current targets.
7. Add PlasticAdrift transition validation.
8. Promote FlowNet-M when the baseline has failed in useful, measured ways.
## Boundary
Saga owns reusable planetary flow mechanics. Clients own meaning.
Fensalir clients may ask for currents, winds, debris drift, cloud motion, ash
transport, or gas-giant bands. They may not create a private flow authority that
bypasses Saga packets and Fensalir Transport claims.
That is the deal: the planet can be wild, but the authority has to be clean.

15
src/Saga/Saga.csproj Normal file
View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
<ProjectReference Include="..\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

66
src/Saga/SagaOrgan.cs Normal file
View file

@ -0,0 +1,66 @@
using CultMath;
using GameCult.Geometry;
using System.Numerics;
namespace Saga;
public static class SagaOrgan
{
public const string Name = "Saga";
public const string Description =
"Cube-sphere stochastic planetary flow for Fensalir Transport fields.";
}
public readonly record struct SagaFlowDomain(
string DomainId,
double PlanetRadiusMeters,
string ProjectionId,
string GridVersion);
public readonly record struct SagaFlowRequest(
SagaFlowDomain Domain,
PlanetaryTileAddress Tile,
int SampleResolution,
double TimeSeconds,
SagaCoriolisFrame Coriolis,
IReadOnlyList<SagaForcingSource> ForcingSources);
public readonly record struct SagaCoriolisFrame(
Vector3 RotationAxis,
double AngularVelocityRadiansPerSecond,
double LocalF,
Vector2 LocalBeta);
public readonly record struct SagaForcingSource(
string SourceId,
SagaForcingKind Kind,
SagaForcingGeometry Geometry,
Vector3 PlanetDirection,
Vector3 LocalTangentDirection,
double Intensity,
double RadiusMeters,
double Attenuation,
double TimePhase);
public readonly record struct SagaAdvectionSample(
Vector2 MeanVelocity,
SagaCovariance2D Diffusivity,
double Confidence);
public readonly record struct SagaCovariance2D(double C00, double C01, double C11);
public enum SagaForcingKind
{
Energy,
Mass,
Momentum,
}
public enum SagaForcingGeometry
{
Directional,
Point,
Area,
Volume,
}

View file

@ -305,7 +305,38 @@
{"timestamp":"2026-05-30T01:40:00+01:00","topic":"reservoir-budget-pressure-probe","lesson":"The reservoir quality harness now converts same-time higher-work-grid reference error into per-tile budget pressure without making the probe a runtime owner. measure-reservoir-budget-pressure.ps1 reads a sequence manifest, compares a candidate final capture against the native-domain reference final capture at the same ready frame, applies disocclusion/rejection masks when present, and ranks fixed tiles by mean reference error times mask coverage. On reservoir-budget-pressure-20260530-001046.* the probe measured 1306/42864 pixels and found the top 5/50 tiles carried 99.3305% of measured reference-error delta, giving a concrete target for adaptive update policy while preserving the no-native-resolution-reservoir invariant.","files":["scripts/measure-reservoir-budget-pressure.ps1","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-budget-pressure-20260530-001046.*"}
{"timestamp":"2026-05-30T01:58:00+01:00","topic":"reservoir-spatial-reuse-budget","lesson":"The renderer now has a first runtime sampling budget inside the reservoir history update owner. GraphicsSettings.FieldReservoirSpatialReuseBudget flows through UI, runtime options, CLI/env overrides, capture scripts, frame constants, and D3D12ReservoirHistoryUpdateCS. Current and temporal rows are still written for every reservoir work-grid pixel; only the 3x3 spatial neighbor reuse pass is spent on the deterministic budget phase. The default is 0.5, so the machine combines half presentation grid reservoir surfaces with half spatial reuse sampling by default. Smoke reservoir-sequence-20260530-003006-* compared native scale 0.5/spatial budget 0.5 against reference scale 0.75/spatial budget 1.0; masked reference-error-native covered 3.8261% of frame pixels, and reservoir-budget-pressure-20260530-003006.* found the top 5/50 tiles carried 90.8321% of measured reference-error delta.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/AquariumHost.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","scripts/capture-fensalir-frame.ps1","scripts/capture-reservoir-mode-sequence.ps1","tests/Aquarium.Engine.Tests/FieldReservoirAbiTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-sequence-20260530-003006-*"}
{"timestamp":"2026-05-30T02:10:00+01:00","topic":"reservoir-spatial-budget-debug","lesson":"The spatial reuse sampling budget now has a visible owner-layer probe. Render debug mode 20, Reservoir Spatial Budget, colors work-grid pixels that spend 3x3 spatial reuse green and skipped pixels blue; capture-reservoir-mode-sequence.ps1 names the mode spatial-budget. Smoke reservoir-sequence-20260530-003746-* captured the debug view at native scale 0.5/spatial budget 0.5. A bitmap probe on the 304x141 present output counted 32078 green pixels, 6818 blue pixels, and 3968 filtering/other pixels, proving the budget branch is observable where the user sees it.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","scripts/capture-reservoir-mode-sequence.ps1","tests/Aquarium.Engine.Tests/FieldReservoirAbiTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml","state/scratch.md"],"source":"artifacts/fensalir-captures/reservoir-sequence-20260530-003746-*"}
{"timestamp":"2026-05-30T22:02:00+01:00","topic":"d3d12-resize-reference-release","lesson":"The Fensalir resize crash was a hidden-reference ownership bug in the swapchain path. Resize disposed DirectWrite overlays and D3D11-on-12 wrapped backbuffers, but the D3D11 immediate context could still retain references unless it was explicitly cleared and flushed before IDXGISwapChain::ResizeBuffers. Resize now clears/flushes the overlay context and drops program-output shared textures before recreating size-dependent surfaces. A visible dev slot survived seven programmatic window resizes and logged target rebuilds without stderr.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","tests/Aquarium.Engine.Tests/D3D12ResizeTests.cs","state/scratch.md","state/map.yaml"],"source":"artifacts/dev-reload/latest.out.log"}
{"timestamp":"2026-05-31T02:58:00+01:00","topic":"eve-direct2d-client-surface","lesson":"Fensalir's DirectWrite/Direct2D overlay is a first-class Eve client surface. It should consume shared Eve/CultMesh UI documents and lower them into AquariumUiDocument, while provider repos keep ownership of dashboard state, accepted commands, and app-specific semantics.","files":["docs/eve-direct2d-client-surface.md","README.md","docs/aquarium-engine-doctrine.md","state/map.yaml"],"source":"Eve cross-client surface cut"}
{"timestamp":"2026-05-30T22:24:00+01:00","topic":"reservoir-reconstruction-loss-curve","lesson":"Reservoir superresolution quality now has a curve metric instead of only smoke deltas. measure-reservoir-loss-curve.ps1 reads candidate/reference sequence manifests and reports MAE, MSE, RMSE, PSNR, changed pixels, max channel delta, and optional disocclusion/rejection-masked loss. The 320x180 smoke sweep reservoir-sequence-20260530-221043-* compared native scale 0.5/spatial budget 0.5 against reference scale 0.75/spatial budget 1.0 at ready frames 1,2,4,8. Whole-frame PSNR was 21.668, 20.981, 20.911, and 21.690 dB; the non-monotonic curve means current sampling/reconstruction is not yet converging cleanly against the higher-budget reference. Performance still needs GPU timestamp queries; capture wall time is process-level and not a pass cost metric.","files":["scripts/measure-reservoir-loss-curve.ps1","scripts/capture-reservoir-mode-sequence.ps1","state/scratch.md","state/map.yaml"],"source":"artifacts/fensalir-captures/reservoir-loss-curve-20260530-221043.csv"}
{"timestamp":"2026-05-30T00:29:26+01:00","topic":"surfacepage-uav-depth-socket","lesson":"SurfacePage resources can now be GPU-resident unordered-access outputs as well as shader-readable pages. D3D12FieldTexture2D tracks UAV capability, creates UAV descriptors, and D3D12FieldResourceRegistry resolves UnorderedAccess SurfacePages while reusing empty pages across version changes when shape/format/access are unchanged. This gives Mimir stereo-depth/height outputs a real compute-writable resource socket without private output textures or CPU readback.","files":["src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12FieldResourceRegistry.cs","tests/Aquarium.Engine.Tests/D3D12SharedTextureInteropTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter RegistryResolvesUnorderedAccessSurfacePageForComputeDepthOutput"}
{"timestamp":"2026-05-30T00:37:29+01:00","topic":"stereo-depth-lowering-contract","lesson":"FieldEvidence now carries typed stereo-depth lowering metadata for future D3D12 SGM dispatch. AquariumFieldStereoDepthLowering binds a Height claim to shader-readable left/right texture resources, profile/calibration/camera-pair identity, depth range/disparity settings, and a compute-writable UAV disparity SurfacePage. Validation rejects claim encoding mismatches, payload/resource mismatches, non-UAV disparity outputs, and missing/non-readable inputs. This keeps Mimir libSGM-provenance depth inside Fensalir resource authority rather than a private texture, CPU readback, CUDA, or TensorRT path.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","tests/Aquarium.Engine.Tests/FieldEvidenceContractTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter StereoDepth"}
{"timestamp":"2026-05-30T00:43:53+01:00","topic":"stereo-depth-dispatch-readiness","lesson":"D3D12Renderer now evaluates stereo-depth lowerings for dispatch readiness without installing a fake depth kernel. A lowering is counted ready only when the SurfacePage Height claim is planned, left/right input Texture2D resources resolve through the field resource registry, and the disparity SurfacePage resolves with UAV capability. Capacity logs report ready/unplanned/unresolved counts and explicitly say the kernel is not installed.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore"}
{"timestamp":"2026-05-30T00:45:50+01:00","topic":"stereo-depth-sgm-kernel-constants","lesson":"AquariumFieldStereoDepthLowering now carries SGM-shaped kernel constants instead of leaving the future D3D12 shader to infer policy: min disparity, disparity levels, aggregation path count, census radius, and P1/P2 smoothness penalties. Validation requires positive census/P1/P2 values and P2 \u003e= P1. This preserves libSGM-style provenance as typed dispatch data before the HLSL kernel exists.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","tests/Aquarium.Engine.Tests/FieldEvidenceContractTests.cs","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --filter StereoDepth"}
{"timestamp":"2026-05-30T21:59:00+01:00","topic":"packed-leap-stereo-depth-kernel","lesson":"D3D12Renderer now installs and dispatches the first stereo-depth compute lane for the live Mimir Leap shape. D3D12StereoDepth.hlsl samples a packed R8G8 Texture2D as left/right IR lanes, performs a small local SAD/block match over the lowering's disparity/radius constants, and writes R16F disparity to the planned UAV SurfacePage. The dispatch only runs for planned Height claims whose left/right keys are the same packed texture and whose input/output resources resolve. This is a live depth proof and provenance-bearing socket for the later libSGM-style SGM rebuild, not calibrated metric depth or full SGM.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12StereoDepth.hlsl","docs/perfect-fensalir-machine-roadmap.md","state/map.yaml"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; temp Vortice.D3DCompiler probe compiled D3D12PackedStereoDepthCS bytes=2168; full headless host compile attempted but hung before capture"}
{"timestamp":"2026-05-30T22:33:00+01:00","topic":"headless-pipeline-hang-root-cause","lesson":"The apparent Fensalir headless hang was an initial shader pipeline failure hidden behind the loading-frame loop. dotnet-stack showed the main thread sleeping in D3D12Renderer.RenderPipelineLoadingFrame; a dump of the background pipeline task revealed FileNotFoundException for Render/Shaders/D3D12SdfCommon.hlsli beside the Aquarium.Fensalir client shader output. D3D12FensalirSpine.hlsl includes engine SDF shared files by relative path, but Aquarium.Fensalir.csproj only copied client shaders. The fix copies D3D12SdfCommon.hlsli, D3D12SdfMath.hlsli, and D3D12SdfProxy.hlsli into the client shader output and makes initial pipeline failure throw instead of spinning silently. Verification: Fensalir and Engine builds passed, then headless Engine run compiled pipelines, captured artifacts/runtime/fensalir-stereo-depth-pipeline-smoke.png, and exited.","files":["src/Aquarium.Fensalir/Aquarium.Fensalir.csproj","src/Aquarium.Engine/Render/D3D12Renderer.cs","state/evidence.jsonl","state/map.yaml"],"source":"dotnet-stack report plus dotnet-dump analyze on stuck headless process; dotnet run --project src/Aquarium.Engine/Aquarium.Engine.csproj --no-build -- --headless --capture-frame artifacts/runtime/fensalir-stereo-depth-pipeline-smoke.png"}
{"ts":"2026-05-30T23:10:27+01:00","type":"decision","status":"ok","note":"Added the first Fensalir Leap point-cloud render lane: Mesh resources whose SourceUri is derived-from a SurfacePage allocate UAV-capable point-list buffers, D3D12PointCloudFromDisparityCS fills PositionNormalUvColor vertices/indices from disparity, and D3D12PointCloudPS renders them into scene candidate MRTs. Verified engine/client build, stereo/resize tests, and headless shader pipeline capture artifacts/runtime/fensalir-point-cloud-pipeline-smoke.png."}
{"timestamp":"2026-05-30T23:41:55+01:00","topic":"reservoir-presentation-reconstruction","lesson":"The first final-pass reservoir reconstruction denoiser is worth keeping only after maturity gating. The shader samples a bounded resolved-reservoir neighborhood in D3D12ReservoirPresentationResolvePS and weights neighbors by field id, travel, normal, coverage, detail, color, guide confidence, domain validity, and neighbor agreement. Ungated and age-eager versions improved some frames but regressed the curve; the kept gate requires reservoir maturity before filter strength is allowed. Latest 320x180 sweep reservoir-sequence-20260530-232852-* versus higher-work-grid reference improved f2/f4/f8 and reduced average RMSE from 21.944 to 21.221 against the pre-reconstruction 20260530-221043 curve, while f1 still regressed and remains the next target.","files":["src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl","state/scratch.md","state/map.yaml"],"source":"artifacts/fensalir-captures/reservoir-loss-curve-20260530-232852.csv"}
{"timestamp":"2026-05-31T23:58:00+01:00","topic":"system-loopback-audio-stems","lesson":"System speaker capture now enters Fensalir through an explicit AquariumAudioDocument capture request. AquariumSynthHost owns the WASAPI loopback device, publishes mono system_mix AquariumAudioStemFrame packets through IAquariumAudioStemBus, and clients consume those stems without owning capture APIs. The host drains pending audio requests once when the scene-ready audio host is created, so short clients do not strand Start-time requests. This closes the live-audio input seam for visualizers such as Perlines while preserving the engine audio host as the authority for OS audio I/O.","files":["src/Aquarium.Engine.Contracts/Audio/AquariumAudio.cs","src/Aquarium.Engine/AquariumHost.cs","src/Aquarium.Engine/Audio/AquariumSynthHost.cs","src/Aquarium.Engine/Audio/WasapiLoopbackCaptureDevice.cs","tests/Aquarium.Engine.Tests/SynthPlaybackTests.cs","state/map.yaml"],"source":"dotnet build Fensalir.sln; dotnet test tests/Aquarium.Engine.Tests/Aquarium.Engine.Tests.csproj --no-build; scripts/dev-reload.ps1 -Headless -RetainSlots 1 -ClientProject E:\\Projects\\Perlines\\src\\Perlines\\Perlines.csproj showed WASAPI loopback started for profile perlines:system-loopback"}
{"ts":"2026-05-31T03:52:00+01:00","type":"verification","status":"ok","note":"Mimir FieldEvidence camera Feature/SensorObservation claims now plan to Fensalir's GpuSensorFusion backend instead of deferring. D3D12Renderer derives sensor metadata and SRV bindings from resolved camera Texture2D resources and dispatches D3D12GpuSensorFusionCS. Live Mimir proof planned 12 packets with 0 deferred; headless D3D12 dispatched LeapStereoIr plus both Bayer8 PS3 Eyes at 16,384 sparse samples per texture, generated 49,152 gaussians, and reported sensor-fusion timing around 0.059 ms/frame. Kiyo YUY2 claims are selected but skipped until a format-aware YUY2 conversion/feature lane exists; packed video must not masquerade as RGBA.","files":["src/Aquarium.Engine.Contracts/Render/AquariumFieldEvidence.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/D3D12FieldTextureFormat.cs","docs/temporal-sdf-gaussian-field.md","state/map.yaml"],"source":"dotnet build Fensalir.sln; Mimir --perfect-machine-live-smoke --seconds 1.0; Mimir.App headless captured artifacts/runtime/mimir-perfect-machine-gpu-feature-extraction-headless.png"}
{"ts":"2026-05-31T04:18:00+01:00","type":"verification","status":"ok","note":"GpuSensorFusion now samples YUY2 camera Texture2D resources instead of skipping them. Fensalir exposes DXGI_FORMAT_YUY2 through the Microsoft-documented R8G8B8A8_UNORM SRV view, passes declared pixel format into the GPU sensor camera packet, and D3D12GpuSensorFusion.hlsl decodes Y0/U/Y1/V to RGB before feature scoring. Mimir.App headless all-sensor proof dispatched five camera textures with zero unsupported field textures, generated 81,920 sparse gaussians at 16,384 samples per texture, reported sensor-fusion GPU timing around 0.08 ms/frame, and captured artifacts/runtime/mimir-perfect-machine-yuy2-gpu-feature-extraction-headless.png.","files":["src/Aquarium.Engine.Contracts/Render/AquariumRenderPlan.cs","src/Aquarium.Engine/Render/D3D12ExternalSensorTexture.cs","src/Aquarium.Engine/Render/D3D12FieldTexture2D.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12GpuSensorFusion.hlsl","docs/temporal-sdf-gaussian-field.md","state/map.yaml"],"source":"Microsoft DXGI_FORMAT docs for YUY2 view mapping; dotnet build Fensalir.sln; Mimir.App headless captured artifacts/runtime/mimir-perfect-machine-yuy2-gpu-feature-extraction-headless.png"}
{"timestamp":"2026-05-31T08:48:00+01:00","type":"verification","status":"ok","note":"Aquarium.EveSurface no longer assumes every Eve dashboard document is VoidBot-shaped. Generic providers now render their node list and selected detail through Surface Nodes / State Graph panels. Verified by connecting to Mimir.EveDashboard provider mimir.stream.layout and capturing the decoded surface at E:/Projects/Mimir/artifacts/runtime/mimir-eve-surface-layout-generic-decoder.png.","files":["src/Aquarium.EveSurface/EveSurfaceRuntime.cs"],"source":"dotnet build src/Aquarium.EveSurface/Aquarium.EveSurface.csproj; live Mimir.EveDashboard ws://127.0.0.1:8795/eve/deck provider mimir.stream.layout"}
{"ts":"2026-05-31T18:06:36+01:00","type":"implementation","status":"ok","note":"AquariumUiDocument now carries Eve/CultUI-style composition surfaces in addition to legacy debug panels. Surface authoring defaults to vertical groups and uses explicit Horizontal scopes for columns; DirectWrite lowering subdivides each parent rectangle by axis and child weight instead of measuring children into sibling space, making overlap structurally impossible for lowered siblings.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj --no-restore"}
{"ts":"2026-05-31T18:18:00+01:00","type":"implementation","status":"ok","note":"Eve/CultUI Direct2D lowering now distinguishes fixed preferred control rows from flexible panes/groups. Vertical scopes allocate compact preferred extents to leaf controls such as text, toggles, selects, sliders, metrics, and buttons, then give remaining space to flexible children by weight. This preserves the no-overlap subdivision invariant while avoiding equal-height slabs for ordinary controls.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj --no-restore; relaunched Mimir.App with mimir-runtime.desktop-editor.no-leap.mic-reference-sync.local.json"}
{"ts":"2026-05-31T18:24:00+01:00","type":"verification","status":"ok","note":"Aquarium.Sample.Minimal now carries a reusable CultUI smoke surface so Eve/CultUI layout can be verified without Mimir camera, USB, FFT, or sync workload. The smoke surface exercises nested vertical/horizontal scopes, panes, cards, rows, selects, toggles, sliders, buttons, metrics, live text, and clipped long readouts. DirectWrite lowering now derives preferred container height from nested child layout and displays metric raw values while clamping only the fill bar; the headless capture showed readable nested grids with no sibling overlap.","files":["src/Aquarium.Sample.Minimal/MinimalRuntime.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj --no-restore; scripts/dev-reload.ps1 -ClientProject src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj; capture artifacts/fensalir-captures/cultui-smoke-minimal-fixed.png"}
{"ts":"2026-05-31T22:05:00+01:00","type":"implementation","status":"ok","note":"Eve/CultUI surfaces now include a DirectWrite preview primitive for editor/compositor surfaces. The primitive renders a bounded 16:9 program canvas and normalized item rectangles supplied by the client, allowing Mimir to show OBS-style program arrangement without inventing a Mimir-only overlay path.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; dotnet build src/Aquarium.Sample.Minimal/Aquarium.Sample.Minimal.csproj --no-restore"}
{"ts":"2026-05-31T22:17:00+01:00","type":"implementation","status":"ok","note":"CultUI preview elements are now flexible layout surfaces rather than fixed preferred-height controls. Containers with flexible children report as flexible to their parent, so editor panes containing a compositor preview can take the available center-panel space instead of collapsing to a small fixed strip.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj --no-restore; Mimir headless capture artifacts/runtime/mimir-editor-preview-maximized-headless.png"}
{"ts":"2026-05-31T22:55:00+01:00","type":"implementation","status":"ok","note":"CultUI preview lowering treats an empty preview label as an intentional chrome-free canvas: no nested frame/title is drawn, and padding collapses to two pixels. Mimir uses this for the central OBS-style program compositor so the preview fills the editor center instead of rendering as a card-within-a-pane.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj; Mimir.App --headless --headless-width 1280 --headless-height 720 --capture-frame artifacts/runtime/mimir-editor-flat-2d-program-mixer-headless.png"}
{"ts":"2026-06-01T13:48:00+01:00","type":"implementation","status":"ok","note":"CultUI preview elements now support direct compositor gestures. AquariumUiElement can carry a preview interaction callback, D3D12Renderer hit-tests preview item bodies and selected corner handles in normalized canvas coordinates, captures input while dragging, and DirectWriteOverlay draws visible selected corner handles. Clients still own gesture meaning; Fensalir owns the reusable UI hit-test and rendering affordance.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"}
{"ts":"2026-06-01T14:12:00+01:00","type":"implementation","status":"ok","note":"CultUI preview affordances now include hover/active state, empty-canvas hits, drag-end events, and normalized guide rendering. D3D12Renderer emits hover/begin/drag/end interactions for preview bodies, handles, and empty canvas; DirectWriteOverlay draws hovered/active selected handles and client-provided x/y guides. Added Up/Down/PageUp/PageDown/Control key codes for editor hotkeys.","files":["src/Aquarium.Engine.Contracts/Input/InputState.cs","src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Platform/Win32Window.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"}
{"ts":"2026-06-01T15:05:00+01:00","type":"implementation","status":"ok","note":"Eve remains the portable dashboard authority; the C# Aquarium/CultUI UI contract is the reference API shape. Added content padding and fixed-extent layout fields to retained surface documents, made pane headers conditional, and kept DirectWrite drawing plus D3D12 preview hit-testing on the same document geometry. Mimir now uses this for an edge-to-edge headerless editor shell with fixed output controls and an expanding inspector.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj -p:UseSharedCompilation=false; Mimir headless capture artifacts/runtime/mimir-output-panel-headless.png"}
{"ts":"2026-06-01T15:40:00+01:00","type":"implementation","status":"ok","note":"AquariumUiElement now carries an optional typed AquariumUiBinding descriptor for Verse-backed CultCache/CultMesh fields: document schema/id, field path, value kind, access, authority, and optional command id. Fensalir lowerings keep using local callbacks for native actuation; the binding is provenance/state authority metadata, not renderer-owned dashboard semantics.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","state/map.yaml"]}
{"ts":"2026-06-01T15:48:00+01:00","type":"verification","status":"ok","note":"Release build passed for Aquarium.Engine after adding AquariumUiBinding; Mimir Release builds and scene-editor smoke also passed against the updated contract.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -c Release -p:UseSharedCompilation=false; dotnet run --project E:/Projects/Mimir/src/Mimir.BufferSmoke/Mimir.BufferSmoke.csproj -c Release -- --scene-editor-smoke"}
{"ts":"2026-06-01T20:15:00+01:00","type":"implementation","status":"ok","note":"Render debug mode 21 is now an owner-layer overdraw view. Scene/evidence pixel shaders write a fifth additive R16_FLOAT overdraw target after discard/depth eligibility; presentation samples that target as a heat map. Coverage, step count, reservoir stats, and spatial-budget modes remain separate diagnostics instead of being relabeled as overdraw.","files":["src/Aquarium.Engine.Contracts/GraphicsSettings.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12SdfCommon.hlsli","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Engine/Render/Shaders/D3D12TemporalGaussian.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12FractalSplatRender.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12PointCloud.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12TubeField.hlsl","src/Aquarium.Engine/Render/Shaders/D3D12Post.hlsl"],"source":"dotnet build Fensalir.sln; AQUARIUM_RENDER_DEBUG_MODE=21 .\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -ClientProject src\\Aquarium.Fensalir\\Aquarium.Fensalir.csproj"}
{"timestamp":"2026-07-12T19:25:00+01:00","topic":"stateless-spherical-erosion","lesson":"Cube-sphere erosion must be a pure planet-space point function rather than six face-local filters or a repair pass. CultMath now owns matching C# and HLSL kernels keyed by unit direction, tangent base gradient, seed, and bounded octave parameters; Zyphos composes the result after its continental field, while cube faces remain lookup and scheduling charts.","files":["E:\\Projects\\CultMath\\src\\CultMath\\SphericalErosion.cs","E:\\Projects\\CultMath\\shaders\\CultMath.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","state/map.yaml"],"source":"user requested a C# CPU implementation in CultMath matching the GPU code"}
{"timestamp":"2026-07-12T20:30:00+01:00","topic":"advanced-erosion-gpu-parity","lesson":"Float-based cell hashes diverged enough between C# and HLSL to select different erosion pivots across a 2052-sample corpus. CultMath now uses the same integer avalanche hash in both paths, and Fensalir verifies the actual HLSL kernel with a standalone D3D12 compute/readback probe instead of inferring parity from similar source text or booting the renderer.","files":["E:\\Projects\\CultMath\\src\\CultMath\\AdvancedErosionFilter.cs","E:\\Projects\\CultMath\\shaders\\AdvancedErosionFilter.hlsl","src/Aquarium.Engine/Render/D3D12ComputeProbe.cs","src/Aquarium.Engine/Render/D3D12ShaderCompiler.cs","tests/Aquarium.Engine.Tests/D3D12AdvancedErosionParityTests.cs"],"source":"measured CPU/GPU parity implementation"}
{"timestamp":"2026-07-12T21:00:00+01:00","topic":"erosion-physical-frequency-bands","lesson":"Planetary erosion LOD is now a filter over one world-space spectrum: CultMath selects active octaves from physical wavelength and sample spacing, fractionally weights the last resolvable octave, and reports omitted height. Zyphos uses that contract through smooth planet-space triplanar charts, so changing view footprint changes represented bands rather than switching terrain authorities.","files":["E:\\Projects\\CultMath\\src\\CultMath\\ErosionFrequencyBands.cs","E:\\Projects\\CultMath\\src\\CultMath\\AdvancedErosionFilter.cs","E:\\Projects\\CultMath\\shaders\\AdvancedErosionFilter.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl"],"source":"phase 3 of stateless planetary erosion rendering plan"}
{"timestamp":"2026-07-12T21:35:00+01:00","topic":"planetary-page-boundary-authority","lesson":"Using each tile's locally measured angular texel size caused independently generated sibling pages to choose different erosion weights at the same shared boundary. Band selection now uses a level-global nominal angular footprint; local projection distortion remains summary evidence. GPU-generated sibling boundaries are identical and borders sample the spherical field without neighbor residency.","files":["src/Aquarium.Engine.Fractal/Lod/PlanetarySurfacePage.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 4 cross-page GPU verification"}
{"timestamp":"2026-07-12T22:00:00+01:00","topic":"planetary-page-summary-reduction","lesson":"Planetary erosion pages now reduce minimum/maximum displacement, maximum tangent slope, and maximum unresolved height through the GPU kernel and verify those results against CPU reduction. These summaries are the conservative evidence needed by later intersection and residency; they are not inferred from parameters.","files":["src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPageSummary.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 4 GPU summary gate"}
{"timestamp":"2026-07-12T23:15:00+01:00","topic":"page-backed-planetary-intersection","lesson":"Planetary erosion rendering now generates a persistent D3D12 page and GPU summary, binds both to Zyphos SDF tracing, uses measured displacement/slope/unresolved evidence for bounds and conservative radial stepping, and retains the shared bracketed sign-change refiner. Compiling the previously unregistered planet proxy proved that advanced erosion cannot remain nested in the Shader Model 5 pixel trace; exact direct evaluation is therefore the CPU/compute oracle, while visible misses use the resident ancestor or base field. Zyphos now registers and publishes planet, Umbros, and star in matching object-index order.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","src/Aquarium.Zyphos/ZyphosRenderPlan.cs","src/Aquarium.Zyphos/ZyphosSceneBuilder.cs","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 5 compile, D3D12 summary, lifecycle, and direct-versus-page hit verification"}
{"timestamp":"2026-07-13T00:10:00+01:00","topic":"planetary-residual-quadtree-lod","lesson":"Planetary LOD now keeps six root pages and represents a selected camera path as parent low frequencies plus child-only residual bands. CultMath's declared unresolved-height bound divided by projected world footprint chooses the level. Content and presentation versions are separate, so arrival/departure fades update metadata without regenerating pages; departing chains remain resident through zero weight before eviction. GPU tests prove parent plus child residual reconstructs the child filtered field, and timeline tests cover descent, teleport, midpoint, eviction, and reload. Visible capture evidence is still required before claiming the no-pop Phase 6 exit.","files":["src/Aquarium.Engine.Contracts/Render/AquariumRenderPlan.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Zyphos/ZyphosPlanetarySurfacePages.cs","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrain.hlsli","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 6 residual LOD implementation and numerical/timeline verification"}
{"timestamp":"2026-07-13T00:40:00+01:00","topic":"planetary-composed-differentials","lesson":"Planetary pages now carry a full 3D planet-space tangent gradient in world-distance units. Zyphos combines that derivative with the base/authored terrain differential and the erosion-times-land product derivative, then derives its visible radial-graph normal from the same composed field. The generic SDF proxy delegates normals through a client hook instead of differentiating the conservatively scaled trace distance. Ridge and gully outputs from the same pages now affect planet color and roughness, and the CPU surface-normal helper implements the same contract.","files":["src/Aquarium.Engine.Fractal/Lod/PlanetarySurfacePage.cs","src/Aquarium.Engine/Render/Shaders/D3D12SdfProxy.hlsli","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPage.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosTerrainPageSummary.hlsl","src/Aquarium.Zyphos/Shaders/D3D12ZyphosPlanet.hlsl","tests/Aquarium.Engine.Tests/D3D12PlanetarySurfacePageTests.cs"],"source":"phase 7 differential, normal, material, CPU/GPU contract verification"}

View file

@ -21,6 +21,12 @@ current_model:
implementation: src/Aquarium.Engine.Fractal
tests: tests/Aquarium.Engine.Fractal.Tests
receipt_tool: tools/Aquarium.Fractal.Receipt
saga:
implementation: src/Saga
purpose: >
Stochastic planetary flow middleware for cube-sphere Transport fields,
arbitrary forcing, explicit Coriolis/rotation conditioning, and learned
atmosphere/ocean/debris advection packets.
demos:
fensalir_splash: src/Aquarium.Fensalir
minimal: src/Aquarium.Sample.Minimal
@ -44,6 +50,11 @@ invariants:
- Fensalir may own reusable built-in field resources, such as the blackbody
ramp, when the resource is generic engine utility. Clients should reference
those through contracts instead of hardcoded project-local asset paths.
- Client shaders that include engine shared HLSL files must receive those
includes in their output shader directory or use explicit engine include
paths. Missing shared includes are shader-pipeline failures, not loading
states. Initial pipeline build failure now throws instead of spinning behind
the startup/loading frame.
- Epiphany Aquarium depends on Fensalir; Fensalir must not depend on Epiphany.
- Demo projects inside this repo are allowed to exercise engine features but must not become hidden engine policy.
- Public `Aquarium.*` assembly names remain for continuity until a deliberate namespace/package migration.
@ -60,6 +71,98 @@ invariants:
terrain should be a quadtree of brush-painted surface tiles, subdivided by
projected footprint, curvature, brush/material detail, silhouettes, and
temporal uncertainty before SDF probes emit detail-adaptive surface splats.
- Deterministic planetary geometry is owned by GameCult.Geometry's scale-aware
planetary field runtime. It owns field identity, point/batch and
double-position CPU queries, tangent/QSC cube-sphere topology, bordered
pages and child residual composition, projected-error LOD, residual
lifecycle, ray/path/region queries, coarse patch topology, and map
projections/tiles. GameCult.Geometry maintains matching HLSL for field evaluation,
topology, page interpolation/residuals, and radial refinement. Clients own
base fields, terrain parameters, gameplay meaning, and presentation policy.
- The production lowering for stateless planetary erosion is specified in
docs/stateless-planetary-erosion-rendering-plan.md: direct evaluation is the
oracle, while GPU-generated cube-sphere pages carry cached displacement,
differential, mask, bound, and frequency-band evidence for rendering.
- CPU/GPU terrain parity is measured through a standalone D3D12 compute and
readback probe. Shader include expansion is owned by D3D12ShaderCompiler
and shared with the renderer; parity probes must not boot a swapchain or
duplicate GameCult.Geometry equations.
- Planetary erosion frequencies are selected in world-distance wavelengths
from the current sample footprint. The last resolvable octave fades
fractionally and reports its omitted-height bound; Zyphos lowers the planar
reference filter through smooth planet-space triplanar charts, not cube-face
terrain ownership.
- Planetary surface pages use one level-global nominal angular footprint for
band selection. Per-tile projection distortion is summary evidence only;
it may not change terrain at a shared boundary. Page borders evaluate the
spherical field beyond tile bounds without reading neighbor residency.
- D3D12 owns persistent planetary page input, output, metadata, and summary
buffers, their bindings, dispatch, and draw submission. GameCult.Geometry owns the
page layout, spherical sample directions, canonical GPU input/metadata
payload, residual semantics, interpolation, and summaries. Client adapters
only convert CultMath primitives to their host ABI. Client compute entry points bind the client base field to
those functions before GPU reduction. Zyphos registers planet, Umbros, and
star proxies in object index order and publishes all three SDF objects from
one scene builder.
- Production planet visibility is owned by coarse cube-sphere raster patches,
not the general SDF proxy. Patch vertices consume the resident ancestor
field; covered pixels perform four bounded radial corrections against the
complete resident page field, then use the shared differential/material
contract. Missing pages lower to the nearest resident ancestor (the base
field at root). Direct GameCult.Geometry evaluation and the bracketed planet SDF are
verification oracles only; neither owns production visibility.
- D3D12 binds the page atlas, metadata, summaries, and page set to both patch
shader stages at t81-t84. Patch projection follows the renderer's established
camera-basis convention, including clip-space Y inversion and normalized
forward depth. Ordinary `LessEqual` depth owns front/back visibility;
diagnostic `Always` depth is not a production path.
- Zyphos's legacy two-million-splat fractal reservoir is no longer a visible
planet writer. It previously ran after the raster patch and erased the
page-backed candidate. Fractal grammar and authored brushes remain inputs
to terrain composition; `PlanetarySurfacePages` plus the patch lowering own
the visible surface and corrected pixel depth.
- Patch density is explicit page-set policy, carried to GPU in page-set state
rather than hardcoded independently in C# and HLSL. Zyphos currently uses a
64 by 64 grid per root face; fine relief remains a four-step per-pixel page
refinement. Raster normals use derivatives of the refined world position,
while CPU/direct queries retain the analytic planet-space differential.
`AQUARIUM_PROFILE_PLANETARY_PAGES=1` deliberately regenerates unchanged
pages so their one-shot GPU cost can be measured; production content-version
caching remains authoritative.
- Deterministic capture controls can select spatial domain, orbit distance,
exact residual age, or a cross-face eviction midpoint. Capture priming is
presentation evidence only: production arrival/departure ownership remains
the time-based resident-page lifecycle.
- Planet patch debug modes 22-28 expose selected face/level/blend, page
bounds and unresolved error, refined normal, ridge/gully evidence, and
cross-edge discontinuity/missing residency. Mode 27 evaluates direct
filtered erosion against the summed page field at the selected spacing;
mode 28 exposes content fingerprint, spacing ratio, and deepest level.
The unresolved bound and measured direct error remain distinct signals.
- The bounded radial correction step is owned by GameCult.Geometry in matching C# and
HLSL bodies. Zyphos composes four steps with its terrain-offset callback;
the renderer does not carry a private correction equation.
- Planetary page residency is owned by GameCult.Geometry and separates content from
presentation state. ContentVersion changes only when the resident tile set
changes; PresentationVersion carries arrival/departure weights without
regenerating erosion. Six root pages remain resident. GameCult.Geometry's
projected-error selector uses unresolved-height bounds against camera
footprint to request one ancestor chain, and its residual lifecycle keeps
departing chains addressable until their weights reach zero.
- Root pages contain the resolvable low-frequency erosion field. Every child
page stores `child filtered field - parent filtered field`; the planet
shader sums the containing ancestor contributions and weights only those
residuals during transitions. Independent full-spectrum pages are never
blended. D3D12 flattens resident payloads into one output atlas with indexed
metadata and one GPU summary per page at t81-t84.
- Planetary page differentials are world-distance tangent gradients expressed
as a 3D planet-space vector, not face-local UV derivatives. Zyphos composes
the page gradient with finite differentials of its authored/base residual,
including the land-mask product term, and derives the radial graph normal
as `normalize(direction - tangentGradient)`. The generic SDF proxy exposes
a client normal hook; other SDFs retain its finite-distance fallback.
Ridge and gully page evidence feeds the same planet material evaluation.
`PlanetarySurfaceDifferential.SurfaceNormal` is the matching CPU contract.
- The shared reservoir is a fractal domain reservoir, not a texel cache.
Pixel rows are the current D3D12 execution projection of selected domain
evidence. Area-domain contributors must carry selected subpixel or local
@ -78,6 +181,12 @@ invariants:
direct SDF proxies, meshes, volumes, fractal probes, and Mimir sensor
observations must publish bounded Form/Appearance/Transport field claims
before they can participate in reusable field evidence.
- Eve is the portable retained-dashboard authority. The C# Aquarium/CultUI
contract is the reference API shape for those documents; Fensalir Direct2D
and D3D12 hit testing lower that document locally, but must not become the
owner of dashboard semantics or client policy. `AquariumUiBinding` names
the Verse-backed CultCache/CultMesh field behind a UI element; local
callbacks remain lowering actuators, not a second state authority.
- Final composition reads the resolved HDR field texture produced by the
shared reservoir history update pass. TubeField render lowerings inject
visibility proposals into the shared field reservoir; they must not regain
@ -106,11 +215,29 @@ invariants:
owns an `IAquariumAudioStemBus` service for published processed stems,
optional monitor playback is explicit, and OBS-facing publication remains
a later host cut.
- System audio capture is an engine-owned audio-host request, not a client
sidecar. Clients enqueue a profile-keyed system-loopback capture request on
`AquariumAudioDocument`; Fensalir owns WASAPI loopback capture, publishes a
mono `system_mix` stem through `IAquariumAudioStemBus`, and clients consume
it through the same stem service as streaming DSP outputs.
- The named D3D12 program-output texture is an external publication surface.
After copying the completed backbuffer into it, the renderer must leave the
resource in `Common` so OBS/EVE/native consumers can transition it for
GPU-side reads without inheriting Fensalir's internal copy-destination
state.
- Fensalir's DirectWrite/Direct2D overlay is an Eve client surface for shared
CultMesh UI documents. The adapter should lower those documents into
`AquariumUiDocument` and route commands back through Eve/CultNet; provider
repos still own dashboard state, accepted commands, and meaning. CultUI
bindings carry document schema/id, path, value kind, access, authority, and
optional command id so provider state can move into CultCache without
changing the native renderer contract.
preview elements may expose reusable item-body/corner-handle gestures in
normalized preview coordinates, but the client remains the owner of what
those gestures do. The preview primitive also carries hover/active state,
empty-canvas hits, drag-end events, and client-provided normalized guides
so compositor surfaces can show snap/selection feedback without becoming
renderer-owned policy.
- Planetary ocean/atmosphere flow is an engine field organ when reduced to
reusable stochastic advection machinery. Fensalir owns cube-sphere flow
addressing, dataset provenance, model contracts, stochastic advection
@ -168,6 +295,30 @@ next_actions:
`reservoir-sequence-20260530-003746-*` proves it. The next cut is turning
budget-pressure evidence into a renderer-owned per-tile adaptive update
contract plus future exact producer SDF replay.
- Window resize no longer crashes on the D3D12 swapchain path: resize now
disposes DirectWrite overlays and wrapped backbuffers, clears/flushes the
D3D11-on-12 context before `ResizeBuffers`, and drops program-output shared
textures before recreating size-dependent surfaces. Visible resize probe
`20260530-215113-639e6612` survived seven programmatic resizes.
- Reservoir reconstruction now has a loss-curve reporter:
`scripts/measure-reservoir-loss-curve.ps1` computes MAE/MSE/RMSE/PSNR
against same-time higher-work-grid native references. Smoke
`reservoir-sequence-20260530-221043-*` at 320x180 gave whole-frame PSNR
21.668, 20.981, 20.911, and 21.690 dB for ready frames 1, 2, 4, and 8.
D3D12 now owns fixed per-frame GPU timestamp queries for frame record,
scene candidate, reservoir resolve/update, bloom, presentation resolve, and
active compute lowerings, so quality can be plotted against pass cost
instead of process capture wall time. The presentation resolve pass also
owns the first full-resolution reconstruction filter: it samples a bounded
resolved-reservoir neighborhood, validates neighbors by field id, travel,
normal, coverage, detail, color, guide confidence, domain validity, and
maturity, and writes only to the final present buffer. Latest smoke
`reservoir-sequence-20260530-232852-*` improved f2/f4/f8 and lowered
average RMSE from 21.944 to 21.221 against the 20260530-221043
pre-reconstruction curve, while f1 still regressed. Next cut: merge loss
and GPU timing into one benchmark artifact and tune first-frame
reconstruction maturity without adding native-resolution reservoir
intermediates.
- Build the planetary flow organ in staged cuts: provenance manifest, static
relief and reanalysis cube-sphere bake pipeline, stochastic advection
contracts, synthetic runtime slice, ERA5/OSCAR baseline learning, then
@ -224,10 +375,24 @@ next_actions:
until a real planar copy path exists; GPU producers can still write NV12
leases directly. This makes imported handles an edge case, not the primary
live camera authority. Missing or unresolved TubeField ramps bind a Fensalir-owned linear fallback ramp.
Camera texture claims can therefore resolve resources, but their first
visual use stays debug-view-only for now; production visual-fusion and
feature-extraction lowerings are deferred until the renderer and audio work
are fixed. TubeField compute now
Camera texture claims can therefore resolve resources. Camera-sensor
Feature claims with `SensorObservation` proposal policy now plan to the
`GpuSensorFusion` backend instead of deferring. `D3D12Renderer` derives
sensor-camera packets and SRV table entries from planned FieldEvidence
Texture2D resources, then dispatches `D3D12GpuSensorFusionCS` into the
temporal-gaussian buffer. The 2026-05-31 Mimir all-sensor proof planned all
12 packets with zero deferred requests; the first D3D12 host dispatched
feature extraction for LeapStereoIr plus both Bayer8 PS3 Eyes at 16,384
sparse samples per texture, generating 49,152 gaussians with sensor-fusion
timing around 0.059 ms/frame. The follow-up YUY2 decode cut binds
`DXGI_FORMAT_YUY2` resources through the Microsoft-documented
`R8G8B8A8_UNORM` SRV view, decodes `Y0/U/Y1/V` in
`D3D12GpuSensorFusion.hlsl`, and the Mimir all-sensor proof dispatched all
five camera textures with zero unsupported field textures, generating
81,920 sparse gaussians at about 0.08 ms/frame sensor-fusion GPU time.
Packed video is not allowed to masquerade as ordinary RGBA; declared format
metadata selects the decode path.
TubeField compute now
emits one GPU-resident D3D12 DrawIndexed indirect argument packet per
material/resource batch, and render binds the batch source/ramp state then
executes the packet with ExecuteIndirect. The first material is opaque field
@ -244,20 +409,34 @@ next_actions:
compute-writable UAV-backed pages. Empty SurfacePage version is content
freshness, not texture identity, so compute outputs can update without
reallocating when shape/format/access are unchanged. FieldEvidence now has
an explicit `AquariumFieldStereoDepthLowering` sidecar for future calibrated
an explicit `AquariumFieldStereoDepthLowering` sidecar for calibrated
stereo compute dispatch: validation requires a Height claim whose payload is
the compute-writable disparity SurfacePage, shader-readable left/right
Texture2D or RollingTexture resources, profile/calibration/camera-pair
identity, and coherent output shape/range. This keeps Mimir's libSGM-shaped
D3D12 depth path inside typed field/resource authority instead of a private
texture or CUDA/TensorRT dependency. The D3D12 renderer now evaluates
stereo-depth dispatch readiness without writing fake depth: a lowering is
ready only when its Height claim is planned and left/right input textures
plus UAV disparity SurfacePage resolve through the field resource registry;
capacity logs still state that the kernel is not installed. The lowering
stereo-depth dispatch readiness and runs the first packed-Leap compute
kernel: a lowering is eligible only when its Height claim is planned and
left/right input textures plus UAV disparity SurfacePage resolve through
the field resource registry. The first `D3D12StereoDepth.hlsl` lane handles
the immediate Mimir Leap shape where left and right observations are packed
into the R/G channels of one Texture2D, then writes crude R16F disparity by
local SAD/block matching. This is a live depth proof, not final
libSGM-style SGM or calibrated metric depth. The lowering
sidecar now carries the first SGM-shaped kernel constants too: min
disparity, disparity levels, aggregation paths, census radius, and P1/P2
smoothness penalties. VolumeTexture
smoothness penalties. The first Leap point-cloud render lane now follows
that disparity socket: Mesh packages with
`SourceUri=derived-from:<surface-page-resource>` allocate UAV-capable
vertex/index buffers, `D3D12PointCloudFromDisparityCS` fills a standard
`PositionNormalUvColor` PointList from the disparity SurfacePage, and
`D3D12PointCloudPS` renders it into the scene candidate MRTs through the
normal depth/camera path. This is intentionally sourced from typed Mesh and
SurfacePage resources; CPU point arrays and unproven RGB depth are not live
authorities. Headless pipeline smoke captured
`artifacts/runtime/fensalir-point-cloud-pipeline-smoke.png`.
VolumeTexture
resources now have the same
first resolver shape: contracts and DSL can declare GPU-resident 3D
textures with dimensions, depth, format, and version, and the D3D12 registry

Some files were not shown because too many files have changed in this diff Show more