Split Epiphany client from Fensalir engine
This commit is contained in:
parent
e0dad7b1f6
commit
c0841d4652
287 changed files with 194 additions and 37974 deletions
88
AGENTS.md
88
AGENTS.md
|
|
@ -1,86 +1,44 @@
|
|||
# Aquarium Engine Instructions
|
||||
# Epiphany Aquarium Instructions
|
||||
|
||||
## Purpose
|
||||
|
||||
Aquarium Engine is the native C# runtime for the Epiphany Aquarium client. It
|
||||
owns the window, renderer, input loop, live-reload boundary, persistent client
|
||||
state, and eventual diegetic UI machinery.
|
||||
This repo is the Epiphany Aquarium client. It owns Epiphany-facing runtime
|
||||
semantics, agent visuals, CultCache documents, CultNet interpretation, voice
|
||||
surfaces, and client-authored render plans.
|
||||
|
||||
This repo is not the Epiphany harness. The visible frame belongs to Aquarium.
|
||||
The native host and renderer live in the adjacent Fensalir repo:
|
||||
`E:\Projects\Fensalir`.
|
||||
|
||||
## Operating Doctrine
|
||||
|
||||
- The renderer is the source of truth for visible world pixels.
|
||||
- Keep the frame legible: explicit passes, explicit ownership, explicit state.
|
||||
- Do not leave lies in the walls. When building engine systems meant to last,
|
||||
refuse half-assed shortcuts, hidden compensators, mystery state, and
|
||||
prototype-shaped production code. Prefer full-ass, research-backed,
|
||||
steel-plated resilience with padded failure boundaries so users do not cut
|
||||
themselves on sharp internals.
|
||||
- This is game engine work: every millisecond counts. Do not spend runtime
|
||||
casually on avoidable JSON churn, spaghetti state, redundant ray work,
|
||||
unbounded full-resolution filters, or convenience abstractions that hide
|
||||
performance costs. If a shortcut is temporary, mark it as scaffolding and cut
|
||||
it before it becomes architecture.
|
||||
- Before inventing renderer, simulation, storage, or post-processing machinery,
|
||||
check standard literature, production talks, engine docs, or proven local
|
||||
patterns. Build the durable machine, not a sequence of shy approximations that
|
||||
accrete into Jenga.
|
||||
- When a visual, physical, or simulation spec describes a continuous form, start
|
||||
with continuous field math or a coherent parameterization that owns the form.
|
||||
Do not assemble the thing from piecewise capsules, boxes, slabs, adapters, or
|
||||
helper organs just because that is the shortest path to pixels. Piecewise
|
||||
construction is allowed only when the form itself is piecewise or when a
|
||||
measured constraint makes the continuous model impractical; document that cut
|
||||
line and keep the compensator small.
|
||||
- The camera target is the Grid center. Grid radius follows zoom distance.
|
||||
Pitch, yaw, and screen projection do not resize the Grid.
|
||||
- Body anchors are world-space simulation state. They do not ride with the Grid
|
||||
visibility window.
|
||||
- Lighting is diegetic. Self, field sources, and Aquarium-owned transport light
|
||||
the scene; ambient convenience light is guilty until proven useful.
|
||||
- Overlay UI and diegetic UI are different beasts. Overlay text can use
|
||||
DirectWrite. Scene text should become renderer-owned MSDF/SDF billboards when
|
||||
it belongs in the world.
|
||||
- CultCache is the runtime memory organ. Camera state, reload state, renderer
|
||||
knobs, UI state, body state, and future Epiphany connection state should be
|
||||
typed Cult documents, not loose JSON or process-only folklore.
|
||||
- CultNet is the communication surface for Epiphany agents. Add transport
|
||||
deliberately and keep authority boundaries typed.
|
||||
- Epiphany owns meaning; Fensalir owns the machine that displays and reloads it.
|
||||
- Keep dependency direction one-way: this repo can reference Fensalir, but
|
||||
Fensalir must not learn Epiphany nouns.
|
||||
- Client render code should publish explicit contracts and shaders. Do not sneak
|
||||
renderer policy, D3D12 plumbing, or host lifecycle code back into this repo.
|
||||
- CultCache documents are typed client state, not loose JSON sidecars.
|
||||
- CultNet surfaces are semantic input. Spatial placement, body projection, and
|
||||
renderer budget belong to the client/runtime contract, not to chat folklore.
|
||||
- If an engine capability is missing, add it in Fensalir and consume it here.
|
||||
Do not build a small private engine because the real one is across the hall.
|
||||
|
||||
## Persistent State
|
||||
|
||||
- `state/map.yaml` is the canonical project map.
|
||||
- `state/memory.json` is durable engine doctrine and taste.
|
||||
- `state/evidence.jsonl` stores distilled lessons that should change future
|
||||
behavior.
|
||||
- `state/map.yaml` is the canonical client map.
|
||||
- `state/memory.json` is durable Epiphany Aquarium doctrine and taste.
|
||||
- `state/evidence.jsonl` stores lessons that should change future behavior.
|
||||
- `state/scratch.md` is disposable working context for the active slice.
|
||||
|
||||
Update these when the engine learns something durable. Delete stale guidance
|
||||
instead of embalming it.
|
||||
Update state when ownership, contracts, or client doctrine changes.
|
||||
|
||||
## Verification
|
||||
|
||||
Use focused checks:
|
||||
|
||||
```powershell
|
||||
dotnet build Aquarium.Engine.sln
|
||||
dotnet build EpiphanyAquarium.sln
|
||||
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4
|
||||
```
|
||||
|
||||
For normal iteration:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-watch.ps1
|
||||
```
|
||||
|
||||
For shipping:
|
||||
|
||||
```powershell
|
||||
.\scripts\publish-win-x64.ps1 -Clean -Zip
|
||||
```
|
||||
|
||||
## Style
|
||||
|
||||
Prefer small, intentional commits. Push completed work. Keep docs about the live
|
||||
system; put old branch scars only in evidence when they alter future decisions.
|
||||
Keep docs about the live system. Historical scars belong in evidence only when
|
||||
they change future decisions.
|
||||
|
|
|
|||
|
|
@ -1,322 +0,0 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Contracts", "src\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj", "{08D15A38-B6AB-459B-8BF9-3CCA8555A022}"
|
||||
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", "..\CultMath\src\CultMath\CultMath.csproj", "{0089E341-25F2-43FC-8FD9-CBD1AE646D91}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany", "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj", "{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack", "..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj", "{A8EDAE19-805B-4B09-9566-BE63937CA1B9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching", "..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj", "{6E874823-17F8-453C-A077-4A101D425C84}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Logging", "..\CultLib\src\GameCult.Logging\GameCult.Logging.csproj", "{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack.Analyzers", "..\CultLib\src\GameCult.Caching.MessagePack.Analyzers\GameCult.Caching.MessagePack.Analyzers.csproj", "{225265F1-8278-48D7-B271-BE8841D0C1CA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.MessagePack.Generator", "..\CultLib\src\GameCult.Caching.MessagePack.Generator\GameCult.Caching.MessagePack.Generator.csproj", "{435C602F-025E-4948-8285-E83E693DAA7B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking", "..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj", "{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Sample.Minimal", "src\Aquarium.Sample.Minimal\Aquarium.Sample.Minimal.csproj", "{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Tests", "tests\Aquarium.Engine.Tests\Aquarium.Engine.Tests.csproj", "{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany.AgentPreview", "src\Aquarium.Epiphany.AgentPreview\Aquarium.Epiphany.AgentPreview.csproj", "{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Zyphos", "src\Aquarium.Zyphos\Aquarium.Zyphos.csproj", "{801B1913-6AE1-4B8C-8030-4669AC4FE25E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Fractal", "src\Aquarium.Engine.Fractal\Aquarium.Engine.Fractal.csproj", "{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Engine.Fractal.Tests", "tests\Aquarium.Engine.Fractal.Tests\Aquarium.Engine.Fractal.Tests.csproj", "{A9D41C48-7E78-4D75-A814-0C1500245EFC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.LocalCast", "src\Aquarium.LocalCast\Aquarium.LocalCast.csproj", "{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.LocalCast.Tests", "tests\Aquarium.LocalCast.Tests\Aquarium.LocalCast.Tests.csproj", "{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{07C2787E-EAC7-C090-1BA3-A61EC2A24D84}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Fractal.Receipt", "tools\Aquarium.Fractal.Receipt\Aquarium.Fractal.Receipt.csproj", "{C0D1BFAC-B1B4-4350-92A7-D537AE382909}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Fensalir", "src\Aquarium.Fensalir\Aquarium.Fensalir.csproj", "{F9274D99-3CC6-42B6-9817-EC7F19663B1D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|x64.Build.0 = Release|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022}.Release|x86.Build.0 = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0089E341-25F2-43FC-8FD9-CBD1AE646D91}.Release|x86.Build.0 = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.Build.0 = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A8EDAE19-805B-4B09-9566-BE63937CA1B9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6E874823-17F8-453C-A077-4A101D425C84}.Release|x86.Build.0 = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{68AF73A1-65FE-4BCE-BDF6-8CB3DF12033E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{225265F1-8278-48D7-B271-BE8841D0C1CA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{435C602F-025E-4948-8285-E83E693DAA7B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BDE44048-7BA1-45C0-A69C-F70E5188FFE1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C0D1BFAC-B1B4-4350-92A7-D537AE382909}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{08D15A38-B6AB-459B-8BF9-3CCA8555A022} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{51FE01D8-9EEF-46B2-8D8C-3307AC2221C1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{7564E6C2-FF01-4B5B-8C2B-F9CD796858BA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{37363FC6-5AA3-49B4-A3B1-E4AB2CC986EB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{801B1913-6AE1-4B8C-8030-4669AC4FE25E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{F9274D99-3CC6-42B6-9817-EC7F19663B1D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{FF232A59-4E8D-4D56-A2C3-ACC02F5371D0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{A9D41C48-7E78-4D75-A814-0C1500245EFC} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{E8C66A46-1F56-484D-8CDD-5C0A3FD84A5F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{E6560D72-B018-4E00-AEBD-C1B9419E0BC7} = {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}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
54
EpiphanyAquarium.sln
Normal file
54
EpiphanyAquarium.sln
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany", "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj", "{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquarium.Epiphany.AgentPreview", "src\Aquarium.Epiphany.AgentPreview\Aquarium.Epiphany.AgentPreview.csproj", "{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x64.Build.0 = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{33654E0D-B31A-4062-B6AC-C1CE4C1A1F65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{F12A367A-8D8E-490D-B60E-3FDBEA3E07E2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
252
README.md
252
README.md
|
|
@ -1,248 +1,54 @@
|
|||
# Fensalir
|
||||
# Epiphany Aquarium
|
||||
|
||||
<p align="center">
|
||||
<img src="Fensalir-Icon.png" alt="Fensalir icon" width="160" />
|
||||
</p>
|
||||
Epiphany Aquarium is the Epiphany-owned client for the Fensalir native runtime.
|
||||
This repo owns client semantics: agent bodies, camera intent, CultCache state,
|
||||
CultNet interpretation, voice surfaces, and Epiphany-specific shaders.
|
||||
|
||||
Fensalir is the C# native rendering spine for Aquarium clients: windowing,
|
||||
D3D12 rendering, input, reload, persistent state, debug UI, and packageable
|
||||
engine machinery.
|
||||
|
||||
Epiphany is the first serious client in this repo. It is not the engine's
|
||||
identity.
|
||||
|
||||
## Intent
|
||||
|
||||
- Native C# host with a legible renderer spine and explicit ownership.
|
||||
- Vortice/D3D12 backend with graphics API details contained inside the engine.
|
||||
- `Aquarium.Engine.Contracts` as the client-facing boundary.
|
||||
- `Aquarium.Epiphany` as the current Epiphany Aquarium client.
|
||||
- `Aquarium.Sample.Minimal` as a small non-Epiphany boundary proof.
|
||||
- DirectWrite/Direct2D overlay text after the scene pass for crisp debug and UI
|
||||
typography.
|
||||
- Diegetic scene/UI surfaces driven by client objects instead of admin chrome.
|
||||
- CultCache for typed settings, persistent client state, and reload recovery.
|
||||
- CultNet-ready runtime identity for clients that need Epiphany communication.
|
||||
- Procedural audio and visual state treated as coupled signals.
|
||||
|
||||
## Current Shape
|
||||
|
||||
The engine opens a Win32 window, owns a D3D12 swapchain, compiles and hot-reloads
|
||||
HLSL, allocates render targets, preserves last-good shader/runtime state across
|
||||
failed reloads, hosts the debug overlay, and presents scene-linear HDR through
|
||||
the current post stack.
|
||||
|
||||
Client code is loaded behind `Aquarium.Engine.Contracts`, so a client can reload
|
||||
while the engine host, window, renderer, and D3D device stay alive. Runtime state
|
||||
is banked through CultCache MessagePack documents instead of loose JSON.
|
||||
|
||||
`Aquarium.Epiphany` currently supplies the Grid-centered camera policy, Epiphany
|
||||
agent SDF shaders, body placement, CultCache documents, and future CultNet
|
||||
interpretation. Those are client semantics. The engine consumes them as generic
|
||||
rendering and runtime data.
|
||||
Fensalir owns the window, renderer, D3D12 backend, audio device path, hot reload,
|
||||
debug chrome, contracts, fractal machinery, and engine demos. It lives next to
|
||||
this repo at `E:\Projects\Fensalir`.
|
||||
|
||||
## Repository Shape
|
||||
|
||||
This repository intentionally contains both the reusable Aquarium engine and the
|
||||
Epiphany client while the client API is still being carved into its final shape:
|
||||
- `src/Aquarium.Epiphany`: the runtime loaded by the Fensalir host.
|
||||
- `src/Aquarium.Epiphany.AgentPreview`: isolated Epiphany agent preview tool.
|
||||
- `docs`: Epiphany/client boundary notes.
|
||||
- `state`: repo memory, map, evidence, and scratch state for the client.
|
||||
- `scripts`: thin wrappers that delegate host/watch/reload work to Fensalir.
|
||||
|
||||
- `src/Aquarium.Engine.Contracts`: Vortice-free client API and shared data
|
||||
contracts.
|
||||
- `src/Aquarium.Engine`: Win32 host, D3D12 backend, reload transport, debug UI,
|
||||
audio host, and engine-owned assets.
|
||||
- `src/Aquarium.Epiphany`: Epiphany Aquarium runtime, state interpretation,
|
||||
render-plan configuration, and agent shaders.
|
||||
- `src/Aquarium.Zyphos`: planetary-scale rendering demo client with its own
|
||||
runtime, controls, scene state, and SDF planet shaders.
|
||||
- `src/Aquarium.Sample.Minimal`: tiny non-Epiphany client that proves the host
|
||||
can run something other than Epiphany.
|
||||
The client references Fensalir source projects directly while the package
|
||||
boundary is still young. Do not copy engine contracts back into this repo.
|
||||
|
||||
Splitting Aquarium and Epiphany into separate repos is plausible, but not owed
|
||||
yet. The cut becomes clean when the contracts are stable enough that Epiphany can
|
||||
move out without taking renderer policy, shader include plumbing, or reload
|
||||
folklore with it. Until then, the repo split would mostly create paperwork and a
|
||||
new place for confusion to hide.
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
dotnet build EpiphanyAquarium.sln
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Aquarium currently expects the sibling `CultMath` and `CultLib` repos at
|
||||
`E:\Projects\CultMath` and `E:\Projects\CultLib`, or the equivalent
|
||||
`..\..\..\CultMath` / `..\..\..\CultLib` paths from the engine project.
|
||||
|
||||
Aquarium consumes `AquaSynth.Faust` from the repo-local `packages` NuGet feed,
|
||||
not as a live project reference to `AquaSynth`. That package pulls
|
||||
`AquaSynth.Core` transitively for the built-in patch editor. This keeps Aquarium
|
||||
builds stable while synth work is in flight. To intentionally update that
|
||||
boundary, pack a new synth version and bump the `Aquarium.Engine` package
|
||||
reference:
|
||||
|
||||
```powershell
|
||||
.\scripts\update-synth-package.ps1
|
||||
```
|
||||
|
||||
```powershell
|
||||
dotnet build Aquarium.Engine.sln
|
||||
dotnet run --project src\Aquarium.Engine\Aquarium.Engine.csproj -- --client-assembly src\Aquarium.Epiphany\bin\Debug\net10.0\Aquarium.Epiphany.dll
|
||||
```
|
||||
|
||||
Open or build `Aquarium.Engine.sln`. It is the single solution inventory for the
|
||||
repo.
|
||||
|
||||
`src\Aquarium.Sample.Minimal` is a tiny non-Epiphany client used to keep the
|
||||
engine boundary honest. It references only `Aquarium.Engine.Contracts`, declares
|
||||
an `AquariumRenderPlan`, and boots through the same host/client loader path.
|
||||
|
||||
`src\Aquarium.Zyphos` is the first nontrivial non-Epiphany demo. It renders a
|
||||
rotating spherical planet, procedural continents, atmospheric rim light,
|
||||
night-side city glints, and an orbiting moon through the same host, renderer,
|
||||
reload, UI, bloom, and SDF proxy machinery used by Epiphany.
|
||||
|
||||
For iteration while an Aquarium window is already open, use the dev reload
|
||||
runner instead of `dotnet run`:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-reload.ps1
|
||||
```
|
||||
|
||||
It builds an apphost executable into a fresh disposable slot under
|
||||
`artifacts\dev-reload`, records the slot/PID in PowerShell CLIXML, and launches
|
||||
the runtime with a MessagePack CultCache store at
|
||||
`artifacts\dev-reload\cultcache\aquarium-client.msgpack`.
|
||||
The previous script-owned process is stopped only after the replacement build
|
||||
succeeds. This keeps MSBuild away from the locked normal `bin\Debug` output
|
||||
while a live window is still open.
|
||||
The wrapper calls `E:\Projects\Fensalir\scripts\dev-reload.ps1` with
|
||||
`src\Aquarium.Epiphany\Aquarium.Epiphany.csproj` as the client project.
|
||||
|
||||
Run a different client project through the same slot machinery with
|
||||
`-ClientProject`:
|
||||
For a headless smoke:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-reload.ps1 -ClientProject src\Aquarium.Zyphos\Aquarium.Zyphos.csproj
|
||||
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4
|
||||
```
|
||||
|
||||
If the visible dev window was closed but the last slot is still present, reopen
|
||||
it without rebuilding:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-reload.ps1 -Reopen
|
||||
```
|
||||
|
||||
Visible and headless runs keep separate PID state and logs, so a headless smoke
|
||||
does not replace the visible dev window.
|
||||
|
||||
For automatic rebuild/restart on source changes, use the watcher:
|
||||
For the watcher:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-watch.ps1
|
||||
```
|
||||
|
||||
Watch a specific client project the same way:
|
||||
## Boundary
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-watch.ps1 -ClientProject src\Aquarium.Zyphos\Aquarium.Zyphos.csproj
|
||||
```
|
||||
|
||||
The watcher polls source files, waits for writes to settle, builds into a new
|
||||
slot, and only replaces the running Aquarium after the new build succeeds. If a
|
||||
build fails, the previous good process keeps running and that same broken source
|
||||
fingerprint is not retried until files change again.
|
||||
It also watches the recorded script-owned process. If the visible window is
|
||||
closed or the recorded process dies, the watcher relaunches from a fresh slot on
|
||||
the next poll instead of sitting there with its hands folded like this is fine.
|
||||
To reopen a closed visible window from the last good slot without rebuilding,
|
||||
run the watcher with:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-watch.ps1 -ReopenWhenClosed
|
||||
```
|
||||
|
||||
Shader edits do not restart the process. The dev watcher fingerprints
|
||||
`.hlsl`/`.hlsli` files under both the engine and selected client shader roots,
|
||||
copies them into the running apphost slot, and lets the renderer rebuild the
|
||||
active D3D12 pipelines from its current shader source root. A bad shader edit
|
||||
leaves the previous working shaders bound and writes the compiler failure to
|
||||
stderr.
|
||||
|
||||
Epiphany client/runtime code is split into `Aquarium.Epiphany` behind
|
||||
`Aquarium.Engine.Contracts`. The host loads that client DLL through a collectible
|
||||
assembly load context, while `dev-watch.ps1` builds client-only changes into
|
||||
`artifacts\dev-reload\live-slots` and updates `live-current.txt`. The running
|
||||
host sees the pointer change, loads and starts the new runtime first, then
|
||||
disposes the old one only after the replacement is valid. A bad client DLL leaves
|
||||
the previous runtime alive and reports the reload failure to stderr. The watcher
|
||||
waits for the host log to acknowledge the exact client DLL path before calling a
|
||||
client reload successful; pointer updates without host acknowledgement are treated
|
||||
as reload failures. Successful reloads rehydrate through CultCache without
|
||||
restarting the window or D3D device.
|
||||
Host, renderer, contract, project, script, and `src\Aquarium.Engine\Assets`
|
||||
content changes still rebuild and restart the apphost. Runtime content belongs
|
||||
to the process image; shader hot reload cannot make a running renderer discover
|
||||
new files it never loaded.
|
||||
|
||||
Runtime live state is a typed CultCache document, not a loose sidecar file. The
|
||||
first document is `epiphany.aquarium.live_state`, currently banking camera
|
||||
target, yaw, pitch, distance, time, and save generation every few frames so a
|
||||
reload can rehydrate without pretending memory is vibes.
|
||||
|
||||
Stop the script-owned Aquarium process:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-stop.ps1
|
||||
```
|
||||
|
||||
Headless smoke:
|
||||
|
||||
```powershell
|
||||
dotnet run --project src\Aquarium.Engine\Aquarium.Engine.csproj -- --headless --client-assembly src\Aquarium.Epiphany\bin\Debug\net10.0\Aquarium.Epiphany.dll
|
||||
```
|
||||
|
||||
Headless reload smoke without touching the normal build output:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-reload.ps1 -Headless
|
||||
```
|
||||
|
||||
Renderer/client boundary check:
|
||||
|
||||
```powershell
|
||||
.\scripts\verify-boundaries.ps1
|
||||
```
|
||||
|
||||
Headless watch smoke:
|
||||
|
||||
```powershell
|
||||
.\scripts\dev-watch.ps1 -Headless
|
||||
```
|
||||
|
||||
Shipping publish:
|
||||
|
||||
```powershell
|
||||
.\scripts\publish-win-x64.ps1 -Clean -Zip
|
||||
```
|
||||
|
||||
This writes a self-contained Windows build to
|
||||
`artifacts\publish\win-x64`, verifies the executable, Epiphany client DLL,
|
||||
contracts DLL, icon, bundled fonts, and shader source, then optionally creates
|
||||
`artifacts\publish\EpiphanyAquariumEngine-win-x64.zip`.
|
||||
|
||||
## Persistent State
|
||||
|
||||
This repo carries its own Aquarium memory:
|
||||
|
||||
- `state/map.yaml`: canonical project map and next actions.
|
||||
- `state/memory.json`: durable taste, doctrine, renderer rules, and warnings.
|
||||
- `state/evidence.jsonl`: compact lessons that should change future behavior.
|
||||
- `state/scratch.md`: disposable working context for the current pass.
|
||||
|
||||
Keep that state current when the engine learns something durable. Dead lessons
|
||||
go out with the trash; the repo is not a scrapbook.
|
||||
|
||||
## Docs
|
||||
|
||||
- `docs/aquarium-engine-doctrine.md`: engine doctrine and invariants.
|
||||
- `docs/engine-client-boundary.md`: Engine/Epiphany ownership split.
|
||||
- `docs/epiphany-extraction-render-api-plan.md`: migration plan for the client
|
||||
extraction and ergonomic render graph API.
|
||||
- `docs/vortice-spine.md`: native host/renderer spine.
|
||||
- `docs/hlsl-renderer.md`: current shader path.
|
||||
- `docs/input.md`: camera and input contract.
|
||||
- `docs/cult-runtime-surface.md`: CultCache/CultNet runtime surface.
|
||||
Epiphany may depend on Fensalir contracts and renderer services. Fensalir must
|
||||
not depend on Epiphany policy, role names, CultNet surfaces, or client layout.
|
||||
If a change needs new engine authority, make it in `E:\Projects\Fensalir` first
|
||||
and consume the resulting contract here.
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
# Aquarium Engine Doctrine
|
||||
|
||||
Aquarium is a native engine now. The job is not to wrap a dashboard in a nicer
|
||||
skin; the job is to build a field machine that can host agents, state, sound,
|
||||
and UI as living objects.
|
||||
|
||||
## Frame Contract
|
||||
|
||||
- One camera target sits on the global XY plane.
|
||||
- That target is the Grid center.
|
||||
- Grid radius is derived from camera zoom distance.
|
||||
- Orbit changes yaw and pitch. Zoom changes distance. Pan moves the target.
|
||||
- Body anchors remain in world space.
|
||||
- The renderer may move the Grid visibility window, but world-space texture and
|
||||
field domains must stay stable.
|
||||
|
||||
## Renderer Contract
|
||||
|
||||
- The visible world is shader-owned.
|
||||
- Traditional geometry is scaffolding only when it helps the renderer stand up.
|
||||
- Raymarched bodies and Grid terrain must shade from the same field story that
|
||||
created their hits.
|
||||
- Broad-phase structures may cull, bin, and skip, but they must remain
|
||||
conservative. A culling primitive does not get to edit visible geometry.
|
||||
- Planet-local detail is local to the body domain. Moving a body must move its
|
||||
surface with it, not scroll the noise through the body.
|
||||
- Grid texturing and weather sample world-space domains.
|
||||
- HDR bloom spreads energy before tonemapping. ACES or its successor owns final
|
||||
display compression.
|
||||
|
||||
## Lighting Contract
|
||||
|
||||
- Self is the first diegetic emitter.
|
||||
- Surfaces should never be brighter than the light source unless the transport
|
||||
story explicitly explains it.
|
||||
- No ambient or global fill light in the default Aquarium path.
|
||||
- The long-term lighting target is a field sampled by terrain, bodies,
|
||||
atmospheres, particles, and diegetic UI.
|
||||
- Debug modes must expose hit coverage, normals, depth, field occupancy,
|
||||
lighting energy, and post-stack behavior before beauty work gets clever.
|
||||
|
||||
## UI Contract
|
||||
|
||||
- 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.
|
||||
- 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,
|
||||
not optional little manners.
|
||||
|
||||
## State Contract
|
||||
|
||||
- Runtime state is CultCache-backed and typed.
|
||||
- Live reload must rehydrate from cache before pretending it survived.
|
||||
- Shader reload may fail without killing the last good frame.
|
||||
- Live assembly reload may fail without killing the last good runtime.
|
||||
- Persistent state should be small, structured, and useful for future work.
|
||||
|
|
@ -8,7 +8,7 @@ Aquarium uses CultLib directly for runtime identity and reload recovery.
|
|||
it to a `CultCache`. The dev scripts pass:
|
||||
|
||||
```powershell
|
||||
--cache E:\Projects\Aquarium-Engine\artifacts\dev-reload\cultcache\aquarium-client.msgpack
|
||||
--cache E:\Projects\Fensalir\artifacts\dev-reload\cultcache\aquarium-client.msgpack
|
||||
```
|
||||
|
||||
Direct runs can also use `--cache <path>` or `AQUARIUM_CULTCACHE_PATH`.
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
# D3D12 Best Practices Audit
|
||||
|
||||
## Verdict
|
||||
|
||||
The current D3D12 path is a solid foundation for Aquarium's focused client
|
||||
renderer. It proves per-frame command allocators, fence-protected reuse,
|
||||
shader-visible descriptors, persistent mapped constants, explicit render-target
|
||||
state, brush-rendered Grid height, HDR/bloom presentation, temporal diagnostic
|
||||
targets, async shader pipeline builds, and a narrow DirectWrite overlay bridge.
|
||||
|
||||
## Good Current Decisions
|
||||
|
||||
- Command allocators are per swapchain frame and waited on before reset.
|
||||
- Shader-visible descriptor slots are not overwritten while in flight.
|
||||
- Constant upload memory is persistently mapped, 256-byte aligned, and allocated
|
||||
through a per-frame upload ring.
|
||||
- Offscreen render target state is tracked and redundant transitions are
|
||||
skipped.
|
||||
- Backbuffer state flows through the same transition helper style instead of raw
|
||||
assumed barriers.
|
||||
- D3D12 objects are named, and command-list events mark the frame for
|
||||
PIX/graphics diagnostics.
|
||||
- Shader-visible descriptors are split into static and per-frame transient
|
||||
arenas. Transient arenas reset only after the owning frame fence clears.
|
||||
- D3D12 resources are registered by name, and capacity diagnostics report upload,
|
||||
transient descriptor, static descriptor, and RTV usage.
|
||||
- The height-field target is a proper brush pass into scalar `R16_Float`.
|
||||
- Transparent event lanes are not preserved as dormant scaffolding. When Grid
|
||||
linework, particles, glyph motes, or other coverage events return, they need a
|
||||
live producer/consumer contract instead of alpha-blended final-frame hacks.
|
||||
- HDR bloom is pre-tonemap, multi-level, and separable.
|
||||
- The D3D11On12 bridge is overlay-only, keeping native hinted text without
|
||||
mixing overlay UI into scene rendering.
|
||||
|
||||
## Temporary Acceptable Debt
|
||||
|
||||
- Committed resources are used directly. Replace with D3D12MA or placed-resource
|
||||
suballocation before the renderer owns many textures/buffers.
|
||||
- Descriptor arenas are simple bump allocators. Add free lists or descriptor
|
||||
table paging only when real resource churn demands it.
|
||||
- Static shader and RTV descriptor arenas rebuild wholesale on resize. This is
|
||||
deliberately simple and correct for swapchain-dependent resources.
|
||||
- Upload ring is per-frame and reset after fence wait. It is not yet a global
|
||||
transient allocator with suballocation statistics or overflow diagnostics.
|
||||
- Removed-pass scaffolding should stay out of the live frame. Runtime
|
||||
volumetrics, medium/froxel targets, and medium history lanes are shelved.
|
||||
|
||||
## Required Before Larger Renderer Work
|
||||
|
||||
- Add GPU timestamp queries when frame cost starts mattering beyond CPU timing
|
||||
diagnostics.
|
||||
- Add descriptor-owner diagnostics beyond heap-level capacity summaries when
|
||||
descriptor churn becomes hard to reason about.
|
||||
- Keep future renderer systems tied to explicit live pass contracts before
|
||||
adding new render targets or history lanes.
|
||||
|
||||
## Rule
|
||||
|
||||
Build the resource machine first, then move pixels through it. Dead experiments
|
||||
belong to Git, not to the live frame graph.
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# D3D12 Renderer
|
||||
|
||||
D3D12 is the live renderer. The old D3D11 scene backend has been removed; the
|
||||
only remaining D3D11 use is the narrow D3D11On12 bridge required for native
|
||||
DirectWrite/Direct2D overlay text.
|
||||
|
||||
## Current State
|
||||
|
||||
- D3D12 owns the device, command queue, flip-discard swapchain, RTV heap,
|
||||
per-backbuffer command allocators, command list, fence-backed present loop,
|
||||
static and transient shader-visible descriptor arenas, upload buffers,
|
||||
renderer-owned height-field, HDR scene, bloom, temporal diagnostic targets,
|
||||
named objects, command-list events, and explicit tracked transitions.
|
||||
- The height-field pass uploads Aquarium frame constants plus client-authored
|
||||
brush data, renders a base field, then draws additive brush quads into a
|
||||
128x128 scalar `R16_Float` target.
|
||||
- The fullscreen scene pass traces the background and height-field surface.
|
||||
Epiphany bodies render afterward through client-declared SDF proxy pipelines
|
||||
that share engine-owned proxy mechanics and write into the same scene targets.
|
||||
- Presentation is scene-linear until the post pass. The scene renders to
|
||||
`R16G16B16A16_Float`, a three-level bloom pyramid performs firefly-safe
|
||||
downsample plus separable blur, and final presentation applies exposure,
|
||||
bloom/veil, and ACES.
|
||||
- The scene pass writes color/travel, field id/normal metadata, temporal
|
||||
control, and event targets. The present shader writes ping-pong history and
|
||||
exposes debug views for raw scene, history, control, identity, bloom, and
|
||||
exposed luminance.
|
||||
- DirectWrite and Direct2D remain for crisp overlay text through D3D11On12:
|
||||
D3D12 renders the frame, then the overlay acquires the swapchain backbuffer,
|
||||
draws debug UI, releases it to Present, and never participates in world
|
||||
rendering.
|
||||
- Shader and PSO creation runs off the main thread. Startup holds the splash
|
||||
until the first pipeline set is ready. Runtime shader edits under the engine
|
||||
or Epiphany shader roots are copied into the running apphost slot and trigger a
|
||||
background rebuild; successful builds swap in after a GPU wait and failures
|
||||
keep the previous pipeline set.
|
||||
- Resize waits for the GPU, releases swapchain-dependent resources, rebuilds
|
||||
descriptor arenas, and recreates backbuffer views plus dependent render
|
||||
targets.
|
||||
|
||||
## Invariants
|
||||
|
||||
- `IAquariumRenderer` is the host boundary. The host should not learn renderer
|
||||
internals as D3D12 grows teeth.
|
||||
- D3D12 work must be validated by actual headless runs, not just compilation.
|
||||
- Transparent-looking Grid output is an event lane, not canonical opaque depth.
|
||||
- DirectWrite overlay text belongs behind the narrow D3D11On12 bridge. Do not
|
||||
use it for diegetic/world text or let it leak into the scene graph.
|
||||
|
|
@ -1,83 +1,23 @@
|
|||
# Engine And Client Boundary
|
||||
# Fensalir Dependency Boundary
|
||||
|
||||
Aquarium Engine is not the Epiphany frontend client.
|
||||
Epiphany Aquarium is a client of Fensalir.
|
||||
|
||||
`Aquarium.Engine` owns the native machinery: Win32 windowing, D3D12 device
|
||||
state, swapchain presentation, resource lifetime, shader compilation, reload
|
||||
transport, input translation, render target allocation, and future render graph
|
||||
execution.
|
||||
This repo owns:
|
||||
|
||||
`Aquarium.Engine.Contracts` owns the Vortice-free public boundary clients use to
|
||||
describe runtime state, render intent, UI, audio patches, and persistent
|
||||
settings. Contracts may name reusable engine concepts. They may not smuggle
|
||||
Epiphany policy into the host under a nicer namespace.
|
||||
- Epiphany agent/body semantics.
|
||||
- Epiphany shader code.
|
||||
- Client render-plan configuration.
|
||||
- CultCache documents for Epiphany Aquarium state.
|
||||
- CultNet interpretation and voice surfaces.
|
||||
|
||||
`Aquarium.Epiphany` owns the client meaning: CultCache runtime state, CultNet
|
||||
interpretation, camera intent, cursor semantics, agent visual state, diegetic
|
||||
layout rules, and the render graph configuration that turns Epiphany state into
|
||||
world pixels.
|
||||
`E:\Projects\Fensalir` owns:
|
||||
|
||||
`Aquarium.Sample.Minimal` exists as the tiny smoke test. `Aquarium.Zyphos`
|
||||
exists as the first real non-Epiphany demo: a planetary client with its own
|
||||
runtime, controls, scene state, and SDF shaders. If a feature cannot be
|
||||
explained without Epiphany or Zyphos, it is not engine API yet.
|
||||
- Native windowing and process host.
|
||||
- D3D12 renderer, shader compilation, and render graph execution.
|
||||
- Input translation, audio output, debug chrome, and live reload.
|
||||
- `Aquarium.Engine.Contracts`.
|
||||
- Reusable fractal, field, reservoir, and demo machinery.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
- Engine code may expose rendering abstractions. It may not know Epiphany roles,
|
||||
sub-agent identities, CultNet surface semantics, or frontend-specific layout
|
||||
policy.
|
||||
- Epiphany code may configure cameras, render targets, shader passes, body
|
||||
registries, and resource bindings through engine APIs. It may not reach into
|
||||
D3D12 implementation details.
|
||||
- Hot reload is transport. It is not architecture. The reload boundary exists so
|
||||
the client can change without killing the device, not so client policy can
|
||||
leak into the engine under a nicer hat.
|
||||
- If a feature name includes `Self`, `Face`, `Imagination`, `Eyes`, `Body`,
|
||||
`Hands`, `Soul`, `Life`, `CultNet`, or Epiphany state semantics, the default
|
||||
home is `Aquarium.Epiphany` unless it is a data-only contract.
|
||||
- If a feature owns descriptors, command lists, fences, barriers, render target
|
||||
transitions, shader object lifetime, or swapchain presentation, the default
|
||||
home is `Aquarium.Engine`.
|
||||
|
||||
## Next Engine API Shape
|
||||
|
||||
The next durable renderer cut should make the client configure a small set of
|
||||
engine primitives:
|
||||
|
||||
- Render targets: format, dimensions, history policy, clear policy, and exported
|
||||
handles.
|
||||
- Cameras: view/projection data, previous-frame data, jitter policy, and named
|
||||
consumers.
|
||||
- Shader passes: shader module, root bindings, target outputs, depth policy,
|
||||
dispatch/draw shape, and reload identity.
|
||||
- Scene resources: structured buffers, textures, samplers, body registries, and
|
||||
light registries behind typed handles.
|
||||
- Frame graph edges: explicit read/write dependencies so pass ordering is data,
|
||||
not hidden renderer folklore.
|
||||
|
||||
Do not build this by piling adapters around the current monolith. First expose
|
||||
the smallest real engine abstraction that lets `Aquarium.Epiphany` describe one
|
||||
pass without D3D12 details. Then move one client-owned concept through it. Then
|
||||
repeat.
|
||||
|
||||
## Repository Split Line
|
||||
|
||||
Aquarium should eventually be usable without carrying the Epiphany client in the
|
||||
same repository. That does not mean the repo should be split before the boundary
|
||||
is mechanically clean.
|
||||
|
||||
Keep both in this repo while the public contracts, graph declarations, shader
|
||||
include layout, reload transport, and sample-client path are still changing
|
||||
together. Split Epiphany out only when:
|
||||
|
||||
- `Aquarium.Engine` and `Aquarium.Engine.Contracts` contain no Epiphany role,
|
||||
CultNet-surface, Grid-policy, or agent-layout semantics.
|
||||
- A non-Epiphany client can declare cameras, targets, shaders, UI, and
|
||||
presentation without referencing `Aquarium.Epiphany`.
|
||||
- Epiphany shaders, state documents, and visual grammar live entirely on the
|
||||
client side of the contracts.
|
||||
- The host can load an external client assembly without repo-local path lore.
|
||||
|
||||
Until those are true, a repo split would not simplify ownership. It would just
|
||||
make the same leak harder to see.
|
||||
The dependency direction is one-way. If Epiphany needs new engine authority,
|
||||
add that authority in Fensalir and consume it here through contracts. Do not
|
||||
copy contracts, host scripts, or renderer helpers into this repo.
|
||||
|
|
|
|||
|
|
@ -1,666 +0,0 @@
|
|||
# Epiphany Extraction And Render API Plan
|
||||
|
||||
Aquarium is the engine. Epiphany is one client app.
|
||||
|
||||
The current solution has the first physical boundary in place:
|
||||
|
||||
- `Aquarium.Engine` owns the host, D3D12 renderer, input loop, shader reload,
|
||||
debug overlay, and client runtime loader.
|
||||
- `Aquarium.Engine.Contracts` is the shared API surface between host and client.
|
||||
- `Aquarium.Epiphany` owns the current runtime state, camera rig, CultCache
|
||||
documents, and client factory.
|
||||
|
||||
That split is real but shallow. The engine still carries Epiphany-shaped world
|
||||
knowledge inside `D3D12Renderer`: fixed role counts, Self/cursor constants,
|
||||
agent shader paths, body light setup, Grid brush generation, orbit fallback
|
||||
motion, and the body visual buffer layout. The client can reload, but it cannot
|
||||
yet describe what gets rendered. The renderer remains a monolith with a client
|
||||
mask taped to the front. Rude, but accurate.
|
||||
|
||||
## Design Target
|
||||
|
||||
Client authors should be able to describe a frame in C# using named engine
|
||||
abstractions:
|
||||
|
||||
- render targets with formats, size policies, clears, history, and persistence
|
||||
- cameras with projection, view, previous-frame state, jitter, and named use
|
||||
- shaders with hot reload, includes, entry points, root bindings, and defaults
|
||||
- passes that declare reads, writes, depth, viewport, draw shape, and diagnostics
|
||||
- structured buffers, constants, textures, samplers, cubemaps, and body registries
|
||||
- standard features such as bloom, tonemapping, overlay, IBL, resize handling,
|
||||
shader reload, transient descriptors, barriers, and debug views
|
||||
|
||||
Aquarium should make the ordinary path boring:
|
||||
|
||||
```csharp
|
||||
public sealed class EpiphanyClient : AquariumClient
|
||||
{
|
||||
protected override void Configure(AquariumApp app)
|
||||
{
|
||||
var mainCamera = app.Cameras.Orbit("main")
|
||||
.LookAtGrid()
|
||||
.WithHistory();
|
||||
|
||||
var gridHeight = app.RenderTargets.Create("grid-height")
|
||||
.FixedSize(128, 128)
|
||||
.Format(RenderFormat.R16Float)
|
||||
.Clear(0.0f);
|
||||
|
||||
var scene = app.RenderTargets.Hdr("scene")
|
||||
.MatchWindow()
|
||||
.WithHistory();
|
||||
|
||||
app.Graph.Pass("grid-height")
|
||||
.Fullscreen("Shaders/GridHeight.hlsl", "GridHeightPS")
|
||||
.Write(gridHeight)
|
||||
.BindFrame()
|
||||
.Bind("GridBrushes", GridBrushes);
|
||||
|
||||
app.Graph.Pass("scene")
|
||||
.Fullscreen("Shaders/Scene.hlsl", "ScenePS")
|
||||
.Read(gridHeight)
|
||||
.Write(scene.Color, scene.Metadata, scene.Control)
|
||||
.Depth(scene.Depth)
|
||||
.UseCamera(mainCamera)
|
||||
.Bind("BodyLights", BodyLights);
|
||||
|
||||
app.Features.Bloom(scene.Color);
|
||||
app.Features.Tonemap(scene.Color).Present();
|
||||
app.Features.DirectWriteOverlay();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That is the level of API Aquarium should expose: explicit enough to be durable,
|
||||
ergonomic enough that nobody has to learn descriptor heaps before rendering a
|
||||
flower. Descriptor heaps are Aquarium's problem. If the client author asks for a
|
||||
sharp object, the engine hands them a handle, not the knife factory.
|
||||
|
||||
## Literature Anchor
|
||||
|
||||
This plan follows the render graph pattern used in production engines:
|
||||
|
||||
- Unity describes render graphs as high-level render-pass and resource
|
||||
declarations that simplify pipeline configuration and let the system manage
|
||||
resources efficiently: https://docs.unity.cn/Packages/com.unity.render-pipelines.core%4014.0/manual/render-graph-system.html
|
||||
- Unreal's Rendering Dependency Graph defers execution until all passes are
|
||||
gathered, then uses whole-frame knowledge for scheduling, barriers, memory,
|
||||
async compute, and validation:
|
||||
https://dev.epicgames.com/documentation/ru-ru/unreal-engine/rendering-dependency-graph?application_version=4.27
|
||||
- Frostbite's FrameGraph talk frames the same problem as a graph of passes and
|
||||
resources that keeps rendering features modular without giving up efficiency:
|
||||
https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-RenderingArc
|
||||
- Granite's Vulkan render graph writeup is useful implementation pressure:
|
||||
declare reads and writes, validate, traverse dependencies, assign logical
|
||||
resources to physical resources, generate barriers, alias compatible
|
||||
transient targets, then execute:
|
||||
https://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/
|
||||
|
||||
The Aquarium version should start smaller than those engines. No async compute,
|
||||
no memory aliasing, no multipass merging in the first cut. First: client-owned
|
||||
declarations, engine-owned execution.
|
||||
|
||||
## Current Coupling Inventory
|
||||
|
||||
These are the seams to cut.
|
||||
|
||||
### Shared Contracts
|
||||
|
||||
`Aquarium.Engine.Contracts` currently exposes:
|
||||
|
||||
- `IAquariumRuntime`
|
||||
- `IAquariumRuntimeFactory`
|
||||
- `AquariumRuntimeOptions`
|
||||
- `AquariumFrame`
|
||||
- `GraphicsSettings`
|
||||
- `GridFrame`
|
||||
- input state
|
||||
|
||||
`AquariumFrame` is too specific. It assumes one Grid, one camera position, one
|
||||
cursor, one time scalar, and a renderer that knows what those mean.
|
||||
|
||||
### Host
|
||||
|
||||
`AquariumHost` owns:
|
||||
|
||||
- client assembly load and reload pointer
|
||||
- window creation
|
||||
- renderer construction
|
||||
- debug input routing
|
||||
- settings synchronization
|
||||
- mouse projection onto the Grid plane
|
||||
|
||||
The mouse projection is client policy. The engine may provide ray utilities,
|
||||
but `ProjectMouseToGridPlane` belongs in the client or in a reusable camera
|
||||
helper chosen by the client.
|
||||
|
||||
### Renderer
|
||||
|
||||
`D3D12Renderer` owns both engine machinery and Epiphany scene policy:
|
||||
|
||||
- engine machinery: device, swapchain, fences, command list, descriptor arenas,
|
||||
upload ring, shader compilation, render targets, overlays, resize, resource
|
||||
transitions, pipeline state
|
||||
- Epiphany policy: Grid height target, Self gravity brush, role agent count,
|
||||
fixed agent body shader list, cursor hibiscus buffer slot, body lights, orbit
|
||||
fallback positions, Grid placement math, agent state scalars, debug mode names
|
||||
|
||||
The renderer should retain the machinery and lose the policy.
|
||||
|
||||
### Shader Assets
|
||||
|
||||
Engine shader assets currently include Epiphany bodies:
|
||||
|
||||
- `D3D12SelfBody.hlsl`
|
||||
- `D3D12FaceAgent.hlsl`
|
||||
- `D3D12ImaginationAgent.hlsl`
|
||||
- `D3D12EyesAgent.hlsl`
|
||||
- `D3D12BodyAgent.hlsl`
|
||||
- `D3D12HandsAgent.hlsl`
|
||||
- `D3D12SoulAgent.hlsl`
|
||||
- `D3D12LifeAgent.hlsl`
|
||||
- `D3D12CursorBody.hlsl`
|
||||
- `D3D12AgentCharacters.hlsli`
|
||||
|
||||
Those should move under the Epiphany client shader root once the engine can load
|
||||
client-declared shaders. Engine-owned includes should remain only when they are
|
||||
generic: SDF primitives, body proxy mechanics, PBR lighting, fullscreen vertex
|
||||
helpers, packing helpers, and graph binding conventions.
|
||||
|
||||
## Target Project Shape
|
||||
|
||||
First stable shape inside this repo:
|
||||
|
||||
```text
|
||||
src/
|
||||
Aquarium.Engine.Contracts/ public client-facing API
|
||||
Aquarium.Engine/ host plus D3D12 backend implementation
|
||||
Aquarium.Epiphany/ Epiphany client app/runtime
|
||||
```
|
||||
|
||||
Longer-term package shape:
|
||||
|
||||
```text
|
||||
src/
|
||||
Aquarium.Engine.Abstractions/ public API, no Vortice dependency
|
||||
Aquarium.Engine.D3D12/ D3D12 backend
|
||||
Aquarium.Engine.Host/ default Win32 host executable
|
||||
Aquarium.Epiphany/ client app, eventually movable out of repo
|
||||
```
|
||||
|
||||
Do not split projects before the API shape earns it. Premature project shrapnel
|
||||
is not architecture. It is paperwork with delusions.
|
||||
|
||||
## Public API Model
|
||||
|
||||
### Client Entry
|
||||
|
||||
Replace `IAquariumRuntime.Frame` with a richer client lifecycle:
|
||||
|
||||
```csharp
|
||||
public interface IAquariumClient : IDisposable
|
||||
{
|
||||
void Configure(AquariumAppBuilder app);
|
||||
void Load(AquariumLoadContext load);
|
||||
void Update(AquariumUpdateContext update);
|
||||
void Render(AquariumRenderContext render);
|
||||
void FlushState();
|
||||
}
|
||||
```
|
||||
|
||||
`Configure` declares stable graph shape. `Update` mutates CPU state. `Render`
|
||||
uploads per-frame data and selects dynamic pass instances. The engine compiles
|
||||
and executes the graph.
|
||||
|
||||
Keep a compatibility adapter from the old `IAquariumRuntime` while migrating,
|
||||
then remove it when Epiphany is fully on the new lifecycle.
|
||||
|
||||
### Handles
|
||||
|
||||
Expose typed handles, not D3D12 objects:
|
||||
|
||||
```csharp
|
||||
public readonly record struct RenderTargetHandle(string Name);
|
||||
public readonly record struct DepthTargetHandle(string Name);
|
||||
public readonly record struct CameraHandle(string Name);
|
||||
public readonly record struct ShaderHandle(string Name);
|
||||
public readonly record struct BufferHandle<T>(string Name) where T : unmanaged;
|
||||
public readonly record struct TextureHandle(string Name);
|
||||
public readonly record struct PassHandle(string Name);
|
||||
```
|
||||
|
||||
Names are diagnostic identity. The engine maps them to physical resources.
|
||||
|
||||
### Render Targets
|
||||
|
||||
Render target declarations should cover the common case without ceremony:
|
||||
|
||||
```csharp
|
||||
app.RenderTargets.Create("scene")
|
||||
.MatchWindow(scale: 1.0f)
|
||||
.Format(RenderFormat.Rgba16Float)
|
||||
.Clear(Color.Black)
|
||||
.Sampled()
|
||||
.History(frames: 2);
|
||||
|
||||
app.RenderTargets.Create("grid-height")
|
||||
.FixedSize(128, 128)
|
||||
.Format(RenderFormat.R16Float)
|
||||
.Clear(0.0f)
|
||||
.Sampled();
|
||||
```
|
||||
|
||||
Engine responsibilities:
|
||||
|
||||
- allocate and resize physical resources
|
||||
- allocate RTV, DSV, SRV, UAV descriptors
|
||||
- create transient descriptors per frame
|
||||
- preserve history targets
|
||||
- validate format and usage combinations
|
||||
- transition states
|
||||
- expose debug names and graph viewer data
|
||||
|
||||
### Cameras
|
||||
|
||||
The engine should provide camera math utilities and history storage. The client
|
||||
chooses semantics:
|
||||
|
||||
```csharp
|
||||
var camera = app.Cameras.Perspective("main")
|
||||
.Position(() => CameraRig.Position)
|
||||
.LookAt(() => CameraRig.Target3)
|
||||
.VerticalFovDegrees(58)
|
||||
.NearFar(0.05f, 500.0f)
|
||||
.WithPreviousFrame()
|
||||
.WithMouseRays();
|
||||
```
|
||||
|
||||
Engine-owned:
|
||||
|
||||
- view/projection matrices
|
||||
- inverse matrices
|
||||
- viewport transforms
|
||||
- previous-frame storage
|
||||
- jitter policy
|
||||
- screen ray generation helpers
|
||||
|
||||
Client-owned:
|
||||
|
||||
- what the camera tracks
|
||||
- Grid center
|
||||
- orbit controls
|
||||
- mouse-to-world projection policy
|
||||
|
||||
### Shaders And Includes
|
||||
|
||||
Shader declarations should hide D3DCompile details but expose the real contract:
|
||||
|
||||
```csharp
|
||||
var sceneShader = app.Shaders.Hlsl("scene")
|
||||
.File("Shaders/Scene.hlsl")
|
||||
.Vertex("FullscreenTriangleVS")
|
||||
.Pixel("ScenePS")
|
||||
.IncludeDirectory("Shaders/Includes")
|
||||
.HotReload();
|
||||
```
|
||||
|
||||
Engine-owned features:
|
||||
|
||||
- include expansion
|
||||
- compile diagnostics
|
||||
- previous-good shader retention
|
||||
- pipeline rebuild in the background
|
||||
- root binding validation
|
||||
- standard include library
|
||||
|
||||
Client-owned:
|
||||
|
||||
- shader source files
|
||||
- shader pass declarations
|
||||
- object-specific SDFs and materials
|
||||
|
||||
### Passes
|
||||
|
||||
Passes should declare resource use explicitly:
|
||||
|
||||
```csharp
|
||||
app.Graph.Pass("bloom-prefilter")
|
||||
.Fullscreen(sceneShader)
|
||||
.Read("scene")
|
||||
.Write("bloom-0")
|
||||
.BindFrame()
|
||||
.When(features => features.BloomEnabled);
|
||||
```
|
||||
|
||||
Initial pass types:
|
||||
|
||||
- fullscreen graphics pass
|
||||
- proxy rectangle graphics pass
|
||||
- instanced graphics pass
|
||||
- compute pass
|
||||
- copy/blit pass
|
||||
- present pass
|
||||
- overlay pass
|
||||
|
||||
Initial graph compiler:
|
||||
|
||||
- validate resource existence
|
||||
- validate acyclic dependencies
|
||||
- topologically sort by declared reads/writes
|
||||
- emit D3D12 transitions
|
||||
- allocate transient frame descriptors
|
||||
- rebuild on graph, shader, target, or window changes
|
||||
|
||||
Later graph compiler:
|
||||
|
||||
- transient target aliasing
|
||||
- pass culling
|
||||
- async compute
|
||||
- graph visualization
|
||||
- queue scheduling
|
||||
|
||||
## It-Just-Works Feature Layer
|
||||
|
||||
The public API should include feature builders over the low-level graph. They
|
||||
generate normal graph declarations, not secret renderer code.
|
||||
|
||||
### Presentation
|
||||
|
||||
```csharp
|
||||
app.Features.Presentation("default")
|
||||
.From(scene.Color)
|
||||
.Exposure(settings.SceneExposure)
|
||||
.Bloom(settings.Bloom)
|
||||
.Tonemap(Tonemap.AcesFitted)
|
||||
.Present();
|
||||
```
|
||||
|
||||
### Bloom
|
||||
|
||||
```csharp
|
||||
app.Features.Bloom(scene.Color)
|
||||
.Levels(3)
|
||||
.FireflySafe()
|
||||
.PreTonemap();
|
||||
```
|
||||
|
||||
### IBL
|
||||
|
||||
```csharp
|
||||
app.Features.ImageBasedLighting("studio")
|
||||
.Pmrem("Textures/studio2_pmrem.dds")
|
||||
.Irradiance("Textures/studio2_irradiance.dds")
|
||||
.BindAs("StudioIbl");
|
||||
```
|
||||
|
||||
### SDF Bodies
|
||||
|
||||
```csharp
|
||||
app.Features.SdfBodies("agents")
|
||||
.Buffer<AgentVisualGpu>("AgentVisuals", maxCount: 64)
|
||||
.SharedInclude("Shaders/D3D12SdfProxy.hlsli")
|
||||
.Role("Self", "Shaders/SelfBody.hlsl")
|
||||
.Role("Imagination", "Shaders/ImaginationBody.hlsl")
|
||||
.DepthAgainst(scene.Depth)
|
||||
.Write(scene.Color, scene.Metadata, scene.Control);
|
||||
```
|
||||
|
||||
The engine supplies proxy drawing, bounds, depth writes, common lighting, and
|
||||
debug counters. The client supplies the body list and shaders.
|
||||
|
||||
### Debug
|
||||
|
||||
The debug layer should read graph metadata:
|
||||
|
||||
- pass list and timing
|
||||
- target dimensions, formats, and history state
|
||||
- shader compile status
|
||||
- descriptor arena usage
|
||||
- upload ring usage
|
||||
- selected resource visualizers
|
||||
- graph validation warnings
|
||||
|
||||
Debug views should be registered, not hardcoded:
|
||||
|
||||
```csharp
|
||||
app.Debug.Views
|
||||
.Add("Final", scene.Color)
|
||||
.Add("Bloom", bloom.Output)
|
||||
.Add("Agent Steps", scene.Metadata.Channel("steps"));
|
||||
```
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 0: Freeze The Boundary
|
||||
|
||||
Goal: prevent new leaks while the old renderer still exists.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Keep `docs/engine-client-boundary.md` authoritative.
|
||||
- Add an architectural test or script check that `src/Aquarium.Engine` contains
|
||||
no `Aquarium.Epiphany` references.
|
||||
- Add a second check that engine shader names do not include Epiphany role names
|
||||
once shaders move.
|
||||
- Mark current fixed renderer path as legacy in docs, not as permanent API.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Boundary checks run in local verification.
|
||||
- New work has an obvious place to live.
|
||||
|
||||
### Phase 1: Rename Contracts Into Abstractions
|
||||
|
||||
Goal: turn `Aquarium.Engine.Contracts` into an actual engine API surface.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Introduce `AquariumAppBuilder`, `AquariumClient`, graph handles, resource
|
||||
descriptors, camera descriptors, shader descriptors, pass descriptors, and
|
||||
feature descriptors.
|
||||
- Keep old `IAquariumRuntime` beside the new API temporarily.
|
||||
- Add XML docs or README examples for the common path.
|
||||
- Keep all contracts Vortice-free.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A tiny sample client can declare one target, one fullscreen shader, and a
|
||||
presentation pass without referencing `Aquarium.Engine.Render` or Vortice.
|
||||
|
||||
### Phase 2: Build The Minimal Graph Compiler Inside Engine
|
||||
|
||||
Goal: execute client-declared resources and passes without replacing the whole
|
||||
renderer at once.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add internal `RenderGraph`, `RenderGraphCompiler`, `CompiledRenderGraph`, and
|
||||
`RenderGraphExecutor`.
|
||||
- Map public descriptors to internal D3D12 resources.
|
||||
- Support fullscreen graphics passes first.
|
||||
- Support named render target reads and writes.
|
||||
- Support one depth target.
|
||||
- Generate transitions from declared pass use.
|
||||
- Keep shader hot reload and previous-good pipeline behavior.
|
||||
- Add graph validation errors with pass/resource names.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Existing scene, bloom, and present path can be represented as graph passes,
|
||||
even if some callbacks still call existing private methods.
|
||||
|
||||
### Phase 3: Convert Presentation And Bloom To Features
|
||||
|
||||
Goal: move generic post-processing out of bespoke renderer flow.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Express HDR scene target, bloom pyramid, history targets, and present pass as
|
||||
graph resources.
|
||||
- Implement `app.Features.Bloom`.
|
||||
- Implement `app.Features.Presentation`.
|
||||
- Move render debug mode registration into graph/debug descriptors.
|
||||
- Keep DirectWrite overlay as an engine feature.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A non-Epiphany sample can render a color target, bloom it, tonemap it, and
|
||||
present without defining Grid, agents, cursor, or CultNet.
|
||||
|
||||
### Phase 4: Move Epiphany Shaders And Body Policy To Client
|
||||
|
||||
Goal: remove role bodies and cursor semantics from engine source.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Move role shader files and `D3D12AgentCharacters.hlsli` into
|
||||
`src/Aquarium.Epiphany/Shaders`.
|
||||
- Keep generic includes in engine:
|
||||
- fullscreen helpers
|
||||
- SDF primitives
|
||||
- PBR lighting
|
||||
- body proxy mechanics
|
||||
- packing and frame constants
|
||||
- Replace fixed `RoleAgentCount`, fixed shader path arrays, and fixed
|
||||
`AgentVisualGpu` ownership with a client-declared SDF body feature.
|
||||
- Move `BuildAgentVisualTable`, `BuildBodyLightTable`, `BuildGridHeightBrushes`,
|
||||
orbit fallback positions, cursor radius, and Self constants into
|
||||
`Aquarium.Epiphany`.
|
||||
- Let Epiphany upload buffers by handle.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `src/Aquarium.Engine` contains no role names, no cursor hibiscus, no Self
|
||||
constants, no agent orbit fallback, and no Epiphany shader paths.
|
||||
|
||||
### Phase 5: Move Grid Policy To Epiphany Or A Reusable Feature
|
||||
|
||||
Goal: decide whether Grid is a generic Aquarium feature or Epiphany client
|
||||
policy.
|
||||
|
||||
Recommended split:
|
||||
|
||||
- Engine feature: height-field target, brush rendering primitive, reflective
|
||||
surface shader helper, debug visualization.
|
||||
- Epiphany policy: Grid center, radius rule, Self gravity bowl, role gravity
|
||||
wells, radial wave parameters, body placement rule.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Expose `app.Features.HeightField`.
|
||||
- Expose CPU helper for evaluating brush stacks when the client asks for it.
|
||||
- Move current brush stack construction into Epiphany.
|
||||
- Move mouse-to-Grid projection out of `AquariumHost`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A different client can choose no Grid, a different height field, or a static
|
||||
render target without touching engine code.
|
||||
|
||||
### Phase 6: Replace Frame Contract
|
||||
|
||||
Goal: stop `AquariumFrame` from being the rendering protocol.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Introduce `AquariumFrameContext` for time, window size, input, graph resource
|
||||
access, frame constants, and upload helpers.
|
||||
- Let clients define their own frame constants structs.
|
||||
- Keep engine standard constants optional:
|
||||
- resolution
|
||||
- time
|
||||
- camera matrices
|
||||
- previous camera matrices
|
||||
- presentation settings
|
||||
- Provide `Frame.UploadConstant<T>`, `Frame.UploadBuffer<T>`, and
|
||||
`Frame.SetGraphValue`.
|
||||
- Remove renderer dependence on `AquariumFrame`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `IAquariumRenderer.Render` consumes a compiled graph frame, not an Epiphany
|
||||
shaped frame record.
|
||||
|
||||
### Phase 7: Split Host From Backend When Earned
|
||||
|
||||
Goal: make Aquarium usable as an engine package, not only an executable.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Split public abstractions from D3D12 backend if the contract has stabilized.
|
||||
- Keep Win32 host as the default app host.
|
||||
- Let clients choose hosted mode:
|
||||
- engine-owned window
|
||||
- headless
|
||||
- later embedded/native handle mode
|
||||
- Package shader include library and default features as engine assets.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A fresh client can reference Aquarium abstractions, implement a client class,
|
||||
run through the host, and declare render graph resources without copying
|
||||
Epiphany code.
|
||||
|
||||
## Order Of Attack
|
||||
|
||||
Do not start by moving every file. That produces thrilling amounts of rubble.
|
||||
|
||||
Recommended first implementation slice:
|
||||
|
||||
1. Add public render target, shader, pass, camera, and feature descriptor types
|
||||
to contracts.
|
||||
2. Add a graph builder that can describe the current fixed pipeline without
|
||||
executing it.
|
||||
3. Add a debug dump of the declared graph.
|
||||
4. Convert bloom and presentation to execute from graph declarations.
|
||||
5. Convert body proxy draws to a client-declared body registry.
|
||||
6. Move Epiphany shaders out of engine.
|
||||
7. Move Grid and agent/cursor frame construction out of renderer.
|
||||
|
||||
Each slice must preserve:
|
||||
|
||||
- shader reload keeps last good pipelines
|
||||
- failed client reload keeps previous client alive
|
||||
- resize rebuilds all size-bound targets
|
||||
- debug overlay remains readable
|
||||
- headless smoke still completes
|
||||
- engine source stays free of Epiphany references
|
||||
|
||||
## Verification Gates
|
||||
|
||||
Every migration pass should run:
|
||||
|
||||
```powershell
|
||||
dotnet build Aquarium.Engine.sln --no-restore
|
||||
.\scripts\dev-reload.ps1 -Headless -RetainSlots 4
|
||||
.\scripts\verify-boundaries.ps1
|
||||
```
|
||||
|
||||
The final `rg` should shrink over time. At the end it should be empty except
|
||||
for generic documentation comments if any survive, and they probably should not.
|
||||
|
||||
## Non-Goals For The First Graph
|
||||
|
||||
- no async compute
|
||||
- no transient memory aliasing
|
||||
- no general material system
|
||||
- no visual node editor
|
||||
- no automatic shader reflection dependency on day one
|
||||
- no project split until the API stops moving under our feet
|
||||
- no "compatibility mode" escape hatch that lets client code call D3D12 directly
|
||||
|
||||
The first graph exists to establish ownership and data flow. Optimization comes
|
||||
after the graph tells the truth.
|
||||
|
||||
## Success Definition
|
||||
|
||||
Aquarium is clean when:
|
||||
|
||||
- Engine code can render a non-Epiphany sample client.
|
||||
- Epiphany can add a render target, camera, shader, buffer, and pass without
|
||||
editing `D3D12Renderer`.
|
||||
- Epiphany shaders live with Epiphany.
|
||||
- Engine shader includes are generic.
|
||||
- Resource lifetime, descriptors, barriers, resize, hot reload, presentation,
|
||||
and debug inspection are engine-owned.
|
||||
- Client code describes intent. Engine code makes it work.
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# HLSL Renderer
|
||||
|
||||
Aquarium's visible world is currently a D3D12 HLSL renderer with a small,
|
||||
explicit frame graph.
|
||||
|
||||
## Current Path
|
||||
|
||||
1. Engine-owned `D3D12HeightField.hlsl` renders a 128x128 scalar height-field
|
||||
target from client-authored brush data.
|
||||
2. Engine-owned `D3D12Scene.hlsl` traces the background and Grid into
|
||||
scene-linear HDR targets.
|
||||
3. Epiphany-owned body shaders each get their own proxy pipeline:
|
||||
`D3D12FaceAgent.hlsl`, `D3D12ImaginationAgent.hlsl`,
|
||||
`D3D12EyesAgent.hlsl`, `D3D12BodyAgent.hlsl`, `D3D12HandsAgent.hlsl`,
|
||||
`D3D12SoulAgent.hlsl`, `D3D12LifeAgent.hlsl`, `D3D12SelfBody.hlsl`, and
|
||||
`D3D12CursorBody.hlsl`. Shared proxy mechanics live in the engine includes
|
||||
`D3D12SdfCommon.hlsli`, `D3D12SdfProxy.hlsli`, and `D3D12SdfMath.hlsli`.
|
||||
4. Engine-owned `D3D12Post.hlsl` builds bloom, resolves diagnostics/history, applies
|
||||
exposure and ACES, then presents.
|
||||
5. `DirectWriteOverlay` draws crisp final-pixel debug UI after scene rendering.
|
||||
|
||||
C# feeds explicit frame constants: resolution, time, Grid radius, camera
|
||||
position, presentation controls, previous frame state, cursor anchors, and debug
|
||||
mode. CultMath is the CPU-side math grammar. Vortice math stays at the graphics
|
||||
API membrane.
|
||||
|
||||
## Grid
|
||||
|
||||
The Grid height target is centered on the camera target; body anchors remain in
|
||||
world space. The target stores height in `.r` only. A future useful expansion is
|
||||
packed gradients in `.g/.b` if normal cost becomes worth the extra bandwidth.
|
||||
|
||||
Grid linework is derivative-aware in the final scene pass. Cartesian lines use
|
||||
screen-space `fwidth` against their world-domain coordinate so minor and major
|
||||
lines stay the same pixel width across zoom distances. Terrain isolines derive
|
||||
from the Grid height field, while field lines quantize terrain gradient angle
|
||||
and fade out on flat surfaces.
|
||||
|
||||
The fullscreen scene pass owns environment and Grid only. Bodies are not folded
|
||||
into that pass. After the Grid writes scene color and travel-derived depth,
|
||||
each body gets its own proxy draw and shader pipeline. The shared proxy vertex
|
||||
shader computes a conservative screen rectangle from the uploaded body center
|
||||
and bound radius. The body-specific pixel shader raymarches only that object
|
||||
and writes travel-derived depth so the Grid and nearer bodies arbitrate
|
||||
visibility through the depth target.
|
||||
|
||||
## Solids
|
||||
|
||||
Epiphany uploads Self, Face, Imagination, Eyes, Body, Hands, Soul, Life, and the
|
||||
cursor through the engine `AquariumSdfObject` buffer, including current center,
|
||||
previous center, and state scalars. Aquarium owns the D3D12 upload, proxy draw,
|
||||
depth, and presentation machinery; Epiphany owns which bodies exist, where they
|
||||
are, and which shader files describe them.
|
||||
|
||||
The cursor is a luminous three-lobed hibiscus SDF that lives on the XY plane:
|
||||
its center is one cursor radius above XY, so its lower contact tip lands at
|
||||
Grid z=0 instead of sampling the Grid height field. The previous MathWorld
|
||||
teardrop formula remains in `D3D12SdfMath.hlsli` for later reuse. Agent visuals
|
||||
carry their previous centers in the structured buffer for temporal reprojection.
|
||||
|
||||
Lighting is currently direct and diegetic: Self is the emitter. No ambient fill
|
||||
is allowed to quietly solve a bad light story.
|
||||
|
||||
## Temporal Diagnostics
|
||||
|
||||
The temporal resolver is documented in `docs/tsr-inspired-taa-spec.md`. The live
|
||||
frame keeps color, metadata, and control history. Projection jitter uses a small
|
||||
Halton sequence, and final presentation blends validated history. SDF object
|
||||
highlights get a narrow hot-current history path so transient specular spikes
|
||||
can be damped without relaxing travel, field, or normal validation. Bloom is
|
||||
clamped at prefilter for unsupported single-pixel HDR spikes, then attenuated
|
||||
when the resolved scene has damped a much hotter current sample, so transient
|
||||
fireflies do not keep blooming through the current-frame bloom path.
|
||||
Dormant event lanes are not preserved without a producer and pass contract.
|
||||
|
||||
Debug modes:
|
||||
|
||||
- `0` final
|
||||
- `1` raw current scene
|
||||
- `2` reprojected history sample
|
||||
- `3` history age
|
||||
- `4` history weight
|
||||
- `5` coverage and step ratio
|
||||
- `6` current field identity
|
||||
- `7` bloom contribution
|
||||
- `8` exposed luminance
|
||||
- `9` agent identity
|
||||
- `10` agent SDF step count
|
||||
|
||||
`F1` cycles modes. Number keys select the first modes directly. Startup mode can
|
||||
be set with `--render-debug` or `AQUARIUM_RENDER_DEBUG_MODE`.
|
||||
|
||||
## HDR
|
||||
|
||||
Presentation applies explicit exposure before display transformation and adds a
|
||||
low-gain pre-tonemap bloom/veil pyramid. The bloom pass renders exposed
|
||||
scene-linear HDR color into half-, quarter-, and eighth-resolution targets, uses
|
||||
firefly-safe downsampling, blurs each level with separable horizontal/vertical
|
||||
passes, then contributes gently before the ACES fit. This is not threshold glow.
|
||||
|
||||
## Overlay Text
|
||||
|
||||
Readable overlay text is handled by `DirectWriteOverlay`, using DirectWrite for
|
||||
font layout/rasterization and Direct2D for final draw. D3D12 reaches that path
|
||||
through D3D11On12 only after the scene frame is rendered. The bridge is
|
||||
overlay-only.
|
||||
|
||||
The debug controls live in `Render/Ui/DebugUi.cs`. The grammar is a native
|
||||
version of CultLib's generator API: panels, sections, sliders, toggles, options,
|
||||
and buttons are declared in code and bound to engine state with explicit read
|
||||
and write delegates. The visual language is compact, flat, and allowed to have
|
||||
taste. Frighteningly controversial, apparently.
|
||||
|
||||
The overlay owns a private DirectWrite font collection built from bundled Google
|
||||
Fonts files in `Assets/Fonts`: Montserrat for thin small-cap display text and
|
||||
Ubuntu Sans for body/debug copy. Runtime must not depend on system-installed
|
||||
fonts.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# Input
|
||||
|
||||
Input is currently Win32-backed and intentionally small.
|
||||
|
||||
## Flow
|
||||
|
||||
- `Win32Window` translates window messages into `InputState`.
|
||||
- `AquariumHost` begins each input frame, pumps messages, then updates runtime.
|
||||
- `AquariumRuntime` passes input into `OrbitCameraRig`.
|
||||
- The renderer receives the updated camera and Grid frame as constants.
|
||||
|
||||
## Controls
|
||||
|
||||
- Middle mouse drag: orbit camera around the Grid center.
|
||||
- Mouse wheel: exponential zoom.
|
||||
- Right mouse drag: pan along the Grid plane.
|
||||
- `WASD`: pan along the Grid plane.
|
||||
|
||||
The camera target remains the Grid center. Panning moves that shared target;
|
||||
orbiting changes only yaw/pitch; zoom changes only camera distance and therefore
|
||||
Grid radius.
|
||||
|
|
@ -1,683 +0,0 @@
|
|||
# Perfect Machine Architecture
|
||||
|
||||
## Objective
|
||||
|
||||
Build one real-time spatial evidence machine that can render and resolve:
|
||||
|
||||
- fast 2D scalar-field splats on tileable surfaces;
|
||||
- fast 3D scalar-field splats in object/world volumes;
|
||||
- SDF/level-set surfaces for opaque solids;
|
||||
- density/extinction fields for flames, smoke, fog, plasma, and uncertain
|
||||
sensor volumes;
|
||||
- 2D fields projected onto 3D domains such as cube-sphere planets and toroidal
|
||||
station decks;
|
||||
- camera/microphone sensor evidence from Mimir-style capture;
|
||||
- cached structural probes emitted by IFS `.aquageo` grammars;
|
||||
- stochastic updates that converge under CPU, GPU, RAM, and SSD budgets.
|
||||
|
||||
The product requirement is not "infinite detail." It is a compact authored
|
||||
description that can put detail where pixels can see it, keep enough evidence
|
||||
alive to stabilize it, and refuse work that cannot justify its cost.
|
||||
|
||||
## Research Spine
|
||||
|
||||
This architecture is not invented from the floorboards:
|
||||
|
||||
- ReSTIR DI and GI prove reservoir reuse as a real-time sampling strategy for
|
||||
expensive candidates across time and screen space.
|
||||
- GRIS generalizes reservoir resampling beyond direct light candidates through
|
||||
target functions, correlated samples, domains, and shift mappings.
|
||||
- EWA surface/volume splatting and 3D Gaussian splatting prove anisotropic
|
||||
projected support as a practical rendering primitive.
|
||||
- Geometry clipmaps and Nanite-style virtualized geometry prove that visible
|
||||
projected error and resident summaries beat global leaf traversal.
|
||||
- TSDF fusion/KinectFusion prove that streamed sensor samples can accumulate
|
||||
into a coherent implicit spatial field.
|
||||
- Instant-NGP's multiresolution hash encoding is a warning and inspiration:
|
||||
stochastic evidence can be fast when the data layout is explicit and GPU
|
||||
friendly, but Aquarium should preserve deterministic summaries and bounds as
|
||||
safety authority.
|
||||
|
||||
## Prime Invariants
|
||||
|
||||
- Authored semantic domains are the source of truth.
|
||||
- Backend packets are compiled output and may be deleted/rebuilt.
|
||||
- Every domain, node, claim, probe, payload, page, and reservoir has a stable
|
||||
key.
|
||||
- Every reusable sample must name its target function and source PDF or state
|
||||
why it is non-probabilistic structural evidence.
|
||||
- Every LOD subtree has a conservative parent summary.
|
||||
- Learned/stochastic priority may rank work; it may not own field safety,
|
||||
visibility safety, or calibration safety.
|
||||
- Missing children render through parent summaries. A frame does not wait for
|
||||
SSD.
|
||||
- TAA owns pixel history, not producer identity.
|
||||
- Mimir sensor fusion and fractal rendering share the evidence machinery; they
|
||||
do not share client policy.
|
||||
|
||||
## System Pipeline
|
||||
|
||||
```text
|
||||
Authored intent or live sensor input
|
||||
-> Domain binding
|
||||
-> Evidence candidate generation
|
||||
-> Target evaluation
|
||||
-> Resampled importance reservoir
|
||||
-> Temporal reuse validation
|
||||
-> Spatial/domain reuse validation
|
||||
-> Occupancy graph update
|
||||
-> Residency/page scheduling
|
||||
-> Backend packet lowering
|
||||
-> 2D/3D/projected field splat passes
|
||||
-> Temporal resolve guide buffers
|
||||
-> Debug/evidence telemetry
|
||||
```
|
||||
|
||||
The machine is allowed to have multiple candidate producers. The reservoir
|
||||
contract makes them comparable without pretending they are the same thing.
|
||||
|
||||
## Module Network
|
||||
|
||||
```text
|
||||
Aquarium.Engine.Contracts
|
||||
Stable DTOs: domains, claims, nodes, summaries, probes, reservoir guides,
|
||||
payload pages, backend packets, debug rows.
|
||||
|
||||
Aquarium.Engine.Fractal
|
||||
Pure CPU algorithms: domain math, grammar expansion, ownership trees,
|
||||
summaries, scoring, reservoirs, occupancy graph updates, residency planning,
|
||||
packet planning.
|
||||
|
||||
Aquarium.Engine.SensorFusion
|
||||
Future shared adapter layer: camera/audio feature candidates, calibration
|
||||
confidence, raw retention lowerings, Mimir-facing packet contracts.
|
||||
|
||||
Aquarium.Engine.Render
|
||||
D3D12 resources, page tables, structured buffers, field splat passes,
|
||||
surface/volume resolves, TAA guide buffers, debug visualization.
|
||||
|
||||
Aquarium.Zyphos
|
||||
World policy, cube-sphere/tile roots, planet grammar seeds, setting-safe
|
||||
visuals.
|
||||
|
||||
Aquarium.Epiphany
|
||||
Agent/body policy, role grammar selection, semantic bindings.
|
||||
|
||||
Mimir/LocalCast
|
||||
Sensor capture, raw rolling retention, feature extraction, calibration facts.
|
||||
It does not own resolved spatial evidence history.
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
### Domain
|
||||
|
||||
```text
|
||||
DomainKey
|
||||
ParentKey
|
||||
Kind: solar/orbital/planet/cubeTile/torus/surface2d/object3d/volume3d/sensorRig
|
||||
Frame
|
||||
Projection
|
||||
Bounds
|
||||
Periodicity
|
||||
Owner
|
||||
```
|
||||
|
||||
Domains say where coordinates live and how they map to parent space. A domain is
|
||||
allowed to be 2D, 3D, or 2D projected onto 3D.
|
||||
|
||||
### Claim
|
||||
|
||||
```text
|
||||
ClaimKey
|
||||
DomainKey
|
||||
NodeKey
|
||||
Layer: form/appearance/transport
|
||||
Kind: height/sdf2d/sdf3d/density/extinction/material/phase/emission/light/feature/confidence
|
||||
LocalFrame
|
||||
Envelope
|
||||
Payload
|
||||
Tags
|
||||
CostTier
|
||||
Seed
|
||||
```
|
||||
|
||||
Claims are authored or inferred statements about a field. They are not renderer
|
||||
packets.
|
||||
|
||||
### Field Layer Split
|
||||
|
||||
The old shorthand was `SDF / PBR / Radiosity`. That naming was useful for the
|
||||
first opaque-object path, but it is not the architecture.
|
||||
|
||||
The durable split is:
|
||||
|
||||
```text
|
||||
Form -> what exists where
|
||||
Appearance -> how it interacts locally with light, color, and material
|
||||
Transport -> how energy moves through or from it
|
||||
```
|
||||
|
||||
Opaque solids lower through level-set form:
|
||||
|
||||
```text
|
||||
phi(x) = signed distance
|
||||
gradient(phi) = surface normal
|
||||
surface exists near phi = 0
|
||||
```
|
||||
|
||||
Transparent media lower through density/extinction form:
|
||||
|
||||
```text
|
||||
rho(x) = density / extinction / confidence
|
||||
gradient(rho) = direction of local density change
|
||||
volume exists where rho contributes above threshold
|
||||
```
|
||||
|
||||
Sensor fusion may begin as confidence density and sharpen into surface claims
|
||||
when multi-view/acoustic evidence makes a stable boundary plausible. Fractal
|
||||
terrain may begin as SDF/height surfaces. Flames stay participating fields:
|
||||
their form is density/extinction, their appearance is emission/color/phase, and
|
||||
their transport is emission/scattering reuse.
|
||||
|
||||
Implementation note: current packet names still say SDF/PBR/radiosity in places
|
||||
because the first GPU receipt was built for opaque splats. Treat those as
|
||||
backend packet names, not the conceptual contract.
|
||||
|
||||
### Probe
|
||||
|
||||
```text
|
||||
ProbeKey
|
||||
DomainKey
|
||||
LocalPosition
|
||||
LocalNormalOrGradient
|
||||
BoundRadius
|
||||
PayloadHandle
|
||||
TargetContribution
|
||||
SourcePdf
|
||||
Confidence
|
||||
ObservedFrame
|
||||
ObservedTime
|
||||
ProducerKind
|
||||
FieldLayer
|
||||
FieldEncoding
|
||||
```
|
||||
|
||||
A structural probe can come from an IFS grammar. A sensor probe can come from a
|
||||
camera/audio feature. Both become candidates only after they have a target
|
||||
function and validation rules.
|
||||
|
||||
### Reservoir
|
||||
|
||||
```text
|
||||
SelectedSample
|
||||
SelectedTarget
|
||||
WeightSum
|
||||
CandidateCount
|
||||
ContributionWeight
|
||||
ValidationMask
|
||||
SampleAge
|
||||
DomainKey
|
||||
```
|
||||
|
||||
The reservoir owns resampling math. It does not own stable tracks, packet
|
||||
lowering, or TAA history.
|
||||
|
||||
### Occupancy Graph
|
||||
|
||||
```text
|
||||
NodeKey
|
||||
DomainKey
|
||||
Bounds
|
||||
SummaryPayload
|
||||
ChildPayload
|
||||
MeanContribution
|
||||
Variance
|
||||
Confidence
|
||||
SampleAge
|
||||
ResidencyState
|
||||
LastVisibleFrame
|
||||
LastUpdateFrame
|
||||
```
|
||||
|
||||
The occupancy graph is the shared heuristic memory of where useful field
|
||||
evidence likely lives. It is structural enough for IFS trees and statistical
|
||||
enough for sensor fusion.
|
||||
|
||||
### Page
|
||||
|
||||
```text
|
||||
PageKey
|
||||
PayloadKind
|
||||
ByteRangeOrHandle
|
||||
ResidentTier: gpu/ram/ssd/missing
|
||||
EstimatedGpuCost
|
||||
EstimatedRamBytes
|
||||
EstimatedSsdBytes
|
||||
LastUseFrame
|
||||
```
|
||||
|
||||
Pages are storage decisions, not semantic identity.
|
||||
|
||||
## Algorithms
|
||||
|
||||
### 1. Domain Binding
|
||||
|
||||
Every candidate is first bound to a domain path. Cube-sphere terrain binds to a
|
||||
face/tile path. Torus surfaces bind to periodic `u/v` patches. Agent details
|
||||
bind to object-local sheets, curves, or volumes. Sensor features bind to a
|
||||
sensor rig frame and then to a calibrated world or object domain.
|
||||
|
||||
Validation starts here. If two samples do not share a valid lineage or shift
|
||||
mapping, they do not reuse each other.
|
||||
|
||||
### 2. IFS Grammar Expansion
|
||||
|
||||
The `.aquageo` DSL emits semantic claims and ownership nodes:
|
||||
|
||||
```text
|
||||
grammar -> domains -> claims -> ownership tree -> summaries
|
||||
```
|
||||
|
||||
Expansion is deterministic by stable key and seed. Generated leaves do not all
|
||||
need to be resident. The ownership tree must summarize itself before it earns
|
||||
LOD rights.
|
||||
|
||||
### 3. Sensor Candidate Lowering
|
||||
|
||||
Camera and microphone producers retain raw capture ordering, calibration, and
|
||||
provenance. Aquarium receives resolved candidate observations:
|
||||
|
||||
```text
|
||||
camera/audio feature -> calibrated local/world candidate -> target evaluator
|
||||
```
|
||||
|
||||
Sensor adapters own modality interpretation. Aquarium owns spatial evidence
|
||||
history after lowering.
|
||||
|
||||
### 4. Target Evaluation
|
||||
|
||||
Targets are comparable scalar contributions:
|
||||
|
||||
- projected SDF/form error;
|
||||
- density/extinction contribution;
|
||||
- appearance delta over visible coverage;
|
||||
- transport/radiance contribution;
|
||||
- feature confidence times calibration confidence;
|
||||
- expected pixel influence;
|
||||
- expected information gain for an uncertain region.
|
||||
|
||||
The source PDF is the probability by which the candidate was proposed. If the
|
||||
candidate is deterministic structural evidence, it uses an explicit proposal
|
||||
policy rather than a magical free sample.
|
||||
|
||||
### 5. Reservoir Update
|
||||
|
||||
Each reservoir stores one representative sample plus the weight mass of the
|
||||
candidates it represents:
|
||||
|
||||
```text
|
||||
candidateWeight = target / sourcePdf
|
||||
weightSum += candidateWeight
|
||||
candidateCount += representedCount
|
||||
select candidate with probability candidateWeight / weightSum
|
||||
```
|
||||
|
||||
Final contribution weight follows the RIS/ReSTIR shape:
|
||||
|
||||
```text
|
||||
weightSum / (candidateCount * selectedTarget)
|
||||
```
|
||||
|
||||
Temporal reuse and spatial/domain reuse are explicit passes, not side effects.
|
||||
|
||||
### 6. Reuse Validation
|
||||
|
||||
Temporal reuse validates:
|
||||
|
||||
- camera motion/reprojection;
|
||||
- previous/current domain lineage;
|
||||
- disocclusion;
|
||||
- field id/material class;
|
||||
- local-frame error;
|
||||
- conservative bounds;
|
||||
- sample age and confidence.
|
||||
|
||||
Spatial/domain reuse validates:
|
||||
|
||||
- neighbor surface compatibility;
|
||||
- tile adjacency or periodic wrap;
|
||||
- cube-face seam mapping;
|
||||
- torus seam mapping;
|
||||
- object-local parent frame stability;
|
||||
- sensor calibration agreement.
|
||||
|
||||
GRIS-style shift mappings live here. A shift is an owned object with tests, not
|
||||
a helper function hiding inside a shader.
|
||||
|
||||
### 7. Occupancy Graph Update
|
||||
|
||||
The occupancy graph receives selected reservoirs and sparse high-quality probes.
|
||||
It updates node statistics under budget:
|
||||
|
||||
```text
|
||||
mean, variance, confidence, sampleAge, lastVisible, lastUpdated
|
||||
```
|
||||
|
||||
Unvisited nodes decay. Stale uncertainty grows. Visible, uncertain, stale, and
|
||||
near-threshold nodes retain nonzero exploration probability.
|
||||
|
||||
### 8. Selected Cut And Residency
|
||||
|
||||
A hierarchy cut is selected from summaries:
|
||||
|
||||
```text
|
||||
projected error
|
||||
material delta
|
||||
coverage
|
||||
current reservoir confidence
|
||||
estimated GPU cost
|
||||
residency state
|
||||
```
|
||||
|
||||
Children stream only when the cut justifies them. Parent summaries remain
|
||||
renderable. Eviction favors low-score high-cost payloads and protects
|
||||
near-threshold nodes from thrash.
|
||||
|
||||
### 9. Backend Lowering
|
||||
|
||||
The same semantic field can lower to:
|
||||
|
||||
- 2D SDF tile pages;
|
||||
- 2D height/material pages;
|
||||
- 3D SDF splat packets;
|
||||
- 3D density/extinction splat packets;
|
||||
- transparent emission/scattering splat packets;
|
||||
- 2D-projected-to-3D splat packets;
|
||||
- sensor confidence volumes;
|
||||
- debug overlays.
|
||||
|
||||
The first renderer path should use compact-support anisotropic envelopes. They
|
||||
borrow Gaussian splatting's covariance discipline without inheriting infinite
|
||||
support as a default runtime tax. Surface and volume encodings may share the
|
||||
same envelope math; they do not share resolve rules.
|
||||
|
||||
### 10. TAA Guide Integration
|
||||
|
||||
TAA consumes guide signals:
|
||||
|
||||
- reservoir confidence;
|
||||
- sample age;
|
||||
- domain validity;
|
||||
- temporal detail;
|
||||
- invalidation reason;
|
||||
- field id and motion.
|
||||
|
||||
TAA does not own reservoirs. It only decides how much pixel history may survive.
|
||||
|
||||
## Resource Tradeoffs
|
||||
|
||||
### CPU
|
||||
|
||||
CPU performs grammar expansion, coarse scoring, stochastic scheduling, page
|
||||
requests, eviction decisions, and selected-cut planning. It must not rescore
|
||||
every leaf or rebuild payload pages mid-frame.
|
||||
|
||||
### RAM
|
||||
|
||||
RAM keeps summaries, estimator state, hot/warm metadata, selected cuts, and
|
||||
recent probes. It does not keep every generated child payload.
|
||||
|
||||
### SSD
|
||||
|
||||
SSD stores payload pages, summary pages, probe history, and optional training
|
||||
datasets. SSD never blocks a frame; parent summaries render while requests are
|
||||
pending.
|
||||
|
||||
### GPU
|
||||
|
||||
GPU evaluates selected packets, splats, SDF proxy passes, density/transparent
|
||||
volume passes, compute scoring where profitable, page-table sampling, and debug
|
||||
views. It must not walk the authored grammar tree.
|
||||
|
||||
## Render Frame Flow
|
||||
|
||||
```text
|
||||
1. Gather visible domains and active sensor regions.
|
||||
2. Load resident summaries and previous reservoirs.
|
||||
3. Generate structural and sensor candidates under CPU budget.
|
||||
4. Evaluate targets and source PDFs.
|
||||
5. Update local reservoirs.
|
||||
6. Validate temporal reuse.
|
||||
7. Validate spatial/domain reuse.
|
||||
8. Update occupancy graph statistics.
|
||||
9. Select hierarchy cut under CPU/GPU/RAM/SSD budgets.
|
||||
10. Queue missing pages; keep parent summaries active.
|
||||
11. Lower selected evidence to 2D/3D/projected field splat packets.
|
||||
12. Render surface and/or transparent field passes.
|
||||
13. Resolve with TAA guide buffers.
|
||||
14. Emit debug telemetry and evidence logs.
|
||||
```
|
||||
|
||||
## Test Boundaries
|
||||
|
||||
Pure unit seams:
|
||||
|
||||
- projection round trips and seam continuity;
|
||||
- domain lineage and shift mapping;
|
||||
- DSL expansion determinism;
|
||||
- ownership bounds;
|
||||
- summary conservativeness;
|
||||
- reservoir math;
|
||||
- stochastic scheduler convergence;
|
||||
- residency selection under fake stores.
|
||||
|
||||
Mock boundaries:
|
||||
|
||||
- `IFractalRandom`;
|
||||
- `IFractalClock`;
|
||||
- `IContributionProbe`;
|
||||
- `IFractalPayloadStore`;
|
||||
- `ISensorCandidateSource`;
|
||||
- `IDomainShiftMap`;
|
||||
- `ITaaGuideSink`;
|
||||
- `IFractalDebugSink`.
|
||||
|
||||
GPU parity:
|
||||
|
||||
- envelope evaluator;
|
||||
- projection evaluator;
|
||||
- packet decoding;
|
||||
- selected-cut fade;
|
||||
- guide-buffer packing.
|
||||
|
||||
Performance fixtures:
|
||||
|
||||
- fixed camera path;
|
||||
- fixed DSL world;
|
||||
- fixed sensor trace;
|
||||
- fixed budget profile for GTX 1070-class hardware;
|
||||
- output CPU ms, GPU ms, payload count, page requests, selected cut size,
|
||||
reservoir confidence, and convergence error.
|
||||
|
||||
## Waterfall Implementation Phases
|
||||
|
||||
### Phase A: Architecture Freeze
|
||||
|
||||
Deliver this document, the public thesis article, updated memory, and a cut
|
||||
line for the TAA guide-buffer fork.
|
||||
|
||||
### Phase B: Explicit Reservoir Guide Layout
|
||||
|
||||
Add a dedicated reservoir guide history target rather than packing previous
|
||||
reservoir validity into existing control channels. Preserve current-control.w
|
||||
as the current confidence lane.
|
||||
|
||||
Guide schema:
|
||||
|
||||
```text
|
||||
x: reservoir confidence
|
||||
y: reservoir sample age
|
||||
z: domain validity
|
||||
w: invalidation code
|
||||
```
|
||||
|
||||
Current state: SDF and temporal Gaussian passes write a scene reservoir guide
|
||||
MRT. Resolve reads current and previous guide textures, folds confidence and
|
||||
domain validity into history validation, and writes a ping-ponged history
|
||||
reservoir guide target. `history-control.w` remains pixel history age.
|
||||
|
||||
### Phase C: Fractal Probe Pipeline
|
||||
|
||||
Turn IFS node probes into typed reservoir candidates with renderer-facing camera
|
||||
motion, disocclusion, material, and visibility validation.
|
||||
|
||||
Current state: `FractalStructuralProbeGenerator` lowers an
|
||||
`AquariumFractalSummary` into `FractalProbeSample` candidates with local center,
|
||||
bound radius, projected target contribution, source PDF, material delta, and
|
||||
payload handle. Renderer-facing temporal validation remains open.
|
||||
`FractalStructuralProbeReservoir` builds a selected-cut reservoir from an
|
||||
ownership tree, summaries, and projected pixels per world unit; Zyphos now
|
||||
exposes that reservoir in its fractal render plan/debug dump.
|
||||
`FractalProbeReuseValidator.ValidateTemporal` now names the renderer-facing
|
||||
rejection reasons for camera motion, disocclusion, material mismatch, and
|
||||
visibility before those checks are lowered to GPU reuse passes.
|
||||
|
||||
### Phase D: Occupancy Graph
|
||||
|
||||
Promote contribution state into the heuristic occupancy graph and add
|
||||
confidence decay, stale uncertainty growth, and convergence telemetry.
|
||||
|
||||
Current state: `FractalOccupancyGraph` now owns per-node contribution state,
|
||||
last visible frame, last observed score, and update probability. The
|
||||
contribution cache routes through this graph, and unobserved nodes decay
|
||||
confidence while stale uncertainty grows. Residency planning now keeps resident
|
||||
payloads by score-per-cost and emits eviction decisions for low-value expensive
|
||||
payloads. Convergence telemetry and richer resource-tier residency fields remain
|
||||
open.
|
||||
|
||||
### Phase E: 2D Surface Field Tile Backend
|
||||
|
||||
Build cached 2D SDF/height/material/confidence pages for cube-sphere and torus domains.
|
||||
Use parent summaries while child pages stream.
|
||||
|
||||
Current state: `AquariumFractalSurfacePageKey` and
|
||||
`AquariumFractalSurfacePage` define renderer-agnostic page metadata for height,
|
||||
2D signed-distance, material, and confidence pages. `FractalSurfacePagePlanner`
|
||||
turns a selected IFS cut into stable page metadata, and the Zyphos render plan
|
||||
now exposes height, 2D signed-distance, material, and confidence pages beside
|
||||
its structural probe reservoir. `FractalSurfacePageResidencyPlanner` adds a
|
||||
mockable page-store boundary with byte-budgeted resident, missing, requested,
|
||||
and evicted page decisions. `FractalSurfacePageRasterizer` now lowers resident
|
||||
pages to CPU height, 2D signed-distance support, material, and confidence
|
||||
sample payloads using the same shaped brush envelope math as the live brush
|
||||
compiler. The real page store, atlas, and D3D12 lowering remain open.
|
||||
|
||||
### Phase F: 3D Form Field Splat Backend
|
||||
|
||||
Build compact-support 3D form splat packets for object/volume domains. Opaque
|
||||
solids use SDF/level-set packets; transparent media use density/extinction
|
||||
packets. Keep distance safety conservative and LOD gated for solids, and keep
|
||||
volume cost bounded by explicit extinction/support budgets.
|
||||
|
||||
Current state: `FractalProbeSample` now carries explicit field layer and
|
||||
encoding metadata. The same reservoir sample type can represent opaque
|
||||
`Form/SignedDistance` and transparent `Form/Density` or `Form/Extinction`
|
||||
evidence without creating a second cache. `AquariumFractalSdfSplat3D` and
|
||||
`FractalSdfSplat3DCompiler` own the opaque SDF lowering path;
|
||||
`AquariumFractalDensitySplat3D` and `FractalDensitySplat3DCompiler` own the
|
||||
transparent density/extinction lowering path. Reuse rejects mismatched
|
||||
encodings before lineage/local-shift reuse can smear density and SDF together.
|
||||
The `.aquageo` DSL accepts `density` and `extinction` claims beside `height`,
|
||||
and its `flame` recursive primitive now emits density claims instead of height
|
||||
claims. Height brush lowering filters to height payloads, leaving transparent
|
||||
Form claims for field lowering. Zyphos now exposes a first structural SDF splat
|
||||
from its probe reservoir. D3D12 lowering and object/body recursive form
|
||||
integration remain open.
|
||||
|
||||
### Phase G: 2D-Projected-To-3D Field Backend
|
||||
|
||||
Project 2D field pages onto 3D domains: cube-sphere planets, torus stations,
|
||||
curved sheets, object-local surfaces, and transparent sheets/volumes when the
|
||||
domain mapping supports them.
|
||||
|
||||
Current state: `FractalProjectedSdfSplatCompiler` lowers resident
|
||||
`SignedDistance2D` page payloads into compact 3D SDF splats through an explicit
|
||||
`Vector2 -> Vector3` mapping and surface normal. Zyphos caches a first bounded
|
||||
set of projected surface splats from its resident SDF pages using a flat local
|
||||
mapping; real cube-sphere and torus mappings remain open.
|
||||
|
||||
### Phase H: Mimir Adapter
|
||||
|
||||
Add sensor candidate adapters after the fractal reservoir path proves the
|
||||
contract. Camera/audio features become candidates; Aquarium owns resolved
|
||||
evidence. Sensor fusion initially emits confidence/density form claims; only
|
||||
coherent gradients and multi-view/acoustic agreement should promote them into
|
||||
surface claims.
|
||||
|
||||
### Phase I: GPU Reservoirs And Spatial Reuse
|
||||
|
||||
Move hot reservoirs to GPU buffers, add spatial reuse across screen tiles and
|
||||
domain neighbors, and keep CPU/GPU parity fixtures.
|
||||
|
||||
Current state: `tools/Aquarium.Fractal.Receipt` is the first GPU-resident
|
||||
receipt harness. It allocates an 80-byte packed SDF splat UAV plus three
|
||||
separate 64-byte reservoir UAVs for SDF envelopes, PBR material envelopes, and
|
||||
radiosity because that was the first opaque-object packet slice. Conceptually
|
||||
those are the first Form, Appearance, and Transport reservoirs. The receipt
|
||||
runs independent D3D12 compute passes for splat population, form/SDF reservoir
|
||||
sampling, appearance/PBR reservoir sampling, and transport/radiosity reservoir
|
||||
sampling; the three reservoir passes share only the stochastic update budget
|
||||
vocabulary, not packet anatomy. On the local GTX 1070, the budgeted receipt
|
||||
kept `2,000,000` splats and `2,000,000` reservoirs per layer resident, updated
|
||||
`50,000` entries per reservoir layer per frame with two candidates per update,
|
||||
and measured `4.471 ms/frame`, `223.7 FPS` equivalent over `120` frames. This
|
||||
proves compute-side GPU residency and budgeted temporal convergence for the
|
||||
layered cache; final shaded visibility still has to consume the same packed
|
||||
buffers. Next packet evolution should add density/extinction Form rows and
|
||||
participating-medium Appearance/Transport rows instead of forcing flames through
|
||||
opaque SDF/PBR semantics.
|
||||
|
||||
### Phase J: Learned Priority Gate
|
||||
|
||||
Only after telemetry exists, train or calibrate a predictor for update/residency
|
||||
priority. Delete it if it does not beat the heuristic on held-out camera/sensor
|
||||
traces.
|
||||
|
||||
## Cut Lines
|
||||
|
||||
- No learned predictor before telemetry.
|
||||
- No recursive 3D form before 2D tile summaries and debug views are boring.
|
||||
- No consumer-owned stable temporal cache.
|
||||
- No GPU grammar traversal.
|
||||
- No SSD frame stalls.
|
||||
- No reuse without an explicit domain shift and validation contract.
|
||||
- No debug-free recursive density.
|
||||
|
||||
## Sources
|
||||
|
||||
- NVIDIA ReSTIR DI:
|
||||
https://research.nvidia.com/publication/2020-07_spatiotemporal-reservoir-resampling-real-time-ray-tracing-dynamic-direct
|
||||
- NVIDIA ReSTIR GI:
|
||||
https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing
|
||||
- GRIS:
|
||||
https://graphics.cs.utah.edu/research/projects/gris/sig22_GRIS.pdf
|
||||
- RTXDI ReSTIR docs:
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirGI.md
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirPT.md
|
||||
- EWA splatting:
|
||||
https://dash.harvard.edu/bitstream/handle/1/4138240/Zwicker_EWA.pdf
|
||||
- Object-space EWA surface splatting:
|
||||
https://publications.ri.cmu.edu/object-space-ewa-surface-splatting-a-hardware-accelerated-approach-to-high-quality-point-rendering/
|
||||
- 3D Gaussian Splatting:
|
||||
https://arxiv.org/abs/2308.04079
|
||||
- Geometry clipmaps:
|
||||
https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry
|
||||
- Nanite:
|
||||
https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf
|
||||
- KinectFusion:
|
||||
https://www.microsoft.com/en-us/research/publication/kinectfusion-real-time-dense-surface-mapping-tracking/
|
||||
- Curless and Levoy volumetric fusion:
|
||||
https://graphics.stanford.edu/papers/volrange/volrange.pdf
|
||||
- Instant-NGP:
|
||||
https://research.nvidia.com/publication/2022-07_instant-neural-graphics-primitives-multiresolution-hash-encoding
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# Stochastic Transparent Surface Pipeline
|
||||
|
||||
Aquarium needs a shared path for transparent stochastic surfaces: Grid lines,
|
||||
billboard particles, glyph motes, glints, future sprite cards, and any other
|
||||
thing that is visible as coverage rather than as one opaque surface hit.
|
||||
|
||||
The important rule is that these objects do not own canonical depth. A ray may
|
||||
encounter several candidates, and blue-noise/stochastic coverage may choose
|
||||
different candidates across frames. Treating one event as "the" surface creates
|
||||
self-occlusion, false depth rejection, and temporal history murder.
|
||||
|
||||
## Classes
|
||||
|
||||
### Opaque Solids
|
||||
|
||||
Examples: Epiphany agent bodies, cursor bodies, and future solid SDF objects.
|
||||
|
||||
Output:
|
||||
|
||||
- color
|
||||
- travel/depth
|
||||
- field id
|
||||
- normal
|
||||
- material id
|
||||
- reactive/velocity if needed
|
||||
|
||||
This is the ordinary one-ray-one-hit contract.
|
||||
|
||||
### Stochastic Transparent Events
|
||||
|
||||
Examples: Grid line support, particles, billboard cards.
|
||||
|
||||
Output per event:
|
||||
|
||||
- travel
|
||||
- field or batch id
|
||||
- support/coverage
|
||||
- premultiplied color
|
||||
- approximate normal or facing axis when useful
|
||||
- thickness/depth band when useful
|
||||
|
||||
The event stream is sorted front-to-back and clipped by opaque solid travel.
|
||||
Events do not clip each other as opaque surfaces. They composite.
|
||||
|
||||
## Frame Shape
|
||||
|
||||
1. Render opaque solid scene and depth/travel.
|
||||
2. Render/integrate participating media against opaque solid travel.
|
||||
3. Generate stochastic transparent events up to opaque solid travel.
|
||||
4. Apply blue-noise coverage decisions or alpha-to-coverage equivalent.
|
||||
5. Composite selected transparent events premultiplied front-to-back.
|
||||
6. Write transparent temporal descriptors separate from opaque metadata.
|
||||
|
||||
The Grid should not be a special scene surface. It should emit events from its
|
||||
heightfield/line-support intersection. Particles emit events from their sorted
|
||||
billboards or bins. Both land in the same transparent event pipe.
|
||||
|
||||
## Temporal Contract
|
||||
|
||||
Transparent history cannot validate like opaque history.
|
||||
|
||||
Opaque validation compares one depth, normal, field id, and color neighborhood.
|
||||
Transparent validation should compare a distribution summary:
|
||||
|
||||
- accumulated support/coverage
|
||||
- nearest event travel
|
||||
- weighted mean travel
|
||||
- travel variance or thickness band
|
||||
- dominant transparent class id
|
||||
- optional batch/field id where stable
|
||||
|
||||
History should survive ordinary stochastic hit/miss changes inside the same
|
||||
support distribution. It should reject when the distribution moves, disappears,
|
||||
becomes occluded by a solid, or changes class.
|
||||
|
||||
## Grid Events
|
||||
|
||||
The Grid is an implicit heightfield plus analytic line support, not an opaque
|
||||
terrain surface.
|
||||
|
||||
A robust Grid event generator should:
|
||||
|
||||
- use the existing slope-aware `terrainGap = p.z - terrainHeight(p.xy)` march
|
||||
- find refined heightfield crossings up to opaque solid travel
|
||||
- evaluate line/isoline/field-line support at each crossing
|
||||
- emit only crossings with meaningful support
|
||||
- composite events front-to-back in the transparent pipe
|
||||
- keep opaque scene metadata untouched
|
||||
|
||||
It must not:
|
||||
|
||||
- write Grid as the scene's opaque travel
|
||||
- let one Grid crossing occlude later Grid crossings
|
||||
- fixed-step sample a thin band and hope not to miss it
|
||||
- invent a Grid-only temporal path that particles cannot use
|
||||
|
||||
## Particle Events
|
||||
|
||||
Particles should use the same pipe.
|
||||
|
||||
The particle system supplies candidate events from sorted/binned billboards.
|
||||
Each event carries travel, coverage, premultiplied color, and a stable batch or
|
||||
particle id when available. Stochastic coverage chooses visibility. Temporal
|
||||
validation compares the transparent distribution rather than demanding exact
|
||||
per-particle depth stability.
|
||||
|
||||
## Debug Views
|
||||
|
||||
Required before relying on the pipe:
|
||||
|
||||
- transparent event count
|
||||
- selected stochastic event coverage
|
||||
- accumulated transparent alpha
|
||||
- nearest transparent travel
|
||||
- weighted transparent travel
|
||||
- transparent travel variance/thickness
|
||||
- transparent class id
|
||||
- opaque terminator travel
|
||||
|
||||
If these views do not agree with the final composite, the pipe is lying.
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# Temporal Field Gaussian
|
||||
|
||||
## Objective
|
||||
|
||||
Aquarium owns the renderer feature for live volumetric point-cloud/splat input:
|
||||
clients provide stable world-space Gaussian observations, and the engine turns
|
||||
them into a buffered, reprojectable, D3D12-rendered field. LocalCastBridge can
|
||||
then feed sensor-fusion output into Aquarium as a normal scene contract instead
|
||||
of smuggling renderer policy through client code.
|
||||
|
||||
The historical `SDF` name is now backend baggage. This contract is a temporal
|
||||
Form-field path: opaque producers may resolve to SDF/level-set surfaces, while
|
||||
flames, smoke, transparent gradients, and sensor fusion may stay as
|
||||
density/extinction/confidence fields until evidence justifies a surface.
|
||||
|
||||
## Current Mechanism
|
||||
|
||||
The client-facing contract is `AquariumSceneState.TemporalGaussianField`.
|
||||
Each `AquariumTemporalSdfGaussian` carries stable identity, current and previous
|
||||
center, velocity, oriented radii, color/opacity, confidence, history weight,
|
||||
compact-kernel controls, and field id.
|
||||
|
||||
`Aquarium.Engine.Fractal.Temporal.TemporalGaussianAccumulator` consumes
|
||||
`TemporalGaussianObservation` rows and lowers them through the shared
|
||||
`TemporalSpatialEvidenceReservoir`. The reservoir owns the live buffering
|
||||
policy:
|
||||
|
||||
- stable keys identify tracks across camera/sensor frames
|
||||
- presentation delay lets late sensor data converge before rendering
|
||||
- accumulation window expires old tracks
|
||||
- max-track budgets evict low-confidence stale tracks before renderer lowering
|
||||
- smoothed velocity predicts the presented center
|
||||
- history weight is confidence scaled by age inside the window
|
||||
|
||||
The Gaussian field is one backend for this reservoir, not the reservoir's
|
||||
identity. Fractal form probes, visual point-cloud features, transparent density
|
||||
samples, and future acoustic constraints can all enter as stable keyed spatial
|
||||
evidence as long as each producer declares confidence, time, bounds, field
|
||||
layer, field encoding, and payload lowering rules.
|
||||
`TemporalSpatialEvidenceLowering` owns the current packet conversions for
|
||||
`TemporalGaussianObservation`, `AquariumTemporalSdfGaussian`, and
|
||||
`AquariumGpuFusionSeed`; consumers should use those helpers instead of packing
|
||||
payload vectors privately.
|
||||
|
||||
The D3D12 renderer lowers the field into `D3D12TemporalGaussianPacket`, uploads
|
||||
the active packet span into a 1,048,576-entry structured buffer, and renders an
|
||||
instanced proxy-quad pass from
|
||||
`D3D12TemporalGaussian.hlsl`. The pixel shader evaluates a compact anisotropic
|
||||
Gaussian kernel in world space, writes HDR scene color/travel, field metadata,
|
||||
normal, and temporal-control coverage so the existing resolve sees the field as
|
||||
diegetic scene content.
|
||||
|
||||
`Aquarium.LocalCast` is the first concrete live client for this contract. It
|
||||
reads `localcast.visual.render_frame` from LocalCastBridge's typed CultCache
|
||||
MessagePack document. That point/seed route is now explicitly a fallback and
|
||||
debug harness: it proves the million-slot temporal Gaussian draw and keeps OBS
|
||||
fed while the live capture path moves into Aquarium. The production ownership
|
||||
line is `AquariumGpuSensorFrame`: camera and Leap inputs arrive as calibrated
|
||||
sensor records plus shared GPU texture handles, the D3D12 backend owns their
|
||||
metadata buffers, and fusion kernels lower those GPU-resident inputs into the
|
||||
temporal Gaussian buffer consumed by the SDF Gaussian draw.
|
||||
|
||||
LocalCast GPU fusion also has its own live-history accumulator. New frames
|
||||
update stable-key tracks, stale tracks expire after a bounded horizon, and the
|
||||
whole retained set is sent to the GPU seed buffer every frame. The default
|
||||
history horizon is 18 seconds and may be overridden with
|
||||
`LOCALCAST_GPU_HISTORY_SECONDS` from 1 to 120 seconds. That is the reconstruction
|
||||
buffer: long enough to harvest samples, bounded enough to remain a machine
|
||||
instead of a scrapbook with a power cord.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Field accumulation happens in world space before pixel history. TAA is the
|
||||
resolver, not the owner of sensor identity.
|
||||
- Stable keys belong to the producer/accumulator boundary; shader packets are
|
||||
backend output and do not invent identity.
|
||||
- The compact support kernel has a finite bound. Renderer cost must scale from
|
||||
declared bounds, not from infinite translucent fog.
|
||||
- Density/opacity observations are not required to become surfaces. A
|
||||
transparent medium may remain a participating Form field with Appearance and
|
||||
Transport payloads instead of being collapsed into a fake SDF.
|
||||
- Client code may construct observations or a field for diagnostics, but
|
||||
Aquarium owns live sensor texture import, packet layout, root binding, shader
|
||||
evaluation, fusion, and temporal-control metadata.
|
||||
- JSON is not a renderer boundary. CultCache/CultNet producers should lower into
|
||||
typed contract rows before Aquarium sees the data.
|
||||
|
||||
## Cut Line
|
||||
|
||||
This cut deliberately claims million-slot ingestion, not a finished million-splat
|
||||
renderer architecture. The live D3D12 path can draw up to 1,048,576 temporal
|
||||
Gaussians through instanced proxy quads, only uploads the active seed/packet
|
||||
span, and now owns the GPU lowering step. Python/LocalCastBridge may still
|
||||
produce calibration artifacts and reference captures, but it must not own
|
||||
per-frame dense stereo, feature tracking, or reconstruction compute. The next
|
||||
scaling cut belongs to the Aquarium renderer: shared texture import, packed
|
||||
camera planes, Leap packed-map channel extraction, selected-cut residency,
|
||||
tiled/bin dispatch, GPU accumulation, and clustered visibility.
|
||||
|
||||
## GPU Fusion Spine
|
||||
|
||||
The active GPU boundary is deliberately narrow:
|
||||
|
||||
```text
|
||||
LocalCast calibration/device metadata
|
||||
-> AquariumGpuSensorFrame { calibrated cameras + shared GPU textures }
|
||||
-> D3D12 sensor metadata buffers + imported texture SRVs
|
||||
-> D3D12 LocalCast fusion compute shader
|
||||
-> RWStructuredBuffer<TemporalGaussian>
|
||||
-> instanced SDF Gaussian draw
|
||||
-> TAA/resolve
|
||||
```
|
||||
|
||||
Ownership:
|
||||
|
||||
- `AquariumGpuSensorFrame` is the live renderer contract for GPU-owned fusion
|
||||
input.
|
||||
- `AquariumGpuFusionField` remains a temporary fallback/debug contract for
|
||||
already-derived point claims.
|
||||
- `LocalCastGpuFusionMapper` converts typed LocalCast point claims into compact
|
||||
seeds only for that fallback path.
|
||||
- `LocalCastGpuFusionAccumulator` is only the LocalCast adapter around
|
||||
`TemporalSpatialEvidenceReservoir`; it maps fallback stable seeds into shared
|
||||
evidence observations and lowers reservoir samples back to
|
||||
`AquariumGpuFusionSeed`.
|
||||
- `D3D12LocalCastFusion.hlsl` owns the first compute lowering pass.
|
||||
- `D3D12Renderer` owns GPU sensor camera metadata storage, UAV-capable temporal
|
||||
Gaussian storage, dispatch, and the transition back to shader-resource state
|
||||
for the draw.
|
||||
- `AquariumAcousticFieldFrame` carries the ultrasonic chirplet timing oracle and
|
||||
room/position constraints. Visual evidence may arrive late and be refined, but
|
||||
acoustic timing is the clock witness for aligning the delayed broadcast world.
|
||||
- `AquariumCalibrationEventFrame` carries deliberate calibration actions such
|
||||
as claps. A clap gives the system one event that every camera can see and the
|
||||
audio timing oracle can timestamp to microsecond-scale uncertainty, making it
|
||||
the cheap, brutal alignment hammer for camera clock offsets and pose drift.
|
||||
|
||||
Current Aquarium cut: the contract, D3D12 camera metadata buffer, external
|
||||
texture importer, and sensor SRV table exist. A producer may provide either a
|
||||
duplicated shared handle or a named shared handle for each camera/Leap plane.
|
||||
When sensor textures are present, Aquarium can dispatch fusion without fallback
|
||||
seeds and write RGB-derived Gaussian samples into the temporal buffer. Those
|
||||
samples are no longer isolated per-camera flecks: the shader computes a compact
|
||||
per-sample visual descriptor, compares it against a neighboring camera stream,
|
||||
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.
|
||||
|
||||
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
|
||||
over the several-second broadcast delay window. Deliberate clap events should
|
||||
feed that buffer as high-confidence timing correspondences: visual impact frame
|
||||
per camera against acoustic oracle time, then pose/clock correction under the
|
||||
same delayed broadcast horizon.
|
||||
|
||||
The first shader pass also uses camera-facing proxy planes to evaluate each
|
||||
kernel. True ray-integrated volume compositing, Gaussian depth sorting, and
|
||||
per-Gaussian previous-center reprojection remain the next renderer cuts.
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
# Temporal Spatial Evidence Reservoir
|
||||
|
||||
## Objective
|
||||
|
||||
Aquarium owns the shared temporal evidence machine for two customers:
|
||||
|
||||
- fractal rendering, where Form/Appearance/Transport candidates need bounded
|
||||
reuse across pixels, frames, and nested domains;
|
||||
- Mimir/LocalCast sensor fusion, where camera and microphone features need a
|
||||
delayed coherence window before the resolved field is rendered.
|
||||
|
||||
The old stable-key accumulator was useful, but it was not ReSTIR. The live
|
||||
architecture now splits the problem into a small resampled-importance core, a
|
||||
spatial evidence track layer, typed lowerings, renderer passes, and TAA history.
|
||||
|
||||
## Research Spine
|
||||
|
||||
- NVIDIA ReSTIR DI repeatedly resamples candidate light samples, then applies
|
||||
spatial and temporal resampling to share useful samples across nearby pixels
|
||||
and frames. The paper reports equal-error speedups of 6-60x, with biased
|
||||
variants reaching 35-65x in its tested direct-lighting workloads.
|
||||
Source: https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/
|
||||
- ReSTIR GI applies the same screen-space spatiotemporal reuse idea to
|
||||
multi-bounce indirect paths and reports 9.3x-166x MSE improvement at one
|
||||
sample per pixel in tested scenes.
|
||||
Source: https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing
|
||||
- GRIS generalizes RIS/ReSTIR to correlated samples, unknown PDFs, varied
|
||||
domains, and shift mappings. That is the important part for Aquarium:
|
||||
fractal domains and sensor-fusion samples are not all light samples.
|
||||
Source: https://research.nvidia.com/labs/rtr/publication/lin2022generalized/
|
||||
- RTXDI's integration docs make the pass boundary concrete: acquire initial
|
||||
samples, store reservoir data, validate temporal reuse through reprojection,
|
||||
validate spatial reuse against nearby surfaces, shift candidate paths between
|
||||
domains, then shade/denoise from the final reservoir.
|
||||
Sources:
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirGI.md
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirPT.md
|
||||
|
||||
## Pipeline Map
|
||||
|
||||
```text
|
||||
producer observations
|
||||
-> candidate generator
|
||||
-> target evaluator
|
||||
-> ResampledImportanceReservoir<TSample>
|
||||
-> temporal reuse pass
|
||||
-> spatial/domain reuse pass
|
||||
-> validation + shift mapping
|
||||
-> TemporalSpatialEvidenceReservoir track layer when stable fields are needed
|
||||
-> TemporalSpatialEvidenceLowering
|
||||
-> backend packet stream
|
||||
-> renderer/fusion passes
|
||||
-> TAA guide/history buffers
|
||||
```
|
||||
|
||||
## Ownership
|
||||
|
||||
`ResampledImportanceReservoir<TSample>` owns RIS/ReSTIR-style candidate math:
|
||||
|
||||
- one selected representative sample;
|
||||
- summed importance weight of accepted candidates;
|
||||
- number of source candidates represented by the reservoir;
|
||||
- selected sample target value;
|
||||
- final contribution weight `weightSum / (candidateCount * selectedTarget)`.
|
||||
|
||||
Candidate generators own proposal distributions. A fractal renderer may propose
|
||||
form probe candidates from projected error, node bounds, blue-noise screen
|
||||
tiles, or resident children. Mimir may propose visual/audio feature candidates
|
||||
from sensor confidence and calibration state. The reservoir does not know these
|
||||
domains; it only receives target and source-PDF values.
|
||||
|
||||
Every evidence sample should state its layer and encoding before lowering:
|
||||
Form, Appearance, or Transport; then SDF, height, density, extinction,
|
||||
material, phase, emission, radiance, feature, or confidence as appropriate.
|
||||
Opaque worlds can promote Form evidence into SDF surfaces. Flames and uncertain
|
||||
sensor fields may remain density/extinction/confidence volumes. That is not a
|
||||
failure to find the surface; it is the correct field.
|
||||
|
||||
Reuse passes own validity and shift mapping. A sample may be reused only when
|
||||
the target domain can explain it. For pixels this means depth/normal/material
|
||||
compatibility, motion vectors, conservative visibility, and disocclusion tests.
|
||||
For fractal domains it means matching domain ancestry, bounded local-frame
|
||||
error, resident payload compatibility, conservative surface bounds for SDF
|
||||
encodings, and conservative support/extinction bounds for density encodings.
|
||||
For sensor fusion it means time delay, calibration confidence, modality
|
||||
agreement, and feature reprojection error.
|
||||
|
||||
`TemporalSpatialEvidenceReservoir` owns stable resolved spatial tracks after
|
||||
candidate selection when the output is a field with persistent identity:
|
||||
|
||||
- stable key identity;
|
||||
- accumulation window and presentation delay;
|
||||
- confidence-weighted smoothing;
|
||||
- velocity prediction;
|
||||
- history weight from confidence and sample age;
|
||||
- expiry;
|
||||
- max-track eviction by confidence and age.
|
||||
|
||||
`TemporalSpatialEvidenceLowering` owns packet conversion. Consumers do not pack
|
||||
payload vectors by private convention when a lowering helper exists.
|
||||
|
||||
TAA owns pixel history validation and temporal guide buffers. It may consume
|
||||
reservoir confidence, sample age, domain id, motion, and temporal-detail lanes,
|
||||
but it does not own producer identity or stable spatial evidence.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Reservoir math is a pure, testable core before it becomes HLSL.
|
||||
- A reservoir is not a dictionary of tracks. A track layer may use reservoirs,
|
||||
but it does not replace candidate resampling.
|
||||
- Reuse is invalid until a pass proves the shift/validation contract for the
|
||||
source and target domains.
|
||||
- Conservative bounds remain the safety authority. Learned or stochastic
|
||||
priority may decide what to refresh first; it must not replace field bounds,
|
||||
visibility bounds, or calibration bounds.
|
||||
- CPU, GPU, RAM, and SSD budgets are inputs to candidate generation and
|
||||
residency. They are not hidden side effects of renderer convenience code.
|
||||
- Consumer repos do not grow parallel stable-key temporal caches.
|
||||
|
||||
## Current State
|
||||
|
||||
Built:
|
||||
|
||||
- `ResampledImportanceCandidate<TSample>` stores a sample, target value, source
|
||||
PDF, and represented candidate count.
|
||||
- `ResampledImportanceReservoir<TSample>` accepts candidates by weight
|
||||
proportional to `target / sourcePdf`, merges reservoirs, preserves represented
|
||||
candidate count, and exposes the RIS contribution weight.
|
||||
- `TemporalSpatialEvidenceReservoir` remains the stable track layer used by
|
||||
temporal Gaussian fields and LocalCast fallback GPU fusion.
|
||||
- `FractalContributionCache` remains the node-summary LOD estimator, and now
|
||||
frames scheduled update nodes as weighted reservoir candidates with a
|
||||
per-frame reservoir snapshot for debug and tests. It is the candidate source,
|
||||
not the full temporal/spatial reuse pass.
|
||||
- `FractalProbeSample` is the first typed fractal form/detail sample shape for
|
||||
the ReSTIR/GRIS path. It carries domain key, node key, local center, bound
|
||||
radius, target contribution, source PDF, material delta, and payload handle.
|
||||
`FractalProbeReuseValidator` currently proves domain lineage and local-shift
|
||||
compatibility; renderer-facing camera, disocclusion, material, and visibility
|
||||
checks still need to be added before temporal/spatial reuse is complete.
|
||||
|
||||
Not built yet:
|
||||
|
||||
- GPU reservoir buffers;
|
||||
- camera/disocclusion/material validation for fractal form/appearance reservoirs;
|
||||
- spatial neighbor reuse across screen tiles and cube-sphere neighbor domains;
|
||||
- GRIS-style domain shift mappings for nested `.aquageo` domains;
|
||||
- expanded TAA guide-buffer storage for previous-frame reservoir confidence,
|
||||
sample age, domain validity, and invalidation reason;
|
||||
- SSD/RAM residency queues driven by reservoir contribution estimates.
|
||||
|
||||
## TAA Guide Layout
|
||||
|
||||
Reservoir/TAA guide data has a dedicated current scene target and ping-ponged
|
||||
history target. It is deliberately separate from `history-control.w`, which
|
||||
continues to own pixel history age.
|
||||
|
||||
```text
|
||||
x: reservoir confidence
|
||||
y: reservoir sample age
|
||||
z: domain validity
|
||||
w: invalidation code
|
||||
```
|
||||
|
||||
The first live producers are SDF surfaces and temporal Gaussian splats, but the
|
||||
schema is not surface-only. Resolve reads the current and previous guide
|
||||
textures, folds confidence and domain validity into history validation, then
|
||||
writes the next history guide. Future ReSTIR/GRIS passes should extend the
|
||||
producer side of this schema with explicit Form/Appearance/Transport fields
|
||||
rather than packing more reservoir folklore into scene-control channels.
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
1. Keep the CPU reservoir core pure and exhaustive under unit tests.
|
||||
2. Add typed reservoir samples for fractal field probes: domain key, local
|
||||
frame, bound radius, target contribution, source PDF, layer/encoding, payload
|
||||
handle, and material or transport delta.
|
||||
3. Turn `FractalContributionCache` into a candidate generator that refreshes
|
||||
nodes under CPU budget and submits candidates to the reservoir core.
|
||||
4. Add temporal reuse for fractal probes using camera motion, domain ancestry,
|
||||
local-frame error, and conservative bounds.
|
||||
5. Add spatial reuse across screen tiles and quadtree neighbors.
|
||||
6. Lower selected reservoirs into GPU field packets. SDF packets are the solid
|
||||
surface encoding; density/extinction packets are the transparent/sensor
|
||||
volume encoding. Expose debug views for weight sum, selected target,
|
||||
candidate count, confidence, age, and invalidation reason.
|
||||
7. Weave reservoir confidence and temporal detail into TAA guide buffers so the
|
||||
history filter can distinguish stable reused evidence from fresh stochastic
|
||||
noise. The first pass uses current scene-control.w for reservoir confidence
|
||||
and keeps history-control.w as age; full previous-frame reservoir validity
|
||||
still needs an expanded guide layout.
|
||||
8. Add Mimir-facing candidate adapters only after the fractal path proves the
|
||||
contract: modality features are candidates, not a second reservoir system.
|
||||
9. Port the pure core to HLSL and add CPU/GPU parity fixtures.
|
||||
10. Add residency scheduling: hot selected reservoirs keep GPU packets resident,
|
||||
warm reservoirs keep RAM payloads, cold reservoirs fall to SSD handles.
|
||||
|
||||
## Cut Line
|
||||
|
||||
If a new subsystem stores temporal candidates, it must state whether it owns raw
|
||||
producer retention, resampled candidate selection, stable resolved tracks,
|
||||
packet lowering, or TAA history. If it cannot name that authority, it is not an
|
||||
architecture. It is a decorative leak.
|
||||
|
|
@ -1,301 +0,0 @@
|
|||
# TSR-Inspired TAA Spec
|
||||
|
||||
Aquarium needs an owned temporal resolver, not a copied implementation. The
|
||||
target is inspired by public TSR behavior: stable subpixel detail, aggressive
|
||||
history validation, visible debug terms, and special handling for pixels whose
|
||||
appearance is not fully described by motion vectors.
|
||||
|
||||
## Inputs
|
||||
|
||||
The resolver owns these signals:
|
||||
|
||||
- current HDR scene color
|
||||
- current ray travel
|
||||
- previous HDR history color
|
||||
- previous ray travel
|
||||
- current camera/Grid constants
|
||||
- previous camera/Grid constants
|
||||
- current and previous projection jitter
|
||||
- stochastic coverage from diegetic Grid surfaces
|
||||
|
||||
Later gates add:
|
||||
|
||||
- field id
|
||||
- normal
|
||||
- explicit velocity
|
||||
- reactive mask
|
||||
- transparency/composition mask
|
||||
- resurrection history
|
||||
|
||||
## Current Pass Contract
|
||||
|
||||
The scene pass renders into an HDR texture:
|
||||
|
||||
```text
|
||||
rgb = linear scene color
|
||||
a = ray travel in world units
|
||||
```
|
||||
|
||||
The pass does not tonemap. Tonemapping belongs after temporal resolve.
|
||||
|
||||
## Reprojection
|
||||
|
||||
For the first implementation, motion is camera-derived:
|
||||
|
||||
1. Reconstruct the current world hit from current camera, current jittered ray,
|
||||
and current travel.
|
||||
2. Project that world hit into the previous camera basis.
|
||||
3. Remove previous jitter to find the previous history UV.
|
||||
4. Sample previous history if the UV is valid.
|
||||
5. Compare previous history travel against the reprojected point's expected
|
||||
distance from the previous camera, not against current-frame travel.
|
||||
|
||||
This is sufficient for camera motion and stochastic Grid coverage. It is not the
|
||||
final answer for animated bodies or field animation.
|
||||
|
||||
## Rejection
|
||||
|
||||
Reject or weaken history when:
|
||||
|
||||
- previous UV is outside the frame
|
||||
- travel differs too much
|
||||
- current or previous travel is invalid
|
||||
- current neighborhood color bounds do not contain the reprojected history
|
||||
|
||||
The first pass uses a 3x3 neighborhood min/max clamp and travel-difference
|
||||
weighting. Later passes add field/material ids, normals, disocclusion masks, and
|
||||
reactive masks.
|
||||
|
||||
## Resolve
|
||||
|
||||
The resolver outputs both:
|
||||
|
||||
- tonemapped final backbuffer color
|
||||
- linear HDR history for the next frame
|
||||
|
||||
Current blend target:
|
||||
|
||||
```text
|
||||
resolved = lerp(current, clamped_history, history_weight)
|
||||
```
|
||||
|
||||
History weight starts conservative. Stochastic Grid coverage needs accumulation,
|
||||
but ghosting is worse than shimmer while the renderer lacks full velocity,
|
||||
material id, and reactive masks. The resolver should earn trust before taking
|
||||
more history.
|
||||
|
||||
## Non-Negotiables
|
||||
|
||||
- Do not use Playdead-style TAA as the final model; it ghosts too easily for the
|
||||
scene we are building.
|
||||
- Do not vendor Unreal TSR code. Study behavior and build a clean-room resolver.
|
||||
- Do not hide missing renderer signals with larger blend weights.
|
||||
- Do not make the Grid a final-image HUD layer; it is diegetic scene UI and must
|
||||
respect nearer solids.
|
||||
|
||||
## Implementation Gates
|
||||
|
||||
### Gate 1: Owned Temporal Pass
|
||||
|
||||
- HDR scene target
|
||||
- history ping-pong
|
||||
- camera jitter
|
||||
- current/previous camera reprojection
|
||||
- travel rejection
|
||||
- 3x3 color clamp
|
||||
- final tonemap after resolve
|
||||
|
||||
### Gate 2: Renderer Signals
|
||||
|
||||
- material/field id buffer
|
||||
- normal buffer
|
||||
- explicit velocity for animated SDF bodies
|
||||
- coverage/reactive masks for stochastic Grid and future particles
|
||||
|
||||
Current Gate 2A implementation:
|
||||
|
||||
- scene metadata target stores stable field id plus surface normal
|
||||
- history metadata ping-pongs alongside color/travel history
|
||||
- field id mismatch rejects history
|
||||
- normal mismatch weakens/rejects history
|
||||
- Grid stochastic surfaces write Grid travel/normal even when the current
|
||||
dither sample does not draw color, so the temporal resolver accumulates the
|
||||
diegetic surface rather than reprojecting from the solid behind it
|
||||
|
||||
Current Gate 2B implementation:
|
||||
|
||||
- Self, Grid, and each orbiting planet have distinct field ids
|
||||
- orbiting planet hits map their current world hit back to the planet's previous
|
||||
center before camera reprojection
|
||||
- this gives analytic SDF bodies object motion without a full velocity buffer
|
||||
yet
|
||||
|
||||
Current Gate 2C implementation:
|
||||
|
||||
- `AquariumSceneState.TemporalGaussianField` carries world-space temporal SDF
|
||||
Gaussian splats with stable keys, current/previous center, velocity,
|
||||
confidence, history weight, field id, and compact-kernel controls
|
||||
- `TemporalGaussianAccumulator` buffers sensor observations in world space before
|
||||
rendering, using a presentation delay plus accumulation window instead of
|
||||
asking final-pixel TAA to discover identity after projection
|
||||
- D3D12 uploads the field into a structured buffer and renders it through
|
||||
`D3D12TemporalGaussian.hlsl`, writing color/travel, field metadata, normals,
|
||||
and temporal-control coverage into the existing scene/resolve path
|
||||
|
||||
Still missing:
|
||||
|
||||
- separate field ids once the SDF field registry exists
|
||||
- reactive/coverage masks
|
||||
- richer disocclusion classification
|
||||
- a general velocity buffer for non-rigid fields and deformation
|
||||
- per-Gaussian previous-center reprojection in the resolve pass
|
||||
|
||||
### Gate 3: Stochastic Event Control
|
||||
|
||||
Current Gate 3A implementation:
|
||||
|
||||
- scene pass writes a temporal-control target separate from color/travel and
|
||||
field/normal metadata
|
||||
- temporal control currently stores:
|
||||
|
||||
```text
|
||||
x = reactive strength
|
||||
y = stochastic/composition coverage
|
||||
z = reserved
|
||||
w = reserved
|
||||
```
|
||||
|
||||
- Grid coverage feeds the coverage channel
|
||||
- low-coverage Grid samples raise reactive strength and reduce history weight
|
||||
- the resolve combines reactive and coverage weights with travel, field, normal,
|
||||
and neighborhood-color validation
|
||||
|
||||
Current Gate 3B implementation:
|
||||
|
||||
- temporal-control history ping-pongs alongside color/travel and field/normal
|
||||
history
|
||||
- resolve samples previous-frame temporal control at the reprojected UV
|
||||
- coverage discontinuity reduces history
|
||||
- current temporal control is written into the history set for the next frame
|
||||
|
||||
Current Gate 3C implementation:
|
||||
|
||||
- temporal-control history `w` stores accepted history age in frames
|
||||
- validation grows age when reprojected history survives the travel, field,
|
||||
normal, color, and coverage checks
|
||||
- history authority ramps with age, so newly accepted history can contribute but
|
||||
does not immediately get the same weight as stable history
|
||||
- age resets to zero on validation failure
|
||||
|
||||
Current Gate 3D implementation:
|
||||
|
||||
- travel validation uses the reprojected previous-world position's distance from
|
||||
the previous camera
|
||||
- raw current-frame travel is no longer compared directly to previous history
|
||||
travel, because camera motion changes the ray distance even when the world hit
|
||||
is stable
|
||||
|
||||
Current Gate 3E implementation:
|
||||
|
||||
- stochastic Grid history bypasses opaque-surface neighborhood color clamping
|
||||
and current-frame color-delta rejection
|
||||
- Grid validation still uses field id, travel, normal, coverage continuity,
|
||||
reactive weight, and history age
|
||||
- this prevents a current dither miss from declaring the true neighborhood to be
|
||||
background and crushing the very history that should accumulate stochastic
|
||||
coverage
|
||||
|
||||
Current Gate 3F implementation:
|
||||
|
||||
- stochastic Grid history uses a high-retention blend policy distinct from
|
||||
opaque surfaces
|
||||
- Grid history weight no longer scales down with current coverage or reactive
|
||||
strength; low coverage is exactly where temporal accumulation is needed
|
||||
- opaque surfaces keep the stricter color/reactive validation path
|
||||
- debug mode `2` should now show a smoother history signal than raw scene mode
|
||||
`1` when the Grid is stable
|
||||
|
||||
Current Gate 3G implementation:
|
||||
|
||||
- identity/control buffers are point-loaded, not linearly sampled
|
||||
- field id equality is exact after point-loading, so Grid/Self/planet boundaries
|
||||
hard-reject mismatched history
|
||||
- Grid history color is sampled with the same nearest pixel used for metadata so
|
||||
bright foreground silhouettes do not bilinearly bleed into background Grid
|
||||
history
|
||||
- Grid max history weight is reduced from the too-soft `0.975` to `0.94`
|
||||
|
||||
Current Gate 3H implementation:
|
||||
|
||||
- Grid temporal `coverage` is now line/contour/field-line support, not broad
|
||||
overlay alpha
|
||||
- broad weather/field tint can still render through stochastic coverage, but it
|
||||
does not keep history age alive across the whole Grid surface
|
||||
- Grid history is gated by current support, so stale stochastic hits cannot
|
||||
billow away from the analytic line support
|
||||
- Grid max history weight is reduced again to `0.90` with a lower fresh-history
|
||||
scale for crisper line response
|
||||
|
||||
Current Gate 3I implementation:
|
||||
|
||||
- Grid resolve reconstructs the analytic Grid overlay at the current world hit
|
||||
and uses premultiplied overlay color as the current color estimate
|
||||
- the scene pass may still use stochastic coverage, but the temporal resolver
|
||||
no longer treats the current binary hit/miss sample as the true radiance
|
||||
- debug mode `1` can remain noisy because it shows the raw stochastic scene;
|
||||
mode `0` should now use the analytic Grid estimate plus validated history
|
||||
|
||||
Current Gate 3J implementation:
|
||||
|
||||
- debug mode `1` is restored to raw current scene color, before Grid analytic
|
||||
reconstruction
|
||||
- Grid final color no longer blends previous color history; it uses the analytic
|
||||
current Grid estimate and writes that estimate into history
|
||||
- history/age/weight debug modes remain useful diagnostics, but stale Grid color
|
||||
history no longer drives the final visible Grid
|
||||
|
||||
Current Gate 3L/3M implementation:
|
||||
|
||||
- projection jitter uses a small Halton sequence, currently scaled below half a
|
||||
pixel so the resolve gets sample diversity without visible whole-frame wobble
|
||||
- current and previous jitter are both uploaded through frame constants; previous
|
||||
history projection removes the previous jitter before sampling history
|
||||
- final presentation blends validated temporal color history in mode `0`
|
||||
- SDF object highlights have a narrow hot-current path: when the current SDF
|
||||
sample is much brighter than valid reprojected history, color rejection is
|
||||
relaxed so history can damp transient specular fireflies while travel, field,
|
||||
normal, coverage, and coverage-continuity validation still apply
|
||||
- bloom presentation is attenuated when the resolved scene luminance is much
|
||||
lower than the raw current scene luminance, so current-frame fireflies do not
|
||||
keep blooming after temporal history has damped the underlying scene color
|
||||
- bloom prefilter clamps unsupported single-pixel HDR spikes against local
|
||||
neighborhood luminance before downsample/blur, so one-frame fireflies do not
|
||||
spread into neighboring pixels before the temporal resolve sees them
|
||||
- modes `2` through `4` show the sampled history, accepted history age, and
|
||||
history weight
|
||||
|
||||
Still missing:
|
||||
|
||||
- reactive reset for lighting and density changes
|
||||
- separate history policy for opaque surfaces and stochastic surfaces
|
||||
|
||||
### Gate 4: TSR Features Worth Stealing In Spirit
|
||||
|
||||
- flicker analysis for high-frequency linework and stochastic coverage
|
||||
- history resurrection for reappearing stable details
|
||||
- debug visualizations for rejection, clamp, velocity, travel, and history age
|
||||
|
||||
Current debug view controls:
|
||||
|
||||
- `F1` cycles temporal debug modes in the running window
|
||||
- `0` final resolved color
|
||||
- `1` raw current scene color
|
||||
- `2` reprojected history color used by the resolver
|
||||
- `3` accepted history age
|
||||
- `4` history weight
|
||||
- `5` current temporal control: reactive in red and Grid support/coverage in
|
||||
green
|
||||
- `6` current field identity palette for Grid, Self, and planets
|
||||
- `7` bloom/veil contribution after exposure and before tonemapping
|
||||
- `8` exposed luminance bands
|
||||
- `--render-debug` and `AQUARIUM_RENDER_DEBUG_MODE` set the startup mode
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Vortice Spine
|
||||
|
||||
Aquarium starts from a thin native spine:
|
||||
|
||||
- `Platform.Win32Window` owns window creation and the message pump.
|
||||
- `Render.D3D12Renderer` owns the D3D12 device, command queue, swapchain,
|
||||
frame resources, render targets, shaders, and present path.
|
||||
- `AquariumRuntime` owns simulation state and emits one `AquariumFrame` per
|
||||
tick.
|
||||
- `OrbitCameraRig` and `GridFrame` preserve the first invariant: the camera
|
||||
target is the Grid center, and Grid radius derives from zoom distance rather
|
||||
than pitch, yaw, or screen projection.
|
||||
|
||||
## Why Vortice
|
||||
|
||||
Vortice gives direct access to D3D/DXGI without forcing an engine protocol on
|
||||
top. It is low ceremony enough to build the renderer Aquarium needs while
|
||||
keeping C# as the everyday working environment.
|
||||
|
||||
## Current Renderer
|
||||
|
||||
The renderer owns the visible Aquarium frame:
|
||||
|
||||
1. Camera and Grid constants come from the client runtime.
|
||||
2. A height-field pass renders the scalar field target from client brush data.
|
||||
3. A fullscreen D3D12 HLSL pass traces the background and height-field surface.
|
||||
4. Client-declared SDF proxy pipelines render Epiphany bodies into the shared
|
||||
scene targets.
|
||||
5. HDR bloom and presentation run before the swapchain is handed to the overlay.
|
||||
6. DirectWrite overlay text and debug UI draw after the scene pass.
|
||||
|
||||
No framework-owned deferred path or ambient light is hiding under the floor.
|
||||
When richer lighting returns, it enters through an Aquarium-owned renderer
|
||||
contract, not convenience globals.
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# Zyphos and Eusocial Interbeing Sync
|
||||
|
||||
Zyphos simulates the world defined by the neighboring Eusocial Interbeing vault
|
||||
at `E:\Projects\Eusocial Interbeing`. The vault is the setting authority.
|
||||
Aquarium is the renderer and interaction testbed.
|
||||
|
||||
## Objective
|
||||
|
||||
Use Zyphos as a visible planetary demo for Eusocial Interbeing without turning
|
||||
Aquarium into a second lore repository.
|
||||
|
||||
## Current Mechanism
|
||||
|
||||
- Eusocial Interbeing stores canon and design inference in Obsidian notes under
|
||||
`Eusocial Interbeing/`.
|
||||
- Aquarium.Zyphos renders Zyphos, fixed-sky Umbros, atmosphere, night-side
|
||||
glints, and first-pass eclipse cadence through the shared Aquarium host and
|
||||
SDF renderer path.
|
||||
- `ZyphosUmbrosSystem` owns the current render-scale binary constants:
|
||||
Zyphos radius, Umbros radius ratio, 8-Zyphos-radii separation, sea level, and
|
||||
star phase used by the shader.
|
||||
- `Worlds/zyphos-first-fractal-terrain.aquageo` owns the first semantic world
|
||||
patch: solar, orbital, planetary, and lat/long domains wrap the tile terrain
|
||||
so later fractal grammar nodes have real nested reference frames instead of
|
||||
undocumented coordinate folklore.
|
||||
- No automatic sync exists. Changes cross the boundary by explicit notes and
|
||||
state-map updates.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Setting canon belongs in the vault.
|
||||
- Aquarium docs may describe render requirements, demo scope, and questions, but
|
||||
must not become the authoritative setting bible.
|
||||
- Zyphos bends to physics. If orbital mechanics, light budget, eclipse geometry,
|
||||
or climate constraints narrow an early visual idea, the demo should use that
|
||||
pressure as worldbuilding fuel rather than hiding an impossible sky in shader
|
||||
code.
|
||||
- Zyphos-specific code belongs under `src/Aquarium.Zyphos`; generic renderer
|
||||
machinery belongs under Aquarium.Engine only when it serves multiple clients.
|
||||
- Design inference must be labeled before it hardens into canon.
|
||||
- Rendering constraints should feed back into the vault as questions, not silent
|
||||
lore edits hidden in shader code.
|
||||
|
||||
## First Conversation
|
||||
|
||||
The first Zyphos pass should stage the planet as a dimly lit close-binary world
|
||||
with two legible continental systems:
|
||||
|
||||
- Umbros is the slightly smaller twin of Zyphos, mutually tidally locked with it
|
||||
and fixed in the sky except for precession/libration;
|
||||
- the primary star is dim, so the habitable zone is close and the biosphere is
|
||||
energy-starved relative to Earth;
|
||||
- the working vault baseline gives Umbros a very large apparent diameter
|
||||
(roughly 10-13 degrees for the current 8-10 Earth-radii separation candidates)
|
||||
and daily central eclipses around an hour near the relevant equatorial track;
|
||||
- the biosphere is founded on mutable cellular memory exchange, so sentience and
|
||||
eusocial contracts exist across cellular, organismal, social, and ecological
|
||||
scales instead of being reserved for a few special species;
|
||||
|
||||
- the Airawa home continent, where living memory networks, mother trees,
|
||||
disconnected networks, and imperial memetic infrastructure create visible
|
||||
ecological-political structure;
|
||||
- the Sa'auei'a continent, where nomadic reciprocity, breeding grounds, remembered
|
||||
routes, and mobile family-unit infrastructure create a different map logic.
|
||||
|
||||
This does not require detailed species bodies yet. The first useful render target
|
||||
is planetary-scale readability: landmass logic, ecological network hints,
|
||||
settlement/non-settlement patterns, and night-side signals that reflect distinct
|
||||
civilizational structures.
|
||||
|
||||
Aquarium should treat these as lighting, sky, time-of-day, and ecological
|
||||
pressure constraints. The accepted physical baseline lives in the vault note
|
||||
`Eusocial Interbeing/World/Zyphos Umbros Binary System.md`.
|
||||
The accepted biosphere baseline lives in
|
||||
`Eusocial Interbeing/Ecology/Mutable Memory Endosymbiosis.md`.
|
||||
First concrete biosphere handles live in
|
||||
`Eusocial Interbeing/Ecology/Zyphos Biosphere Examples.md`.
|
||||
Future inhabitant-language names should be coordinated through Weksa at
|
||||
`E:\Projects\weksa`, with names derived from language-project state rather than
|
||||
English label substitution.
|
||||
|
||||
## Feedback Path
|
||||
|
||||
When Zyphos work creates a worldbuilding question, update the vault note
|
||||
`Eusocial Interbeing/World/Zyphos Simulation Brief.md` first. Aquarium should
|
||||
then reference the settled result, not carry private lore.
|
||||
|
||||
When the vault changes canon that affects the demo, update this file and
|
||||
`state/map.yaml` so Zyphos does not keep simulating an obsolete world because a
|
||||
shader once looked nice. The shader has no voting rights.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,28 +0,0 @@
|
|||
# D3D12 Source Manifest
|
||||
|
||||
## Primary Sources
|
||||
|
||||
- Microsoft Learn, Resource binding overview:
|
||||
https://learn.microsoft.com/en-us/windows/win32/direct3d12/resource-binding-flow-of-control
|
||||
- Microsoft Learn, Fence-based resource management:
|
||||
https://learn.microsoft.com/en-us/windows/win32/direct3d12/fence-based-resource-management
|
||||
- Microsoft Learn, Memory Management in Direct3D 12:
|
||||
https://learn.microsoft.com/en-us/windows/win32/direct3d12/memory-management
|
||||
- Microsoft Learn, Direct3D 12 core interfaces:
|
||||
https://learn.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-interfaces
|
||||
- Microsoft DirectX Developer Blog, A Look Inside D3D12 Resource State Barriers:
|
||||
https://devblogs.microsoft.com/directx/a-look-inside-d3d12-resource-state-barriers/
|
||||
- NVIDIA Technical Blog, Advanced API Performance: Barriers:
|
||||
https://developer.nvidia.com/blog/advanced-api-performance-barriers/
|
||||
- AMD GPUOpen, RDNA Performance Guide:
|
||||
https://gpuopen.com/learn/rdna-performance-guide/
|
||||
- AMD GPUOpen, D3D12 Memory Allocator:
|
||||
https://gpuopen.com/d3d12-memory-allocator/
|
||||
- AMD GPUOpen, Effective Use of the New D3D12_HEAP_TYPE_GPU_UPLOAD:
|
||||
https://gpuopen.com/learn/using-d3d12-heap-type-gpu-upload/
|
||||
|
||||
## Scope
|
||||
|
||||
This pass focused on D3D12 renderer architecture, not beginner API syntax:
|
||||
descriptor heaps, root signatures, command allocators, fences, upload/ring
|
||||
buffers, memory allocation, resource barriers, and GPU/CPU synchronization.
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# D3D12 Synthesis
|
||||
|
||||
## Core Model
|
||||
|
||||
D3D12 is an explicit ownership API. The renderer owns command memory, resource
|
||||
states, descriptor lifetime, upload lifetime, and GPU/CPU synchronization. The
|
||||
driver stops hiding most of that work. Good D3D12 code is therefore less about
|
||||
calling the right incantations and more about making ownership impossible to
|
||||
misread.
|
||||
|
||||
The source material converges on a few hard rules:
|
||||
|
||||
- Keep root signatures stable and few. Bind a root signature once per command
|
||||
list or pass family, not as routine draw noise.
|
||||
- Use descriptor heaps deliberately. Shader-visible CBV/SRV/UAV heaps are
|
||||
application-versioned memory; never overwrite descriptors still referenced by
|
||||
in-flight GPU work.
|
||||
- Treat RTV/DSV descriptors differently from shader-visible descriptors. RTV/DSV
|
||||
bindings are versioned during command recording, but shader-visible heap
|
||||
contents are not protected by the runtime.
|
||||
- Reset command allocators only after the GPU has completed the work recorded
|
||||
into them.
|
||||
- Use fences to reclaim transient upload/descriptors/resources. A frame fence is
|
||||
the coarse tool; finer fences can protect suballocations later.
|
||||
- Prefer ring/suballocation systems for transient uploads and descriptors. Fixed
|
||||
one-resource-per-use committed allocations are acceptable scaffolding, not the
|
||||
renderer we keep.
|
||||
- Track resource states explicitly and emit the minimum correct barriers. Avoid
|
||||
broad generic states and redundant transitions.
|
||||
- Batch or group barriers when that becomes practical; barriers and fences can
|
||||
reduce GPU parallelism.
|
||||
- Keep command submissions reasonably chunky. Tiny command lists and frequent
|
||||
queue submissions pay CPU and GPU scheduling costs.
|
||||
- Use PIX and vendor validation early. D3D12 mistakes are often legal-looking
|
||||
until the GPU catches fire behind a curtain.
|
||||
|
||||
## Descriptor Doctrine
|
||||
|
||||
Microsoft's binding model puts descriptor heap content lifetime squarely on the
|
||||
application for shader-visible heaps. The long-term Aquarium shape should be:
|
||||
|
||||
- One large shader-visible CBV/SRV/UAV heap for the frame, or one persistent
|
||||
heap plus suballocated dynamic ranges.
|
||||
- A CPU-visible/staging descriptor store for reusable descriptors.
|
||||
- Per-frame transient descriptor ranges that are reclaimed by fence value.
|
||||
- Stable descriptor ranges for long-lived textures/buffers.
|
||||
- Minimal `SetDescriptorHeaps` calls. Heap changes can invalidate descriptor
|
||||
table bindings, so pass code should bind the heap once before draws.
|
||||
|
||||
Current Aquarium D3D12 status:
|
||||
|
||||
- Good: shader-visible descriptors are split into static and per-frame
|
||||
transient arenas. Transient CBV/UAV descriptors reset only after the owning
|
||||
frame fence clears, and static descriptors rebuild on resize after a GPU wait.
|
||||
- Temporary: arenas are still simple bump allocators. Keep them until real
|
||||
non-resize churn demands free lists, paging, or a persistent global heap.
|
||||
|
||||
## Upload Doctrine
|
||||
|
||||
Transient CPU-to-GPU data belongs in persistent mapped upload memory, typically
|
||||
suballocated as a ring and reclaimed by fences. Constant buffers require 256-byte
|
||||
alignment. Reading directly from upload memory is acceptable for small constant
|
||||
data, but large frequently-read resources should be copied to default heap
|
||||
memory before shader use.
|
||||
|
||||
Current Aquarium D3D12 status:
|
||||
|
||||
- Good: each frame owns a persistently mapped upload ring, and constant-buffer
|
||||
writes are 256-byte aligned.
|
||||
- Temporary: upload policy is still per-frame and constant-only. Structured
|
||||
scene data should move through typed upload allocations and default-heap
|
||||
buffers before real pass migration grows data volume.
|
||||
- Guardrail: keep direct shader reads from upload memory limited to small
|
||||
constants. Structured/large scene data should move to default memory.
|
||||
|
||||
## Memory Doctrine
|
||||
|
||||
Committed resources are fine while bringing up a backend. They are not a
|
||||
production memory manager. Microsoft recommends classifying resources and
|
||||
budgeting/streaming residency; AMD recommends large heaps and suballocation,
|
||||
with D3D12 Memory Allocator as the practical library path.
|
||||
|
||||
Current Aquarium D3D12 status:
|
||||
|
||||
- Good: committed default resources make ownership explicit during bring-up.
|
||||
- Temporary: once scene/history/froxel resources grow, add D3D12MA or an
|
||||
equivalent placed-resource allocator instead of keeping one committed resource
|
||||
per texture/buffer.
|
||||
|
||||
## Barrier Doctrine
|
||||
|
||||
Barrier correctness is not optional, but barrier volume is a performance risk.
|
||||
The renderer should track state per resource, use the narrowest valid states,
|
||||
avoid redundant transitions, and group barriers when pass complexity grows.
|
||||
|
||||
Current Aquarium D3D12 status:
|
||||
|
||||
- Good: the offscreen render target tracks its current state and transitions
|
||||
only on change.
|
||||
- Good: swapchain backbuffers are wrapped as tracked resources and transition
|
||||
explicitly between present and render-target states.
|
||||
- Watch item: rendering to an offscreen target then copying to the swapchain is
|
||||
useful for proving texture ownership, but final composition should avoid
|
||||
unnecessary full-frame copies unless a post-process graph requires them.
|
||||
|
||||
## Command/Fence Doctrine
|
||||
|
||||
Per-frame command allocators must not be reset until the GPU is done with their
|
||||
recorded work. Frame resources should carry fence values, and transient
|
||||
descriptor/upload/resource allocations should tie reclamation to those values.
|
||||
|
||||
Current Aquarium D3D12 status:
|
||||
|
||||
- Good: one command allocator per swapchain frame and fence wait before reuse.
|
||||
- Temporary: the renderer still uses a single graphics command list and a coarse
|
||||
frame fence. That is fine until multi-threaded recording or async copy/compute
|
||||
becomes real.
|
||||
|
||||
## Aquarium Next Steps
|
||||
|
||||
1. Add a D3D12 frame graph/resource map for named render targets and buffers.
|
||||
2. Promote descriptor allocation from smoke-path arenas into renderer-wide
|
||||
static/transient descriptor policy.
|
||||
3. Extend the upload ring from constants into typed uploads for structured
|
||||
buffers and texture staging.
|
||||
4. Add D3D12MA or a deliberate placed-resource allocator before the resource
|
||||
count becomes serious.
|
||||
5. Add debug names and PIX markers while the graph is still small.
|
||||
6. Track backbuffer resources with the same state wrapper used for offscreen
|
||||
targets.
|
||||
7. Port the first real pass only after descriptors, upload, render targets, and
|
||||
state tracking have durable ownership.
|
||||
|
|
@ -1,561 +0,0 @@
|
|||
# Cube-Sphere Fractal Brush Research
|
||||
|
||||
Aquarium needs one scalable surface/object grammar, not separate tricks for
|
||||
planets, Grid brushes, and agent bodies.
|
||||
|
||||
The working target is a cube-sphere domain: six square faces, each subdivided by
|
||||
quadtree addresses, projected onto a sphere with bounded distortion. Each tile
|
||||
owns a local 2D chart for terrain and surface detail. The same grammar extends
|
||||
inward to 3D object detail through bounded implicit brush nodes.
|
||||
|
||||
This is not a license to build a universal registry-shaped fog machine. The
|
||||
authority should be small:
|
||||
|
||||
- Tile address owns where a surface patch lives.
|
||||
- Projection owns how a face chart maps to the sphere.
|
||||
- Brush grammar owns what detail exists at a scale.
|
||||
- Brush envelope owns where that detail can affect rendering.
|
||||
- Renderer LOD/sampling owns how much of the grammar is evaluated this frame.
|
||||
|
||||
## Surface Domain
|
||||
|
||||
The established name is the quadrilateralized spherical cube, often shortened to
|
||||
QSC or quad sphere. It was designed as a discrete global grid for spherical
|
||||
data, including Earth/sky data, and avoids the polar singularities of lat-long.
|
||||
PROJ describes QSC as equal-area, treating all cube sides equally, and explicitly
|
||||
mentions planetary-scale terrain rendering with quadtree data per cube side.
|
||||
|
||||
The older COBE/Chan-O'Neill family is approximately equal-area and historically
|
||||
important. For engine use, the more relevant graphics framing is cube-to-sphere
|
||||
projection for procedural texturing: generate a point or tile on a cube face,
|
||||
warp the face coordinates to reduce area distortion, then normalize/project to
|
||||
the sphere.
|
||||
|
||||
The cheap mapping ladder:
|
||||
|
||||
1. Identity cube map then normalize: fastest, worst area stretch near cube
|
||||
corners.
|
||||
2. Tangent/univariate warp: still simple, analytic inverse, much better area
|
||||
distribution.
|
||||
3. Fifth-order odd polynomial warp: fastest high-quality univariate candidate
|
||||
in the JCGT comparison; good for GPU hot paths, inverse is approximate.
|
||||
4. COBE-style bivariate polynomial: best area preservation in that comparison,
|
||||
but more expensive.
|
||||
5. Exact equal-area constructions: useful reference, not automatically the right
|
||||
hot shader choice.
|
||||
|
||||
For Aquarium, the first serious candidate should be the fifth-order odd
|
||||
polynomial warp from the JCGT paper, measured against tangent and COBE on the
|
||||
actual 1070 target. It is the right shape of compromise: cheap scalar math,
|
||||
separable face axes, low area error, and no lat-long singularity. If inverse
|
||||
address lookup dominates, tangent may win despite worse area preservation. If
|
||||
offline baking or CPU authoring needs maximum area regularity, COBE/QSC can be
|
||||
used there without forcing it into every pixel path.
|
||||
|
||||
## Multiscale Detail
|
||||
|
||||
Every visible primitive should be able to answer: what detail exists below this
|
||||
scale? The proposed answer is an iterated function system brush grammar.
|
||||
|
||||
For terrain:
|
||||
|
||||
- Each cube-face quadtree tile has a deterministic grammar seed.
|
||||
- The grammar emits bounded 2D brushes at octave/LOD levels.
|
||||
- Coarse levels define form: continents, basins, ridges, crater families,
|
||||
river-like masks, weathered bands.
|
||||
- Fine levels define material/texture response first; traced geometry only when
|
||||
its bound is visible enough to deserve SDF cost.
|
||||
|
||||
For 3D bodies:
|
||||
|
||||
- Each agent/body starts from one coherent base SDF form.
|
||||
- Its grammar emits bounded 3D brush nodes in that local form domain.
|
||||
- A brush may carry texture/material response, displacement, negative space, or
|
||||
form contribution, but it must share an explicit envelope.
|
||||
- The grammar is LOD-gated and budgeted by proxy bounds. No scene-global
|
||||
mega-SDF. No evaluating all organs because one pixel got curious.
|
||||
|
||||
IFS here means a deterministic recursive emitter of child transforms and brush
|
||||
parameters, not necessarily a textbook fractal exposed directly to the user. The
|
||||
important invariant is that detail exists at every scale because the primitive
|
||||
can generate it locally, not because a finite table happened to be hand-filled.
|
||||
|
||||
## IFS DSL Shape
|
||||
|
||||
VibeGeometry is the nearest local evidence for the authoring layer Aquarium
|
||||
needs. It is not about SDFs, but it has spent real work on spatial mapping,
|
||||
procedural graph translation, CSG claims, and keeping geometry machines
|
||||
inspectable.
|
||||
|
||||
The lesson is not "copy Blender nodes." The lesson is that a spatial DSL needs
|
||||
more than primitives and transforms:
|
||||
|
||||
- explicit local frames and frame transitions;
|
||||
- deterministic seeded variation;
|
||||
- named fields/attributes that survive domain changes;
|
||||
- tags explaining why a generated thing exists;
|
||||
- repeat/foreach-style rule composition;
|
||||
- authoring handles/gizmos for parameters humans and agents will tune;
|
||||
- asset/material binding outside the hot geometry grammar;
|
||||
- debug/evidence outputs for each layer, not only the final pretty object.
|
||||
|
||||
Aquarium's IFS DSL should emit semantic brush claims before it emits shader
|
||||
work. A claim is a bounded statement in a local domain:
|
||||
|
||||
```text
|
||||
claim {
|
||||
name
|
||||
kind: material | height | displacement | sdf_form | void | mask | light
|
||||
frame
|
||||
envelope
|
||||
payload
|
||||
tags
|
||||
lod_range
|
||||
cost_tier
|
||||
seed
|
||||
children
|
||||
}
|
||||
```
|
||||
|
||||
For terrain, the claim domain is a cube-face tile chart plus height/material
|
||||
space. For bodies, the claim domain is the object-local anatomical/form frame.
|
||||
For both, rule output stays semantic until a backend lowers it into one of the
|
||||
live render paths: height brush pass, material field, SDF evaluator, impostor
|
||||
splat, debug view, or offline bake.
|
||||
|
||||
Do not flatten this into a bag of anonymous brushes too early. VibeGeometry's
|
||||
`vg_grammar` layer keeps `Solid` and `Void` claims tagged as room, corridor,
|
||||
door, wall, floor, or ceiling before compiling to CSG. Aquarium needs the same
|
||||
shape of truth for craters, ridges, rivers, veins, ribs, petals, apertures,
|
||||
scars, and agent state features. The renderer can be brutal and cheap later;
|
||||
the authoring layer should still know what each brush means.
|
||||
|
||||
## CSG Tree Lesson
|
||||
|
||||
The really useful VibeGeometry path is the CSG tree attempt. It separates three
|
||||
authorities:
|
||||
|
||||
- grammar/rules decide which semantic claims should exist;
|
||||
- the CSG tree decides boolean ownership of space;
|
||||
- the output mesh or brush stream is a compiled verdict, not the source of
|
||||
truth.
|
||||
|
||||
`vg_csg` currently has a small `CsgTreeArena` with brush leaves and branch ops:
|
||||
addition, subtraction, and common/intersection. Branch bounds follow boolean
|
||||
meaning: addition unions child bounds, subtraction keeps the left/source bound,
|
||||
and common intersects child bounds. The tree can compile down to the current
|
||||
ordered brush stream, where the first child of a subtraction remains additive
|
||||
and later children become subtractive operators.
|
||||
|
||||
That flattening bridge matters. Aquarium can preserve an authored IFS/CSG tree
|
||||
even if the first GPU path lowers it to fixed-size brush tables or per-object
|
||||
SDF includes. The tree is the durable design. The shader packet is a backend.
|
||||
|
||||
For Aquarium, the tree should probably not be a full CAD boolean kernel in the
|
||||
hot path. It should be a semantic spatial program that lowers into cheap
|
||||
bounded evaluators:
|
||||
|
||||
```text
|
||||
rule tree
|
||||
-> semantic brush/void/material claims
|
||||
-> boolean/IFS ownership tree
|
||||
-> backend lowering:
|
||||
- 2D tile material/height brushes
|
||||
- 3D object-local SDF form brushes
|
||||
- compact anisotropic splat/impostor batches
|
||||
- offline baked summaries and mips
|
||||
```
|
||||
|
||||
The important tree operations for the first Aquarium DSL:
|
||||
|
||||
- `Union`: multiple claims contribute in the same domain.
|
||||
- `Subtract`: voids, apertures, channels, scars, doorways, river cuts.
|
||||
- `Intersect/Common`: masks, biome regions, anatomical zones, tile clipping.
|
||||
- `Repeat`: emit child rules over a count, curve, grid, face, or stochastic set.
|
||||
- `Refine`: emit lower-scale children when screen/error budget asks for them.
|
||||
- `Capture`: name a value, station, mask, or frame for later child rules.
|
||||
- `Bind`: attach material/asset/state payload outside the form rule.
|
||||
|
||||
The tree must carry bounds at every node. Bounds are not an acceleration garnish;
|
||||
they are the contract that keeps a grammar from becoming a frame-eating sermon.
|
||||
Branch bounds also define LOD and invalidation: if a tile/body query does not
|
||||
touch a subtree's bound, that subtree does not get to spend shader time.
|
||||
|
||||
## VibeGeometry Distillation
|
||||
|
||||
Local source paths worth treating as evidence:
|
||||
|
||||
- `E:\Projects\VibeGeometry\docs\research\procedural-doctrine.md`
|
||||
- `E:\Projects\VibeGeometry\docs\research\translations\ircss-french-houses-node-atlas.md`
|
||||
- `E:\Projects\VibeGeometry\docs\research\translations\ircss-french-houses-node-mechanisms.md`
|
||||
- `E:\Projects\VibeGeometry\docs\research\dream-machine-grammar.md`
|
||||
- `E:\Projects\VibeGeometry\docs\research\realtime-csg-doctrine.md`
|
||||
- `E:\Projects\VibeGeometry\crates\vg_grammar\src`
|
||||
- `E:\Projects\VibeGeometry\crates\vg_csg\src\tree.rs`
|
||||
- `E:\Projects\VibeGeometry\crates\vg_csg\src\frontier.rs`
|
||||
|
||||
Distilled rules:
|
||||
|
||||
- Build the host form first; ornament grows on captured structure.
|
||||
- Attributes/fields are not bookkeeping. They are how identity survives
|
||||
instancing, deletion, scattering, and later detail passes.
|
||||
- Groups can pass batons: running values such as angle, offset, frame, count,
|
||||
seed, aperture, or path state should be explicit outputs.
|
||||
- Scatter has layers: nursery surface, point field, instance body, realization
|
||||
boundary. Aquarium's brush grammar should mirror this with tile/body domain,
|
||||
spawn field, brush claim, and backend lowering.
|
||||
- Paths are topology first. Draw cracks, veins, river paths, routes, or rails
|
||||
after solving connectivity or stations.
|
||||
- Boolean thickets often want tables. Repeated near-clone rules should become
|
||||
one rule plus rows of parameters, not copied child subtrees.
|
||||
- CSG is an adjudicator, not the authoring language. Domain rules should be
|
||||
written in terrain, anatomy, tile, curve, or habitat coordinates, then lowered
|
||||
into boolean/brush evaluation late.
|
||||
- Demand frontiers beat world rebuilds. The query already says which tile,
|
||||
proxy, dirty brush, screen cone, or body bound matters; use that before
|
||||
building generic indexes.
|
||||
|
||||
## Brush Envelope
|
||||
|
||||
The current cheap circular/Gaussian brush idea is too blunt. The next primitive
|
||||
should look more like a shaped Gaussian splat: an anisotropic bounded kernel
|
||||
with rotation, scale, falloff, and optional local shape controls.
|
||||
|
||||
Relevant splatting lineage:
|
||||
|
||||
- EWA/surface/volume splatting uses elliptical Gaussian kernels and screen-space
|
||||
filtering to avoid aliasing and excess blur.
|
||||
- 3D Gaussian Splatting models scenes with explicit anisotropic 3D Gaussians,
|
||||
each carrying position, opacity, scale, rotation/covariance, and appearance,
|
||||
then bins/sorts/rasterizes them efficiently.
|
||||
|
||||
Aquarium should borrow the cheap structural idea, not the whole radiance-field
|
||||
machine. A brush envelope can be stored as:
|
||||
|
||||
```text
|
||||
center
|
||||
orientation basis or compressed rotation
|
||||
radii / inverse covariance diagonal
|
||||
falloff exponent or compact support radius
|
||||
shape parameters for taper, skew, ridge, hollow, or signed lobe response
|
||||
payload type: material, height/displacement, SDF form, mask, light, debug
|
||||
```
|
||||
|
||||
Hot shader evaluation should prefer compact-support anisotropic kernels over
|
||||
infinite Gaussians:
|
||||
|
||||
```text
|
||||
q = transpose(rotation) * (p - center)
|
||||
m = dot(q * invRadii, q * invRadii)
|
||||
support = saturate(1 - m)
|
||||
weight = support * support * (3 - 2 * support)
|
||||
```
|
||||
|
||||
That is not a true Gaussian. Good. Infinite support is expensive folklore unless
|
||||
the renderer can cull it. The useful inheritance from Gaussian splatting is
|
||||
anisotropic covariance-like shape control, tile/proxy binning, and LOD-aware
|
||||
visibility, not religious devotion to exp().
|
||||
|
||||
## Sampling Strategy
|
||||
|
||||
SDF performance hinges on conservative bounds and choosing the right evaluator
|
||||
late. Hart's sphere tracing framing is still the base law: steps are safe when
|
||||
the distance estimate respects a derivative/Lipschitz bound. Aquarium already
|
||||
learned the local version of this for thin agent SDF organs.
|
||||
|
||||
Migration target:
|
||||
|
||||
- Keep proxy bounds first. A tile or body proxy decides whether any expensive
|
||||
grammar can matter.
|
||||
- March conservative macro distance before micro detail.
|
||||
- Evaluate grammar detail only after the ray enters a tile/body budget zone.
|
||||
- Carry LOD tier, cost tier, step count, proxy-hit coverage, and grammar node
|
||||
count debug views before trusting the system.
|
||||
- Use cone/pixel-footprint-aware sampling for material/height detail; do not
|
||||
trace subpixel grammar detail as geometry.
|
||||
- Use prefiltered/mip summaries per tile and per grammar family where possible.
|
||||
- Use stochastic/blue-noise selection for dense fine detail when exact full
|
||||
evaluation would alias or exceed budget.
|
||||
|
||||
The hard rule: form SDF detail must have a conservative envelope and a measured
|
||||
cost. Texture/detail brushes can be abundant; traced form brushes must earn
|
||||
their steps.
|
||||
|
||||
## LOD And Contribution Caching
|
||||
|
||||
The LOD problem is not "pick a depth." The real question is: would this
|
||||
subtree change currently visible pixels enough to justify its cost?
|
||||
|
||||
The model should combine three proven ideas:
|
||||
|
||||
- Geometry clipmaps: keep nested, incrementally updated caches around the
|
||||
viewer for terrain-like domains, and fade between levels in transition
|
||||
regions.
|
||||
- Nanite-style hierarchy cuts: select exactly one useful representation along
|
||||
each ancestor/descendant chain using projected error. Parent summaries let
|
||||
traversal stop without asking every leaf for permission.
|
||||
- Gaussian splat/octree LOD: score an anisotropic primitive by projecting its
|
||||
covariance/envelope footprint into screen space, then choose coarser or finer
|
||||
anchors based on pixel impact and budget.
|
||||
|
||||
For Aquarium's IFS brush tree, every node should cache a compact summary:
|
||||
|
||||
```text
|
||||
nodeId
|
||||
parentId / firstChild / childCount
|
||||
domain: cubeFaceTile | bodyLocal | curveLocal | volumeLocal
|
||||
localFrame
|
||||
worldOrTileBound
|
||||
envelopeBound
|
||||
maxDisplacementOrDistanceError
|
||||
maxMaterialDelta
|
||||
normalConeOrGradientBound
|
||||
opacityOrCoverageBound
|
||||
frequencyBand / octave
|
||||
estimatedCost
|
||||
lastContributionScore
|
||||
lastVisibleFrame
|
||||
residentState
|
||||
summaryPayloadHandle
|
||||
childPayloadHandle
|
||||
```
|
||||
|
||||
The projected contribution score can start simple:
|
||||
|
||||
```text
|
||||
projectedRadiusPx = project_bound_radius(node.bound, camera)
|
||||
projectedErrorPx = project_world_error(node.maxError, node.bound, camera)
|
||||
materialScore = node.maxMaterialDelta * projectedCoveragePx
|
||||
formScore = projectedErrorPx * visibleCoverageEstimate
|
||||
score = max(formScore, materialScore) * importanceBias / estimatedCost
|
||||
```
|
||||
|
||||
This is only a heuristic, but it protects the right invariant: children fade in
|
||||
when their worst possible visible contribution rises above a pixel/error
|
||||
threshold, and fade out when the parent summary is good enough. For anisotropic
|
||||
brushes, use the projected ellipse/covariance footprint rather than a sphere
|
||||
whenever that data is already cheap; otherwise fall back to a conservative
|
||||
sphere/AABB.
|
||||
|
||||
The runtime cut:
|
||||
|
||||
```text
|
||||
parentGoodEnough = parent.projectedErrorPx <= threshold
|
||||
nodeNeeded = node.projectedErrorPx > threshold
|
||||
renderNodeSummary = parentNeeded && !nodeNeeded
|
||||
descend = nodeNeeded && budgetAllows && childrenResidentOrRequestable
|
||||
```
|
||||
|
||||
For smooth transitions, each node gets a fade weight based on hysteresis around
|
||||
the threshold:
|
||||
|
||||
```text
|
||||
fade = smoothstep(thresholdLow, thresholdHigh, projectedErrorPx)
|
||||
```
|
||||
|
||||
The parent summary fades out as children fade in. Geometry clipmap transition
|
||||
regions are the terrain precedent; for object/body brushes this becomes
|
||||
cross-fading material contribution or displacing geometry only after the
|
||||
micro-form is stable enough not to pop. Never fade signed distance itself in a
|
||||
way that breaks conservative marching. Fade payload amplitude, material
|
||||
response, or selected child contribution after the macro hit is established.
|
||||
|
||||
### Dynamic Cache
|
||||
|
||||
The cache should be a demand cache, not a tree-shaped shrine in memory.
|
||||
|
||||
Inputs:
|
||||
|
||||
- camera and previous camera;
|
||||
- visible cube-face tile windows or body proxy hits;
|
||||
- node summaries resident on CPU/GPU;
|
||||
- per-node contribution score from the previous frame;
|
||||
- request budget for new child payloads;
|
||||
- eviction budget for stale low-score payloads.
|
||||
- update budget for how many node weights may be refreshed this frame.
|
||||
|
||||
Per frame:
|
||||
|
||||
1. Traverse only visible roots: active cube faces/tiles and body proxies.
|
||||
2. Project each node summary and compute contribution score.
|
||||
3. If the summary is enough, draw/evaluate it and stop.
|
||||
4. If children are needed and resident, descend.
|
||||
5. If children are needed and missing, render the parent summary, enqueue child
|
||||
payloads, and keep a fade-in reservation.
|
||||
6. Update a small score cache with temporal smoothing:
|
||||
|
||||
```text
|
||||
score = lerp(oldScore, currentScore, currentVisible ? 0.35 : 0.08)
|
||||
```
|
||||
|
||||
7. Evict children whose smoothed score stays below threshold for enough frames,
|
||||
starting with high-cost low-score nodes.
|
||||
|
||||
### Probabilistic Weight Updates
|
||||
|
||||
The contribution cache cannot refresh every branch every frame. A large IFS tree
|
||||
will always have more latent branches than budget. The cache therefore needs an
|
||||
online estimator: every frame it samples a subset of nodes, updates their
|
||||
weights, and lets the whole system converge over time.
|
||||
|
||||
The first version can be a stochastic scheduler, not a neural net:
|
||||
|
||||
```text
|
||||
priority =
|
||||
visibilityProbability
|
||||
* staleWeight
|
||||
* uncertainty
|
||||
* max(previousScore, parentScoreBias)
|
||||
/ max(estimatedUpdateCost, epsilon)
|
||||
```
|
||||
|
||||
Nodes enter the update lottery from visible roots, near-threshold cut nodes,
|
||||
recently visible descendants, and a small exploration set. The scheduler spends
|
||||
the frame's update budget on weighted random samples, not only on the current
|
||||
top scores. Greedy-only updates are how stale branches stay confidently wrong.
|
||||
|
||||
Each node should carry estimator state:
|
||||
|
||||
```text
|
||||
meanContribution
|
||||
varianceOrUncertainty
|
||||
sampleCount
|
||||
lastSampleFrame
|
||||
lastVisibleFrame
|
||||
decayRate
|
||||
confidence
|
||||
```
|
||||
|
||||
On update:
|
||||
|
||||
```text
|
||||
delta = observedContribution - meanContribution
|
||||
meanContribution += alpha * delta
|
||||
variance = lerp(variance, delta * delta, beta)
|
||||
confidence = saturate(confidence + confidenceGain)
|
||||
```
|
||||
|
||||
On non-update:
|
||||
|
||||
```text
|
||||
confidence *= decay
|
||||
uncertainty += staleGrowth
|
||||
```
|
||||
|
||||
This is machine-learning territory in the plain sense: an online model predicts
|
||||
which branches matter, samples under budget, updates from observations, and
|
||||
should converge toward stable weights for stable views. Start with exponential
|
||||
moving averages, variance, and bandit-style exploration. Do not jump straight to
|
||||
a bespoke neural predictor unless the simpler estimator cannot hit fidelity or
|
||||
budget.
|
||||
|
||||
The convergence contract:
|
||||
|
||||
- Every resident visible or near-threshold node must have nonzero probability of
|
||||
being resampled.
|
||||
- High-uncertainty nodes get exploration budget even when their old score is
|
||||
low.
|
||||
- Scores decay when nodes go unseen, but summaries remain available.
|
||||
- Parent and child scores must be monotonic enough for stable cuts: a parent
|
||||
summary cannot claim less possible contribution than all of the children it
|
||||
hides.
|
||||
- Debug views must expose mean, variance/uncertainty, sample age, confidence,
|
||||
and update probability.
|
||||
|
||||
The higher-fidelity path is learned scoring:
|
||||
|
||||
```text
|
||||
features:
|
||||
projected bound, projected anisotropic footprint, depth, view angle,
|
||||
normal/gradient cone, material delta, motion, parent score, tile/body state,
|
||||
historical score/variance, previous visibility, estimated cost
|
||||
|
||||
prediction:
|
||||
expectedContribution
|
||||
uncertainty
|
||||
updatePriority
|
||||
```
|
||||
|
||||
Training signal can come from occasional high-quality probes: render/evaluate a
|
||||
more detailed subtree, compare against the parent summary, and record the
|
||||
observed pixel/material/SDF delta. That makes the cache a budgeted fidelity
|
||||
estimator rather than a pile of thresholds pretending to be wise.
|
||||
|
||||
Cut line: the runtime still needs deterministic safety. A learned predictor may
|
||||
rank update and residency priority; it must not be the only guard for SDF
|
||||
marching bounds or object/tile visibility. Conservative summaries remain law.
|
||||
|
||||
The contribution cache should be keyed by stable domain address, not by transient
|
||||
allocation:
|
||||
|
||||
```text
|
||||
cube face + quadtree path + grammar rule path
|
||||
body id + local grammar rule path
|
||||
```
|
||||
|
||||
For a GTX 1070-class target, keep the first implementation embarrassingly
|
||||
simple:
|
||||
|
||||
- CPU scores coarse roots and streaming decisions.
|
||||
- GPU evaluates fixed-size resident node tables per visible tile/body.
|
||||
- Node summaries are small structured-buffer rows.
|
||||
- A stochastic CPU/GPU scheduler refreshes only a bounded subset of node weights
|
||||
each frame, using uncertainty and stale age so weights converge instead of
|
||||
freezing.
|
||||
- Child payload pages are append-only within a frame and compacted only at safe
|
||||
sync points.
|
||||
- Debug views show projected error, contribution score, uncertainty, sample age,
|
||||
confidence, update probability, selected depth, cache residency, fade weight,
|
||||
and estimated cost.
|
||||
|
||||
The cache must preserve summaries even when child payloads are evicted. A parent
|
||||
summary is the fallback representation and the promise that the tree can always
|
||||
render something coherent. If a subtree cannot summarize its children with a
|
||||
bounded error/material delta, it is not ready to be an LOD subtree.
|
||||
|
||||
## Migration Shape
|
||||
|
||||
1. Add the cube-sphere domain math as generic renderer math, not Zyphos policy.
|
||||
2. Convert Zyphos planet surface addressing from lat-long/spherical noise toward
|
||||
six face quadtrees.
|
||||
3. Replace height brushes with shaped anisotropic envelopes while preserving the
|
||||
existing scalar height target until a better pass contract exists.
|
||||
4. Define a minimal semantic brush-claim format: name, kind, frame, envelope,
|
||||
payload, tags, LOD range, cost tier, seed, and children.
|
||||
5. Preserve an authored IFS/CSG ownership tree even when the first backend
|
||||
lowers it into flattened brush tables or SDF shader code.
|
||||
6. Make one terrain grammar and one agent/body grammar share the same envelope
|
||||
evaluator.
|
||||
7. Add node summaries and contribution-score caching before allowing recursive
|
||||
detail to expand at runtime.
|
||||
8. Add debug visualizations before expanding detail density.
|
||||
|
||||
If the first implementation needs a cut, cut 3D recursive form detail first.
|
||||
Keep the cube-sphere address space and shaped 2D brush envelope. Those are the
|
||||
foundation stones.
|
||||
|
||||
## Sources
|
||||
|
||||
- PROJ QSC documentation: https://proj.org/en/stable/operations/projections/qsc.html
|
||||
- NASA NTRS, "A quadrilateralized spherical cube Earth data base": https://ntrs.nasa.gov/citations/19810002572
|
||||
- Clarberg, "Cube-to-sphere Projections for Procedural Texturing and Beyond,"
|
||||
JCGT 2018: https://jcgt.org/published/0007/02/01/paper.pdf
|
||||
- Kerbl et al., "3D Gaussian Splatting for Real-Time Radiance Field Rendering,"
|
||||
SIGGRAPH/TOG 2023: https://arxiv.org/abs/2308.04079
|
||||
- Hart, "Sphere tracing: A geometric method for the antialiased ray tracing of
|
||||
implicit surfaces," The Visual Computer 1996:
|
||||
https://experts.illinois.edu/en/publications/sphere-tracing-a-geometric-method-for-the-antialiased-ray-tracing/
|
||||
- Zwicker et al., EWA surface/volume splatting lineage:
|
||||
https://vcg.seas.harvard.edu/publications/20011001-ewa-volume-splatting
|
||||
- Local VibeGeometry procedural and CSG research:
|
||||
`E:\Projects\VibeGeometry\docs\research\procedural-doctrine.md`,
|
||||
`E:\Projects\VibeGeometry\docs\research\dream-machine-grammar.md`,
|
||||
`E:\Projects\VibeGeometry\docs\research\realtime-csg-doctrine.md`,
|
||||
`E:\Projects\VibeGeometry\crates\vg_grammar`, and
|
||||
`E:\Projects\VibeGeometry\crates\vg_csg`
|
||||
- Losasso and Hoppe / Asirvatham and Hoppe geometry clipmaps:
|
||||
https://hhoppe.com/proj/gpugcm/
|
||||
- Karis et al., Nanite virtualized geometry:
|
||||
https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf
|
||||
- Ren et al., Octree-GS LOD-structured Gaussian splatting:
|
||||
https://arxiv.org/abs/2403.17898
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# Fensalir Spine Emblem IFS Demo Reference
|
||||
|
||||
## Purpose
|
||||
|
||||
This note preserves the visual target for a possible Fensalir IFS demo scene.
|
||||
The current fullscreen splash art is strong enough to become a procedural scene
|
||||
rather than only a bitmap.
|
||||
|
||||
## Source Asset
|
||||
|
||||
- Splash bitmap: `src/Aquarium.Engine/Assets/Fensalir-Splash.png`
|
||||
- Startup BMP copy: `src/Aquarium.Engine/Assets/Fensalir-Splash.bmp`
|
||||
- Icon derivative: `src/Aquarium.Engine/Assets/Fensalir-Icon-96.png`
|
||||
|
||||
## Prompt Of Record
|
||||
|
||||
Use case: stylized-concept
|
||||
|
||||
Asset type: 16:9 engine splash art concept
|
||||
|
||||
Primary request: A bold, logo-adjacent Fensalir engine splash direction that
|
||||
could sit behind a startup screen, combining Norse fen hall mythology with a
|
||||
neon rendering engine identity.
|
||||
|
||||
Scene/backdrop: minimal dark void fading into shallow marsh water, abstract hall
|
||||
silhouette made from vertical luminous runic pillars and clean renderer-like
|
||||
geometry, reeds only at the extreme edges.
|
||||
|
||||
Subject: a central vertical spine of light formed by stacked translucent
|
||||
polygons, runic strokes, and watery reflections, suggesting the engine as the
|
||||
structural backbone of visible worlds.
|
||||
|
||||
Style/medium: sleek futuristic key visual, premium software splash art, less
|
||||
literal, high contrast, graphic and memorable.
|
||||
|
||||
Composition/framing: centered emblem-like composition, strong silhouette, wide
|
||||
negative space, symmetrical but not flat, the spine rising from reflective water
|
||||
into mist.
|
||||
|
||||
Lighting/mood: intense cyan-white central light, restrained magenta edge glow,
|
||||
tiny gold sparks, calm authority.
|
||||
|
||||
Color palette: black, cyan-white, magenta, muted gold, dark green-black.
|
||||
|
||||
Materials/textures: glass, black metal, wet stone, reflective water,
|
||||
laser-etched rune light, subtle fog.
|
||||
|
||||
Constraints: no readable text, no logo, no watermark, no characters, no
|
||||
weapons, no horned helmets, avoid looking like a crypto logo or generic esports
|
||||
background.
|
||||
|
||||
## Implementation Invariants
|
||||
|
||||
- The central cyan-white vertical spine owns the scene. Everything else supports
|
||||
it.
|
||||
- The geometry should read as stacked translucent diamond/polygon forms, not a
|
||||
literal tower or generic sci-fi logo.
|
||||
- Magenta belongs in restrained side glow and reflected water, not as the main
|
||||
light authority.
|
||||
- The base is black reflective marsh water with minimal reeds at the edges.
|
||||
- The hall/rune language should be abstract and structural. No readable runic
|
||||
text.
|
||||
- The scene must remain legible at splash scale and should degrade cleanly into
|
||||
the 96 px icon silhouette.
|
||||
|
||||
## IFS Demo Cut Line
|
||||
|
||||
Keep the demo procedural if the IFS/fractal machinery can own the central spine,
|
||||
pillar rhythm, and reflections directly. If the implementation needs a bag of
|
||||
hand-placed quads plus post-process glow to resemble the splash, call it a
|
||||
staged splash scene, not an IFS demo. The demo should prove an invariant of the
|
||||
IFS work, not merely cosplay it with decoration.
|
||||
|
|
@ -1,948 +0,0 @@
|
|||
# Fractal Brush Architecture Plan
|
||||
|
||||
Aquarium needs one multiscale spatial machine for planets, Grid surfaces, and
|
||||
agent bodies. The target is not "infinite detail" as a slogan. The target is a
|
||||
bounded semantic tree that can always answer four questions:
|
||||
|
||||
- What does this detail mean?
|
||||
- What pixels could it affect?
|
||||
- Is it worth its CPU, RAM, SSD, and GPU cost this frame?
|
||||
- Which module owns the answer, and how do tests prove that boundary?
|
||||
|
||||
The architecture below is the coherent machine that falls out of the research
|
||||
pass.
|
||||
|
||||
## Core Model
|
||||
|
||||
```text
|
||||
client intent / world seed / agent visual state
|
||||
-> domain roots
|
||||
-> semantic IFS brush grammar
|
||||
-> ownership tree
|
||||
-> node summaries
|
||||
-> contribution cache and residency scheduler
|
||||
-> backend packets
|
||||
-> renderer passes
|
||||
-> debug and evidence
|
||||
```
|
||||
|
||||
The authored tree is the source of truth. Backend packets are compiled output.
|
||||
That distinction is the spine. If the grammar is flattened too early into
|
||||
anonymous shader tables, we lose meaning, invalidation, LOD, test boundaries,
|
||||
and future editing handles in one cheap little bonfire.
|
||||
|
||||
## Module Network
|
||||
|
||||
The architecture should be split into modules with narrow contracts and boring
|
||||
mock seams. The goal is not maximum abstraction. The goal is that every module
|
||||
owns one invariant that can be tested without booting the whole renderer.
|
||||
|
||||
```text
|
||||
Aquarium.Engine.Contracts
|
||||
Fractal domain, claim, tree, summary, packet, and debug data contracts.
|
||||
|
||||
Aquarium.Engine.Fractal
|
||||
Pure CPU domain math, grammar expansion, ownership tree, summaries, scoring,
|
||||
stochastic estimator, residency decisions, and backend packet planning.
|
||||
|
||||
Aquarium.Engine.Render
|
||||
D3D12 resource residency, structured buffers, shader bindings, render graph
|
||||
passes, and debug visualization.
|
||||
|
||||
Aquarium.Zyphos
|
||||
Planet-specific domain roots, world seeds, terrain grammar choices, and
|
||||
setting-safe visual policy.
|
||||
|
||||
Aquarium.Epiphany
|
||||
Agent/body visual state, role grammar choices, and semantic state bindings.
|
||||
|
||||
Tests
|
||||
Contract tests, pure CPU unit tests, renderer packet golden tests, shader
|
||||
parity tests, and smoke/integration tests.
|
||||
```
|
||||
|
||||
### Module Boundaries
|
||||
|
||||
| Module | Owns | Does Not Own | Mock Boundary |
|
||||
| --- | --- | --- | --- |
|
||||
| Contracts | Stable data shapes and versionable packets | Projection algorithms or D3D12 objects | Plain DTO fixtures |
|
||||
| Fractal Domain | Cube-face addresses, projections, local frames | Client lore or GPU resources | `IProjection`, camera fixture |
|
||||
| Grammar | Semantic claims and rule expansion | Rendering or residency | seeded rule fixtures |
|
||||
| Ownership Tree | Boolean/IFS topology, bounds, stable keys | Shader packet layout | tree builder fixtures |
|
||||
| Summary Builder | Conservative error/material summaries | Per-frame scheduling | child-node fixtures |
|
||||
| Contribution Cache | score state, stochastic updates, convergence | payload loading or drawing | deterministic RNG, fake frame clock |
|
||||
| Residency Scheduler | request/evict decisions under budgets | SSD implementation or descriptors | fake payload store |
|
||||
| Backend Planner | selected cut to backend packets | D3D12 upload | packet golden files |
|
||||
| D3D12 Backend | buffers, descriptors, shader passes | semantic grammar | packet fixtures and GPU smoke |
|
||||
| Client Adapters | Zyphos/Epiphany policy | generic renderer math | fake engine contracts |
|
||||
|
||||
The hard rule: a module must be defended by the invariant it owns, not by how
|
||||
cleverly it forwards data.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
This system only deserves to exist if it is testable at its natural seams.
|
||||
|
||||
### Pure Unit Tests
|
||||
|
||||
These run without D3D12:
|
||||
|
||||
- cube-face address round trips;
|
||||
- projection candidate continuity and area-error sampling;
|
||||
- local frame point/vector transforms;
|
||||
- envelope CPU evaluator parity against known samples;
|
||||
- grammar expansion from seed to deterministic claims;
|
||||
- ownership tree bounds for union/subtract/common/repeat;
|
||||
- summary conservativeness against generated children;
|
||||
- projected score monotonicity with distance and error;
|
||||
- stochastic estimator convergence with deterministic RNG;
|
||||
- residency eviction/request decisions under fixed budgets.
|
||||
|
||||
### Contract Tests
|
||||
|
||||
These protect module boundaries:
|
||||
|
||||
- serialized contract rows round-trip without renderer types;
|
||||
- stable keys remain stable across reload and equivalent grammar expansion;
|
||||
- backend packet planner emits the same packet for the same selected cut;
|
||||
- clients can declare domains and grammars without referencing D3D12.
|
||||
|
||||
### Mock Boundaries
|
||||
|
||||
Required mocks/fakes:
|
||||
|
||||
- `IFractalClock`: deterministic frame number and time.
|
||||
- `IFractalRandom`: seeded random/update lottery.
|
||||
- `IProjection`: projection candidate swap for tests.
|
||||
- `IContributionProbe`: fake observed pixel/material/SDF delta.
|
||||
- `IFractalPayloadStore`: fake RAM/SSD payload availability.
|
||||
- `IFractalGpuBudget`: fake GPU table/page budget.
|
||||
- `IFractalDebugSink`: captures telemetry without UI.
|
||||
|
||||
Mocks should sit at resource and observation boundaries, not inside algorithms.
|
||||
Mocking every helper is how tests become a puppet show for implementation
|
||||
details. Good tests feed the module its owned inputs and inspect its owned
|
||||
outputs.
|
||||
|
||||
### Shader Parity Tests
|
||||
|
||||
For GPU math that must match CPU decisions:
|
||||
|
||||
- shaped envelope weight;
|
||||
- cube-face projection used by shader sampling;
|
||||
- packet decoding;
|
||||
- selected-cut fade weights;
|
||||
- conservative SDF bound constants.
|
||||
|
||||
The CPU implementation is the spec. The HLSL implementation is the fast copy.
|
||||
When they disagree, the test should fail before the disagreement becomes a
|
||||
screenshot mystery.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Small but real:
|
||||
|
||||
- build one cube-face terrain grammar into current height brush backend;
|
||||
- render/debug one Zyphos planet frame headless;
|
||||
- render one body proxy with selected-cut packet fixtures;
|
||||
- verify debug modes expose score, uncertainty, selected depth, residency, and
|
||||
cost.
|
||||
|
||||
### Performance Fixtures
|
||||
|
||||
Performance tests must be deterministic:
|
||||
|
||||
- fixed camera paths;
|
||||
- fixed world seed;
|
||||
- fixed update budget;
|
||||
- fixed payload budget;
|
||||
- fixed RNG seed;
|
||||
- output: CPU ms, GPU ms if available, resident payload count, update count,
|
||||
evictions, selected node count, score convergence.
|
||||
|
||||
Passing visual tests without performance fixtures is permission to grow a slow
|
||||
machine with a nice face. No.
|
||||
|
||||
## Ownership
|
||||
|
||||
### Client
|
||||
|
||||
The client owns semantic intent:
|
||||
|
||||
- planet/world seed and setting policy;
|
||||
- agent visual state and role/body grammar selection;
|
||||
- authored grammar parameters and state bindings;
|
||||
- stable domain ids for persistence and cache keys.
|
||||
|
||||
For Zyphos, this means planet style, biome rules, and simulation-facing terrain
|
||||
policy. For Epiphany, this means role/body visual state and semantic feature
|
||||
bindings. The engine should not learn client nouns just because the first
|
||||
grammar is interesting.
|
||||
|
||||
### Engine Contracts
|
||||
|
||||
Contracts own portable declarations:
|
||||
|
||||
- domain roots;
|
||||
- semantic brush claims;
|
||||
- grammar tree nodes;
|
||||
- node summary schema;
|
||||
- backend packet schema;
|
||||
- debug channel names.
|
||||
|
||||
Contracts must stay Vortice-free and renderer-agnostic. They describe the
|
||||
machine; D3D12 is one lowering target.
|
||||
|
||||
### Engine Fractal Core
|
||||
|
||||
The fractal core owns pure CPU algorithms:
|
||||
|
||||
- cube-sphere domain math;
|
||||
- grammar expansion;
|
||||
- ownership tree bounds;
|
||||
- summary generation;
|
||||
- contribution scoring;
|
||||
- stochastic estimator updates;
|
||||
- selected-cut and residency decisions;
|
||||
- backend packet planning.
|
||||
|
||||
This should be the main testable body of the system.
|
||||
|
||||
### Engine Renderer
|
||||
|
||||
The renderer owns:
|
||||
|
||||
- GPU buffers, textures, pages, and descriptors;
|
||||
- screen-space scoring when moved to compute;
|
||||
- backend packet upload;
|
||||
- render graph execution;
|
||||
- shader evaluation;
|
||||
- visual diagnostics.
|
||||
|
||||
## Algorithms
|
||||
|
||||
### 1. Cube-Sphere Surface Domain
|
||||
|
||||
Planetary surfaces use six quadtree cube faces projected to a sphere. This cuts
|
||||
lat-long pole singularities and gives each face a simple 2D tile domain.
|
||||
|
||||
Projection candidates:
|
||||
|
||||
- tangent warp: cheap, analytic inverse, acceptable area behavior;
|
||||
- fifth-order odd polynomial warp: strong GPU candidate, low area error;
|
||||
- COBE/QSC-style bivariate warp: best area behavior, more expensive;
|
||||
- exact equal-area reference: validation and offline bake target, not default
|
||||
hot path.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: tangent/polynomial are cheapest per sample; COBE buys area quality.
|
||||
- Memory: all use the same tile address space; no major residency difference.
|
||||
- CPU/GPU: CPU can use exact or COBE for authoring/bakes; GPU hot shaders can
|
||||
use tangent or polynomial if measured faster.
|
||||
|
||||
Decision rule: measure area error, inverse lookup cost, and shader time on the
|
||||
actual target hardware before choosing.
|
||||
|
||||
### 2. Semantic IFS Brush Grammar
|
||||
|
||||
The grammar emits claims, not final triangles or shader code.
|
||||
|
||||
```text
|
||||
claim {
|
||||
stableKey
|
||||
name
|
||||
kind
|
||||
domain
|
||||
localFrame
|
||||
envelope
|
||||
payload
|
||||
tags
|
||||
lodRange
|
||||
costTier
|
||||
seed
|
||||
children
|
||||
}
|
||||
```
|
||||
|
||||
Kinds:
|
||||
|
||||
- `material`
|
||||
- `height`
|
||||
- `displacement`
|
||||
- `sdf_form`
|
||||
- `void`
|
||||
- `mask`
|
||||
- `light`
|
||||
- `debug`
|
||||
|
||||
Required grammar operations:
|
||||
|
||||
- `Union`
|
||||
- `Subtract`
|
||||
- `Common`
|
||||
- `Repeat`
|
||||
- `Refine`
|
||||
- `Capture`
|
||||
- `Bind`
|
||||
|
||||
VibeGeometry's useful lesson is the split between semantic claims and CSG
|
||||
adjudication. Aquarium should keep that split even when the current renderer
|
||||
only supports a flattened evaluator. The authored tree remains durable; the
|
||||
flattened packets are expendable.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: semantic grammar costs CPU work up front but lets runtime avoid global
|
||||
interpretation in hot shaders.
|
||||
- Memory: preserving tags, frames, and captures costs RAM, but buys cache keys,
|
||||
debugging, and selective invalidation.
|
||||
- SSD: authored grammar is compact; generated payloads and summaries can be
|
||||
cached separately.
|
||||
- GPU: shaders consume fixed compact packets instead of walking prose-shaped
|
||||
abstractions.
|
||||
|
||||
### 3. Ownership Tree
|
||||
|
||||
The ownership tree is a lightweight CSG/IFS program:
|
||||
|
||||
```text
|
||||
Rule tree
|
||||
-> semantic claims
|
||||
-> boolean/IFS ownership tree
|
||||
-> node summaries
|
||||
-> backend lowering
|
||||
```
|
||||
|
||||
It is not a full CAD kernel in the first version. It needs:
|
||||
|
||||
- stable node ids;
|
||||
- parent/child topology;
|
||||
- branch operation;
|
||||
- child ranges;
|
||||
- node bounds;
|
||||
- summary error;
|
||||
- payload handles;
|
||||
- debug labels.
|
||||
|
||||
Bounds follow operation semantics:
|
||||
|
||||
- union bounds are child-bound unions;
|
||||
- subtraction bounds are source/left bounds;
|
||||
- common/intersection bounds are child-bound intersections;
|
||||
- repeat/refine bounds are generated or conservative aggregate bounds.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: tree bounds stop traversal before child work.
|
||||
- Memory: summaries are kept resident even when children are evicted.
|
||||
- SSD: child payloads can stream by stable key.
|
||||
- GPU: renderer receives selected cuts and compact resident node tables.
|
||||
|
||||
### 4. Shaped Brush Envelope
|
||||
|
||||
Every brush has a compact-support anisotropic envelope. It borrows the useful
|
||||
part of Gaussian splatting: covariance-like shape and projected footprint. It
|
||||
does not inherit infinite support as a default tax.
|
||||
|
||||
Hot evaluator:
|
||||
|
||||
```text
|
||||
q = transpose(rotation) * (p - center)
|
||||
m = dot(q * invRadii, q * invRadii)
|
||||
support = saturate(1 - m)
|
||||
weight = support * support * (3 - 2 * support)
|
||||
```
|
||||
|
||||
Shape controls can add taper, skew, ridge, hollow, signed lobe, or curve-local
|
||||
response, but every control must preserve an explicit conservative bound.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: compact support enables culling and fixed packet limits.
|
||||
- Memory: envelope rows are small and SIMD/GPU friendly.
|
||||
- GPU: projected ellipse scoring is better than sphere scoring when affordable;
|
||||
sphere/AABB fallback remains legal.
|
||||
- Fidelity: anisotropy gives terrain ridges, rivers, petals, scars, and veins
|
||||
fewer fake circular footprints.
|
||||
|
||||
### 5. Node Summaries
|
||||
|
||||
Every tree node needs a renderable summary:
|
||||
|
||||
```text
|
||||
stableKey
|
||||
parent
|
||||
childRange
|
||||
domain
|
||||
localFrame
|
||||
worldOrTileBound
|
||||
envelopeBound
|
||||
maxFormError
|
||||
maxMaterialDelta
|
||||
normalOrGradientBound
|
||||
coverageBound
|
||||
frequencyBand
|
||||
estimatedCost
|
||||
summaryPayloadHandle
|
||||
childPayloadHandle
|
||||
```
|
||||
|
||||
The summary is both fallback and promise. If a node cannot summarize its
|
||||
children with bounded error, it is not ready to be an LOD node.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: summaries stop traversal and keep missing children renderable.
|
||||
- Memory: summaries stay hot; child payloads can be evicted.
|
||||
- SSD: summaries form the streaming index.
|
||||
- GPU: fixed summary buffers feed scoring and debug views.
|
||||
|
||||
### 6. Contribution Scoring
|
||||
|
||||
The first score is heuristic and conservative:
|
||||
|
||||
```text
|
||||
projectedRadiusPx = project_bound_radius(node.bound, camera)
|
||||
projectedErrorPx = project_world_error(node.maxFormError, node.bound, camera)
|
||||
materialScore = node.maxMaterialDelta * projectedCoveragePx
|
||||
formScore = projectedErrorPx * visibleCoverageEstimate
|
||||
score = max(formScore, materialScore) * importanceBias / estimatedCost
|
||||
```
|
||||
|
||||
LOD cut:
|
||||
|
||||
```text
|
||||
render summary if parent needed and node is good enough
|
||||
descend if node is not good enough, children are resident, and budget allows
|
||||
request children if node is not good enough and children are missing
|
||||
```
|
||||
|
||||
Fade:
|
||||
|
||||
- use hysteresis around the threshold;
|
||||
- fade payload amplitude, material response, or selected child contribution;
|
||||
- do not fade signed-distance safety.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: cuts avoid leaf scoring.
|
||||
- Memory: only selected subtrees need payload residency.
|
||||
- GPU: scoring can move progressively from CPU to compute.
|
||||
- Fidelity: error thresholds target visible pixel impact, not arbitrary depth.
|
||||
|
||||
### 7. Probabilistic Online Estimator
|
||||
|
||||
The contribution cache cannot refresh every branch every frame. It must update
|
||||
weights probabilistically under budget and converge.
|
||||
|
||||
Estimator state:
|
||||
|
||||
```text
|
||||
meanContribution
|
||||
varianceOrUncertainty
|
||||
sampleCount
|
||||
lastSampleFrame
|
||||
lastVisibleFrame
|
||||
confidence
|
||||
decayRate
|
||||
updateProbability
|
||||
```
|
||||
|
||||
Sampling priority:
|
||||
|
||||
```text
|
||||
priority =
|
||||
visibilityProbability
|
||||
* staleWeight
|
||||
* uncertainty
|
||||
* max(previousScore, parentScoreBias)
|
||||
/ max(estimatedUpdateCost, epsilon)
|
||||
```
|
||||
|
||||
Update:
|
||||
|
||||
```text
|
||||
delta = observedContribution - meanContribution
|
||||
meanContribution += alpha * delta
|
||||
variance = lerp(variance, delta * delta, beta)
|
||||
confidence = saturate(confidence + confidenceGain)
|
||||
```
|
||||
|
||||
Non-update:
|
||||
|
||||
```text
|
||||
confidence *= decay
|
||||
uncertainty += staleGrowth
|
||||
```
|
||||
|
||||
This is machine learning in the practical online-estimation sense. The first
|
||||
version should be an exponential moving average plus variance and bandit-style
|
||||
exploration. A later learned predictor may rank update priority and residency,
|
||||
but it must not own conservative SDF bounds, object visibility, or safety.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- CPU: stochastic scheduling keeps per-frame management bounded.
|
||||
- RAM: estimator state is a few scalars per summary.
|
||||
- SSD: probes and observed deltas can become offline training/evidence data.
|
||||
- GPU: high-quality probes can be sparse and amortized.
|
||||
- Fidelity: exploration prevents stale branches from staying confidently wrong.
|
||||
|
||||
### 8. Residency And Streaming
|
||||
|
||||
Stable key:
|
||||
|
||||
```text
|
||||
planet: cubeFace + quadtreePath + grammarPath
|
||||
body: bodyId + grammarPath
|
||||
curve: ownerId + curvePath + grammarPath
|
||||
```
|
||||
|
||||
Memory tiers:
|
||||
|
||||
- CPU hot: visible roots, node summaries, estimator state, selected cuts.
|
||||
- CPU warm/RAM: nearby summaries and recently used child payload metadata.
|
||||
- SSD: serialized child payload pages, baked summaries, probe history.
|
||||
- GPU hot: selected cut buffers, resident payload pages, tile/body packet tables.
|
||||
|
||||
Residency rules:
|
||||
|
||||
- summaries outlive child payloads;
|
||||
- parent summary renders while child payload streams;
|
||||
- evict low-score high-cost children first;
|
||||
- keep near-threshold children longer to avoid popping;
|
||||
- write payload pages append-only within a frame;
|
||||
- compact only at safe sync points.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: avoid global rebuilds and expensive compaction mid-frame.
|
||||
- Memory: child payloads are elastic; summaries are stable.
|
||||
- SSD: streaming is by stable semantic key, not transient allocation.
|
||||
- GPU: residency buffers stay compact and bounded.
|
||||
|
||||
### 9. Backend Lowering
|
||||
|
||||
One authored tree can lower into several backend shapes:
|
||||
|
||||
- 2D tile height/material brushes;
|
||||
- scalar height-field brush pass;
|
||||
- object-local SDF form packets;
|
||||
- anisotropic impostor/splat batches;
|
||||
- offline mip summaries;
|
||||
- debug overlays.
|
||||
|
||||
The first backend should be deliberately modest:
|
||||
|
||||
- shaped 2D terrain brushes on cube-face tiles;
|
||||
- CPU-side node summary and contribution cache;
|
||||
- fixed GPU structured buffers;
|
||||
- one Zyphos planet demo path;
|
||||
- one Epiphany/body proof path later.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- Time: start with the cheapest visible proof.
|
||||
- Memory: defer 3D recursive form until 2D summaries and scoring are real.
|
||||
- GPU: fixed tables are ugly but honest; graph-driven dynamic evaluation comes
|
||||
after debug views prove the cut.
|
||||
|
||||
## Resource Strategy
|
||||
|
||||
### CPU
|
||||
|
||||
CPU owns grammar expansion, coarse scoring, stochastic update scheduling,
|
||||
streaming requests, eviction, selected cuts, and debug collection. CPU must
|
||||
avoid per-leaf full traversal, synchronous SSD waits, and rebuilding payload
|
||||
pages during rendering.
|
||||
|
||||
### RAM
|
||||
|
||||
RAM owns node summaries, estimator state, warm payload metadata, recent probe
|
||||
results, and selected cuts. RAM should not keep all recursive child payloads
|
||||
resident.
|
||||
|
||||
### SSD
|
||||
|
||||
SSD owns serialized payload pages, offline baked summaries, probe/evidence
|
||||
datasets, and optional learned-predictor training data. SSD is never on the
|
||||
critical path for a missing child this frame; the parent summary renders.
|
||||
|
||||
### GPU
|
||||
|
||||
GPU owns selected-cut evaluation, brush envelope evaluation, height/material
|
||||
passes, SDF proxy evaluation, optional compute scoring, and debug visualization.
|
||||
GPU must not walk the whole grammar tree or trust learned scores for
|
||||
conservative distance safety.
|
||||
|
||||
## Data Structures
|
||||
|
||||
### `AquariumFractalDomain`
|
||||
|
||||
```text
|
||||
domainId
|
||||
kind
|
||||
rootKey
|
||||
rootFrame
|
||||
projection
|
||||
bound
|
||||
clientOwner
|
||||
```
|
||||
|
||||
### `AquariumBrushClaim`
|
||||
|
||||
```text
|
||||
stableKey
|
||||
kind
|
||||
domainId
|
||||
localFrame
|
||||
envelope
|
||||
payload
|
||||
tags
|
||||
lodRange
|
||||
costTier
|
||||
seed
|
||||
```
|
||||
|
||||
### `AquariumFractalNode`
|
||||
|
||||
```text
|
||||
stableKey
|
||||
operation
|
||||
claimRange
|
||||
childRange
|
||||
summary
|
||||
debugName
|
||||
```
|
||||
|
||||
### `AquariumFractalSummary`
|
||||
|
||||
```text
|
||||
bound
|
||||
maxFormError
|
||||
maxMaterialDelta
|
||||
normalOrGradientBound
|
||||
coverageBound
|
||||
estimatedCost
|
||||
summaryPayload
|
||||
childPayload
|
||||
```
|
||||
|
||||
### `AquariumContributionState`
|
||||
|
||||
```text
|
||||
mean
|
||||
variance
|
||||
confidence
|
||||
sampleCount
|
||||
lastSampleFrame
|
||||
lastVisibleFrame
|
||||
updateProbability
|
||||
residentState
|
||||
fade
|
||||
```
|
||||
|
||||
### `AquariumSelectedCut`
|
||||
|
||||
```text
|
||||
domainId
|
||||
nodeKey
|
||||
lodDepth
|
||||
fade
|
||||
payloadHandle
|
||||
debugScore
|
||||
```
|
||||
|
||||
## Render Frame Flow
|
||||
|
||||
```text
|
||||
1. Collect visible domains from camera, cube faces, and SDF proxy bounds.
|
||||
2. Load resident summaries for visible roots.
|
||||
3. Score summaries against the camera and previous history.
|
||||
4. Select a hierarchy cut under CPU/GPU budget.
|
||||
5. Queue missing child payloads; render parent summaries meanwhile.
|
||||
6. Sample a probabilistic subset of node weights for estimator convergence.
|
||||
7. Upload selected cut and resident payload tables.
|
||||
8. Run tile/body backend passes.
|
||||
9. Resolve fade transitions and temporal history.
|
||||
10. Emit debug surfaces and cache telemetry.
|
||||
```
|
||||
|
||||
## Debug Views
|
||||
|
||||
No recursive detail without these:
|
||||
|
||||
- cube face / tile id;
|
||||
- selected LOD depth;
|
||||
- projected error;
|
||||
- mean contribution;
|
||||
- uncertainty;
|
||||
- sample age;
|
||||
- update probability;
|
||||
- residency state;
|
||||
- fade weight;
|
||||
- estimated cost;
|
||||
- selected backend;
|
||||
- SDF step count where form detail is active.
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 0: Architecture Freeze
|
||||
|
||||
Deliverables:
|
||||
|
||||
- this plan;
|
||||
- updated research note;
|
||||
- state memory/map entries.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- the live architecture can be explained without hidden compensators;
|
||||
- module boundaries and test seams are named;
|
||||
- roadmap names what gets cut if complexity rises.
|
||||
|
||||
### Phase 1: Cube-Sphere Projection Harness
|
||||
|
||||
Build:
|
||||
|
||||
- generic cube-face address type;
|
||||
- forward/inverse projection candidates;
|
||||
- area distortion test harness;
|
||||
- GPU shader microbench for candidate mappings;
|
||||
- Zyphos debug view for face/tile coordinates.
|
||||
|
||||
Tests:
|
||||
|
||||
- projection round trips;
|
||||
- face-edge continuity cases;
|
||||
- sampled area distortion baselines;
|
||||
- shader/CPU projection parity.
|
||||
|
||||
Cut line:
|
||||
|
||||
- if projection research blocks rendering, ship tangent first behind an
|
||||
interface and keep the measured comparison harness.
|
||||
|
||||
### Phase 2: Shaped Brush Envelope
|
||||
|
||||
Build:
|
||||
|
||||
- shared CPU/GPU envelope struct;
|
||||
- compact-support anisotropic evaluator in HLSL;
|
||||
- CPU mirror evaluator for tests;
|
||||
- replacement path for current circular height brushes;
|
||||
- debug view for brush bounds and weight.
|
||||
|
||||
Tests:
|
||||
|
||||
- CPU/GPU evaluator parity;
|
||||
- support bound rejection;
|
||||
- deterministic packet serialization;
|
||||
- side-by-side old/new brush fixture.
|
||||
|
||||
Cut line:
|
||||
|
||||
- start with rotation plus radii and falloff only. Taper/skew/ridge wait.
|
||||
|
||||
### Phase 3: Semantic Claims And Ownership Tree
|
||||
|
||||
Build:
|
||||
|
||||
- contract structs for domains, claims, nodes, summaries;
|
||||
- small in-memory grammar builder;
|
||||
- union/subtract/common/repeat/refine/capture/bind operations;
|
||||
- flattening compiler to current brush tables;
|
||||
- debug dump of authored tree and flattened backend packets.
|
||||
|
||||
Tests:
|
||||
|
||||
- seeded grammar determinism;
|
||||
- stable keys across reload;
|
||||
- operation bounds for union/subtract/common;
|
||||
- flattening golden packets;
|
||||
- client adapter does not reference renderer types.
|
||||
|
||||
Cut line:
|
||||
|
||||
- do not implement full CAD CSG. Keep boolean ownership semantic and bounded.
|
||||
|
||||
### Phase 4: Node Summaries
|
||||
|
||||
Build:
|
||||
|
||||
- summary generation for 2D terrain brush trees;
|
||||
- max error/material delta fields;
|
||||
- parent fallback payload;
|
||||
- serialization format for summaries;
|
||||
- debug view for summary bounds and error.
|
||||
|
||||
Tests:
|
||||
|
||||
- summary conservativeness against generated children;
|
||||
- parent fallback when children are missing;
|
||||
- summary serialization round trips;
|
||||
- evicted children do not remove fallback.
|
||||
|
||||
Cut line:
|
||||
|
||||
- if exact summary generation is hard, use conservative amplitude/frequency
|
||||
bounds first. Ugly but safe beats precise lies.
|
||||
|
||||
### Phase 5: Contribution Cache
|
||||
|
||||
Build:
|
||||
|
||||
- stable-keyed contribution table;
|
||||
- projected error scorer;
|
||||
- selected-cut builder;
|
||||
- hysteresis fade weights;
|
||||
- debug views for score, depth, fade, cost.
|
||||
|
||||
Tests:
|
||||
|
||||
- distance monotonicity;
|
||||
- stable cut under small camera motion;
|
||||
- budget clamp behavior;
|
||||
- fade never changes conservative SDF bounds.
|
||||
|
||||
Cut line:
|
||||
|
||||
- keep scoring on CPU until the data layout is stable.
|
||||
|
||||
### Phase 6: Probabilistic Online Updates
|
||||
|
||||
Build:
|
||||
|
||||
- estimator state per node;
|
||||
- stochastic update scheduler;
|
||||
- exploration budget;
|
||||
- confidence decay;
|
||||
- high-quality probe hook;
|
||||
- telemetry for convergence.
|
||||
|
||||
Tests:
|
||||
|
||||
- deterministic RNG schedule fixture;
|
||||
- visible and near-threshold nodes have nonzero update probability;
|
||||
- stale wrong score recovery;
|
||||
- convergence under fixed camera path;
|
||||
- update cost respects budget.
|
||||
|
||||
Cut line:
|
||||
|
||||
- no neural predictor until EMA/variance/bandit scheduling fails with evidence.
|
||||
|
||||
### Phase 7: Residency And Streaming
|
||||
|
||||
Build:
|
||||
|
||||
- CPU warm cache;
|
||||
- GPU payload pages;
|
||||
- SSD-backed payload serialization;
|
||||
- child request queue;
|
||||
- eviction policy;
|
||||
- safe compaction points.
|
||||
|
||||
Tests:
|
||||
|
||||
- fake payload store miss renders parent summary;
|
||||
- request queue ordering;
|
||||
- eviction by score/cost;
|
||||
- frame path never blocks on fake SSD.
|
||||
|
||||
Cut line:
|
||||
|
||||
- first version can keep payloads generated in RAM and simulate SSD requests.
|
||||
The API must still be page/key based.
|
||||
|
||||
### Phase 8: Zyphos Planet Integration
|
||||
|
||||
Build:
|
||||
|
||||
- cube-sphere terrain domain in Zyphos;
|
||||
- first terrain grammar: continents/ridges/craters/material bands;
|
||||
- selected-cut-driven tile payloads;
|
||||
- atmospheric/planet shader sampling from tile domain.
|
||||
|
||||
Tests:
|
||||
|
||||
- planet renders across face edges;
|
||||
- debug views identify face/tile/LOD/cache state;
|
||||
- old spherical-noise path remains only as baseline fixture or is removed.
|
||||
|
||||
Cut line:
|
||||
|
||||
- if 3D body grammar tempts us here, cut it. Planet terrain proves the address,
|
||||
envelope, summary, and LOD cache first.
|
||||
|
||||
### Phase 9: Agent Body Grammar Pilot
|
||||
|
||||
Build:
|
||||
|
||||
- one role body grammar using semantic claims;
|
||||
- object-local summary tree;
|
||||
- material/detail brush payloads before traced form recursion;
|
||||
- SDF proxy integration through selected cut.
|
||||
|
||||
Tests:
|
||||
|
||||
- silhouette coherent across LODs;
|
||||
- fine detail fades by contribution;
|
||||
- SDF step count stays inside budget;
|
||||
- no scene-global mega-SDF.
|
||||
|
||||
Cut line:
|
||||
|
||||
- if form recursion is unstable, keep recursive detail in material payloads and
|
||||
leave traced form as coarse base SDF.
|
||||
|
||||
### Phase 10: Learned Scoring Research Gate
|
||||
|
||||
Build only after telemetry exists:
|
||||
|
||||
- probe dataset writer;
|
||||
- offline analysis of predicted vs observed contribution;
|
||||
- small learned predictor or calibrated regression;
|
||||
- A/B mode against heuristic scheduler.
|
||||
|
||||
Tests:
|
||||
|
||||
- held-out camera paths;
|
||||
- learned mode beats heuristic fidelity/cost clearly;
|
||||
- conservative summaries still own safety;
|
||||
- fallback heuristic remains available.
|
||||
|
||||
Cut line:
|
||||
|
||||
- if learned scoring cannot beat the estimator clearly, delete it.
|
||||
|
||||
## Current First Slice
|
||||
|
||||
The first implementation slice should be:
|
||||
|
||||
```text
|
||||
cube-face address + projection harness
|
||||
-> shaped 2D brush envelope
|
||||
-> semantic claim/tree structs
|
||||
-> flatten to current height brush pass
|
||||
-> debug face/tile/brush/score overlays
|
||||
```
|
||||
|
||||
This gives a coherent foundation without forcing the whole fractal object system
|
||||
to exist at once. The machine earns recursion only after address, envelope,
|
||||
summary, tests, and debug surfaces are real.
|
||||
|
||||
## Sources
|
||||
|
||||
This plan consolidates:
|
||||
|
||||
- `research/rendering/cube-sphere-fractal-brushes.md`
|
||||
- local VibeGeometry `vg_grammar` and `vg_csg` tree work;
|
||||
- QSC / cube-sphere projection research;
|
||||
- EWA and 3D Gaussian splatting envelope ideas;
|
||||
- Hart sphere tracing;
|
||||
- geometry clipmaps;
|
||||
- Nanite-style projected-error hierarchy cuts;
|
||||
- Octree-GS / LOD-structured Gaussian splatting;
|
||||
- Aquarium's existing SDF proxy, height brush, and debug-pass constraints.
|
||||
|
|
@ -1,597 +0,0 @@
|
|||
# Fractal Brush Implementation Roadmap
|
||||
|
||||
This is the execution map for the fractal brush architecture in
|
||||
`research/rendering/fractal-brush-architecture-plan.md`.
|
||||
|
||||
The architecture is allowed to be ambitious. This roadmap is not. It exists to
|
||||
keep the work shippable, testable, and cuttable.
|
||||
|
||||
## Objective
|
||||
|
||||
Build a reusable mapped fractal field system for planetary terrain, tileable
|
||||
surface detail, and later agent/body detail.
|
||||
|
||||
The first live proof is deliberately narrow:
|
||||
|
||||
```text
|
||||
cube-face address + projection harness
|
||||
-> shaped 2D brush envelope
|
||||
-> semantic claim/tree structs
|
||||
-> flatten to current height brush pass
|
||||
-> debug face/tile/brush/score overlays
|
||||
```
|
||||
|
||||
No 3D recursive body form. No learned predictor. No streaming page store. The
|
||||
first pass proves address, envelope, semantic tree, flattening, tests, and
|
||||
debug visibility.
|
||||
|
||||
## Standing Invariants
|
||||
|
||||
- The authored semantic tree is the source of truth.
|
||||
- Backend packets are compiled output and may be replaced.
|
||||
- Every domain, node, claim, and payload has a stable key.
|
||||
- Every LOD subtree has a conservative parent summary.
|
||||
- The cache may learn priority; it may not own field safety.
|
||||
- Missing children render through parent summaries instead of stalling.
|
||||
- Debug views are required before recursive detail density increases.
|
||||
- Each module owns one invariant and exposes a mockable seam.
|
||||
|
||||
## Module Targets
|
||||
|
||||
### `Aquarium.Engine.Contracts`
|
||||
|
||||
Owns portable DTOs:
|
||||
|
||||
- `AquariumFractalDomain`
|
||||
- `AquariumBrushClaim`
|
||||
- `AquariumFractalNode`
|
||||
- `AquariumFractalSummary`
|
||||
- `AquariumContributionState`
|
||||
- `AquariumSelectedCut`
|
||||
- backend packet rows
|
||||
- debug channel rows
|
||||
|
||||
Tests:
|
||||
|
||||
- serialization round trips;
|
||||
- stable key equality;
|
||||
- no renderer/Vortice dependency.
|
||||
|
||||
### `Aquarium.Engine.Fractal`
|
||||
|
||||
Owns pure CPU algorithms:
|
||||
|
||||
- cube-face addressing;
|
||||
- projection candidates;
|
||||
- local frame/domain math;
|
||||
- grammar expansion;
|
||||
- ownership tree bounds;
|
||||
- summary building;
|
||||
- contribution scoring;
|
||||
- stochastic estimator;
|
||||
- selected-cut planning;
|
||||
- residency decisions.
|
||||
|
||||
Tests:
|
||||
|
||||
- regular unit tests with deterministic fakes;
|
||||
- no D3D12 boot required.
|
||||
|
||||
### `Aquarium.Engine.Render`
|
||||
|
||||
Owns D3D12 lowering:
|
||||
|
||||
- GPU buffers;
|
||||
- descriptor binding;
|
||||
- shader packet decode;
|
||||
- height/material/Form/Appearance/Transport backend passes;
|
||||
- debug visualizations.
|
||||
|
||||
Tests:
|
||||
|
||||
- packet golden tests;
|
||||
- CPU/HLSL parity tests;
|
||||
- headless smoke renders.
|
||||
|
||||
### Client Adapters
|
||||
|
||||
`Aquarium.Zyphos` owns planet domain roots and terrain grammar policy.
|
||||
|
||||
`Aquarium.Epiphany` later owns agent/body grammar selection and semantic state
|
||||
bindings.
|
||||
|
||||
Tests:
|
||||
|
||||
- clients emit contracts without renderer types;
|
||||
- stable seeds emit stable keys.
|
||||
|
||||
## Mock Boundaries
|
||||
|
||||
Add these before the modules need them:
|
||||
|
||||
- `IFractalClock`: deterministic frame/time.
|
||||
- `IFractalRandom`: deterministic update lottery.
|
||||
- `IProjection`: projection candidate swap.
|
||||
- `IContributionProbe`: observed pixel/form/appearance/transport delta.
|
||||
- `IFractalPayloadStore`: fake RAM/SSD payload availability.
|
||||
- `IFractalGpuBudget`: fake GPU table/page limits.
|
||||
- `IFractalDebugSink`: telemetry capture without UI.
|
||||
|
||||
Mock resources and observations, not private helper functions. The tests should
|
||||
pin module contracts, not puppet implementation details.
|
||||
|
||||
## Phase 0: Test And Project Skeleton
|
||||
|
||||
Goal: create the module/testing skeleton before algorithm code spreads.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add `src/Aquarium.Engine.Fractal`.
|
||||
- [x] Add `tests/Aquarium.Engine.Fractal.Tests`.
|
||||
- [x] Add project references from tests to contracts/fractal core.
|
||||
- [x] Add deterministic fake clock/random/debug sink test helpers.
|
||||
- [x] Add a boundary test proving `Aquarium.Engine.Fractal` has no D3D12/Vortice dependency.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- `dotnet build Aquarium.Engine.sln`
|
||||
- `dotnet test tests/Aquarium.Engine.Fractal.Tests`
|
||||
|
||||
Cut line:
|
||||
|
||||
- If module wiring fights the existing solution, keep the new core smaller, not
|
||||
sneak fractal logic into `D3D12Renderer`.
|
||||
|
||||
## Phase 1: Cube-Face Address And Projection Harness
|
||||
|
||||
Goal: establish stable planetary tile addresses and measured projection choices.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add `CubeFace`, `CubeTileKey`, and quadtree path helpers.
|
||||
- [x] Add local tile uv to cube-face coordinate conversion.
|
||||
- [x] Implement identity-normalize projection as baseline.
|
||||
- [x] Implement tangent projection candidate.
|
||||
- [ ] Implement fifth-order odd polynomial projection candidate.
|
||||
- [ ] Add placeholder/interface for COBE/QSC candidate.
|
||||
- [x] Add area distortion sampler.
|
||||
- [x] Add face-edge continuity tests.
|
||||
- [x] Add CPU benchmark or fixture reporting candidate costs.
|
||||
- [ ] Add Zyphos debug mode that colors face/tile coordinates.
|
||||
|
||||
Tests:
|
||||
|
||||
- projection round trips where inverse exists;
|
||||
- face-edge continuity cases;
|
||||
- deterministic tile key formatting;
|
||||
- sampled area error fixtures.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- projection report names current default and measured error/cost;
|
||||
- Zyphos can show face/tile debug colors.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Ship tangent projection first if polynomial/COBE cost delays visible work.
|
||||
Keep the harness so the choice stays measurable.
|
||||
|
||||
## Phase 2: Shaped 2D Brush Envelope
|
||||
|
||||
Goal: replace circular brush thinking with compact-support anisotropic fields.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add CPU `FractalBrushEnvelope2D`.
|
||||
- [x] Add HLSL packed envelope row.
|
||||
- [x] Implement compact-support ellipse evaluator.
|
||||
- [x] Add CPU/HLSL parity fixture.
|
||||
- [x] Add brush bounds and support rejection tests.
|
||||
- [x] Add a small adapter that lowers one claim into the current height brush path.
|
||||
- [ ] Add debug overlay for envelope bounds and weight.
|
||||
|
||||
Tests:
|
||||
|
||||
- weight at center/edge/outside support;
|
||||
- rotation parity;
|
||||
- deterministic packet packing;
|
||||
- old circular brush fixture still reproducible through a round envelope.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- current height pass can render shaped 2D envelopes through a compatibility adapter.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Do not add taper/skew/ridge controls until the basic envelope has debug views
|
||||
and parity tests.
|
||||
|
||||
## Phase 3: Semantic Claims And Ownership Tree
|
||||
|
||||
Goal: preserve authoring meaning before backend flattening.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add contract DTOs for domains, claims, nodes, summaries, and selected cuts.
|
||||
- [x] Add stable key builder for planet tile + grammar path.
|
||||
- [x] Add in-memory ownership tree builder.
|
||||
- [x] Implement operations: `Union`, `Subtract`, `Common`, `Repeat`, `Refine`, `Capture`, `Bind`.
|
||||
- [ ] Implement operation bounds.
|
||||
- [x] Add flattening compiler to shaped 2D brush packets.
|
||||
- [x] Add debug dump for authored tree and flattened packets.
|
||||
- [x] Preserve multiple `.aquageo` tile roots in the ownership tree instead of
|
||||
collapsing every claim under the final tile.
|
||||
- [x] Add compact `flame` grammar primitive for deterministic curled recursive
|
||||
brush emission.
|
||||
|
||||
Tests:
|
||||
|
||||
- [x] seeded grammar determinism;
|
||||
- stable keys across equivalent expansion;
|
||||
- union/subtract/common bounds;
|
||||
- flattening golden packets;
|
||||
- client contracts contain no renderer type.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- one toy terrain grammar emits semantic claims, an ownership tree, and flattened brush packets.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Do not build a CAD CSG kernel. This tree adjudicates semantic ownership and
|
||||
lowering, not arbitrary mesh booleans.
|
||||
|
||||
## Phase 4: Node Summaries
|
||||
|
||||
Goal: make every LOD subtree renderable without its children.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add summary builder for 2D brush tree nodes.
|
||||
- [x] Track max height/form error.
|
||||
- [x] Track max material delta placeholder.
|
||||
- [x] Track coverage and estimated cost.
|
||||
- [ ] Add parent fallback payload.
|
||||
- [ ] Add summary serialization.
|
||||
- [ ] Add debug overlay for summary bounds/error.
|
||||
|
||||
Tests:
|
||||
|
||||
- parent summary conservatively covers child height contribution;
|
||||
- child eviction leaves parent renderable;
|
||||
- summary round trip preserves error bounds;
|
||||
- summary cost monotonically covers descendants.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- selected parent summary can render when children are absent.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Use conservative amplitude/frequency bounds before clever exact summaries.
|
||||
|
||||
## Phase 5: Contribution Cache And Selected Cut
|
||||
|
||||
Goal: choose a stable hierarchy cut without scoring every leaf.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add stable-keyed contribution table.
|
||||
- [x] Add projected error scorer.
|
||||
- [x] Add selected-cut builder.
|
||||
- [x] Add hysteresis fade weights.
|
||||
- [x] Add budget clamp.
|
||||
- [x] Add debug channels: score, depth, fade, estimated cost.
|
||||
- [x] Add explicit resource budget plan for CPU update count, GPU estimated
|
||||
packet cost, RAM residency, and SSD request pressure.
|
||||
|
||||
Tests:
|
||||
|
||||
- [x] score decreases with distance for fixed error;
|
||||
- [x] score increases with error for fixed distance;
|
||||
- small camera motion keeps cut stable;
|
||||
- [x] budget clamp chooses parent summaries;
|
||||
- fade never changes conservative SDF/height safety fields.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- toy terrain grammar selects parent at distance and children nearby.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Keep scoring on CPU until data layout and debug channels are stable.
|
||||
|
||||
## Phase 6: Probabilistic Online Updates
|
||||
|
||||
Goal: refresh contribution estimates under budget and converge without
|
||||
pretending an online smoother is ReSTIR.
|
||||
|
||||
Research authority:
|
||||
|
||||
- ReSTIR DI: candidate generation plus temporal and spatial reservoir reuse for
|
||||
real-time direct lighting.
|
||||
https://research.nvidia.com/labs/rtr/publication/bitterli2020spatiotemporal/
|
||||
- ReSTIR GI: path reservoirs for indirect lighting, with final shading fed by a
|
||||
selected reservoir sample.
|
||||
https://research.nvidia.com/publication/2021-06_restir-gi-path-resampling-real-time-path-tracing
|
||||
- GRIS: the generalized theory for correlated samples, unknown PDFs, varied
|
||||
domains, and shift mappings. This is the fractal/SDF/sensor-fusion door.
|
||||
https://research.nvidia.com/labs/rtr/publication/lin2022generalized/
|
||||
- RTXDI integration docs: initial sampling, temporal reprojection, spatial
|
||||
reuse, validation, shift mapping, reservoir buffers, and denoiser guide
|
||||
buffers.
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirGI.md
|
||||
https://github.com/NVIDIA-RTX/RTXDI/blob/main/Doc/RestirPT.md
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add pure `ResampledImportanceReservoir<TSample>` core with selected
|
||||
sample, target, weight sum, represented candidate count, merge, and
|
||||
contribution weight.
|
||||
- [x] Add estimator state: mean, variance/uncertainty, confidence, sample count, sample age.
|
||||
- [x] Add stochastic scheduler with deterministic random source.
|
||||
- [x] Add exploration budget.
|
||||
- [x] Add live contribution cache that observes only scheduled nodes each frame
|
||||
and feeds the next resource plan.
|
||||
- [x] Promote contribution state into `FractalOccupancyGraph` with visibility,
|
||||
update probability, confidence decay, and stale uncertainty growth.
|
||||
- [x] Convert contribution-cache refresh into a candidate generator that submits
|
||||
weighted candidates to the resampled-importance core.
|
||||
- [x] Add fractal SDF probe candidate records: domain key, local frame, bound,
|
||||
projected contribution target, source PDF, payload handle, and material delta.
|
||||
- [x] Add structural probe generator that lowers IFS node summaries into
|
||||
`FractalProbeSample` reservoir candidates.
|
||||
- [x] Add selected-cut structural probe reservoir and expose it through Zyphos'
|
||||
fractal render plan/debug dump.
|
||||
- [ ] Add temporal reuse validation: camera motion, domain ancestry, local-frame
|
||||
error, bounds, and disocclusion/material compatibility. Domain ancestry and
|
||||
local-shift checks exist; camera/disocclusion/material validation still need
|
||||
renderer inputs.
|
||||
- [x] Add CPU temporal reuse validation contract with camera motion,
|
||||
disocclusion, material mismatch, visibility, domain ancestry, local-frame
|
||||
error, and bound rejection reasons.
|
||||
- [ ] Add spatial reuse validation across screen tiles and cube-sphere neighbor
|
||||
domains.
|
||||
- [ ] Feed reservoir confidence, selected target, candidate count, sample age,
|
||||
and invalidation reason into TAA guide/history buffers. Current
|
||||
reservoir confidence, sample age, domain validity, and invalidation code now
|
||||
have explicit scene/history reservoir guide targets; selected target and
|
||||
candidate count still need producer-side packing once GPU reservoirs exist.
|
||||
- [x] Add confidence decay and stale uncertainty growth.
|
||||
- [ ] Add fake contribution probe.
|
||||
- [ ] Add convergence telemetry.
|
||||
- [ ] Add debug channels: uncertainty, sample age, update probability,
|
||||
reservoir weight sum, selected target, candidate count, reuse validity.
|
||||
|
||||
Tests:
|
||||
|
||||
- [x] reservoir accepts candidates proportional to target/source PDF;
|
||||
- [x] reservoir merge preserves represented candidate count and weight sum;
|
||||
- [x] invalid zero-weight candidates do not corrupt reservoir state;
|
||||
- [x] contribution cache exposes scheduled updates as reservoir candidates;
|
||||
- deterministic RNG fixture repeats schedule;
|
||||
- visible nodes keep nonzero update probability;
|
||||
- near-threshold nodes keep nonzero update probability;
|
||||
- stale wrong score recovers after observations;
|
||||
- fixed camera path converges within tolerance;
|
||||
- [x] temporal reuse rejects mismatched domain ancestry;
|
||||
- [x] spatial reuse rejects invalid local-frame shifts;
|
||||
- [x] temporal reuse rejects camera motion, disocclusion, material mismatch,
|
||||
and low visibility;
|
||||
- [x] TAA history confidence is modulated by current reservoir confidence;
|
||||
- [x] TAA history confidence drops when previous-frame reservoir/domain validity changes;
|
||||
- update cost respects budget.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- cache updates only a bounded subset per frame while selected cuts converge,
|
||||
and the selected detail path is driven by tested reservoir math rather than a
|
||||
frozen score table.
|
||||
|
||||
Cut line:
|
||||
|
||||
- No neural predictor until the ReSTIR/GRIS-shaped reservoir pipeline fails
|
||||
with captured telemetry. No consumer-owned temporal cache unless it clearly
|
||||
owns raw capture retention rather than resolved evidence.
|
||||
|
||||
## Phase 7: Residency Simulation
|
||||
|
||||
Goal: prove page/key semantics before real SSD/GPU streaming.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add renderer-agnostic surface page key/metadata contracts for height,
|
||||
2D signed-distance, material, and confidence pages.
|
||||
- [x] Add CPU surface page planner that lowers selected IFS cuts into stable
|
||||
page metadata.
|
||||
- [x] Add mockable surface page store boundary with resident/request/evict
|
||||
decisions under a byte budget.
|
||||
- [x] Add CPU surface page rasterizer for resident height, 2D signed-distance,
|
||||
material, and confidence pages.
|
||||
- [x] Add fake payload store with resident/missing states.
|
||||
- [x] Add child request queue.
|
||||
- [x] Add score/cost eviction policy.
|
||||
- [x] Add parent-summary fallback path.
|
||||
- [x] Clamp simulated RAM residency and SSD requests through the resource plan.
|
||||
- [x] Add debug channels: residency, requests, evictions.
|
||||
|
||||
Tests:
|
||||
|
||||
- [x] missing child renders parent summary;
|
||||
- [x] high-score missing child enqueues request;
|
||||
- [x] low-score high-cost payload evicts first;
|
||||
- [x] frame path never blocks on fake store.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- selected cut can tolerate missing children without frame stalls.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Simulate SSD first. Real async file IO waits until page semantics are boring.
|
||||
|
||||
## Phase 8: Zyphos Planet Proof
|
||||
|
||||
Goal: replace spherical terrain demo path with cube-sphere domain proof.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add Zyphos cube-sphere domain root.
|
||||
- [x] Add first tile grammar: ridges, craters, and material bands.
|
||||
- [x] Add first flame-style microdetail grammar line inside the Zyphos leaf
|
||||
domain.
|
||||
- [x] Lower selected cut to shaped 2D height/material brush packets.
|
||||
- [x] Feed planet shader from tile-local cube-face domains for post-hit
|
||||
material/detail evaluation.
|
||||
- [x] Route Zyphos render-plan construction through the resource budget planner.
|
||||
- [x] Update Zyphos contribution cache every render-plan request while reusing
|
||||
compiled brush packets for equivalent selected cuts.
|
||||
- [x] Feed traced planet geometry from a hard-budgeted top-influence subset of
|
||||
selected tile packets.
|
||||
- [x] Lower the structural probe reservoir into a first compact 3D SDF splat
|
||||
packet for inspection.
|
||||
- [x] Lower resident 2D SDF pages into bounded projected 3D splat packets
|
||||
through an explicit page-to-world mapping boundary.
|
||||
- [x] Add first face/tile/material influence debug view through render debug
|
||||
mode 11.
|
||||
- [ ] Add LOD/cache debug overlays.
|
||||
- [ ] Remove or quarantine old spherical noise path as baseline fixture.
|
||||
|
||||
Tests:
|
||||
|
||||
- face seams render continuously;
|
||||
- [x] tile keys remain stable under camera movement;
|
||||
- debug modes show selected depth and cache state;
|
||||
- headless Zyphos smoke render passes.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- Zyphos renders terrain from semantic cube-sphere tile grammar.
|
||||
|
||||
Cut line:
|
||||
|
||||
- Do not start agent-body recursive form here. Planet terrain proves the shared
|
||||
address, envelope, summary, and cache spine first.
|
||||
|
||||
## Phase 9: Agent/Body Pilot
|
||||
|
||||
Goal: prove the same grammar works for object-local detail.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Pick one role/body with a stable base SDF.
|
||||
- [ ] Add object-local domain root.
|
||||
- [ ] Add 2D material/detail claims before traced form recursion.
|
||||
- [ ] Add object-local summary tree.
|
||||
- [ ] Integrate selected cut with SDF proxy packet.
|
||||
- [ ] Add debug channels for body detail score and SDF cost.
|
||||
|
||||
Tests:
|
||||
|
||||
- silhouette stays coherent across LOD;
|
||||
- material/detail fades by contribution;
|
||||
- SDF step budget remains bounded;
|
||||
- no scene-global mega-SDF path appears.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- one body uses semantic detail grammar without violating proxy bounds.
|
||||
|
||||
Cut line:
|
||||
|
||||
- If form recursion destabilizes tracing, keep recursive detail in material
|
||||
payloads and leave traced form coarse.
|
||||
|
||||
## Phase 10: Learned Scoring Research Gate
|
||||
|
||||
Goal: only add learned scoring after the estimator produces data.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [ ] Add probe dataset writer.
|
||||
- [ ] Capture camera paths, predicted scores, observed deltas, and costs.
|
||||
- [ ] Analyze heuristic error offline.
|
||||
- [ ] Try calibrated regression or tiny predictor.
|
||||
- [ ] Add A/B mode against heuristic estimator.
|
||||
|
||||
Tests:
|
||||
|
||||
- held-out camera path comparison;
|
||||
- learned mode beats heuristic fidelity/cost clearly;
|
||||
- conservative summaries still own safety;
|
||||
- fallback heuristic remains available.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- learned scorer improves measured fidelity/cost without removing deterministic safety.
|
||||
|
||||
Cut line:
|
||||
|
||||
- If it does not beat the estimator clearly, delete it. No ornamental oracle.
|
||||
|
||||
## Phase 11: GPU-Resident Splat And Reservoir Receipt
|
||||
|
||||
Goal: prove hot fractal splat and reservoir population belongs on the GPU, not
|
||||
in managed object graphs.
|
||||
|
||||
Tasks:
|
||||
|
||||
- [x] Add packed GPU splat buffer contract with no per-splat strings or object
|
||||
ownership.
|
||||
- [x] Add native packed CPU buffer helper for the rare staging/readback paths
|
||||
that genuinely need C# memory.
|
||||
- [x] Add D3D12 compute receipt harness that writes millions of splats directly
|
||||
into a GPU-resident UAV.
|
||||
- [x] Add distinct GPU reservoir packet contracts for the first opaque slice:
|
||||
SDF envelopes, PBR material envelopes, and radiosity. These are backend packet
|
||||
names for Form, Appearance, and Transport, not the permanent conceptual
|
||||
architecture.
|
||||
- [x] Sample those three reservoir layers in independent compute passes under a
|
||||
stochastic per-frame update budget.
|
||||
- [x] Measure with GPU timestamp queries and read back only a tiny checksum
|
||||
sample.
|
||||
|
||||
Receipt:
|
||||
|
||||
- GTX 1070, `2,000,000` splats, `2,000,000` SDF/Form reservoirs,
|
||||
`2,000,000` PBR/Appearance reservoirs, and `2,000,000`
|
||||
radiosity/Transport reservoirs.
|
||||
- Packet sizes: `80` bytes/splat, `64` bytes/reservoir.
|
||||
- Budget: `50,000` updates per reservoir pass per frame, `2`
|
||||
candidates/update, full reservoir coverage in `40` frames.
|
||||
- GPU time: `4.471 ms/frame`.
|
||||
- Equivalent throughput: `223.7 FPS`, about `447.3M` splats/sec.
|
||||
- Command: `.\scripts\fractal-splat-receipt.ps1 -Splats 2000000 -Frames 120 -Candidates 2 -ReservoirUpdates 50000`.
|
||||
|
||||
Exit gate:
|
||||
|
||||
- Receipt must show plural-millions of GPU-resident splats and distinct
|
||||
reservoir layers populated or updated at hundreds of FPS equivalent on the
|
||||
target tired GTX 1070.
|
||||
|
||||
Cut line:
|
||||
|
||||
- This is compute-side population and cache-update throughput, not final shaded
|
||||
visibility. Do not sell it as renderer completion until the
|
||||
draw/raster/resolve path consumes the same packed buffers.
|
||||
- The next coherent packet evolution is density/extinction Form plus
|
||||
participating-medium Appearance/Transport. Do not force flames or sensor
|
||||
confidence volumes through solid SDF semantics just because the first packet
|
||||
names said SDF.
|
||||
|
||||
## First Work Packet
|
||||
|
||||
Start here:
|
||||
|
||||
1. Add `Aquarium.Engine.Fractal` and its test project.
|
||||
2. Add cube-face/tile key types.
|
||||
3. Add identity and tangent projection candidates.
|
||||
4. Add area distortion sampler and face-edge tests.
|
||||
5. Add a simple projection report fixture.
|
||||
|
||||
Definition of done:
|
||||
|
||||
- `dotnet build Aquarium.Engine.sln`
|
||||
- new fractal tests pass;
|
||||
- projection report is checked into test output or docs;
|
||||
- `state/evidence.jsonl` records the chosen first projection default and why.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Cube-Sphere Projection Report
|
||||
|
||||
Unit sphere area target per cube-coordinate area is `pi / 6`.
|
||||
Lower mean absolute relative area error is better for terrain density.
|
||||
|
||||
| Projection | Face | Samples | Avg area scale | Min rel area | Max rel area | Mean abs rel error | RMS rel error |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| normalize | PositiveX | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| normalize | NegativeX | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| normalize | PositiveY | 16 | 0.524151 | 0.417233 | 1.887723 | 0.315999 | 0.378463 |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
# Fractal Reference Oracles
|
||||
|
||||
Aquarium needs boring reference pressure before it earns pretty fractal claims.
|
||||
The renderer is not allowed to prove itself by looking interesting.
|
||||
|
||||
## Apophysis/FLAM3 Boundary
|
||||
|
||||
Apophysis 7x is the classic editor lineage for fractal flames, and the
|
||||
repository is GPL-2.0. FLAM3 is the older portable renderer/genome toolchain
|
||||
with the public `.flame`/`.flam3` ecosystem and documented variation list.
|
||||
|
||||
Use them like AquaSynth uses synth references:
|
||||
|
||||
- Reference fixtures live in tests or artifacts, not engine runtime.
|
||||
- GPL renderer/source stays external; Aquarium does not paste implementation
|
||||
code into the runtime.
|
||||
- Aquarium DSL candidates must reproduce reference behavior through numeric
|
||||
gates before GPU rendering claims are accepted.
|
||||
- Image parity receipts should be produced by an external reference renderer
|
||||
when a local FLAM3/Apophysis-compatible binary is available.
|
||||
|
||||
## First Gate
|
||||
|
||||
The first committed fixture is deliberately small:
|
||||
|
||||
- `tests/Aquarium.Engine.Fractal.Tests/Fixtures/Apophysis/linear-sierpinski.flame`
|
||||
- `tests/Aquarium.Engine.Fractal.Tests/Fixtures/Apophysis/linear-sierpinski.aquageo`
|
||||
|
||||
The `.flame` file uses three Apophysis/FLAM3-style linear xforms. The
|
||||
`.aquageo` file expresses the same affine IFS using `affineifs` and
|
||||
`affinemap`. `ApophysisReferenceParityTests` compares both through a fixed
|
||||
transform sequence and through chaos-game moment bounds.
|
||||
|
||||
This is not full flame parity. It is the first ruler on the desk.
|
||||
|
||||
## Next Gates
|
||||
|
||||
1. DONE: parse a minimal `.flame` subset into a renderer-agnostic flame
|
||||
definition: xform weight, color, affine coefficients, and `linear`,
|
||||
`spherical`, and `bubble` variation weights.
|
||||
2. DONE: add exact point parity for `linear`, `spherical`, and `bubble`,
|
||||
plus a deterministic histogram checksum for the mixed fixture.
|
||||
3. DONE: add an optional external FLAM3-compatible render receipt path. When
|
||||
`flam3-render` is available, Aquarium renders the fixture to binary PPM and
|
||||
records image-shape, non-black pixel count, RGB checksum, and luminance
|
||||
checksum under `artifacts/fractal-flame-reference`.
|
||||
4. DONE: lower proven flame definitions into GPU program rows for the receipt
|
||||
shader's `flame 2D affine/variation program` mode.
|
||||
5. Use those rows as a visual test scene only through GPU-resident reservoirs,
|
||||
not by restoring a CPU direct renderer.
|
||||
|
||||
## Local Receipt
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-flame-receipt.ps1
|
||||
```
|
||||
|
||||
Default receipt:
|
||||
|
||||
- fixture: `linear-spherical-bubble.flame`
|
||||
- samples: `8192`
|
||||
- histogram: `64x64`
|
||||
- bounds: `-8,-8,8,8`
|
||||
- seed: `0x0BADC0DE`
|
||||
- expected checksum: `0xAEB1C81B`
|
||||
|
||||
This receipt is Aquarium's local CPU parser/evaluator checksum. It is not yet
|
||||
an external FLAM3/Apophysis renderer receipt.
|
||||
|
||||
## External Renderer Receipt
|
||||
|
||||
FLAM3 stays outside the repo and outside runtime binaries. If a local
|
||||
`flam3-render` executable is installed, run:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-flame-reference-render.ps1 -Flam3Render C:\path\to\flam3-render.exe
|
||||
```
|
||||
|
||||
The script follows FLAM3's documented environment-variable interface: `in`
|
||||
selects the flame file, `out` selects the output image, `format=ppm` requests a
|
||||
plain reference image, `seed` fixes the random sequence, and `qs` controls
|
||||
quality scale. Aquarium then hashes the PPM through
|
||||
`tools/Aquarium.Fractal.Receipt --reference-ppm`.
|
||||
|
||||
This is the first external renderer receipt. It proves the fixture can be
|
||||
rendered by the reference family and gives Aquarium a stable image artifact to
|
||||
compare as the local flame subset grows.
|
||||
|
||||
## JWildfire / FARender Visual Reference
|
||||
|
||||
Aquarium now has a reproducible higher-quality visual reference lane that does
|
||||
not depend on tools already installed on the workstation:
|
||||
|
||||
```powershell
|
||||
.\scripts\install-flame-reference-tools.ps1
|
||||
.\scripts\fractal-flame-jwildfire-reference.ps1 `
|
||||
-Renderer FARender `
|
||||
-Width 1024 `
|
||||
-Height 1024 `
|
||||
-Quality 2000 `
|
||||
-NoDensityEstimation
|
||||
```
|
||||
|
||||
`install-flame-reference-tools.ps1` downloads JWildfire 9.00 and a local
|
||||
Temurin JRE into ignored `artifacts/reference-tools`. These are reference
|
||||
artifacts, not Aquarium runtime dependencies.
|
||||
|
||||
`fractal-flame-jwildfire-reference.ps1` prefers JWildfire's bundled FARender
|
||||
GPU path when available because it accepts an explicit output file and fails
|
||||
the script when no image is produced. The Java batch renderer remains available
|
||||
through `-Renderer JWildfireJava`, but local smoke showed that it can complete
|
||||
without writing an image into the batch directory because JWildfire's output
|
||||
folder is preference-driven. Silent reference success is not a reference.
|
||||
|
||||
Local GTX 1070 smoke receipt from 2026-05-20:
|
||||
|
||||
- renderer: JWildfire 9.00 bundled FARenderJWF
|
||||
- fixture: `artifacts/reference-tools/j-wildfire-9.00/lib/FARenderJWF/selftest.flame`
|
||||
- command: `1024x1024`, quality `2000`, CUDA, no density-estimation phase
|
||||
- time: `7.69 sec`
|
||||
- artifact:
|
||||
`artifacts/fractal-flame-jwildfire-reference/selftest-farender-20260520-233638.png`
|
||||
|
||||
The repository's tiny `linear-spherical-bubble.flame` fixture is still useful
|
||||
for Aquarium subset parity, but FARender crashed on it because the fixture is
|
||||
Apophysis-style minimal XML rather than full JWildfire `variationGroup` flame
|
||||
XML. Do not "fix" that by making the fixture bigger until the parser and DSL
|
||||
contract deliberately support the richer reference dialect.
|
||||
|
||||
## External Density Oracle
|
||||
|
||||
Aquarium's CPU flame oracle is no longer the only visual-parity target. The GPU
|
||||
receipt can compare Aquarium splat density against a binary PPM image rendered
|
||||
by an external renderer:
|
||||
|
||||
```powershell
|
||||
.\scripts\new-jwildfire-baby-fixture.ps1
|
||||
.\scripts\fractal-flame-jwildfire-reference.ps1 `
|
||||
-Renderer FARender `
|
||||
-Flame artifacts\fractal-flame-jwildfire-reference\julian-disc-baby.flame `
|
||||
-Width 768 `
|
||||
-Height 768 `
|
||||
-Quality 2000 `
|
||||
-OutputDirectory artifacts\fractal-flame-jwildfire-reference
|
||||
magick artifacts\fractal-flame-jwildfire-reference\julian-disc-baby-farender-*.png `
|
||||
-depth 8 artifacts\fractal-flame-jwildfire-reference\julian-disc-baby-farender.ppm
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-ProgramFlame artifacts\fractal-flame-jwildfire-reference\julian-disc-baby.flame `
|
||||
-Splats 2000000 `
|
||||
-WarmupSplatUpdates 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 8 `
|
||||
-Frames 10 `
|
||||
-Depth 8 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ReadbackSplats 1000000 `
|
||||
-ReferenceDensityPpm artifacts\fractal-flame-jwildfire-reference\julian-disc-baby-farender.ppm `
|
||||
-HistogramSize 256x256 `
|
||||
-HistogramBounds '-2,-2,2,2'
|
||||
```
|
||||
|
||||
`new-jwildfire-baby-fixture.ps1` derives a smaller, reproducible target from
|
||||
FARender's bundled `selftest.flame`, preserving the boilerplate FARender needs
|
||||
while reducing the nastier variation pressure: `jwf_julian_power` drops from
|
||||
`126` to `8`, `jwf_gaussian_blur` becomes `0`, and the `wfield_*` amount fields
|
||||
are set to zero. This is not a pristine flame design. It is a compatible rung
|
||||
on the ladder that FARender actually renders.
|
||||
|
||||
First external-density receipt, local GTX 1070, 2026-05-21:
|
||||
|
||||
- reference renderer: JWildfire 9.00 bundled FARenderJWF
|
||||
- fixture: `artifacts/fractal-flame-jwildfire-reference/julian-disc-baby.flame`
|
||||
- reference render: `768x768`, quality `2000`, CUDA
|
||||
- Aquarium steady GPU time: `1.145 ms/frame`, `873.4 FPS` equivalent
|
||||
- external density score: `15.33%` in `256x256` bounds `-2,-2,2,2`
|
||||
- score sweep at `128x128`: best tested bound was `-4,-4,4,4` at `32.95%`
|
||||
- contact sheet:
|
||||
`artifacts/fractal-flame-jwildfire-reference/julian-disc-baby-external-contact.png`
|
||||
|
||||
The low external score is useful. Aquarium still scores about `89.94%` against
|
||||
its CPU interpretation of the same baby fixture in `128x128` bounds
|
||||
`-4,-4,4,4`, while FARender density disagrees hard. That means the next work is
|
||||
not "more samples"; it is dialect and camera semantics: FARender flame camera
|
||||
mapping, color/density filtering, chaos/xaos and weighting-field behavior, and
|
||||
variation exactness.
|
||||
|
||||
The first post-oracle correction found two concrete import/math lies:
|
||||
|
||||
- JWildfire `coefs` are packed for FARender's affine as `a b d e c f`; Aquarium
|
||||
had transposed the off-diagonal terms as `a d / b e`.
|
||||
- FARender's `jwf_disc` source uses `atan2(x, y) / PI`; Aquarium used
|
||||
`atan2(y, x) / PI`.
|
||||
|
||||
After fixing both, the same external-density receipt scored `26.87%` at
|
||||
`256x256` bounds `-2,-2,2,2`, up from `15.33%`.
|
||||
|
||||
The parity candidate no longer bins raw splat occupancy. That was the wrong
|
||||
organ. The GPU receipt now builds the Aquarium comparison image from the SDF
|
||||
envelope reservoir layer by stamping each populated envelope into a shaped
|
||||
density histogram. PBR and radiosity reservoirs stay separate; PBR will score
|
||||
surface appearance later, and radiosity is not part of this flame-density gate
|
||||
yet. With full SDF reservoir coverage on the local GTX 1070, the same
|
||||
`julian-disc-baby` receipt scored `22.14%` against the FARender PPM at
|
||||
`256x256` bounds `-2,-2,2,2`, with `42.053 ms/frame` while refreshing all
|
||||
2,000,000 reservoir rows in all three layers. The contact sheet lives at
|
||||
`artifacts/fractal-flame-jwildfire-reference/julian-disc-baby-sdf-density-contact.png`.
|
||||
|
||||
The next correction removed another dialect lie. JWildfire's
|
||||
`variationGroup normal="1"` is not the same thing as an explicit `linear`
|
||||
variation in FARender. A normal-only diagnostic renders as a constant point in
|
||||
FARender, while Aquarium had been adding affine linear geometry. The importer
|
||||
now ignores JWildfire `normal` as variation-group metadata; explicit `linear`
|
||||
still stays linear for Apophysis/FLAM3-style fixtures. The SDF-density receipt
|
||||
also gives subpixel envelopes a minimum one-bin reconstruction footprint so the
|
||||
comparison does not quietly collapse back into point occupancy.
|
||||
|
||||
After those fixes, the `julian-disc-baby` receipt scored `31.36%` against the
|
||||
FARender PPM at `256x256` bounds `-2,-2,2,2`, with `41.692 ms/frame` while
|
||||
refreshing all 2,000,000 reservoir rows in all three layers. Switching the
|
||||
receipt to FARender-style flame camera bounds (`size / scale / cam_zoom`) raised
|
||||
the same gate to `43.13%` with `68.01%` occupancy overlap; pass
|
||||
`-HistogramBounds flame-camera` through `scripts/fractal-splat-receipt.ps1` for
|
||||
that path. The contact sheet lives at
|
||||
`artifacts/fractal-flame-jwildfire-reference/julian-disc-baby-normal-cut-area-contact.png`.
|
||||
|
||||
Full reservoir refreshes must not use the stochastic hash indexer. When
|
||||
`ReservoirUpdatesPerPass == SplatCount`, the compute shader now updates row
|
||||
`updateIndex` directly for each reservoir layer. That keeps under-budget passes
|
||||
stochastic while making full coverage actually full and memory-coherent. The
|
||||
same `julian-disc-baby` full-refresh receipt stayed visually equivalent
|
||||
(`43.09%`, `67.99%` occupancy overlap) but dropped from roughly `41 ms/frame`
|
||||
to `7.508 ms/frame`, or `133.2 FPS` equivalent.
|
||||
|
||||
The remaining visible mismatch is now narrower: Aquarium draws the same family
|
||||
of form, but its density is still too compact and differently weighted. Fixing
|
||||
that belongs in exact JWildfire variation/camera parity, target density
|
||||
weighting, and view-conditioned reservoir feedback.
|
||||
The next oracle step is to compare Aquarium through a matching density image
|
||||
resolve, not raw point occupancy.
|
||||
|
||||
## JWildfire Selftest Faceplant
|
||||
|
||||
Running JWildfire's bundled `selftest.flame` through Aquarium is the correct
|
||||
kind of embarrassing:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-ProgramFlame artifacts\reference-tools\j-wildfire-9.00\lib\FARenderJWF\selftest.flame `
|
||||
-Splats 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 0 `
|
||||
-Frames 1 `
|
||||
-Depth 64 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ReadbackSplats 250000 `
|
||||
-VisualParity `
|
||||
-VisualParityReferenceSamples 1000000 `
|
||||
-HistogramSize 256x256 `
|
||||
-HistogramBounds '-128,-128,128,128'
|
||||
```
|
||||
|
||||
First receipt after adding the JWildfire `variationGroup`/coefficient-layout
|
||||
slice and the `disc`, `julian`, and `gaussian_blur` variation subset, before
|
||||
lowering `post`:
|
||||
|
||||
- GPU time: `66.446 ms/frame`, `15.0 FPS` equivalent
|
||||
- visual score: `5.01%`
|
||||
- occupancy overlap: `3.04%`
|
||||
- L1 distance: `1.899840`
|
||||
- artifact:
|
||||
`artifacts/fractal-jwildfire-faceplant/jwildfire-vs-aquarium-expanded.png`
|
||||
|
||||
The first unexpanded run scored `0.00%` and rendered a single dot. The expanded
|
||||
run produces a recognizable central flame knot, but it still does not match the
|
||||
reference. This is not just "more variations needed." It exposes three distinct
|
||||
gaps:
|
||||
|
||||
1. **DSL expressiveness.** A serious flame dialect needs named variation
|
||||
nodes, parameters, post transforms, palette/color semantics, xaos/chaos
|
||||
weights, and optional weighting fields. The current Aquarium flame subset is
|
||||
a compact test dialect, not a full JWildfire import surface.
|
||||
2. **Translation quality.** JWildfire's coefficient layout differs from the
|
||||
older Apophysis-style fixtures. `variationGroup` can hide the real variation
|
||||
list from the xform attributes. Rich dialect import needs a translation
|
||||
report that says exactly which fields were accepted, approximated, ignored,
|
||||
or rejected.
|
||||
3. **Hot-path shape.** Brute replaying 64 flame iterations per splat in one
|
||||
compute invocation is too slow. Rich flame rendering should store continuing
|
||||
iteration state in GPU reservoirs and advance it stochastically across
|
||||
frames, then density-estimate/shade from those resident samples. The reservoir
|
||||
is the renderer, not an expensive reset button.
|
||||
|
||||
The next coherent slice is therefore not "add 200 variations." It is an
|
||||
import/lowering contract: parse a richer flame AST, produce an explicit
|
||||
translation report, lower supported variation nodes into compact GPU op rows,
|
||||
and carry persistent per-sample iteration state so zoom/movement can reuse
|
||||
sample history instead of starting every splat from zero.
|
||||
|
||||
That import report now exists. GPU and histogram flame receipts emit a sibling
|
||||
`fractal-flame-import-report-*.json` next to the numeric receipt. For the
|
||||
JWildfire selftest flame, the first report after `post` lowering says:
|
||||
|
||||
- accepted fields: `15`
|
||||
- approximated fields: `3`
|
||||
- ignored fields: `45`
|
||||
- rejected fields: `11`
|
||||
- rejected geometry/sampling losses: `wfield_*` weighting-field attributes on
|
||||
the disc xform
|
||||
|
||||
This report is the next expansion gate. A flame may parse successfully and
|
||||
still be a bad import; unsupported fields must stay visible until the DSL,
|
||||
compiler, or renderer can actually own them.
|
||||
|
||||
Lowering `post` required expanding only the small authored-program transform
|
||||
row, not the resident million-splat or reservoir packets. The closer
|
||||
CPU/GPU-slice receipt now gives:
|
||||
|
||||
- bounds `-8,-8,8,8`: `84.19%` GPU-vs-Aquarium-CPU slice score at
|
||||
`129.073 ms/frame`
|
||||
- bounds `-128,-128,128,128`: `88.01%` GPU-vs-Aquarium-CPU slice score at
|
||||
`129.404 ms/frame`
|
||||
- artifact:
|
||||
`artifacts/fractal-jwildfire-faceplant/jwildfire-vs-aquarium-post-close.png`
|
||||
|
||||
This is not external JWildfire visual parity. It says Aquarium's GPU path is
|
||||
now much closer to Aquarium's CPU interpretation of the supported slice. The
|
||||
image is still missing JWildfire's palette, density estimation, weighting-field
|
||||
behavior, render filters, and likely more dialect detail. The speed is also
|
||||
unacceptable for realtime because depth-64 replay is the wrong execution model.
|
||||
|
||||
The first persistent-iteration invariant is now tested on CPU:
|
||||
`FractalFlameIterationState` plus `FractalFlameIterationStepper` prove that
|
||||
advancing one sample by 64 iterations in one call matches advancing the same
|
||||
state in eight 8-iteration chunks. This is not a CPU renderer. It is the
|
||||
mockable contract the GPU state buffer must preserve when the shader stops
|
||||
replaying every splat from zero.
|
||||
|
||||
## GPU Flame Receipt
|
||||
|
||||
The flame fixture can now drive the D3D12 reservoir receipt directly:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-Splats 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 10 `
|
||||
-Frames 60 `
|
||||
-Depth 8 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ProgramFlame tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame
|
||||
```
|
||||
|
||||
Local GTX 1070 receipt from 2026-05-20:
|
||||
|
||||
- resident splats: `2,000,000`
|
||||
- resident reservoir rows: `2,000,000` each for SDF, PBR, and radiosity
|
||||
- program transforms: `3`
|
||||
- splat updates/frame: `50,000`
|
||||
- reservoir updates/pass/frame: `15,000`
|
||||
- GPU time: `1.030 ms/frame`
|
||||
- equivalent FPS: `971.3`
|
||||
- readback checksum: `0x2647D8B08575B848`
|
||||
|
||||
The flame hot path now keeps per-splat iteration state in a GPU UAV instead of
|
||||
replaying every chaos-game ancestry from the origin. `Depth` is the number of
|
||||
iterations advanced when a row is touched; the row stores point, support,
|
||||
material, random state, and accumulated step for the next stochastic update.
|
||||
|
||||
This makes the receipt a two-phase cache:
|
||||
|
||||
- acquisition: seed every resident row and optionally advance the full resident
|
||||
population for a few warmup frames
|
||||
- maintenance: update only the budgeted stochastic subset while the layered
|
||||
SDF/PBR/radiosity reservoirs continue their independent passes
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-ProgramFlame artifacts\reference-tools\j-wildfire-9.00\lib\FARenderJWF\selftest.flame `
|
||||
-Splats 2000000 `
|
||||
-WarmupSplatUpdates 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 8 `
|
||||
-Frames 20 `
|
||||
-Depth 8 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ReadbackSplats 250000 `
|
||||
-VisualParity `
|
||||
-VisualParityReferenceSamples 1000000 `
|
||||
-HistogramSize 256x256 `
|
||||
-HistogramBounds -8,-8,8,8 `
|
||||
-VisualParityImageDirectory artifacts\fractal-jwildfire-faceplant\source `
|
||||
-VisualParityImagePrefix selftest-persistent-warmfull-depth008-frame-020
|
||||
```
|
||||
|
||||
Local GTX 1070 receipt from 2026-05-21:
|
||||
|
||||
- resident splats: `2,000,000`
|
||||
- flame iteration states: `2,000,000` at `32` bytes each
|
||||
- resident reservoir rows: `2,000,000` each for SDF, PBR, and radiosity
|
||||
- warmup splat updates/frame: `2,000,000`
|
||||
- steady splat updates/frame: `50,000`
|
||||
- reservoir updates/pass/frame: `15,000`
|
||||
- measured steady GPU time: `1.011 ms/frame`
|
||||
- equivalent steady FPS: `988.8`
|
||||
- visual parity against Aquarium CPU slice oracle: `84.86%`
|
||||
- cosine similarity: `0.986955`
|
||||
- readback checksum: `0xC633FADC3BD7E5BE`
|
||||
|
||||
Without the full-population acquisition pass, the same steady budget scored only
|
||||
`32.64%` on the JWildfire selftest slice because most rows were still shallow
|
||||
ancestry. That is a useful failure: persistent state is not magic, it is a cache
|
||||
that must be filled before a tiny maintenance budget can look clever.
|
||||
|
||||
## Visual Parity Receipt
|
||||
|
||||
The receipt tool can compare GPU flame splat positions against the local CPU
|
||||
flame oracle:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-Splats 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 0 `
|
||||
-Frames 20 `
|
||||
-Depth 8 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ProgramFlame tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame `
|
||||
-ReadbackSplats 250000 `
|
||||
-VisualParity `
|
||||
-VisualParityReferenceSamples 1000000 `
|
||||
-HistogramSize 128x128 `
|
||||
-HistogramBounds -8,-8,8,8
|
||||
```
|
||||
|
||||
First local scores, GPU 250k readback samples against 1M CPU reference samples:
|
||||
|
||||
| Frames | GPU ms/frame | Distribution score | L1 distance | RMSE | Cosine |
|
||||
| ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 1 | 15.645 | 84.05% | 0.319072 | 0.000090 | 0.995365 |
|
||||
| 5 | 4.017 | 84.10% | 0.318042 | 0.000089 | 0.995402 |
|
||||
| 20 | 1.738 | 84.20% | 0.316019 | 0.000088 | 0.995591 |
|
||||
| 60 + 10 warmup | 0.952 | 84.24% | 0.315136 | 0.000088 | 0.995505 |
|
||||
|
||||
This proves the current GPU flame program has high distribution similarity, but
|
||||
it does **not** prove learned reservoir convergence. The score plateaus because
|
||||
the current flame path initializes resident splats from the program and then
|
||||
performs stochastic replacement; it does not yet use visual error feedback,
|
||||
MIS/ReSTIR weights, or an external rendered-image residual to steer updates.
|
||||
|
||||
## View-Conditioned Parity
|
||||
|
||||
Global parity is a weak metric. Realtime usefulness depends on the current
|
||||
camera window: panning or zooming should keep the visible image smooth while the
|
||||
cache spends new work on underrepresented regions.
|
||||
|
||||
The receipt tool therefore accepts named viewport bounds:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-Splats 2000000 `
|
||||
-SplatUpdates 50000 `
|
||||
-Warmup 10 `
|
||||
-Frames 60 `
|
||||
-Depth 16 `
|
||||
-ReservoirUpdates 15000 `
|
||||
-ProgramFlame tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame `
|
||||
-ReadbackSplats 250000 `
|
||||
-VisualParity `
|
||||
-VisualParityReferenceSamples 1000000 `
|
||||
-HistogramSize 128x128 `
|
||||
-VisualParityView global:-8,-8,8,8 `
|
||||
-VisualParityView mid:-1,-1,1,1 `
|
||||
-VisualParityView close:-0.5,-0.5,0.5,0.5 `
|
||||
-VisualParityView micro:-0.25,-0.25,0.25,0.25
|
||||
```
|
||||
|
||||
First view-conditioned receipt:
|
||||
|
||||
| View | Distribution score | GPU hits | Reference hits | Starved bins | Underrepresented mass |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| global | 97.09% | 249,972 | 999,836 | 1,034 | 2.91% |
|
||||
| mid | 91.93% | 166,654 | 667,105 | 1,792 | 8.07% |
|
||||
| close | 88.97% | 125,448 | 501,033 | 2,266 | 11.03% |
|
||||
| micro | 82.05% | 71,348 | 284,011 | 2,656 | 17.95% |
|
||||
|
||||
This is the first metric that catches the real problem. The reservoir is fast,
|
||||
but it is not yet view-adaptive. It needs camera-conditioned target weights,
|
||||
underrepresented-bin feedback, and temporal reuse validation before it deserves
|
||||
claims about maintaining flame quality during movement and zoom.
|
||||
|
||||
## Contact Sheets
|
||||
|
||||
The quick 1M-sample oracle image is too noisy for visual judgment. Use the image
|
||||
export path with a much larger CPU oracle when making contact sheets:
|
||||
|
||||
```powershell
|
||||
.\scripts\fractal-splat-receipt.ps1 `
|
||||
-ProgramFlame tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame `
|
||||
-VisualParity `
|
||||
-VisualParityReferenceSamples 20000000 `
|
||||
-VisualParityImageDirectory artifacts\fractal-contact-sheet-hq\source `
|
||||
-VisualParityImagePrefix zoom-frame-010 `
|
||||
-ReadbackSplats 250000 `
|
||||
-Depth 16 `
|
||||
-Frames 10 `
|
||||
-VisualParityView zoom:-0.25,-0.25,0.25,0.25
|
||||
```
|
||||
|
||||
The current high-reference contact sheet lives at:
|
||||
|
||||
- `artifacts/fractal-contact-sheet-hq/flame-parity-contact-sheet-hq.png`
|
||||
|
||||
It uses 20M CPU oracle samples and 250k Aquarium readback samples per panel.
|
||||
This still is not a full FLAM3 density-estimated render, but it is no longer a
|
||||
1M-sample noise patch masquerading as ground truth.
|
||||
|
||||
## Sources
|
||||
|
||||
- Apophysis 7x repository: https://github.com/wanily/apophysis7x
|
||||
- FLAM3 repository and variation list: https://github.com/scottdraves/flam3
|
||||
- Draves, *The Fractal Flame Algorithm*: https://flam3.com/flame_draves.pdf
|
||||
|
||||
## Reference Ladder
|
||||
|
||||
Stop calling Aquarium's CPU histogram the visual ground truth. It is a math
|
||||
oracle for a tiny variation subset. It is useful because it is deterministic,
|
||||
small, and debuggable. It is not the bar for beautiful flame rendering.
|
||||
|
||||
Use this ladder:
|
||||
|
||||
1. **Gold visual reference: Chaotica.** Chaotica is the quality target for
|
||||
flame art. Its own manual frames progressive rendering as the alternative
|
||||
to Apophysis/FLAM3 fixed-quality rerendering, and says high quality often
|
||||
means stopping at a sampling level after the image is no longer noisy. Use
|
||||
Chaotica renders as imported reference artifacts when available.
|
||||
2. **Scriptable visual reference: JWildfire/FARender.** JWildfire is LGPL,
|
||||
has a documented headless batch renderer path:
|
||||
`java org.jwildfire.create.tina.batch.HeadlessBatchRendererController <dir> <w> <h> <quality>`.
|
||||
Its current public bundle also ships FARenderJWF, a Windows/NVIDIA GPU
|
||||
renderer that the JWildfire changelog says may still be used while it works.
|
||||
Use `scripts/install-flame-reference-tools.ps1` to hydrate the local tools
|
||||
and `scripts/fractal-flame-jwildfire-reference.ps1 -Renderer FARender` for
|
||||
explicit PNG output.
|
||||
3. **Baseline renderer reference: FLAM3/Apophysis family.** Use FLAM3 for
|
||||
portable `.flame`/`.flam3` compatibility, deterministic external receipts,
|
||||
and old-school density-estimation sanity.
|
||||
4. **Aquarium CPU oracle.** Use only for unit tests, point parity, histogram
|
||||
smoke checks, and shader debugging. Never present it as market-quality
|
||||
visual ground truth.
|
||||
|
||||
Local machine status on 2026-05-20: Chaotica and `flam3-render` were not found
|
||||
on PATH; only Ultra Fractal 6 was visible under Program Files. JWildfire 9.00
|
||||
and Temurin JRE 17 were downloaded into ignored artifacts, and FARenderJWF
|
||||
successfully rendered its bundled selftest flame on the GTX 1070. Aquarium can
|
||||
now compare against a real external flame renderer artifact, but it still
|
||||
cannot claim best-in-market visual parity until imported Chaotica renders or a
|
||||
fully matched JWildfire flame dialect are wired into residual metrics.
|
||||
|
||||
Additional sources:
|
||||
|
||||
- Chaotica features: https://chaoticafractals.com/features
|
||||
- Chaotica progressive rendering: https://www.chaoticafractals.com/manual/rendering/progressive_rendering
|
||||
- Chaotica render settings / sampling level: https://www.chaoticafractals.com/manual/user_interface/render_settings
|
||||
- JWildfire GitHub / LGPL license: https://github.com/thargor6/JWildfire
|
||||
- JWildfire headless batch renderer notes: https://www.jwfsanctuary.club/tutorial/how-to/how-do-i-render-a-full-folder-of-flames/
|
||||
- JWildfire bundled FARender/Swan notes: `artifacts/reference-tools/j-wildfire-9.00/CHANGES.txt`
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
# HDR/PBR Presentation Research
|
||||
|
||||
Aquarium needs an HDR presentation path before the SDF field renderer grows real
|
||||
lighting and participating media. The target is not "more bloom." The target is a
|
||||
scene-linear renderer with believable dynamic range, camera exposure, optical
|
||||
spill, and a display transform that preserves contrast without boiling the frame
|
||||
in arcade syrup.
|
||||
|
||||
## Sources
|
||||
|
||||
- [ACES System overview](https://docs.acescentral.com/background/overview/)
|
||||
- [Epic: How Epic Games is handling Auto Exposure in 4.25](https://www.unrealengine.com/tech-blog/how-epic-games-is-handling-auto-exposure-in-4-25?lang=en-US)
|
||||
- [Unreal Engine bloom documentation](https://dev.epicgames.com/documentation/en-us/unreal-engine/bloom-in-unreal-engine?application_version=5.6)
|
||||
- [Jorge Jimenez: Next Generation Post Processing in Call of Duty: Advanced Warfare](https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/)
|
||||
- [John Hable: Filmic Tonemapping Operators](https://filmicworlds.com/blog/filmic-tonemapping-operators/)
|
||||
- [John Hable: Filmic Tonemapping with Piecewise Power Curves](https://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/)
|
||||
- [Hable: Uncharted 2 HDR Lighting, SIGGRAPH 2010](https://advances.realtimerendering.com/s2010/Hable-Uncharted2%28SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course%29.pdf)
|
||||
- [Lagarde/de Rousiers: Moving Frostbite to Physically Based Rendering 3.0](https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/Moving%20Frostbite%20to%20Physically%20Based%20Rendering%203.0%2C%202014%2C%20Sebastien%20Lagarde.pdf)
|
||||
|
||||
## Distillation
|
||||
|
||||
Modern HDR presentation is a chain, not a knob:
|
||||
|
||||
1. Render lighting and emissive terms in scene-linear HDR.
|
||||
2. Apply exposure in the HDR domain, preferably from a physical-camera or EV100
|
||||
model even if Aquarium initially uses manual exposure.
|
||||
3. Generate bloom/glare from exposed HDR color before tonemapping.
|
||||
4. Composite bloom back into exposed scene-linear color with low intensity and
|
||||
broad support.
|
||||
5. Apply a display transform or filmic tonemap with toe and shoulder.
|
||||
6. Convert to the swapchain/display encoding only at the end.
|
||||
|
||||
The important production lesson is that bloom is not a thresholded special
|
||||
effect for "pixels above 1." In a high-dynamic-range PBR scene, broad low-gain
|
||||
scatter can apply to the whole image while only genuinely bright energy becomes
|
||||
visibly luminous. Threshold bloom is the cheap costume version; it cuts out the
|
||||
low-level optical veil and encourages artists to overdrive emissive values until
|
||||
the frame looks diseased.
|
||||
|
||||
ACES-style and filmic transforms are useful because they give highlights a
|
||||
shoulder and shadows a toe. Aquarium's current tiny ACES approximation is a
|
||||
reasonable placeholder, but it is not a color-management system, not a look
|
||||
authoring surface, and not enough to make the scene feel HDR by itself.
|
||||
|
||||
## Aquarium Gate Plan
|
||||
|
||||
### Gate HDR-A: Explicit Exposure
|
||||
|
||||
Add an exposure scalar in `AquariumFrame`, initially manual and persisted through
|
||||
CultCache later.
|
||||
|
||||
Start simple:
|
||||
|
||||
- `exposedColor = sceneLinearColor * exposure`
|
||||
- default exposure chosen so Self is bright but not pinned white across its
|
||||
whole disc
|
||||
- debug mode for log luminance or EV bands
|
||||
|
||||
Avoid auto exposure until the scene has enough stable luminance content. A
|
||||
volumetric/SDF scene with huge black regions and a giant Self emitter will make
|
||||
naive histogram adaptation twitch like a bad witness.
|
||||
|
||||
### Gate HDR-B: Bloom Pyramid
|
||||
|
||||
Add a pre-tonemap bloom pass:
|
||||
|
||||
- source: exposed HDR scene color
|
||||
- no hard threshold at first
|
||||
- downsample pyramid with firefly-safe weighted averages
|
||||
- upsample with small tent filters and per-level radius
|
||||
- composite with low intensity into exposed scene color before tonemap
|
||||
|
||||
This follows the Jimenez production shape: pyramidal bloom/veil for stability
|
||||
and robustness, not a single huge blur taped over bright pixels.
|
||||
|
||||
### Gate HDR-C: Display Transform
|
||||
|
||||
Keep the current ACES approximation only as `DisplayTransform.AcesFit`.
|
||||
|
||||
Add an explicit display-transform function boundary:
|
||||
|
||||
- ACES fit for default
|
||||
- Hable/filmic curve as an alternate debug/look path
|
||||
- later: LUT-based transform if Aquarium needs an authored look
|
||||
|
||||
Do not bury exposure, bloom, and tonemapping in one shader lump. The whole point
|
||||
is to keep the light transport legible before volumetrics arrive and start
|
||||
lying convincingly.
|
||||
|
||||
### Gate HDR-D: HDR Debug Views
|
||||
|
||||
Add debug views before tuning:
|
||||
|
||||
- scene-linear luminance
|
||||
- exposed luminance
|
||||
- bloom contribution
|
||||
- tonemapped final
|
||||
- over-range / clipped highlight mask
|
||||
|
||||
If Self, planets, Grid, and later clouds cannot be inspected in each stage, the
|
||||
pipeline will become a little shrine to superstition. Burn the shrine early.
|
||||
|
||||
## Rules For Aquarium
|
||||
|
||||
- Bloom happens before tonemapping.
|
||||
- Bloom spreads existing energy; it does not invent light.
|
||||
- Prefer broad low-intensity bloom/veil over hard-threshold glow.
|
||||
- Keep overlay text out of HDR and bloom unless it becomes a diegetic scene
|
||||
surface.
|
||||
- Do not tune emissive values against SDR final output alone. Inspect linear and
|
||||
exposed luminance.
|
||||
- Volumetrics must feed and be fed by the same exposure/display path as solids.
|
||||
- If the frame needs "more HDR," first check exposure, display shoulder, and
|
||||
source radiance before turning up bloom.
|
||||
|
||||
## Immediate Recommendation
|
||||
|
||||
Before the field renderer:
|
||||
|
||||
1. Add explicit exposure constants.
|
||||
2. Split resolve into exposed HDR color and final display transform.
|
||||
3. Add a bloom pyramid between exposure and tonemap.
|
||||
4. Add debug views for luminance and bloom contribution.
|
||||
|
||||
This gives SDF solids/clouds a real presentation pipe to land in. Otherwise the
|
||||
field renderer will arrive carrying gorgeous radiance values into a cramped SDR
|
||||
mail slot, and everyone will pretend the answer is blur radius. It is not.
|
||||
|
||||
## Aquarium Implementation Note
|
||||
|
||||
The first implementation now follows this shape:
|
||||
|
||||
- fixed manual exposure in the frame constants
|
||||
- exposed scene-linear HDR source for bloom
|
||||
- three-level bloom pyramid at half, quarter, and eighth resolution
|
||||
- firefly-safe downsampling
|
||||
- separable horizontal/vertical blur per level
|
||||
- low-gain bloom/veil contribution before the ACES fit
|
||||
- debug mode `7` for bloom contribution and mode `8` for exposed luminance
|
||||
|
||||
The remaining missing piece is a persisted/inspectable exposure control and,
|
||||
later, a proper authored display-transform boundary.
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Bake a Radiance HDR latlong map into a GGX-prefiltered DDS cubemap."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
DXGI_FORMAT_R16G16B16A16_FLOAT = 10
|
||||
D3D10_RESOURCE_DIMENSION_TEXTURE2D = 3
|
||||
D3D11_RESOURCE_MISC_TEXTURECUBE = 0x4
|
||||
|
||||
|
||||
def read_radiance_hdr(path: Path) -> np.ndarray:
|
||||
data = path.read_bytes()
|
||||
cursor = 0
|
||||
while True:
|
||||
end = data.index(b"\n", cursor)
|
||||
line = data[cursor:end].decode("ascii", errors="replace").strip()
|
||||
cursor = end + 1
|
||||
if not line:
|
||||
break
|
||||
|
||||
while True:
|
||||
end = data.index(b"\n", cursor)
|
||||
line = data[cursor:end].decode("ascii", errors="replace").strip()
|
||||
cursor = end + 1
|
||||
if line:
|
||||
resolution = line.split()
|
||||
break
|
||||
|
||||
if len(resolution) != 4 or resolution[0] != "-Y" or resolution[2] != "+X":
|
||||
raise ValueError(f"Unsupported Radiance orientation in {path}: {' '.join(resolution)}")
|
||||
|
||||
height = int(resolution[1])
|
||||
width = int(resolution[3])
|
||||
rgbe = np.empty((height, width, 4), dtype=np.uint8)
|
||||
|
||||
for y in range(height):
|
||||
if data[cursor] != 2 or data[cursor + 1] != 2:
|
||||
raise ValueError(f"Unsupported non-RLE Radiance scanline {y}")
|
||||
scan_width = (data[cursor + 2] << 8) | data[cursor + 3]
|
||||
cursor += 4
|
||||
if scan_width != width:
|
||||
raise ValueError(f"Radiance scanline {y} has width {scan_width}, expected {width}")
|
||||
|
||||
scanline = np.empty((4, width), dtype=np.uint8)
|
||||
for channel in range(4):
|
||||
x = 0
|
||||
while x < width:
|
||||
code = data[cursor]
|
||||
cursor += 1
|
||||
if code > 128:
|
||||
count = code - 128
|
||||
scanline[channel, x : x + count] = data[cursor]
|
||||
cursor += 1
|
||||
else:
|
||||
count = code
|
||||
scanline[channel, x : x + count] = np.frombuffer(data, dtype=np.uint8, count=count, offset=cursor)
|
||||
cursor += count
|
||||
x += count
|
||||
|
||||
rgbe[y] = scanline.T
|
||||
|
||||
exponent = rgbe[..., 3].astype(np.int32)
|
||||
scale = np.zeros((height, width), dtype=np.float32)
|
||||
valid = exponent > 0
|
||||
scale[valid] = np.ldexp(np.ones(np.count_nonzero(valid), dtype=np.float32), exponent[valid] - (128 + 8))
|
||||
return rgbe[..., :3].astype(np.float32) * scale[..., None]
|
||||
|
||||
|
||||
def radical_inverse_vdc(bits: int) -> float:
|
||||
bits = (bits << 16) | (bits >> 16)
|
||||
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1)
|
||||
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2)
|
||||
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4)
|
||||
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8)
|
||||
return bits * 2.3283064365386963e-10
|
||||
|
||||
|
||||
def hammersley(count: int) -> np.ndarray:
|
||||
values = np.empty((count, 2), dtype=np.float32)
|
||||
for i in range(count):
|
||||
values[i, 0] = (i + 0.5) / count
|
||||
values[i, 1] = radical_inverse_vdc(i)
|
||||
return values
|
||||
|
||||
|
||||
def face_directions(face: int, size: int) -> np.ndarray:
|
||||
coord = (np.arange(size, dtype=np.float32) + 0.5) / size * 2.0 - 1.0
|
||||
u, v = np.meshgrid(coord, coord)
|
||||
if face == 0: # +X
|
||||
d = np.stack([np.ones_like(u), -v, -u], axis=-1)
|
||||
elif face == 1: # -X
|
||||
d = np.stack([-np.ones_like(u), -v, u], axis=-1)
|
||||
elif face == 2: # +Y
|
||||
d = np.stack([u, np.ones_like(u), v], axis=-1)
|
||||
elif face == 3: # -Y
|
||||
d = np.stack([u, -np.ones_like(u), -v], axis=-1)
|
||||
elif face == 4: # +Z
|
||||
d = np.stack([u, -v, np.ones_like(u)], axis=-1)
|
||||
elif face == 5: # -Z
|
||||
d = np.stack([-u, -v, -np.ones_like(u)], axis=-1)
|
||||
else:
|
||||
raise ValueError(face)
|
||||
return normalize(d)
|
||||
|
||||
|
||||
def normalize(v: np.ndarray) -> np.ndarray:
|
||||
return v / np.maximum(np.linalg.norm(v, axis=-1, keepdims=True), 1e-20)
|
||||
|
||||
|
||||
def tangent_basis(n: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
up = np.zeros_like(n)
|
||||
up[..., 1] = 1.0
|
||||
alt = np.zeros_like(n)
|
||||
alt[..., 2] = 1.0
|
||||
use_alt = np.abs(n[..., 1:2]) > 0.98
|
||||
up = np.where(use_alt, alt, up)
|
||||
tangent = normalize(np.cross(up, n))
|
||||
bitangent = np.cross(n, tangent)
|
||||
return tangent, bitangent
|
||||
|
||||
|
||||
def sample_latlong(env: np.ndarray, directions: np.ndarray) -> np.ndarray:
|
||||
height, width, _ = env.shape
|
||||
d = normalize(directions)
|
||||
u = np.mod(np.arctan2(d[..., 2], d[..., 0]) / (2.0 * math.pi) + 0.5, 1.0) * width - 0.5
|
||||
v = np.arccos(np.clip(d[..., 1], -1.0, 1.0)) / math.pi * height - 0.5
|
||||
|
||||
x0 = np.floor(u).astype(np.int32)
|
||||
y0 = np.floor(v).astype(np.int32)
|
||||
tx = (u - x0)[..., None]
|
||||
ty = (v - y0)[..., None]
|
||||
x0 %= width
|
||||
x1 = (x0 + 1) % width
|
||||
y0 = np.clip(y0, 0, height - 1)
|
||||
y1 = np.clip(y0 + 1, 0, height - 1)
|
||||
|
||||
c00 = env[y0, x0]
|
||||
c10 = env[y0, x1]
|
||||
c01 = env[y1, x0]
|
||||
c11 = env[y1, x1]
|
||||
return (c00 * (1.0 - tx) + c10 * tx) * (1.0 - ty) + (c01 * (1.0 - tx) + c11 * tx) * ty
|
||||
|
||||
|
||||
def prefilter_face(env: np.ndarray, face: int, size: int, roughness: float, sample_count: int) -> np.ndarray:
|
||||
n = face_directions(face, size)
|
||||
if roughness <= 0.0001:
|
||||
return sample_latlong(env, n).astype(np.float32)
|
||||
|
||||
tangent, bitangent = tangent_basis(n)
|
||||
v = n
|
||||
alpha = max(roughness * roughness, 0.001)
|
||||
result = np.zeros((size, size, 3), dtype=np.float32)
|
||||
weight_sum = np.zeros((size, size, 1), dtype=np.float32)
|
||||
|
||||
for xi1, xi2 in hammersley(sample_count):
|
||||
phi = 2.0 * math.pi * float(xi1)
|
||||
cos_theta = math.sqrt((1.0 - float(xi2)) / (1.0 + (alpha * alpha - 1.0) * float(xi2)))
|
||||
sin_theta = math.sqrt(max(0.0, 1.0 - cos_theta * cos_theta))
|
||||
h = normalize(
|
||||
tangent * (math.cos(phi) * sin_theta)
|
||||
+ bitangent * (math.sin(phi) * sin_theta)
|
||||
+ n * cos_theta
|
||||
)
|
||||
l = normalize(2.0 * np.sum(v * h, axis=-1, keepdims=True) * h - v)
|
||||
ndotl = np.maximum(np.sum(n * l, axis=-1, keepdims=True), 0.0).astype(np.float32)
|
||||
result += sample_latlong(env, l) * ndotl
|
||||
weight_sum += ndotl
|
||||
|
||||
return result / np.maximum(weight_sum, 1e-6)
|
||||
|
||||
|
||||
def irradiance_face(env: np.ndarray, face: int, size: int, sample_count: int) -> np.ndarray:
|
||||
n = face_directions(face, size)
|
||||
tangent, bitangent = tangent_basis(n)
|
||||
result = np.zeros((size, size, 3), dtype=np.float32)
|
||||
|
||||
for xi1, xi2 in hammersley(sample_count):
|
||||
radius = math.sqrt(float(xi1))
|
||||
phi = 2.0 * math.pi * float(xi2)
|
||||
direction = normalize(
|
||||
tangent * (math.cos(phi) * radius)
|
||||
+ bitangent * (math.sin(phi) * radius)
|
||||
+ n * math.sqrt(max(0.0, 1.0 - float(xi1)))
|
||||
)
|
||||
result += sample_latlong(env, direction)
|
||||
|
||||
return result * (math.pi / sample_count)
|
||||
|
||||
|
||||
def dds_header(width: int, height: int, mip_count: int) -> bytes:
|
||||
flags = 0x1 | 0x2 | 0x4 | 0x8 | 0x1000 | 0x20000
|
||||
caps = 0x1000 | 0x8 | 0x400000
|
||||
caps2 = 0x200 | 0x400 | 0x800 | 0x1000 | 0x2000 | 0x4000 | 0x8000
|
||||
pitch = width * 8
|
||||
pixel_format = struct.pack("<8I", 32, 0x4, int.from_bytes(b"DX10", "little"), 0, 0, 0, 0, 0)
|
||||
header = struct.pack(
|
||||
"<7I11I",
|
||||
124,
|
||||
flags,
|
||||
height,
|
||||
width,
|
||||
pitch,
|
||||
0,
|
||||
mip_count,
|
||||
*([0] * 11),
|
||||
)
|
||||
header += pixel_format
|
||||
header += struct.pack("<5I", caps, caps2, 0, 0, 0)
|
||||
dx10 = struct.pack(
|
||||
"<5I",
|
||||
DXGI_FORMAT_R16G16B16A16_FLOAT,
|
||||
D3D10_RESOURCE_DIMENSION_TEXTURE2D,
|
||||
D3D11_RESOURCE_MISC_TEXTURECUBE,
|
||||
1,
|
||||
0,
|
||||
)
|
||||
return b"DDS " + header + dx10
|
||||
|
||||
|
||||
def write_dds(path: Path, mip_faces: list[list[np.ndarray]]) -> None:
|
||||
base_size = mip_faces[0][0].shape[0]
|
||||
payload = bytearray(dds_header(base_size, base_size, len(mip_faces)))
|
||||
for face in range(6):
|
||||
for mip in mip_faces:
|
||||
rgb = np.maximum(mip[face], 0.0).astype(np.float16)
|
||||
alpha = np.ones(rgb.shape[:2] + (1,), dtype=np.float16)
|
||||
payload.extend(np.concatenate([rgb, alpha], axis=-1).tobytes())
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(payload)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--mode", choices=["specular", "irradiance"], default="specular")
|
||||
parser.add_argument("--size", type=int, default=256)
|
||||
parser.add_argument("--samples", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.size <= 0 or args.size & (args.size - 1):
|
||||
raise ValueError("--size must be a power of two")
|
||||
|
||||
env = read_radiance_hdr(args.input)
|
||||
if args.mode == "irradiance":
|
||||
print(f"irradiance: {args.size}x{args.size}, samples={args.samples}")
|
||||
write_dds(
|
||||
args.output,
|
||||
[[irradiance_face(env, face, args.size, args.samples) for face in range(6)]])
|
||||
print(f"wrote {args.output}")
|
||||
return
|
||||
|
||||
mip_count = int(math.log2(args.size)) + 1
|
||||
mip_faces: list[list[np.ndarray]] = []
|
||||
for mip_index in range(mip_count):
|
||||
size = max(1, args.size >> mip_index)
|
||||
roughness = mip_index / max(1, mip_count - 1)
|
||||
sample_count = 1 if mip_index == 0 else max(32, int(args.samples * (0.5 + roughness * 0.5)))
|
||||
print(f"mip {mip_index}/{mip_count - 1}: {size}x{size}, roughness={roughness:.3f}, samples={sample_count}")
|
||||
mip_faces.append([
|
||||
prefilter_face(env, face, size, roughness, sample_count)
|
||||
for face in range(6)
|
||||
])
|
||||
|
||||
write_dds(args.output, mip_faces)
|
||||
print(f"wrote {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
param(
|
||||
[string]$OutputPath,
|
||||
[int]$Width = 1280,
|
||||
[int]$Height = 720,
|
||||
[int]$ReadyFrames = 8,
|
||||
[int]$RetainSlots = 4,
|
||||
[int]$TimeoutSeconds = 120
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
$OutputPath = Join-Path $repoRoot ("artifacts\fensalir-captures\fensalir-fixed-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".png")
|
||||
}
|
||||
|
||||
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
|
||||
$clientProject = Join-Path $repoRoot "src\Aquarium.Fensalir\Aquarium.Fensalir.csproj"
|
||||
$devRoot = Join-Path $repoRoot "artifacts\dev-reload"
|
||||
$slotRoot = Join-Path $devRoot "slots"
|
||||
$slotPath = Join-Path $slotRoot ("fensalir-capture-" + (Get-Date -Format "yyyyMMdd-HHmmss") + "-" + [guid]::NewGuid().ToString("N").Substring(0, 8))
|
||||
$cachePath = Join-Path $devRoot "fensalir-capture-cultcache\aquarium-client.msgpack"
|
||||
$stdoutLog = Join-Path $devRoot "fensalir-capture.out.log"
|
||||
$stderrLog = Join-Path $devRoot "fensalir-capture.err.log"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $slotPath | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $cachePath) | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $OutputPath) | Out-Null
|
||||
|
||||
Write-Host "Building Fensalir capture slot:"
|
||||
Write-Host " $slotPath"
|
||||
dotnet build (Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj") -c Debug -o $slotPath /p:UseAppHost=true
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Engine build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
dotnet build $clientProject -c Debug -o $slotPath /p:UseAppHost=false
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Fensalir client build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$exePath = Join-Path $slotPath "Aquarium.Engine.exe"
|
||||
$clientAssembly = Join-Path $slotPath "Aquarium.Fensalir.dll"
|
||||
$arguments = @(
|
||||
"--headless",
|
||||
"--headless-width", $Width,
|
||||
"--headless-height", $Height,
|
||||
"--client-assembly", $clientAssembly,
|
||||
"--cache", $cachePath,
|
||||
"--shader-source", (Join-Path $slotPath "Render\Shaders\D3D12HeightField.hlsl"),
|
||||
"--capture-frame", $OutputPath
|
||||
)
|
||||
|
||||
$env:AQUARIUM_HEADLESS_READY_FRAMES = [string][Math]::Max(1, $ReadyFrames)
|
||||
$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)
|
||||
$tail = if (Test-Path $stderrLog) { Get-Content -Path $stderrLog -Tail 80 } else { "<stderr missing>" }
|
||||
throw "Fensalir capture timed out after $TimeoutSeconds seconds.`n$($tail -join [Environment]::NewLine)"
|
||||
}
|
||||
|
||||
$process.WaitForExit()
|
||||
$process.Refresh()
|
||||
$exitCode = $process.ExitCode
|
||||
if ($null -eq $exitCode -and (Test-Path $OutputPath)) {
|
||||
$exitCode = 0
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
$tail = if (Test-Path $stderrLog) { Get-Content -Path $stderrLog -Tail 80 } else { "<stderr missing>" }
|
||||
throw "Fensalir capture failed with exit code $exitCode.`n$($tail -join [Environment]::NewLine)"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $OutputPath)) {
|
||||
throw "Fensalir capture did not produce expected PNG: $OutputPath"
|
||||
}
|
||||
|
||||
if ($RetainSlots -gt 0) {
|
||||
Get-ChildItem -Path $slotRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "fensalir-capture-*" } |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -Skip $RetainSlots |
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Fensalir frame captured:"
|
||||
Write-Host " $OutputPath"
|
||||
|
|
@ -1,359 +1,11 @@
|
|||
param(
|
||||
[switch]$Headless,
|
||||
[switch]$NoStop,
|
||||
[switch]$BuildOnly,
|
||||
[switch]$Reopen,
|
||||
[string]$ClientProject = "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj",
|
||||
[int]$RetainSlots = 12,
|
||||
[int]$StartupTimeoutSeconds = 5,
|
||||
[int]$HeadlessTimeoutSeconds = 60
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$RemainingArgs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$projectPath = Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj"
|
||||
$liveProjectPath = if ([System.IO.Path]::IsPathRooted($ClientProject)) { $ClientProject } else { Join-Path $repoRoot $ClientProject }
|
||||
$liveProjectPath = (Resolve-Path $liveProjectPath).Path
|
||||
$liveAssemblyName = [System.IO.Path]::GetFileNameWithoutExtension($liveProjectPath)
|
||||
$devRoot = Join-Path $repoRoot "artifacts\dev-reload"
|
||||
$slotRoot = Join-Path $devRoot "slots"
|
||||
$visibleStatePath = Join-Path $devRoot "state.clixml"
|
||||
$headlessStatePath = Join-Path $devRoot "headless-state.clixml"
|
||||
$statePath = if ($Headless) { $headlessStatePath } else { $visibleStatePath }
|
||||
$buildStatePath = Join-Path $devRoot "last-build.clixml"
|
||||
$liveReloadPointerPath = Join-Path $devRoot "live-current.txt"
|
||||
$cultCacheDirectory = if ($Headless) { "headless-cultcache" } else { "cultcache" }
|
||||
$cultCachePath = Join-Path $devRoot "$cultCacheDirectory\aquarium-client.msgpack"
|
||||
$logName = if ($Headless) { "headless" } else { "latest" }
|
||||
$stdoutLogPath = Join-Path $devRoot "$logName.out.log"
|
||||
$stderrLogPath = Join-Path $devRoot "$logName.err.log"
|
||||
$fensalirRoot = Resolve-Path (Join-Path $repoRoot "..\Fensalir")
|
||||
$clientProject = Join-Path $repoRoot "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj"
|
||||
$script = Join-Path $fensalirRoot "scripts\dev-reload.ps1"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $slotRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $cultCachePath) | Out-Null
|
||||
|
||||
if ($Reopen -and $Headless) {
|
||||
throw "-Reopen is for the visible dev window; do not combine it with -Headless."
|
||||
}
|
||||
|
||||
function Stop-PreviousOwnedProcess {
|
||||
if ($NoStop -or -not (Test-Path $statePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
$state = Import-Clixml -Path $statePath
|
||||
if (-not ($state.PSObject.Properties.Name -contains "pid")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $state.pid) {
|
||||
return
|
||||
}
|
||||
|
||||
$process = Get-Process -Id ([int]$state.pid) -ErrorAction SilentlyContinue
|
||||
if (-not $process) {
|
||||
return
|
||||
}
|
||||
|
||||
$recordedPath = [string]$state.slot
|
||||
$commandLine = $null
|
||||
try {
|
||||
$commandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine
|
||||
}
|
||||
catch {
|
||||
$commandLine = $null
|
||||
}
|
||||
|
||||
if (-not $commandLine -or $commandLine -notlike "*$recordedPath*") {
|
||||
Write-Host "Previous PID $($state.pid) no longer belongs to the recorded slot; leaving it alone."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping previous script-owned Aquarium process $($state.pid)."
|
||||
Stop-Process -Id ([int]$state.pid) -Force
|
||||
}
|
||||
|
||||
function Remove-OldSlots {
|
||||
if ($RetainSlots -lt 1) {
|
||||
return
|
||||
}
|
||||
|
||||
$slots = Get-ChildItem -Path $slotRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -Skip $RetainSlots
|
||||
|
||||
foreach ($slot in $slots) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $slot.FullName -Recurse -Force
|
||||
}
|
||||
catch {
|
||||
Write-Host "Could not remove old slot $($slot.FullName): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-StderrTail {
|
||||
if (-not (Test-Path $stderrLogPath)) {
|
||||
return "<stderr log does not exist>"
|
||||
}
|
||||
|
||||
$tail = Get-Content -Path $stderrLogPath -Tail 80 -ErrorAction SilentlyContinue
|
||||
if (-not $tail) {
|
||||
return "<stderr log is empty>"
|
||||
}
|
||||
|
||||
return ($tail -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
function Wait-ForStartedAquarium {
|
||||
param(
|
||||
[System.Diagnostics.Process]$Process,
|
||||
[bool]$ExpectWindow
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $StartupTimeoutSeconds))
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$Process.Refresh()
|
||||
if ($Process.HasExited) {
|
||||
throw "Aquarium process exited during startup with code $($Process.ExitCode).`n$(Get-StderrTail)"
|
||||
}
|
||||
|
||||
if (-not $ExpectWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($Process.MainWindowHandle -ne [IntPtr]::Zero) {
|
||||
return
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
if ($ExpectWindow) {
|
||||
try {
|
||||
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
# Best effort cleanup before surfacing the broken reload.
|
||||
}
|
||||
|
||||
throw "Aquarium process started but did not open a visible window within $StartupTimeoutSeconds seconds.`n$(Get-StderrTail)"
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForHeadlessAquarium {
|
||||
param(
|
||||
[System.Diagnostics.Process]$Process
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $HeadlessTimeoutSeconds))
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$Process.Refresh()
|
||||
if ($Process.HasExited) {
|
||||
$Process.WaitForExit()
|
||||
$exitCode = [int]$Process.ExitCode
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Headless Aquarium exited with code $exitCode.`n$(Get-StderrTail)"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
try {
|
||||
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
# Best effort cleanup before surfacing the broken smoke test.
|
||||
}
|
||||
|
||||
throw "Headless Aquarium did not exit within $HeadlessTimeoutSeconds seconds; killed PID $($Process.Id).`n$(Get-StderrTail)"
|
||||
}
|
||||
|
||||
function Start-AquariumProcess {
|
||||
param(
|
||||
[string]$SlotPath,
|
||||
[string]$LiveAssemblyPath,
|
||||
[bool]$RunHeadless
|
||||
)
|
||||
|
||||
$exePath = Join-Path $SlotPath "Aquarium.Engine.exe"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
throw "Expected apphost does not exist: $exePath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $LiveAssemblyPath)) {
|
||||
throw "Expected client runtime assembly does not exist: $LiveAssemblyPath"
|
||||
}
|
||||
|
||||
Set-Content -Path $liveReloadPointerPath -Value $LiveAssemblyPath -Encoding UTF8
|
||||
|
||||
$arguments = @(
|
||||
"--cache", $cultCachePath,
|
||||
"--shader-source", (Join-Path $SlotPath "Render\Shaders\D3D12HeightField.hlsl"),
|
||||
"--client-assembly", $LiveAssemblyPath,
|
||||
"--client-reload-pointer", $liveReloadPointerPath
|
||||
)
|
||||
if ($RunHeadless) {
|
||||
$arguments += "--headless"
|
||||
}
|
||||
|
||||
$startProcessParameters = @{
|
||||
FilePath = $exePath
|
||||
ArgumentList = $arguments
|
||||
WorkingDirectory = $SlotPath
|
||||
RedirectStandardOutput = $stdoutLogPath
|
||||
RedirectStandardError = $stderrLogPath
|
||||
PassThru = $true
|
||||
}
|
||||
|
||||
if ($RunHeadless) {
|
||||
$startProcessParameters.WindowStyle = "Hidden"
|
||||
}
|
||||
|
||||
return Start-Process @startProcessParameters
|
||||
}
|
||||
|
||||
function Reopen-VisibleProcess {
|
||||
if (-not (Test-Path $visibleStatePath)) {
|
||||
throw "No visible dev-reload state file exists. Run scripts\dev-reload.ps1 once to create a launch slot."
|
||||
}
|
||||
|
||||
$state = Import-Clixml -Path $visibleStatePath
|
||||
if (-not $state.slot) {
|
||||
throw "Visible dev-reload state does not record a slot."
|
||||
}
|
||||
|
||||
if ($state.PSObject.Properties.Name -contains "pid" -and $state.pid) {
|
||||
$existing = Get-Process -Id ([int]$state.pid) -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Write-Host "Visible Aquarium process $($state.pid) is already running."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$slotPath = [string]$state.slot
|
||||
$liveAssemblyPath = Join-Path $slotPath "$liveAssemblyName.dll"
|
||||
if ($state.PSObject.Properties.Name -contains "liveAssembly" -and $state.liveAssembly) {
|
||||
$liveAssemblyPath = [string]$state.liveAssembly
|
||||
}
|
||||
|
||||
$process = Start-AquariumProcess -SlotPath $slotPath -LiveAssemblyPath $liveAssemblyPath -RunHeadless:$false
|
||||
Wait-ForStartedAquarium -Process $process -ExpectWindow:$true
|
||||
|
||||
@{
|
||||
slot = $slotPath
|
||||
builtAt = $state.builtAt
|
||||
reopenedAt = (Get-Date).ToString("o")
|
||||
pid = $process.Id
|
||||
liveAssembly = $liveAssemblyPath
|
||||
headless = $false
|
||||
stdoutLog = $stdoutLogPath
|
||||
stderrLog = $stderrLogPath
|
||||
} | Export-Clixml -Path $visibleStatePath
|
||||
|
||||
Write-Host "Aquarium reopened from last visible slot."
|
||||
Write-Host " PID: $($process.Id)"
|
||||
Write-Host " Slot: $slotPath"
|
||||
return
|
||||
}
|
||||
|
||||
if ($Reopen) {
|
||||
Reopen-VisibleProcess
|
||||
exit 0
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$slotName = "$timestamp-$([guid]::NewGuid().ToString("N").Substring(0, 8))"
|
||||
$slotPath = Join-Path $slotRoot $slotName
|
||||
New-Item -ItemType Directory -Force -Path $slotPath | Out-Null
|
||||
|
||||
Write-Host "Building Aquarium into disposable slot:"
|
||||
Write-Host " $slotPath"
|
||||
dotnet build $projectPath `
|
||||
-c Debug `
|
||||
-o $slotPath `
|
||||
/p:UseAppHost=true
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
dotnet build $liveProjectPath `
|
||||
-c Debug `
|
||||
-o $slotPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "client runtime build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
if ($BuildOnly) {
|
||||
@{
|
||||
slot = $slotPath
|
||||
builtAt = (Get-Date).ToString("o")
|
||||
stdoutLog = $stdoutLogPath
|
||||
stderrLog = $stderrLogPath
|
||||
} | Export-Clixml -Path $buildStatePath
|
||||
Write-Host "Build-only slot ready."
|
||||
Remove-OldSlots
|
||||
exit 0
|
||||
}
|
||||
|
||||
Stop-PreviousOwnedProcess
|
||||
|
||||
$exePath = Join-Path $slotPath "Aquarium.Engine.exe"
|
||||
$liveAssemblyPath = Join-Path $slotPath "$liveAssemblyName.dll"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
throw "Expected apphost was not produced: $exePath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $liveAssemblyPath)) {
|
||||
throw "Expected client runtime assembly was not produced: $liveAssemblyPath"
|
||||
}
|
||||
|
||||
$process = Start-AquariumProcess -SlotPath $slotPath -LiveAssemblyPath $liveAssemblyPath -RunHeadless:$Headless
|
||||
if ($Headless) {
|
||||
Wait-ForHeadlessAquarium -Process $process
|
||||
|
||||
@{
|
||||
slot = $slotPath
|
||||
builtAt = (Get-Date).ToString("o")
|
||||
liveAssembly = $liveAssemblyPath
|
||||
headless = $true
|
||||
stdoutLog = $stdoutLogPath
|
||||
stderrLog = $stderrLogPath
|
||||
} | Export-Clixml -Path $statePath
|
||||
|
||||
Write-Host "Headless Aquarium completed from disposable slot."
|
||||
Write-Host " Slot: $slotPath"
|
||||
Write-Host " CultCache: $cultCachePath"
|
||||
Write-Host " Stdout: $stdoutLogPath"
|
||||
Write-Host " Stderr: $stderrLogPath"
|
||||
Remove-OldSlots
|
||||
exit 0
|
||||
}
|
||||
|
||||
Wait-ForStartedAquarium -Process $process -ExpectWindow:$true
|
||||
|
||||
@{
|
||||
slot = $slotPath
|
||||
builtAt = (Get-Date).ToString("o")
|
||||
pid = $process.Id
|
||||
liveAssembly = $liveAssemblyPath
|
||||
headless = [bool]$Headless
|
||||
stdoutLog = $stdoutLogPath
|
||||
stderrLog = $stderrLogPath
|
||||
} | Export-Clixml -Path $statePath
|
||||
|
||||
Write-Host "Aquarium running from disposable slot."
|
||||
Write-Host " PID: $($process.Id)"
|
||||
Write-Host " CultCache: $cultCachePath"
|
||||
Write-Host " Stdout: $stdoutLogPath"
|
||||
Write-Host " Stderr: $stderrLogPath"
|
||||
Write-Host "Run this script again to replace only this script-owned process."
|
||||
if (-not $Headless) {
|
||||
Write-Host "Run with -Reopen to reopen this visible slot without rebuilding."
|
||||
}
|
||||
|
||||
Remove-OldSlots
|
||||
& $script -ClientProject $clientProject @RemainingArgs
|
||||
|
|
|
|||
|
|
@ -1,39 +1,5 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$statePath = Join-Path $repoRoot "artifacts\dev-reload\state.clixml"
|
||||
$fensalirRoot = Resolve-Path (Join-Path $repoRoot "..\Fensalir")
|
||||
$script = Join-Path $fensalirRoot "scripts\dev-stop.ps1"
|
||||
|
||||
if (-not (Test-Path $statePath)) {
|
||||
Write-Host "No dev-reload state file exists."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$state = Import-Clixml -Path $statePath
|
||||
if (-not $state.pid) {
|
||||
Write-Host "No script-owned Aquarium PID is recorded."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$process = Get-Process -Id ([int]$state.pid) -ErrorAction SilentlyContinue
|
||||
if (-not $process) {
|
||||
Write-Host "Recorded Aquarium process $($state.pid) is not running."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$recordedPath = [string]$state.slot
|
||||
$commandLine = $null
|
||||
try {
|
||||
$commandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine
|
||||
}
|
||||
catch {
|
||||
$commandLine = $null
|
||||
}
|
||||
|
||||
if (-not $commandLine -or $commandLine -notlike "*$recordedPath*") {
|
||||
Write-Host "PID $($state.pid) no longer belongs to the recorded slot; leaving it alone."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Stopping script-owned Aquarium process $($state.pid)."
|
||||
Stop-Process -Id ([int]$state.pid) -Force
|
||||
& $script
|
||||
|
|
|
|||
|
|
@ -1,561 +1,11 @@
|
|||
param(
|
||||
[switch]$Headless,
|
||||
[switch]$NoInitialLaunch,
|
||||
[switch]$ReopenWhenClosed,
|
||||
[switch]$Once,
|
||||
[string]$ClientProject = "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj",
|
||||
[int]$IntervalMilliseconds = 1000,
|
||||
[int]$DebounceMilliseconds = 350,
|
||||
[int]$RetainSlots = 12,
|
||||
[int]$LiveReloadAckTimeoutSeconds = 6
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$RemainingArgs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$reloadScript = Join-Path $PSScriptRoot "dev-reload.ps1"
|
||||
$liveProjectPath = if ([System.IO.Path]::IsPathRooted($ClientProject)) { $ClientProject } else { Join-Path $repoRoot $ClientProject }
|
||||
$liveProjectPath = (Resolve-Path $liveProjectPath).Path
|
||||
$liveAssemblyName = [System.IO.Path]::GetFileNameWithoutExtension($liveProjectPath)
|
||||
$clientSourceRoot = Split-Path $liveProjectPath
|
||||
$watchLogPath = Join-Path $repoRoot "artifacts\dev-reload\watch.log"
|
||||
$liveSlotRoot = Join-Path $repoRoot "artifacts\dev-reload\live-slots"
|
||||
$liveReloadPointerPath = Join-Path $repoRoot "artifacts\dev-reload\live-current.txt"
|
||||
$shaderPath = Join-Path $repoRoot "src\Aquarium.Engine\Render\Shaders\Aquarium.hlsl"
|
||||
$engineShaderRoot = Join-Path $repoRoot "src\Aquarium.Engine\Render\Shaders"
|
||||
$clientShaderRoot = Join-Path $clientSourceRoot "Shaders"
|
||||
$visibleStatePath = Join-Path $repoRoot "artifacts\dev-reload\state.clixml"
|
||||
$headlessStatePath = Join-Path $repoRoot "artifacts\dev-reload\headless-state.clixml"
|
||||
$ownedProcessStatePath = if ($Headless) { $headlessStatePath } else { $visibleStatePath }
|
||||
$fensalirRoot = Resolve-Path (Join-Path $repoRoot "..\Fensalir")
|
||||
$clientProject = Join-Path $repoRoot "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj"
|
||||
$script = Join-Path $fensalirRoot "scripts\dev-watch.ps1"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $watchLogPath) | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $liveSlotRoot | Out-Null
|
||||
|
||||
if ($Headless -and $ReopenWhenClosed) {
|
||||
throw "-ReopenWhenClosed is for the visible dev window; do not combine it with -Headless."
|
||||
}
|
||||
|
||||
function Write-WatchLog([string]$message) {
|
||||
$line = "$(Get-Date -Format o) $message"
|
||||
Write-Host $line
|
||||
Add-Content -Path $watchLogPath -Value $line
|
||||
}
|
||||
|
||||
function Get-SourceFiles {
|
||||
$roots = @(
|
||||
(Join-Path $repoRoot "src"),
|
||||
(Join-Path $repoRoot "scripts"),
|
||||
(Join-Path $repoRoot "Aquarium.Engine.sln"),
|
||||
(Join-Path $repoRoot "global.json")
|
||||
)
|
||||
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path $root)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$item = Get-Item -LiteralPath $root
|
||||
if (-not $item.PSIsContainer) {
|
||||
$item
|
||||
continue
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $item.FullName -Recurse -File |
|
||||
Where-Object {
|
||||
$_.FullName -notmatch "\\bin\\" -and
|
||||
$_.FullName -notmatch "\\obj\\" -and
|
||||
$_.FullName -notmatch "\\artifacts\\" -and
|
||||
(
|
||||
$_.Extension -in @(".cs", ".csproj", ".json", ".sln", ".ps1") -or
|
||||
$_.FullName -like (Join-Path $repoRoot "src\Aquarium.Engine\Assets\*")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ShaderFiles {
|
||||
foreach ($root in @($engineShaderRoot, $clientShaderRoot)) {
|
||||
if (-not (Test-Path $root)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
Where-Object { $_.Extension -in @(".hlsl", ".hlsli") }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LiveFiles {
|
||||
Get-SourceFiles | Where-Object {
|
||||
$_.FullName -like (Join-Path $clientSourceRoot "*")
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RestartFiles {
|
||||
Get-SourceFiles | Where-Object {
|
||||
$_.FullName -notlike (Join-Path $clientSourceRoot "*")
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Fingerprint($files) {
|
||||
$fingerprintInput = $files |
|
||||
Sort-Object FullName |
|
||||
ForEach-Object {
|
||||
"$($_.FullName)|$($_.Length)|$($_.LastWriteTimeUtc.Ticks)"
|
||||
}
|
||||
|
||||
$text = [string]::Join("`n", $fingerprintInput)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$hashBytes = $sha.ComputeHash($bytes)
|
||||
return -join ($hashBytes | ForEach-Object { $_.ToString("x2") })
|
||||
}
|
||||
finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Hash([string]$text) {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$hashBytes = $sha.ComputeHash($bytes)
|
||||
return -join ($hashBytes | ForEach-Object { $_.ToString("x2") })
|
||||
}
|
||||
finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AquariumFrameContractText {
|
||||
if (-not (Test-Path $shaderPath)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
$text = Get-Content -Raw -LiteralPath $shaderPath
|
||||
$match = [regex]::Match(
|
||||
$text,
|
||||
"cbuffer\s+AquariumFrame\s*:\s*register\(b0\)\s*\{(?<body>.*?)\};",
|
||||
[System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||
if (-not $match.Success) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return $match.Value
|
||||
}
|
||||
|
||||
function Get-SourceFingerprint {
|
||||
$fileFingerprint = Get-Fingerprint (Get-SourceFiles)
|
||||
$contractFingerprint = Get-Hash (Get-AquariumFrameContractText)
|
||||
Get-Hash "$fileFingerprint`nAquariumFrame:$contractFingerprint"
|
||||
}
|
||||
|
||||
function Get-ShaderFingerprint {
|
||||
Get-Fingerprint (Get-ShaderFiles)
|
||||
}
|
||||
|
||||
function Get-AllFingerprint {
|
||||
$sourceFingerprint = Get-SourceFingerprint
|
||||
$shaderFingerprint = Get-ShaderFingerprint
|
||||
Get-Hash "$sourceFingerprint`nShaders:$shaderFingerprint"
|
||||
}
|
||||
|
||||
function Get-LiveFingerprint {
|
||||
Get-Fingerprint (Get-LiveFiles)
|
||||
}
|
||||
|
||||
function Get-RestartFingerprint {
|
||||
$fileFingerprint = Get-Fingerprint (Get-RestartFiles)
|
||||
$contractFingerprint = Get-Hash (Get-AquariumFrameContractText)
|
||||
Get-Hash "$fileFingerprint`nAquariumFrame:$contractFingerprint"
|
||||
}
|
||||
|
||||
function Wait-StableFingerprints {
|
||||
$first = @{
|
||||
all = Get-AllFingerprint
|
||||
live = Get-LiveFingerprint
|
||||
restart = Get-RestartFingerprint
|
||||
shaders = Get-ShaderFingerprint
|
||||
}
|
||||
Start-Sleep -Milliseconds $DebounceMilliseconds
|
||||
$second = @{
|
||||
all = Get-AllFingerprint
|
||||
live = Get-LiveFingerprint
|
||||
restart = Get-RestartFingerprint
|
||||
shaders = Get-ShaderFingerprint
|
||||
}
|
||||
|
||||
while ($first.all -ne $second.all) {
|
||||
$first = $second
|
||||
Start-Sleep -Milliseconds $DebounceMilliseconds
|
||||
$second = @{
|
||||
all = Get-AllFingerprint
|
||||
live = Get-LiveFingerprint
|
||||
restart = Get-RestartFingerprint
|
||||
shaders = Get-ShaderFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
return $second
|
||||
}
|
||||
|
||||
function Invoke-Reload {
|
||||
param([string]$reason)
|
||||
|
||||
Write-WatchLog "Reload requested: $reason"
|
||||
|
||||
$arguments = @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", $reloadScript,
|
||||
"-ClientProject", $liveProjectPath,
|
||||
"-RetainSlots", "$RetainSlots"
|
||||
)
|
||||
|
||||
if ($Headless) {
|
||||
$arguments += "-Headless"
|
||||
}
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
& powershell @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dev-reload failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-OwnedProcessState {
|
||||
if (-not (Test-Path $ownedProcessStatePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
return Import-Clixml -Path $ownedProcessStatePath
|
||||
}
|
||||
catch {
|
||||
Write-WatchLog "Could not read script-owned process state. $($_.Exception.Message)"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RecordedProcessRunning {
|
||||
$state = Get-OwnedProcessState
|
||||
if (-not $state -or -not $state.pid -or -not $state.slot) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$process = Get-Process -Id ([int]$state.pid) -ErrorAction SilentlyContinue
|
||||
if (-not $process) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$recordedPath = [string]$state.slot
|
||||
$commandLine = $null
|
||||
try {
|
||||
$commandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)").CommandLine
|
||||
}
|
||||
catch {
|
||||
$commandLine = $null
|
||||
}
|
||||
|
||||
return [bool]($commandLine -and $commandLine -like "*$recordedPath*")
|
||||
}
|
||||
|
||||
function Test-OwnedProcessRunning {
|
||||
if ($NoInitialLaunch) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return Test-RecordedProcessRunning
|
||||
}
|
||||
|
||||
function Invoke-Reopen {
|
||||
param([string]$reason)
|
||||
|
||||
if ($Headless) {
|
||||
Invoke-Reload $reason
|
||||
return
|
||||
}
|
||||
|
||||
Write-WatchLog "Reopen requested: $reason"
|
||||
|
||||
$arguments = @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", $reloadScript,
|
||||
"-Reopen",
|
||||
"-ClientProject", $liveProjectPath,
|
||||
"-RetainSlots", "$RetainSlots"
|
||||
)
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
& powershell @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dev-reload -Reopen failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-LiveReload {
|
||||
param([string]$reason)
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$liveSlot = Join-Path $liveSlotRoot "$timestamp-$([guid]::NewGuid().ToString("N").Substring(0, 8))"
|
||||
New-Item -ItemType Directory -Force -Path $liveSlot | Out-Null
|
||||
|
||||
Write-WatchLog "Live reload requested: $reason"
|
||||
dotnet build $liveProjectPath -c Debug -o $liveSlot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "client runtime build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$liveAssemblyPath = Join-Path $liveSlot "$liveAssemblyName.dll"
|
||||
if (-not (Test-Path $liveAssemblyPath)) {
|
||||
throw "Expected client runtime assembly was not produced: $liveAssemblyPath"
|
||||
}
|
||||
|
||||
Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8
|
||||
Write-WatchLog "Live reload pointer updated: $liveAssemblyPath"
|
||||
Wait-LiveReloadAcknowledged $liveAssemblyPath
|
||||
}
|
||||
|
||||
function Copy-ShaderTree {
|
||||
param(
|
||||
[string]$SourceRoot,
|
||||
[string]$DestinationRoot
|
||||
)
|
||||
|
||||
if (-not (Test-Path $SourceRoot)) {
|
||||
return
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $SourceRoot -Recurse -File |
|
||||
Where-Object { $_.Extension -in @(".hlsl", ".hlsli") } |
|
||||
ForEach-Object {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $_.FullName)
|
||||
$destinationPath = Join-Path $DestinationRoot $relativePath
|
||||
$destinationDirectory = Split-Path $destinationPath
|
||||
New-Item -ItemType Directory -Force -Path $destinationDirectory | Out-Null
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $destinationPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Read-LogTextAfterLength {
|
||||
param(
|
||||
[string]$Path,
|
||||
[long]$StartLength
|
||||
)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
$stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
if ($StartLength -gt 0 -and $StartLength -lt $stream.Length) {
|
||||
$stream.Seek($StartLength, [System.IO.SeekOrigin]::Begin) | Out-Null
|
||||
}
|
||||
elseif ($StartLength -ge $stream.Length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
$reader = [System.IO.StreamReader]::new($stream)
|
||||
try {
|
||||
return $reader.ReadToEnd()
|
||||
}
|
||||
finally {
|
||||
$reader.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ShaderHotReloadAcknowledged {
|
||||
param(
|
||||
[string]$StdoutLog,
|
||||
[string]$StderrLog,
|
||||
[long]$StdoutStartLength,
|
||||
[long]$StderrStartLength
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $LiveReloadAckTimeoutSeconds))
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$stdoutText = Read-LogTextAfterLength -Path $StdoutLog -StartLength $StdoutStartLength
|
||||
if ($stdoutText -match "D3D12 shader hot reload applied") {
|
||||
Write-WatchLog "Shader hot reload acknowledged by renderer."
|
||||
return
|
||||
}
|
||||
|
||||
$stderrText = Read-LogTextAfterLength -Path $StderrLog -StartLength $StderrStartLength
|
||||
if ($stderrText -match "D3D12 shader hot reload failed") {
|
||||
throw "renderer reported shader hot reload failure. See $StderrLog"
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
throw "shader files were copied, but renderer did not acknowledge hot reload within $LiveReloadAckTimeoutSeconds seconds. Check $StdoutLog and $StderrLog."
|
||||
}
|
||||
|
||||
function Invoke-ShaderHotReload {
|
||||
param([string]$reason)
|
||||
|
||||
Write-WatchLog "Shader hot reload requested: $reason"
|
||||
|
||||
$state = Get-OwnedProcessState
|
||||
if (-not $state -or -not $state.slot) {
|
||||
throw "no script-owned process slot is recorded for shader hot reload."
|
||||
}
|
||||
|
||||
if (-not (Test-RecordedProcessRunning)) {
|
||||
throw "no script-owned process is running to receive shader hot reload."
|
||||
}
|
||||
|
||||
$slotPath = [string]$state.slot
|
||||
$shaderDestinationRoot = Join-Path $slotPath "Render\Shaders"
|
||||
New-Item -ItemType Directory -Force -Path $shaderDestinationRoot | Out-Null
|
||||
|
||||
$stdoutLog = if ($state.PSObject.Properties.Name -contains "stdoutLog" -and $state.stdoutLog) { [string]$state.stdoutLog } else { $stdoutLogPath }
|
||||
$stderrLog = if ($state.PSObject.Properties.Name -contains "stderrLog" -and $state.stderrLog) { [string]$state.stderrLog } else { $stderrLogPath }
|
||||
$stdoutStartLength = if (Test-Path $stdoutLog) { (Get-Item -LiteralPath $stdoutLog).Length } else { 0 }
|
||||
$stderrStartLength = if (Test-Path $stderrLog) { (Get-Item -LiteralPath $stderrLog).Length } else { 0 }
|
||||
|
||||
Copy-ShaderTree -SourceRoot $engineShaderRoot -DestinationRoot $shaderDestinationRoot
|
||||
Copy-ShaderTree -SourceRoot $clientShaderRoot -DestinationRoot $shaderDestinationRoot
|
||||
Write-WatchLog "Shader files copied into running slot: $shaderDestinationRoot"
|
||||
|
||||
Wait-ShaderHotReloadAcknowledged -StdoutLog $stdoutLog -StderrLog $stderrLog -StdoutStartLength $stdoutStartLength -StderrStartLength $stderrStartLength
|
||||
}
|
||||
|
||||
function Wait-LiveReloadAcknowledged {
|
||||
param([string]$liveAssemblyPath)
|
||||
|
||||
if (-not (Test-RecordedProcessRunning)) {
|
||||
throw "live reload pointer was updated, but no script-owned process is running to load it."
|
||||
}
|
||||
|
||||
$state = Get-OwnedProcessState
|
||||
if (-not $state -or -not $state.stdoutLog) {
|
||||
Write-WatchLog "Live reload acknowledgement skipped: no running process log is recorded."
|
||||
return
|
||||
}
|
||||
|
||||
$stdoutLog = [string]$state.stdoutLog
|
||||
$stderrLog = if ($state.stderrLog) { [string]$state.stderrLog } else { $null }
|
||||
$timeoutSeconds = [Math]::Max(1, $LiveReloadAckTimeoutSeconds)
|
||||
$deadline = (Get-Date).AddSeconds($timeoutSeconds)
|
||||
$appliedLine = "Client runtime reload applied: $liveAssemblyPath"
|
||||
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path $stdoutLog) {
|
||||
$stdoutTail = (Get-Content -Path $stdoutLog -Tail 120 -ErrorAction SilentlyContinue) -join "`n"
|
||||
if ($stdoutTail.Contains($appliedLine)) {
|
||||
Write-WatchLog "Live reload acknowledged by host: $liveAssemblyPath"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ($stderrLog -and (Test-Path $stderrLog)) {
|
||||
$stderrTail = (Get-Content -Path $stderrLog -Tail 120 -ErrorAction SilentlyContinue) -join "`n"
|
||||
if ($stderrTail -match "Client runtime reload failed") {
|
||||
throw "host reported client runtime reload failure. See $stderrLog"
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
throw "live reload pointer was updated, but host did not acknowledge loading $liveAssemblyPath within $timeoutSeconds seconds. Check $stdoutLog and $stderrLog."
|
||||
}
|
||||
|
||||
$lastGoodFingerprint = Wait-StableFingerprints
|
||||
$lastFailedFingerprint = $null
|
||||
|
||||
if (-not $NoInitialLaunch) {
|
||||
try {
|
||||
Invoke-Reload "initial launch"
|
||||
$lastGoodFingerprint = Wait-StableFingerprints
|
||||
$lastFailedFingerprint = $null
|
||||
}
|
||||
catch {
|
||||
$lastFailedFingerprint = $lastGoodFingerprint.all
|
||||
Write-WatchLog "Initial launch failed; keeping any previous good process alive. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-WatchLog "Watching Aquarium sources. Press Ctrl+C to stop."
|
||||
|
||||
do {
|
||||
Start-Sleep -Milliseconds $IntervalMilliseconds
|
||||
$currentFingerprint = Wait-StableFingerprints
|
||||
|
||||
if (-not (Test-OwnedProcessRunning)) {
|
||||
try {
|
||||
if ($ReopenWhenClosed) {
|
||||
Invoke-Reopen "script-owned process is not running"
|
||||
}
|
||||
else {
|
||||
Invoke-Reload "script-owned process is not running"
|
||||
}
|
||||
|
||||
$lastGoodFingerprint = Wait-StableFingerprints
|
||||
$lastFailedFingerprint = $null
|
||||
$currentFingerprint = $lastGoodFingerprint
|
||||
}
|
||||
catch {
|
||||
$lastFailedFingerprint = $currentFingerprint.all
|
||||
Write-WatchLog "Relaunch failed; watcher remains alive. $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($Once) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentFingerprint.all -eq $lastGoodFingerprint.all -or $currentFingerprint.all -eq $lastFailedFingerprint) {
|
||||
if ($Once) {
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if ($currentFingerprint.restart -eq $lastGoodFingerprint.restart -and
|
||||
$currentFingerprint.live -eq $lastGoodFingerprint.live -and
|
||||
$currentFingerprint.shaders -ne $lastGoodFingerprint.shaders) {
|
||||
Invoke-ShaderHotReload "shader source change"
|
||||
}
|
||||
elseif ($currentFingerprint.restart -eq $lastGoodFingerprint.restart -and $currentFingerprint.live -ne $lastGoodFingerprint.live) {
|
||||
Invoke-LiveReload "live source change"
|
||||
if ($currentFingerprint.shaders -ne $lastGoodFingerprint.shaders) {
|
||||
Invoke-ShaderHotReload "shader source change accompanying live reload"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Invoke-Reload "host/core/content source change"
|
||||
}
|
||||
|
||||
$lastGoodFingerprint = $currentFingerprint
|
||||
$lastFailedFingerprint = $null
|
||||
}
|
||||
catch {
|
||||
$lastFailedFingerprint = $currentFingerprint.all
|
||||
Write-WatchLog "Reload failed; previous good process stays alive. $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($Once) {
|
||||
break
|
||||
}
|
||||
} while ($true)
|
||||
& $script -ClientProject $clientProject @RemainingArgs
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
param(
|
||||
[ValidateSet("Auto", "FARender", "JWildfireJava")]
|
||||
[string]$Renderer = "Auto",
|
||||
[string]$JWildfireRoot = $env:AQUARIUM_JWILDFIRE_ROOT,
|
||||
[string]$FARender = $env:AQUARIUM_FARENDER,
|
||||
[string]$JWildfireJar = $env:AQUARIUM_JWILDFIRE_JAR,
|
||||
[string]$Java = $env:AQUARIUM_JAVA,
|
||||
[string]$Flame = "artifacts\reference-tools\j-wildfire-9.00\lib\FARenderJWF\selftest.flame",
|
||||
[int]$Width = 1024,
|
||||
[int]$Height = 1024,
|
||||
[int]$Quality = 5000,
|
||||
[string]$OutputDirectory = "artifacts\fractal-flame-jwildfire-reference",
|
||||
[switch]$NoCuda,
|
||||
[switch]$NoDensityEstimation
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($JWildfireRoot)) {
|
||||
$defaultRoot = Join-Path $repoRoot "artifacts\reference-tools\j-wildfire-9.00"
|
||||
if (Test-Path $defaultRoot) {
|
||||
$JWildfireRoot = $defaultRoot
|
||||
}
|
||||
}
|
||||
|
||||
$flameInput = if ([System.IO.Path]::IsPathRooted($Flame)) { $Flame } else { Join-Path $repoRoot $Flame }
|
||||
$flamePath = (Resolve-Path $flameInput).Path
|
||||
$outDir = if ([System.IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory }
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($FARender) -and -not [string]::IsNullOrWhiteSpace($JWildfireRoot)) {
|
||||
$candidate = Join-Path $JWildfireRoot "lib\FARenderJWF\FARender.exe"
|
||||
if (Test-Path $candidate) {
|
||||
$FARender = $candidate
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($JWildfireJar) -and -not [string]::IsNullOrWhiteSpace($JWildfireRoot)) {
|
||||
$candidate = Join-Path $JWildfireRoot "lib\j-wildfire.jar"
|
||||
if (Test-Path $candidate) {
|
||||
$JWildfireJar = $candidate
|
||||
}
|
||||
}
|
||||
|
||||
if ($Renderer -eq "Auto") {
|
||||
$Renderer = if (-not [string]::IsNullOrWhiteSpace($FARender)) { "FARender" } else { "JWildfireJava" }
|
||||
}
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
|
||||
if ($Renderer -eq "FARender") {
|
||||
if ([string]::IsNullOrWhiteSpace($FARender)) {
|
||||
throw "Set -FARender, AQUARIUM_FARENDER, or AQUARIUM_JWILDFIRE_ROOT. FARender is the preferred scriptable GPU visual reference."
|
||||
}
|
||||
|
||||
$farenderPath = (Resolve-Path $FARender).Path
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($flamePath)
|
||||
$outputPath = Join-Path $outDir "$baseName-farender-$stamp.png"
|
||||
$arguments = @($Width, $Height, $flamePath, "-o", $outputPath, "-q", $Quality)
|
||||
if ($NoDensityEstimation) {
|
||||
$arguments += "-nde"
|
||||
}
|
||||
|
||||
if (-not $NoCuda) {
|
||||
$arguments += "-cuda"
|
||||
}
|
||||
|
||||
Push-Location (Split-Path -Parent $farenderPath)
|
||||
try {
|
||||
& $farenderPath @arguments
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "FARender failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$rendered = Get-Item $outputPath -ErrorAction SilentlyContinue
|
||||
if ($null -eq $rendered -or $rendered.Length -le 0) {
|
||||
throw "FARender reported success but did not produce a non-empty image at $outputPath."
|
||||
}
|
||||
|
||||
Write-Host "JWildfire FARender reference image: $outputPath"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Java)) {
|
||||
$localJava = Get-ChildItem (Join-Path $repoRoot "artifacts\reference-tools\temurin-jre-17") -Recurse -Filter java.exe -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($null -ne $localJava) {
|
||||
$Java = $localJava.FullName
|
||||
}
|
||||
else {
|
||||
$javaCommand = Get-Command java -ErrorAction SilentlyContinue
|
||||
if ($null -eq $javaCommand) {
|
||||
throw "Set -Java or AQUARIUM_JAVA to a Java executable. JWildfire's Java renderer needs Java."
|
||||
}
|
||||
|
||||
$Java = $javaCommand.Source
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($JWildfireJar)) {
|
||||
throw "Set -JWildfireJar, AQUARIUM_JWILDFIRE_JAR, or AQUARIUM_JWILDFIRE_ROOT to a local JWildfire install."
|
||||
}
|
||||
|
||||
$jarPath = (Resolve-Path $JWildfireJar).Path
|
||||
$batchDir = Join-Path $outDir "jwildfire-batch-$stamp"
|
||||
New-Item -ItemType Directory -Force -Path $batchDir | Out-Null
|
||||
Copy-Item -LiteralPath $flamePath -Destination (Join-Path $batchDir (Split-Path -Leaf $flamePath))
|
||||
|
||||
& $Java -cp $jarPath org.jwildfire.create.tina.batch.HeadlessBatchRendererController $batchDir $Width $Height $Quality
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "JWildfire headless renderer failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$renderedImages = Get-ChildItem $batchDir -Include *.png,*.jpg,*.jpeg -Recurse -ErrorAction SilentlyContinue
|
||||
if ($renderedImages.Count -eq 0) {
|
||||
Write-Warning "JWildfire Java batch completed, but no image was written into the batch directory. Check JWildfire's configured output folder or use -Renderer FARender for explicit -o output."
|
||||
}
|
||||
|
||||
Write-Host "JWildfire reference render directory: $batchDir"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
param(
|
||||
[string]$Flame = "tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame",
|
||||
[int]$Samples = 8192,
|
||||
[int]$BurnIn = 64,
|
||||
[string]$HistogramSize = "64x64",
|
||||
[string]$Bounds = "-8,-8,8,8",
|
||||
[string]$Seed = "0x0BADC0DE"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
dotnet run --project (Join-Path $repoRoot "tools\Aquarium.Fractal.Receipt\Aquarium.Fractal.Receipt.csproj") -c Release -- `
|
||||
--flame (Join-Path $repoRoot $Flame) `
|
||||
--histogram-samples $Samples `
|
||||
--histogram-burn-in $BurnIn `
|
||||
--histogram-size $HistogramSize `
|
||||
--histogram-bounds $Bounds `
|
||||
--seed $Seed `
|
||||
--out (Join-Path $repoRoot "artifacts\fractal-flame-receipts")
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
param(
|
||||
[string]$Flam3Render = $env:AQUARIUM_FLAM3_RENDER,
|
||||
[string]$Flame = "tests\Aquarium.Engine.Fractal.Tests\Fixtures\Apophysis\linear-spherical-bubble.flame",
|
||||
[string]$Seed = "0x0BADC0DE",
|
||||
[string]$QualityScale = "1",
|
||||
[string]$OutputDirectory = "artifacts\fractal-flame-reference"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Flam3Render)) {
|
||||
throw "Set -Flam3Render or AQUARIUM_FLAM3_RENDER to a local flam3-render executable. Aquarium does not vendor FLAM3 or Apophysis."
|
||||
}
|
||||
|
||||
$rendererCommand = Get-Command $Flam3Render -ErrorAction SilentlyContinue
|
||||
if ($null -eq $rendererCommand) {
|
||||
throw "Could not find flam3-render executable: $Flam3Render"
|
||||
}
|
||||
|
||||
$renderer = $rendererCommand.Source
|
||||
$flameInput = if ([System.IO.Path]::IsPathRooted($Flame)) { $Flame } else { Join-Path $repoRoot $Flame }
|
||||
$flamePath = (Resolve-Path $flameInput).Path
|
||||
$outDir = Join-Path $repoRoot $OutputDirectory
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$imagePath = Join-Path $outDir "flam3-reference-$stamp.ppm"
|
||||
|
||||
$previous = @{
|
||||
in = $env:in
|
||||
out = $env:out
|
||||
format = $env:format
|
||||
seed = $env:seed
|
||||
qs = $env:qs
|
||||
verbose = $env:verbose
|
||||
}
|
||||
|
||||
try {
|
||||
$env:in = $flamePath
|
||||
$env:out = $imagePath
|
||||
$env:format = "ppm"
|
||||
$env:seed = $Seed
|
||||
$env:qs = $QualityScale
|
||||
$env:verbose = "0"
|
||||
& $renderer
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "flam3-render failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$env:in = $previous.in
|
||||
$env:out = $previous.out
|
||||
$env:format = $previous.format
|
||||
$env:seed = $previous.seed
|
||||
$env:qs = $previous.qs
|
||||
$env:verbose = $previous.verbose
|
||||
}
|
||||
|
||||
dotnet run --project (Join-Path $repoRoot "tools\Aquarium.Fractal.Receipt\Aquarium.Fractal.Receipt.csproj") -c Release -- `
|
||||
--reference-ppm $imagePath `
|
||||
--out $outDir
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
param(
|
||||
[int]$Splats = 2000000,
|
||||
[int]$WarmupSplatUpdates = 2000000,
|
||||
[int]$SplatUpdates = 2000000,
|
||||
[int]$Warmup = 30,
|
||||
[int]$Frames = 120,
|
||||
[int]$Depth = 8,
|
||||
[int]$Candidates = 2,
|
||||
[int]$ReservoirUpdates = 50000,
|
||||
[int]$ProgramTransforms = 0,
|
||||
[string]$ProgramFlame = "",
|
||||
[switch]$VisualParity,
|
||||
[int]$VisualParityReferenceSamples = 1000000,
|
||||
[string]$HistogramSize = "64x64",
|
||||
[string]$HistogramBounds = "-8,-8,8,8",
|
||||
[string]$ReferenceDensityPpm = "",
|
||||
[string[]]$VisualParityView = @(),
|
||||
[string]$VisualParityImageDirectory = "",
|
||||
[string]$VisualParityImagePrefix = "visual-parity",
|
||||
[int]$ReadbackSplats = 64
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$programArgs = @("--program-transforms", "$ProgramTransforms")
|
||||
if (-not [string]::IsNullOrWhiteSpace($ProgramFlame)) {
|
||||
$programFlamePath = if ([System.IO.Path]::IsPathRooted($ProgramFlame)) { $ProgramFlame } else { Join-Path $repoRoot $ProgramFlame }
|
||||
$programArgs = @("--program-flame", $programFlamePath)
|
||||
}
|
||||
|
||||
$parityArgs = @()
|
||||
if ($VisualParity -or -not [string]::IsNullOrWhiteSpace($ReferenceDensityPpm)) {
|
||||
$parityArgs = @(
|
||||
"--visual-parity-reference-samples", "$VisualParityReferenceSamples",
|
||||
"--histogram-size", $HistogramSize,
|
||||
"--histogram-bounds", $HistogramBounds
|
||||
)
|
||||
if ($VisualParity) {
|
||||
$parityArgs = @("--visual-parity") + $parityArgs
|
||||
}
|
||||
foreach ($view in $VisualParityView) {
|
||||
$parityArgs += @("--visual-parity-view", $view)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($VisualParityImageDirectory)) {
|
||||
$parityImageDir = if ([System.IO.Path]::IsPathRooted($VisualParityImageDirectory)) { $VisualParityImageDirectory } else { Join-Path $repoRoot $VisualParityImageDirectory }
|
||||
$parityArgs += @("--visual-parity-image-dir", $parityImageDir, "--visual-parity-image-prefix", $VisualParityImagePrefix)
|
||||
}
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($ReferenceDensityPpm)) {
|
||||
$referenceDensityPpmPath = if ([System.IO.Path]::IsPathRooted($ReferenceDensityPpm)) { $ReferenceDensityPpm } else { Join-Path $repoRoot $ReferenceDensityPpm }
|
||||
$parityArgs += @("--reference-density-ppm", $referenceDensityPpmPath)
|
||||
}
|
||||
|
||||
dotnet run --project (Join-Path $repoRoot "tools\Aquarium.Fractal.Receipt\Aquarium.Fractal.Receipt.csproj") -c Release -- `
|
||||
--splats $Splats `
|
||||
--warmup-splat-updates $WarmupSplatUpdates `
|
||||
--splat-updates $SplatUpdates `
|
||||
--warmup $Warmup `
|
||||
--frames $Frames `
|
||||
--depth $Depth `
|
||||
--candidates $Candidates `
|
||||
--reservoir-updates $ReservoirUpdates `
|
||||
$programArgs `
|
||||
$parityArgs `
|
||||
--readback-splats $ReadbackSplats
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
param(
|
||||
[string]$Destination = "artifacts\reference-tools",
|
||||
[string]$JWildfireUrl = "https://jwildfire.overwhale.com/downloads/j-wildfire-9.00.zip",
|
||||
[string]$TemurinJreUrl = "https://api.adoptium.net/v3/binary/latest/17/ga/windows/x64/jre/hotspot/normal/eclipse",
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$destinationPath = if ([System.IO.Path]::IsPathRooted($Destination)) { $Destination } else { Join-Path $repoRoot $Destination }
|
||||
New-Item -ItemType Directory -Force -Path $destinationPath | Out-Null
|
||||
|
||||
$jwildfireZip = Join-Path $destinationPath "j-wildfire-9.00.zip"
|
||||
$jwildfireRoot = Join-Path $destinationPath "j-wildfire-9.00"
|
||||
if ($Force -or -not (Test-Path $jwildfireRoot)) {
|
||||
if ($Force -or -not (Test-Path $jwildfireZip)) {
|
||||
Invoke-WebRequest -Uri $JWildfireUrl -OutFile $jwildfireZip
|
||||
}
|
||||
|
||||
Expand-Archive -LiteralPath $jwildfireZip -DestinationPath $destinationPath -Force
|
||||
}
|
||||
|
||||
$temurinZip = Join-Path $destinationPath "temurin-jre-17.zip"
|
||||
$temurinRoot = Join-Path $destinationPath "temurin-jre-17"
|
||||
if ($Force -or -not (Test-Path $temurinRoot)) {
|
||||
if ($Force -or -not (Test-Path $temurinZip)) {
|
||||
Invoke-WebRequest -Uri $TemurinJreUrl -OutFile $temurinZip
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $temurinRoot | Out-Null
|
||||
Expand-Archive -LiteralPath $temurinZip -DestinationPath $temurinRoot -Force
|
||||
}
|
||||
|
||||
$java = Get-ChildItem $temurinRoot -Recurse -Filter java.exe | Select-Object -First 1
|
||||
$farender = Join-Path $jwildfireRoot "lib\FARenderJWF\FARender.exe"
|
||||
$jar = Join-Path $jwildfireRoot "lib\j-wildfire.jar"
|
||||
|
||||
if (-not (Test-Path $jar)) {
|
||||
throw "JWildfire install completed, but lib\j-wildfire.jar was not found under $jwildfireRoot."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $farender)) {
|
||||
Write-Warning "JWildfire installed, but FARender.exe was not found. Java rendering may still work."
|
||||
}
|
||||
|
||||
if ($null -eq $java) {
|
||||
throw "Temurin JRE install completed, but java.exe was not found under $temurinRoot."
|
||||
}
|
||||
|
||||
Write-Host "AQUARIUM_JWILDFIRE_ROOT=$jwildfireRoot"
|
||||
Write-Host "AQUARIUM_JWILDFIRE_JAR=$jar"
|
||||
Write-Host "AQUARIUM_FARENDER=$farender"
|
||||
Write-Host "AQUARIUM_JAVA=$($java.FullName)"
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
param(
|
||||
[string]$Source = "artifacts\reference-tools\j-wildfire-9.00\lib\FARenderJWF\selftest.flame",
|
||||
[string]$Output = "artifacts\fractal-flame-jwildfire-reference\julian-disc-baby.flame"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$sourcePath = if ([System.IO.Path]::IsPathRooted($Source)) { $Source } else { Join-Path $repoRoot $Source }
|
||||
$outputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $repoRoot $Output }
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $outputPath) | Out-Null
|
||||
|
||||
$text = [System.IO.File]::ReadAllText((Resolve-Path $sourcePath))
|
||||
$text = $text.Replace('JulianDisc - 535389044', 'julian-disc-baby')
|
||||
$text = $text.Replace('jwf_julian_power="126"', 'jwf_julian_power="8"')
|
||||
$text = $text.Replace('jwf_gaussian_blur="0.039366260122290476"', 'jwf_gaussian_blur="0.0"')
|
||||
$text = $text.Replace('wfield_var_amount="0.21416904132907888"', 'wfield_var_amount="0.0"')
|
||||
$text = $text.Replace('wfield_color_amount="0.26564915787570376"', 'wfield_color_amount="0.0"')
|
||||
$text = $text.Replace('wfield_jitter_amount="0.13887944166913668"', 'wfield_jitter_amount="0.0"')
|
||||
|
||||
[System.IO.File]::WriteAllText($outputPath, $text, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Host "JWildfire baby fixture: $outputPath"
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
param(
|
||||
[switch]$Zip,
|
||||
[switch]$NoSelfContained,
|
||||
[switch]$Clean,
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Runtime = "win-x64",
|
||||
[string]$OutputRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$projectPath = Join-Path $repoRoot "src\Aquarium.Engine\Aquarium.Engine.csproj"
|
||||
$liveProjectPath = Join-Path $repoRoot "src\Aquarium.Epiphany\Aquarium.Epiphany.csproj"
|
||||
$publishRoot = if ([string]::IsNullOrWhiteSpace($OutputRoot)) {
|
||||
Join-Path $repoRoot "artifacts\publish"
|
||||
}
|
||||
else {
|
||||
$OutputRoot
|
||||
}
|
||||
$publishPath = Join-Path $publishRoot $Runtime
|
||||
$zipPath = Join-Path $publishRoot "EpiphanyAquariumEngine-$Runtime.zip"
|
||||
|
||||
if ($Clean -and (Test-Path $publishPath)) {
|
||||
Remove-Item -LiteralPath $publishPath -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $publishPath | Out-Null
|
||||
|
||||
$selfContained = if ($NoSelfContained) { "false" } else { "true" }
|
||||
$publishArguments = @(
|
||||
"publish",
|
||||
$projectPath,
|
||||
"-c", $Configuration,
|
||||
"-r", $Runtime,
|
||||
"--self-contained", $selfContained,
|
||||
"-o", $publishPath,
|
||||
"/p:UseAppHost=true"
|
||||
)
|
||||
|
||||
Write-Host "Publishing Aquarium Engine:"
|
||||
Write-Host " Project: $projectPath"
|
||||
Write-Host " Client: $liveProjectPath"
|
||||
Write-Host " Runtime: $Runtime"
|
||||
Write-Host " Output: $publishPath"
|
||||
dotnet @publishArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
dotnet publish $liveProjectPath -c $Configuration -r $Runtime --self-contained false -o $publishPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "client runtime publish failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot "sync-faust-toolchain.ps1") -DestinationRoot (Join-Path $publishPath "Tools\Faust")
|
||||
|
||||
$requiredFiles = @(
|
||||
"Aquarium.Engine.exe",
|
||||
"Aquarium.Epiphany.dll",
|
||||
"Aquarium.Engine.Contracts.dll",
|
||||
"AquaSynth.Core.dll",
|
||||
"AquaSynth.Faust.dll",
|
||||
"Tools\Faust\lib\faust.dll",
|
||||
"Tools\Faust\share\faust\stdfaust.lib",
|
||||
"Assets\Fensalir-Icon.ico",
|
||||
"Assets\Fensalir-Icon-96.png",
|
||||
"Assets\Fensalir-Splash.bmp",
|
||||
"Assets\Fensalir-Splash.png",
|
||||
"Assets\Fonts\Montserrat[wght].ttf",
|
||||
"Assets\Fonts\UbuntuSans[wdth,wght].ttf",
|
||||
"Render\Shaders\D3D12HeightField.hlsl",
|
||||
"Render\Shaders\D3D12SdfCommon.hlsli",
|
||||
"Render\Shaders\D3D12SdfProxy.hlsli",
|
||||
"Render\Shaders\D3D12SelfBody.hlsl",
|
||||
"Render\Shaders\D3D12CursorBody.hlsl"
|
||||
)
|
||||
|
||||
foreach ($relativePath in $requiredFiles) {
|
||||
$path = Join-Path $publishPath $relativePath
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
throw "Publish output is missing required file: $relativePath"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Publish output verified."
|
||||
|
||||
if ($Zip) {
|
||||
if (Test-Path $zipPath) {
|
||||
Remove-Item -LiteralPath $zipPath -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $publishPath "*") -DestinationPath $zipPath -CompressionLevel Optimal
|
||||
Write-Host "Zip ready:"
|
||||
Write-Host " $zipPath"
|
||||
}
|
||||
|
||||
Write-Host "Shipping build ready:"
|
||||
Write-Host " $publishPath"
|
||||
|
|
@ -21,11 +21,11 @@ if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
|
|||
New-Item -ItemType Directory -Force -Path $slotPath | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
|
||||
|
||||
Write-Host "Building isolated preview renderer:"
|
||||
Write-Host "Building isolated Epiphany preview renderer:"
|
||||
Write-Host " $slotPath"
|
||||
dotnet build $projectPath -c Debug -o $slotPath /p:UseAppHost=true
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "preview build failed with exit code $LASTEXITCODE."
|
||||
throw "Preview build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$exePath = Join-Path $slotPath "Aquarium.Epiphany.AgentPreview.exe"
|
||||
|
|
@ -41,7 +41,7 @@ if (-not (Test-Path $exePath)) {
|
|||
--frames $FramesPerView `
|
||||
--time $TimeSeconds
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "preview render failed with exit code $LASTEXITCODE."
|
||||
throw "Preview render failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
Write-Host "Preview frames written:"
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
param(
|
||||
[string]$SourceRoot,
|
||||
[string]$DestinationRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SourceRoot)) {
|
||||
$SourceRoot = if ($env:AQUASYNTH_FAUST_HOME) { $env:AQUASYNTH_FAUST_HOME } elseif ($env:AQUARIUM_FAUST_HOME) { $env:AQUARIUM_FAUST_HOME } else { "C:\Program Files\Faust" }
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DestinationRoot)) {
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$DestinationRoot = Join-Path $repoRoot "artifacts\faust\win-x64"
|
||||
}
|
||||
|
||||
$source = Resolve-Path $SourceRoot
|
||||
$destination = $DestinationRoot
|
||||
$sourceLib = Join-Path $source "lib\faust.dll"
|
||||
$sourceShare = Join-Path $source "share\faust"
|
||||
$sourceBin = Join-Path $source "bin"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceLib)) {
|
||||
throw "Faust DLL not found at $sourceLib"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceShare)) {
|
||||
throw "Faust libraries not found at $sourceShare"
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $destination) {
|
||||
Remove-Item -LiteralPath $destination -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $destination "lib") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $destination "bin") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $destination "share\faust") | Out-Null
|
||||
|
||||
Copy-Item -LiteralPath $sourceLib -Destination (Join-Path $destination "lib\faust.dll") -Force
|
||||
|
||||
if (Test-Path -LiteralPath $sourceBin) {
|
||||
Get-ChildItem -LiteralPath $sourceBin -File -Filter *.dll |
|
||||
Copy-Item -Destination (Join-Path $destination "bin") -Force
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $sourceShare -File -Filter *.lib |
|
||||
Copy-Item -Destination (Join-Path $destination "share\faust") -Force
|
||||
|
||||
$version = & (Join-Path $sourceBin "faust.exe") --version 2>$null | Select-Object -First 1
|
||||
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($version)) {
|
||||
Set-Content -Path (Join-Path $destination "VERSION.txt") -Value $version -Encoding UTF8
|
||||
}
|
||||
|
||||
Write-Host "Faust toolchain synchronized:"
|
||||
Write-Host " Source: $source"
|
||||
Write-Host " Destination: $destination"
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
param(
|
||||
[string]$SynthRepo = "E:\Projects\AquaSynth",
|
||||
[string]$Configuration = "Release"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$feed = Join-Path $root "packages"
|
||||
$coreProject = Join-Path $SynthRepo "src\AquaSynth.Core\AquaSynth.Core.csproj"
|
||||
$faustProject = Join-Path $SynthRepo "src\AquaSynth.Faust\AquaSynth.Faust.csproj"
|
||||
|
||||
if (-not (Test-Path $coreProject)) {
|
||||
throw "AquaSynth.Core project not found at $coreProject"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $faustProject)) {
|
||||
throw "AquaSynth.Faust project not found at $faustProject"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $feed | Out-Null
|
||||
dotnet pack $coreProject -c $Configuration -o $feed
|
||||
dotnet pack $faustProject -c $Configuration -o $feed
|
||||
|
||||
Write-Host "Packed AquaSynth.Core and AquaSynth.Faust into $feed"
|
||||
Write-Host "If the package version changed, update src\Aquarium.Engine\Aquarium.Engine.csproj to match."
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$engineRoots = @(
|
||||
(Join-Path $repoRoot "src\Aquarium.Engine"),
|
||||
(Join-Path $repoRoot "src\Aquarium.Engine.Contracts")
|
||||
)
|
||||
|
||||
$forbiddenEngineTerms = "grid|agent|body|epiphany"
|
||||
$engineReferences = rg -n -i -- $forbiddenEngineTerms $engineRoots
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
throw "Aquarium Engine and contracts must not contain Epiphany/client policy terms:`n$engineReferences"
|
||||
}
|
||||
elseif ($LASTEXITCODE -ne 1) {
|
||||
throw "Boundary search for Aquarium Engine terms failed."
|
||||
}
|
||||
|
||||
Write-Host "Aquarium boundary checks passed."
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Render;
|
||||
|
||||
namespace Aquarium.Engine;
|
||||
|
||||
public readonly record struct AquariumFrame(ViewFrame View, Vector3 CameraPosition, Vector3 CameraTarget, float TimeSeconds, Vector2 CursorWorld, AquariumSceneState Scene)
|
||||
{
|
||||
public AquariumFrame(ViewFrame view, Vector3 cameraPosition, float timeSeconds)
|
||||
: this(view, cameraPosition, new Vector3(view.Center, 0.0f), timeSeconds, view.Center, AquariumSceneState.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public AquariumFrame(ViewFrame view, Vector3 cameraPosition, float timeSeconds, Vector2 cursorWorld)
|
||||
: this(view, cameraPosition, new Vector3(view.Center, 0.0f), timeSeconds, cursorWorld, AquariumSceneState.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public AquariumFrame(ViewFrame view, Vector3 cameraPosition, float timeSeconds, Vector2 cursorWorld, AquariumSceneState scene)
|
||||
: this(view, cameraPosition, new Vector3(view.Center, 0.0f), timeSeconds, cursorWorld, scene)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFrameInput(Vector2 MousePosition, int Width, int Height);
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
using Aquarium.Engine.Audio;
|
||||
using Aquarium.Engine.Input;
|
||||
using Aquarium.Engine.Render;
|
||||
using Aquarium.Engine.Ui;
|
||||
|
||||
namespace Aquarium.Engine;
|
||||
|
||||
public interface IAquariumRuntime : IDisposable
|
||||
{
|
||||
AquariumRuntimeOptions Options { get; }
|
||||
|
||||
AquariumFrame Frame { get; }
|
||||
|
||||
GraphicsSettings GraphicsSettings { get; set; }
|
||||
|
||||
AquariumRenderPlan RenderPlan { get; }
|
||||
|
||||
AquariumUiDocument Ui { get; }
|
||||
|
||||
AquariumAudioDocument Audio { get; }
|
||||
|
||||
AquariumSynthDocument Synth { get; }
|
||||
|
||||
void Start();
|
||||
|
||||
void Update(float deltaSeconds, InputState input);
|
||||
|
||||
AquariumFrame ComposeFrame(AquariumFrame frame, AquariumFrameInput input);
|
||||
|
||||
void FlushState();
|
||||
}
|
||||
|
||||
public interface IAquariumRuntimeFactory
|
||||
{
|
||||
IAquariumRuntime Create(AquariumRuntimeOptions options);
|
||||
}
|
||||
|
||||
public readonly record struct AquariumRuntimeOptions(bool Headless, string? CultCachePath, int? RenderDebugModeOverride = null);
|
||||
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Aquarium.Engine.Audio;
|
||||
|
||||
public sealed class AquariumAudioDocument
|
||||
{
|
||||
private readonly ConcurrentQueue<AquariumPcmAudioChunk> pcmChunks = new();
|
||||
|
||||
public static AquariumAudioDocument Empty { get; } = new();
|
||||
|
||||
public void EnqueuePcm16Base64(string base64Data, int sampleRate, int channels, float gain = 1.0f, float pan = 0.0f)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(base64Data) || sampleRate <= 0 || channels <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = Convert.FromBase64String(base64Data);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var frameCount = bytes.Length / Math.Max(1, channels) / 2;
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mono = new float[frameCount];
|
||||
var byteIndex = 0;
|
||||
var safeGain = Math.Clamp(gain, 0.0f, 4.0f);
|
||||
for (var frame = 0; frame < frameCount; frame++)
|
||||
{
|
||||
var sum = 0.0f;
|
||||
for (var channel = 0; channel < channels && byteIndex + 1 < bytes.Length; channel++)
|
||||
{
|
||||
var sample = (short)(bytes[byteIndex] | (bytes[byteIndex + 1] << 8));
|
||||
sum += sample / 32768.0f;
|
||||
byteIndex += 2;
|
||||
}
|
||||
|
||||
mono[frame] = Math.Clamp(sum / channels, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
var safePan = Math.Clamp(pan, -1.0f, 1.0f);
|
||||
var leftGain = safeGain * MathF.Sqrt((1.0f - safePan) * 0.5f);
|
||||
var rightGain = safeGain * MathF.Sqrt((1.0f + safePan) * 0.5f);
|
||||
pcmChunks.Enqueue(new AquariumPcmAudioChunk(mono, sampleRate, leftGain, rightGain));
|
||||
}
|
||||
|
||||
public IReadOnlyList<AquariumPcmAudioChunk> DrainPcmChunks(int maxChunks = 64)
|
||||
{
|
||||
var drained = new List<AquariumPcmAudioChunk>();
|
||||
while (drained.Count < maxChunks && pcmChunks.TryDequeue(out var chunk))
|
||||
{
|
||||
drained.Add(chunk);
|
||||
}
|
||||
|
||||
return drained;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record AquariumPcmAudioChunk(float[] MonoSamples, int SampleRate, float LeftGain = 1.0f, float RightGain = 1.0f);
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
namespace Aquarium.Engine.Audio;
|
||||
|
||||
public sealed class AquariumSynthDocument
|
||||
{
|
||||
private readonly List<AquariumSynthPatch> patches = [];
|
||||
|
||||
public static AquariumSynthDocument Empty { get; } = new();
|
||||
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
public float MasterGain { get; init; } = 0.55f;
|
||||
|
||||
public IReadOnlyList<AquariumSynthPatch> Patches => patches;
|
||||
|
||||
public static AquariumSynthDocument Combine(params AquariumSynthDocument[] documents)
|
||||
{
|
||||
var combined = new AquariumSynthDocument
|
||||
{
|
||||
Enabled = documents.Any(document => document.Enabled),
|
||||
MasterGain = documents.Length == 0 ? 1.0f : documents.Max(document => document.MasterGain)
|
||||
};
|
||||
foreach (var document in documents)
|
||||
{
|
||||
if (!document.Enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var patch in document.Patches)
|
||||
{
|
||||
combined.patches.Add(patch);
|
||||
}
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public AquariumSynthDocument Patch(
|
||||
string id,
|
||||
string script,
|
||||
AquariumSynthTrigger trigger,
|
||||
float gain = 1.0f,
|
||||
int faustCompileRevision = 0,
|
||||
string? faustName = null,
|
||||
Action<AquariumSynthPatchStatus>? statusSink = null)
|
||||
{
|
||||
patches.Add(new AquariumSynthPatch(id, script, trigger, gain, faustCompileRevision, faustName ?? id, statusSink));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record AquariumSynthPatch(
|
||||
string Id,
|
||||
string Script,
|
||||
AquariumSynthTrigger Trigger,
|
||||
float Gain = 1.0f,
|
||||
int FaustCompileRevision = 0,
|
||||
string FaustName = "aquarium_patch",
|
||||
Action<AquariumSynthPatchStatus>? StatusSink = null);
|
||||
|
||||
public sealed record AquariumSynthPatchStatus(
|
||||
string Id,
|
||||
AquariumSynthPatchCompileState State,
|
||||
string Message,
|
||||
int Revision,
|
||||
double ChangedAtSeconds);
|
||||
|
||||
public enum AquariumSynthPatchCompileState
|
||||
{
|
||||
Idle,
|
||||
Queued,
|
||||
Compiling,
|
||||
Ready,
|
||||
Failed
|
||||
}
|
||||
|
||||
public readonly record struct AquariumSynthTrigger(float IntervalSeconds, float PhaseSeconds = 0.0f, int FireRevision = 0)
|
||||
{
|
||||
public static AquariumSynthTrigger Repeat(float intervalSeconds, float phaseSeconds = 0.0f) =>
|
||||
new(MathF.Max(0.001f, intervalSeconds), phaseSeconds);
|
||||
|
||||
public static AquariumSynthTrigger Manual(int fireRevision) =>
|
||||
new(float.PositiveInfinity, 0.0f, fireRevision);
|
||||
}
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public readonly record struct AquariumFractalKey
|
||||
{
|
||||
public AquariumFractalKey(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Fractal key must not be empty.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AquariumFractalDomainKind
|
||||
{
|
||||
Solar,
|
||||
Orbital,
|
||||
Planetary,
|
||||
LatLong,
|
||||
CubeSphereTile,
|
||||
Surface2D,
|
||||
Object3D,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalDomain(
|
||||
AquariumFractalKey Key,
|
||||
AquariumFractalDomainKind Kind,
|
||||
AquariumFractalKey ParentKey,
|
||||
Vector4 Parameters0,
|
||||
Vector4 Parameters1);
|
||||
|
||||
public enum AquariumFractalPayloadKind
|
||||
{
|
||||
Height,
|
||||
Material,
|
||||
SignedDistance,
|
||||
Density,
|
||||
Extinction,
|
||||
}
|
||||
|
||||
public enum AquariumFieldLayer
|
||||
{
|
||||
Form,
|
||||
Appearance,
|
||||
Transport,
|
||||
}
|
||||
|
||||
public enum AquariumFieldEncoding
|
||||
{
|
||||
SignedDistance,
|
||||
Height,
|
||||
Density,
|
||||
Extinction,
|
||||
Material,
|
||||
Phase,
|
||||
Emission,
|
||||
Radiance,
|
||||
Feature,
|
||||
Confidence,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AquariumFieldEncodingFlags
|
||||
{
|
||||
None = 0,
|
||||
SignedDistance = 1 << 0,
|
||||
Height = 1 << 1,
|
||||
Density = 1 << 2,
|
||||
Extinction = 1 << 3,
|
||||
Material = 1 << 4,
|
||||
Phase = 1 << 5,
|
||||
Emission = 1 << 6,
|
||||
Radiance = 1 << 7,
|
||||
Feature = 1 << 8,
|
||||
Confidence = 1 << 9,
|
||||
}
|
||||
|
||||
public enum AquariumFractalSurfacePageKind
|
||||
{
|
||||
Height,
|
||||
SignedDistance2D,
|
||||
Material,
|
||||
Confidence,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalSurfacePageKey
|
||||
{
|
||||
public AquariumFractalSurfacePageKey(
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
AquariumFractalSurfacePageKind kind,
|
||||
int mipLevel)
|
||||
{
|
||||
if (domainKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("Surface page domain key must not be empty.", nameof(domainKey));
|
||||
}
|
||||
|
||||
if (nodeKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("Surface page node key must not be empty.", nameof(nodeKey));
|
||||
}
|
||||
|
||||
if (mipLevel < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(mipLevel), mipLevel, "Surface page mip level must not be negative.");
|
||||
}
|
||||
|
||||
DomainKey = domainKey;
|
||||
NodeKey = nodeKey;
|
||||
Kind = kind;
|
||||
MipLevel = mipLevel;
|
||||
}
|
||||
|
||||
public AquariumFractalKey DomainKey { get; }
|
||||
|
||||
public AquariumFractalKey NodeKey { get; }
|
||||
|
||||
public AquariumFractalSurfacePageKind Kind { get; }
|
||||
|
||||
public int MipLevel { get; }
|
||||
|
||||
public string Value => $"{DomainKey.Value}:{NodeKey.Value}:{Kind}:mip{MipLevel:D2}";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalSurfacePage(
|
||||
AquariumFractalSurfacePageKey Key,
|
||||
Vector4 BoundsMinMax,
|
||||
int Width,
|
||||
int Height,
|
||||
int PayloadHandle,
|
||||
float MaxError);
|
||||
|
||||
public readonly record struct AquariumFractalSdfSplat3DKey
|
||||
{
|
||||
public AquariumFractalSdfSplat3DKey(
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
int payloadHandle)
|
||||
{
|
||||
if (domainKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("3D SDF splat domain key must not be empty.", nameof(domainKey));
|
||||
}
|
||||
|
||||
if (nodeKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("3D SDF splat node key must not be empty.", nameof(nodeKey));
|
||||
}
|
||||
|
||||
if (payloadHandle < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(payloadHandle), payloadHandle, "3D SDF splat payload handle must not be negative.");
|
||||
}
|
||||
|
||||
DomainKey = domainKey;
|
||||
NodeKey = nodeKey;
|
||||
PayloadHandle = payloadHandle;
|
||||
}
|
||||
|
||||
public AquariumFractalKey DomainKey { get; }
|
||||
|
||||
public AquariumFractalKey NodeKey { get; }
|
||||
|
||||
public int PayloadHandle { get; }
|
||||
|
||||
public string Value => $"{DomainKey.Value}:{NodeKey.Value}:sdf3d:{PayloadHandle:D6}";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalSdfSplat3D(
|
||||
AquariumFractalSdfSplat3DKey Key,
|
||||
Vector3 Center,
|
||||
Quaternion Orientation,
|
||||
Vector3 Radii,
|
||||
float Falloff,
|
||||
float ShapePower,
|
||||
float DistanceOffset,
|
||||
float MaterialValue,
|
||||
float Confidence);
|
||||
|
||||
public readonly record struct AquariumFractalDensitySplat3DKey
|
||||
{
|
||||
public AquariumFractalDensitySplat3DKey(
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
int payloadHandle)
|
||||
{
|
||||
if (domainKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("3D density splat domain key must not be empty.", nameof(domainKey));
|
||||
}
|
||||
|
||||
if (nodeKey.Value is null)
|
||||
{
|
||||
throw new ArgumentException("3D density splat node key must not be empty.", nameof(nodeKey));
|
||||
}
|
||||
|
||||
if (payloadHandle < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(payloadHandle), payloadHandle, "3D density splat payload handle must not be negative.");
|
||||
}
|
||||
|
||||
DomainKey = domainKey;
|
||||
NodeKey = nodeKey;
|
||||
PayloadHandle = payloadHandle;
|
||||
}
|
||||
|
||||
public AquariumFractalKey DomainKey { get; }
|
||||
|
||||
public AquariumFractalKey NodeKey { get; }
|
||||
|
||||
public int PayloadHandle { get; }
|
||||
|
||||
public string Value => $"{DomainKey.Value}:{NodeKey.Value}:density3d:{PayloadHandle:D6}";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalDensitySplat3D(
|
||||
AquariumFractalDensitySplat3DKey Key,
|
||||
Vector3 Center,
|
||||
Quaternion Orientation,
|
||||
Vector3 Radii,
|
||||
float Falloff,
|
||||
float ShapePower,
|
||||
float Density,
|
||||
float Extinction,
|
||||
float Confidence);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedFractalSdfSplat3D(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 Orientation,
|
||||
Vector4 RadiiFalloff,
|
||||
Vector4 MaterialConfidence,
|
||||
Vector4 Key);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedFractalDensitySplat3D(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 Orientation,
|
||||
Vector4 RadiiFalloff,
|
||||
Vector4 DensityExtinctionConfidence,
|
||||
Vector4 Key);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedFractalIfsTransform(
|
||||
Vector4 OffsetScaleAmplitude,
|
||||
Vector4 RadiiRotationFalloff,
|
||||
Vector4 MaterialSeedShape,
|
||||
Vector4 TileAddress,
|
||||
Vector4 PostMatrix,
|
||||
Vector4 PostTranslation);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedFractalFlameState(
|
||||
Vector4 PointSupportMaterial,
|
||||
Vector4 RandomStep);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedSdfEnvelopeReservoir(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 RadiiFalloff,
|
||||
Vector4 WeightTargetCount,
|
||||
Vector4 Validation);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedPbrMaterialReservoir(
|
||||
Vector4 BaseColorRoughMetal,
|
||||
Vector4 NormalVariance,
|
||||
Vector4 WeightTargetCount,
|
||||
Vector4 Validation);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumPackedRadiosityReservoir(
|
||||
Vector4 RadianceDistance,
|
||||
Vector4 DirectionOcclusion,
|
||||
Vector4 WeightTargetCount,
|
||||
Vector4 Validation);
|
||||
|
||||
public readonly record struct AquariumBrushClaim(
|
||||
AquariumFractalKey Key,
|
||||
AquariumFractalKey DomainKey,
|
||||
AquariumFractalKey NodeKey,
|
||||
AquariumFractalPayloadKind PayloadKind,
|
||||
Vector2 Center,
|
||||
Vector2 Radii,
|
||||
float RotationRadians,
|
||||
float Falloff,
|
||||
float ShapePower,
|
||||
float Amplitude,
|
||||
int Seed,
|
||||
string Tags,
|
||||
float WaveAmplitude = 0.0f,
|
||||
float WaveFrequency = 0.0f,
|
||||
float WaveSpeed = 0.0f,
|
||||
float WaveSinePower = 0.0f);
|
||||
|
||||
public enum AquariumFractalOperation
|
||||
{
|
||||
Union,
|
||||
Subtract,
|
||||
Common,
|
||||
Repeat,
|
||||
Refine,
|
||||
Capture,
|
||||
Bind,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumFractalNode(
|
||||
AquariumFractalKey Key,
|
||||
AquariumFractalKey DomainKey,
|
||||
AquariumFractalKey ParentKey,
|
||||
AquariumFractalOperation Operation,
|
||||
int FirstChildIndex,
|
||||
int ChildCount,
|
||||
int FirstClaimIndex,
|
||||
int ClaimCount,
|
||||
Vector4 BoundsMinMax,
|
||||
int Seed);
|
||||
|
||||
public readonly record struct AquariumFractalSummary(
|
||||
AquariumFractalKey NodeKey,
|
||||
Vector4 BoundsMinMax,
|
||||
float MaxHeightError,
|
||||
float MaxMaterialDelta,
|
||||
float EstimatedCost,
|
||||
int DescendantCount,
|
||||
AquariumFieldEncodingFlags FormEncodingMask = AquariumFieldEncodingFlags.None);
|
||||
|
||||
public readonly record struct AquariumContributionState(
|
||||
AquariumFractalKey NodeKey,
|
||||
float MeanContribution,
|
||||
float Variance,
|
||||
float Confidence,
|
||||
int SampleCount,
|
||||
uint LastSampledFrame,
|
||||
bool Resident);
|
||||
|
||||
public readonly record struct AquariumSelectedCut(
|
||||
AquariumFractalKey NodeKey,
|
||||
float Score,
|
||||
float Fade,
|
||||
bool UsesSummary,
|
||||
bool RequestedChildren);
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
namespace Aquarium.Engine;
|
||||
|
||||
public readonly record struct GraphicsSettings(
|
||||
int RenderDebugMode,
|
||||
float SceneExposure,
|
||||
float BloomIntensity,
|
||||
float BloomVeilIntensity)
|
||||
{
|
||||
public const int MinRenderDebugMode = 0;
|
||||
public const int MaxRenderDebugMode = 11;
|
||||
public const float MinSceneExposure = 0.02f;
|
||||
public const float MaxSceneExposure = 1.2f;
|
||||
public const float MinBloomIntensity = 0.0f;
|
||||
public const float MaxBloomIntensity = 0.24f;
|
||||
public const float MinBloomVeilIntensity = 0.0f;
|
||||
public const float MaxBloomVeilIntensity = 0.08f;
|
||||
|
||||
public static GraphicsSettings Default { get; } = new(
|
||||
RenderDebugMode: 0,
|
||||
SceneExposure: 0.16f,
|
||||
BloomIntensity: 0.072f,
|
||||
BloomVeilIntensity: 0.014f);
|
||||
|
||||
public GraphicsSettings Normalized()
|
||||
{
|
||||
return new GraphicsSettings(
|
||||
Math.Clamp(RenderDebugMode, MinRenderDebugMode, MaxRenderDebugMode),
|
||||
Math.Clamp(SceneExposure, MinSceneExposure, MaxSceneExposure),
|
||||
Math.Clamp(BloomIntensity, MinBloomIntensity, MaxBloomIntensity),
|
||||
Math.Clamp(BloomVeilIntensity, MinBloomVeilIntensity, MaxBloomVeilIntensity));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Input;
|
||||
|
||||
public sealed class InputState
|
||||
{
|
||||
private readonly HashSet<KeyCode> downKeys = [];
|
||||
private readonly HashSet<KeyCode> pressedKeys = [];
|
||||
private readonly HashSet<MouseButton> downMouseButtons = [];
|
||||
private readonly HashSet<MouseButton> pressedMouseButtons = [];
|
||||
private readonly List<char> textInput = [];
|
||||
|
||||
public Vector2 MousePosition { get; private set; }
|
||||
|
||||
public Vector2 MouseDelta { get; private set; }
|
||||
|
||||
public float WheelDelta { get; private set; }
|
||||
|
||||
public IReadOnlyList<char> TextInput => textInput;
|
||||
|
||||
public bool LeftMouseDown => IsMouseDown(MouseButton.Left);
|
||||
|
||||
public bool MiddleMouseDown => IsMouseDown(MouseButton.Middle);
|
||||
|
||||
public bool RightMouseDown => IsMouseDown(MouseButton.Right);
|
||||
|
||||
public bool IsKeyDown(KeyCode key) => downKeys.Contains(key);
|
||||
|
||||
public bool IsKeyPressed(KeyCode key) => pressedKeys.Contains(key);
|
||||
|
||||
public bool IsMouseDown(MouseButton button) => downMouseButtons.Contains(button);
|
||||
|
||||
public bool IsMousePressed(MouseButton button) => pressedMouseButtons.Contains(button);
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
MouseDelta = Vector2.Zero;
|
||||
WheelDelta = 0.0f;
|
||||
pressedKeys.Clear();
|
||||
pressedMouseButtons.Clear();
|
||||
textInput.Clear();
|
||||
}
|
||||
|
||||
public void SetMousePosition(Vector2 position)
|
||||
{
|
||||
MouseDelta += position - MousePosition;
|
||||
MousePosition = position;
|
||||
}
|
||||
|
||||
public void AddWheelDelta(float delta)
|
||||
{
|
||||
WheelDelta += delta;
|
||||
}
|
||||
|
||||
public void SetMouseButton(MouseButton button, bool isDown)
|
||||
{
|
||||
if (isDown)
|
||||
{
|
||||
if (!downMouseButtons.Contains(button))
|
||||
{
|
||||
pressedMouseButtons.Add(button);
|
||||
}
|
||||
|
||||
downMouseButtons.Add(button);
|
||||
}
|
||||
else
|
||||
{
|
||||
downMouseButtons.Remove(button);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetKey(KeyCode key, bool isDown)
|
||||
{
|
||||
if (isDown)
|
||||
{
|
||||
if (!downKeys.Contains(key))
|
||||
{
|
||||
pressedKeys.Add(key);
|
||||
}
|
||||
|
||||
downKeys.Add(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
downKeys.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTextInput(char value)
|
||||
{
|
||||
textInput.Add(value);
|
||||
}
|
||||
|
||||
public InputState WithoutInteractiveInput()
|
||||
{
|
||||
var snapshot = new InputState();
|
||||
snapshot.SetMousePosition(MousePosition);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public enum MouseButton
|
||||
{
|
||||
Left,
|
||||
Middle,
|
||||
Right,
|
||||
}
|
||||
|
||||
public enum KeyCode
|
||||
{
|
||||
W,
|
||||
A,
|
||||
S,
|
||||
D,
|
||||
Digit0,
|
||||
Digit1,
|
||||
Digit2,
|
||||
Digit3,
|
||||
Digit4,
|
||||
Digit5,
|
||||
Digit6,
|
||||
Digit7,
|
||||
Digit8,
|
||||
Digit9,
|
||||
RenderDebugCycle,
|
||||
DebugUiToggle,
|
||||
Backspace,
|
||||
Delete,
|
||||
Enter,
|
||||
LeftArrow,
|
||||
RightArrow,
|
||||
Home,
|
||||
End,
|
||||
Shift,
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
||||
public sealed class AquariumApp
|
||||
{
|
||||
public AquariumApp(AquariumRenderPlan? plan = null)
|
||||
{
|
||||
Plan = plan ?? new AquariumRenderPlan();
|
||||
RenderTargets = new AquariumRenderTargetBuilder(Plan.Graph);
|
||||
Cameras = new AquariumCameraBuilder(Plan.Graph);
|
||||
Shaders = new AquariumShaderBuilder(Plan.Shaders);
|
||||
Graph = new AquariumGraphBuilder(Plan.Graph);
|
||||
Features = new AquariumFeatureBuilder(Plan.Graph);
|
||||
Debug = new AquariumDebugBuilder(Plan.Graph);
|
||||
}
|
||||
|
||||
public AquariumRenderPlan Plan { get; }
|
||||
|
||||
public AquariumRenderTargetBuilder RenderTargets { get; }
|
||||
|
||||
public AquariumCameraBuilder Cameras { get; }
|
||||
|
||||
public AquariumShaderBuilder Shaders { get; }
|
||||
|
||||
public AquariumGraphBuilder Graph { get; }
|
||||
|
||||
public AquariumFeatureBuilder Features { get; }
|
||||
|
||||
public AquariumDebugBuilder Debug { get; }
|
||||
}
|
||||
|
||||
public sealed class AquariumRenderTargetBuilder(AquariumRenderGraphDescription graph)
|
||||
{
|
||||
public AquariumRenderTargetDraft Create(string name)
|
||||
{
|
||||
return new AquariumRenderTargetDraft(graph, name);
|
||||
}
|
||||
|
||||
public AquariumRenderTargetSet Hdr(string name)
|
||||
{
|
||||
var color = graph.RenderTarget(name, RenderFormat.Rgba16Float, AquariumTargetSize.MatchWindow(), historyFrames: 2);
|
||||
var metadata = graph.RenderTarget($"{name}-metadata", RenderFormat.Rgba16Float, AquariumTargetSize.MatchWindow(), historyFrames: 2);
|
||||
var control = graph.RenderTarget($"{name}-control", RenderFormat.Rgba16Float, AquariumTargetSize.MatchWindow(), historyFrames: 2);
|
||||
return new AquariumRenderTargetSet(color, metadata, control, new DepthTargetHandle($"{name}-depth"));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumRenderTargetDraft(AquariumRenderGraphDescription graph, string name)
|
||||
{
|
||||
private RenderFormat format = RenderFormat.Rgba16Float;
|
||||
private AquariumTargetSize size = AquariumTargetSize.MatchWindow();
|
||||
private bool sampled = true;
|
||||
private int historyFrames;
|
||||
|
||||
public AquariumRenderTargetDraft FixedSize(int width, int height)
|
||||
{
|
||||
size = AquariumTargetSize.Fixed(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft MatchWindow(float scale = 1.0f)
|
||||
{
|
||||
size = AquariumTargetSize.MatchWindow(scale);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft Format(RenderFormat value)
|
||||
{
|
||||
format = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft Sampled(bool value = true)
|
||||
{
|
||||
sampled = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft History(int frames)
|
||||
{
|
||||
historyFrames = Math.Max(0, frames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft Clear(float value)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumRenderTargetDraft Clear(Vector4 value)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public RenderTargetHandle Register()
|
||||
{
|
||||
return graph.RenderTarget(name, format, size, sampled, historyFrames);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumRenderTargetSet(
|
||||
RenderTargetHandle Color,
|
||||
RenderTargetHandle Metadata,
|
||||
RenderTargetHandle Control,
|
||||
DepthTargetHandle Depth);
|
||||
|
||||
public sealed class AquariumCameraBuilder(AquariumRenderGraphDescription graph)
|
||||
{
|
||||
public CameraHandle Perspective(string name)
|
||||
{
|
||||
return graph.Camera(name);
|
||||
}
|
||||
|
||||
public CameraHandle Orbit(string name)
|
||||
{
|
||||
return graph.Camera(name);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumShaderBuilder(AquariumShaderManifest manifest)
|
||||
{
|
||||
public AquariumShaderBuilder Root(string path)
|
||||
{
|
||||
manifest.Root(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder Core(string heightField, string scene, string post)
|
||||
{
|
||||
manifest.HeightField(heightField).Scene(scene).Post(post);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder TemporalGaussian(string path)
|
||||
{
|
||||
manifest.TemporalGaussian(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder LocalCastFusion(string path)
|
||||
{
|
||||
manifest.LocalCastFusion(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder FractalReservoir(string path)
|
||||
{
|
||||
manifest.FractalReservoir(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder FractalSplatRender(string path)
|
||||
{
|
||||
manifest.FractalSplatRender(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder Include(string path)
|
||||
{
|
||||
manifest.Include(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder SdfShader(string path)
|
||||
{
|
||||
manifest.SdfShader(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderBuilder SdfLibrary(string path)
|
||||
{
|
||||
manifest.SdfLibrary(path);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumGraphBuilder(AquariumRenderGraphDescription graph)
|
||||
{
|
||||
public AquariumPassDraft Pass(string name)
|
||||
{
|
||||
return new AquariumPassDraft(graph, name);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumPassDraft(AquariumRenderGraphDescription graph, string name)
|
||||
{
|
||||
public PassHandle Fullscreen()
|
||||
{
|
||||
return graph.Pass(name, AquariumPassKind.Fullscreen);
|
||||
}
|
||||
|
||||
public PassHandle Proxy()
|
||||
{
|
||||
return graph.Pass(name, AquariumPassKind.Proxy);
|
||||
}
|
||||
|
||||
public PassHandle Present()
|
||||
{
|
||||
return graph.Pass(name, AquariumPassKind.Present);
|
||||
}
|
||||
|
||||
public PassHandle Overlay()
|
||||
{
|
||||
return graph.Pass(name, AquariumPassKind.Overlay);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumFeatureBuilder(AquariumRenderGraphDescription graph)
|
||||
{
|
||||
public PassHandle Bloom(RenderTargetHandle source)
|
||||
{
|
||||
return graph.Pass($"bloom:{source.Name}", AquariumPassKind.Fullscreen);
|
||||
}
|
||||
|
||||
public PassHandle Presentation(RenderTargetHandle source)
|
||||
{
|
||||
return graph.Pass($"present:{source.Name}", AquariumPassKind.Present);
|
||||
}
|
||||
|
||||
public PassHandle DirectWriteOverlay()
|
||||
{
|
||||
return graph.Pass("directwrite-overlay", AquariumPassKind.Overlay);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumDebugBuilder(AquariumRenderGraphDescription graph)
|
||||
{
|
||||
public AquariumDebugBuilder View(string name, RenderTargetHandle target)
|
||||
{
|
||||
graph.DebugView(name, target);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,503 +0,0 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Render;
|
||||
|
||||
public sealed class AquariumRenderPlan
|
||||
{
|
||||
public AquariumRenderPlan()
|
||||
{
|
||||
Shaders = new AquariumShaderManifest();
|
||||
Graph = new AquariumRenderGraphDescription();
|
||||
}
|
||||
|
||||
public AquariumShaderManifest Shaders { get; }
|
||||
|
||||
public AquariumRenderGraphDescription Graph { get; }
|
||||
}
|
||||
|
||||
public sealed class AquariumShaderManifest
|
||||
{
|
||||
private readonly List<string> sdfShaderPaths = [];
|
||||
private readonly List<string> includePaths = [];
|
||||
|
||||
public string? ShaderRoot { get; private set; }
|
||||
|
||||
public string HeightFieldShader { get; private set; } = "D3D12HeightField.hlsl";
|
||||
|
||||
public string SceneShader { get; private set; } = "D3D12Scene.hlsl";
|
||||
|
||||
public string TemporalGaussianShader { get; private set; } = "D3D12TemporalGaussian.hlsl";
|
||||
|
||||
public string LocalCastFusionShader { get; private set; } = "D3D12LocalCastFusion.hlsl";
|
||||
|
||||
public string FractalReservoirShader { get; private set; } = "D3D12FractalReservoirCompute.hlsl";
|
||||
|
||||
public string FractalSplatRenderShader { get; private set; } = "D3D12FractalSplatRender.hlsl";
|
||||
|
||||
public string PostShader { get; private set; } = "D3D12Post.hlsl";
|
||||
|
||||
public string SdfCommonInclude { get; private set; } = "D3D12SdfCommon.hlsli";
|
||||
|
||||
public string SdfProxyInclude { get; private set; } = "D3D12SdfProxy.hlsli";
|
||||
|
||||
public string SdfMathInclude { get; private set; } = "D3D12SdfMath.hlsli";
|
||||
|
||||
public string? SdfLibraryInclude { get; private set; }
|
||||
|
||||
public IReadOnlyList<string> SdfShaderPaths => sdfShaderPaths;
|
||||
|
||||
public IReadOnlyList<string> IncludePaths => includePaths;
|
||||
|
||||
public AquariumShaderManifest Root(string path)
|
||||
{
|
||||
ShaderRoot = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest HeightField(string path)
|
||||
{
|
||||
HeightFieldShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest Scene(string path)
|
||||
{
|
||||
SceneShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest TemporalGaussian(string path)
|
||||
{
|
||||
TemporalGaussianShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest LocalCastFusion(string path)
|
||||
{
|
||||
LocalCastFusionShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest FractalReservoir(string path)
|
||||
{
|
||||
FractalReservoirShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest FractalSplatRender(string path)
|
||||
{
|
||||
FractalSplatRenderShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest Post(string path)
|
||||
{
|
||||
PostShader = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest SdfCommon(string path)
|
||||
{
|
||||
SdfCommonInclude = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest SdfProxy(string path)
|
||||
{
|
||||
SdfProxyInclude = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest SdfMath(string path)
|
||||
{
|
||||
SdfMathInclude = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest SdfLibrary(string? path)
|
||||
{
|
||||
SdfLibraryInclude = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest Include(string path)
|
||||
{
|
||||
includePaths.Add(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumShaderManifest SdfShader(string path)
|
||||
{
|
||||
sdfShaderPaths.Add(path);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AquariumRenderGraphDescription
|
||||
{
|
||||
private readonly List<AquariumRenderTargetDescription> renderTargets = [];
|
||||
private readonly List<AquariumCameraDescription> cameras = [];
|
||||
private readonly List<AquariumPassDescription> passes = [];
|
||||
private readonly List<AquariumDebugViewDescription> debugViews = [];
|
||||
|
||||
public IReadOnlyList<AquariumRenderTargetDescription> RenderTargets => renderTargets;
|
||||
|
||||
public IReadOnlyList<AquariumCameraDescription> Cameras => cameras;
|
||||
|
||||
public IReadOnlyList<AquariumPassDescription> Passes => passes;
|
||||
|
||||
public IReadOnlyList<AquariumDebugViewDescription> DebugViews => debugViews;
|
||||
|
||||
public RenderTargetHandle RenderTarget(string name, RenderFormat format, AquariumTargetSize size, bool sampled = true, int historyFrames = 0)
|
||||
{
|
||||
var handle = new RenderTargetHandle(name);
|
||||
renderTargets.Add(new AquariumRenderTargetDescription(handle, format, size, sampled, historyFrames));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public CameraHandle Camera(string name)
|
||||
{
|
||||
var handle = new CameraHandle(name);
|
||||
cameras.Add(new AquariumCameraDescription(handle));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public PassHandle Pass(string name, AquariumPassKind kind)
|
||||
{
|
||||
var handle = new PassHandle(name);
|
||||
passes.Add(new AquariumPassDescription(handle, kind));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public AquariumRenderGraphDescription DebugView(string name, RenderTargetHandle target)
|
||||
{
|
||||
debugViews.Add(new AquariumDebugViewDescription(name, target));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RenderTargetHandle(string Name);
|
||||
|
||||
public readonly record struct DepthTargetHandle(string Name);
|
||||
|
||||
public readonly record struct CameraHandle(string Name);
|
||||
|
||||
public readonly record struct ShaderHandle(string Name);
|
||||
|
||||
public readonly record struct BufferHandle<T>(string Name) where T : unmanaged;
|
||||
|
||||
public readonly record struct TextureHandle(string Name);
|
||||
|
||||
public readonly record struct PassHandle(string Name);
|
||||
|
||||
public readonly record struct AquariumRenderTargetDescription(
|
||||
RenderTargetHandle Handle,
|
||||
RenderFormat Format,
|
||||
AquariumTargetSize Size,
|
||||
bool Sampled,
|
||||
int HistoryFrames);
|
||||
|
||||
public readonly record struct AquariumCameraDescription(CameraHandle Handle);
|
||||
|
||||
public readonly record struct AquariumPassDescription(PassHandle Handle, AquariumPassKind Kind);
|
||||
|
||||
public readonly record struct AquariumDebugViewDescription(string Name, RenderTargetHandle Target);
|
||||
|
||||
public readonly record struct AquariumTargetSize(AquariumTargetSizeKind Kind, int Width, int Height, float Scale)
|
||||
{
|
||||
public static AquariumTargetSize Fixed(int width, int height) => new(AquariumTargetSizeKind.Fixed, width, height, 1.0f);
|
||||
|
||||
public static AquariumTargetSize MatchWindow(float scale = 1.0f) => new(AquariumTargetSizeKind.MatchWindow, 0, 0, scale);
|
||||
}
|
||||
|
||||
public enum AquariumTargetSizeKind
|
||||
{
|
||||
Fixed,
|
||||
MatchWindow,
|
||||
}
|
||||
|
||||
public enum RenderFormat
|
||||
{
|
||||
Unknown,
|
||||
R16Float,
|
||||
Rgba16Float,
|
||||
Bgra8Unorm,
|
||||
Depth32Float,
|
||||
}
|
||||
|
||||
public enum AquariumPassKind
|
||||
{
|
||||
Fullscreen,
|
||||
Proxy,
|
||||
Instanced,
|
||||
Compute,
|
||||
Copy,
|
||||
Present,
|
||||
Overlay,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumSdfLight(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 RadianceFieldId);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct AquariumSdfObject(
|
||||
Vector4 CenterRadius,
|
||||
Vector4 PreviousCenterPad,
|
||||
Vector4 State);
|
||||
|
||||
public readonly record struct AquariumHeightFieldBrush(
|
||||
Vector2 Center,
|
||||
float Radius,
|
||||
float Power,
|
||||
float Amplitude,
|
||||
float WaveAmplitude,
|
||||
float WaveFrequency,
|
||||
float WaveSpeed,
|
||||
float WaveSinePower,
|
||||
float RadiusY = 0.0f,
|
||||
float RotationRadians = 0.0f,
|
||||
float EnvelopeFalloff = 0.0f,
|
||||
float DomainFace = -1.0f,
|
||||
float DomainLevel = 0.0f,
|
||||
float DomainX = 0.0f,
|
||||
float DomainY = 0.0f);
|
||||
|
||||
public readonly record struct AquariumTemporalSdfGaussian(
|
||||
string StableKey,
|
||||
Vector3 Center,
|
||||
Vector3 PreviousCenter,
|
||||
Vector3 Velocity,
|
||||
Vector3 Radii,
|
||||
Quaternion Orientation,
|
||||
Vector4 ColorOpacity,
|
||||
float Confidence,
|
||||
float HistoryWeight,
|
||||
float LastObservedTimeSeconds,
|
||||
float Falloff,
|
||||
float ShapePower,
|
||||
int FieldId);
|
||||
|
||||
public readonly record struct AquariumGpuFusionSeed(
|
||||
string StableKey,
|
||||
Vector3 Center,
|
||||
Vector3 PreviousCenter,
|
||||
Vector3 Velocity,
|
||||
Vector3 Radii,
|
||||
Vector4 ColorOpacity,
|
||||
float Confidence,
|
||||
float HistoryWeight,
|
||||
float Falloff,
|
||||
float ShapePower,
|
||||
int FieldId);
|
||||
|
||||
public enum AquariumGpuSensorKind
|
||||
{
|
||||
Unknown,
|
||||
RgbCamera,
|
||||
HighRateTracker,
|
||||
LeapPackedMap,
|
||||
}
|
||||
|
||||
public enum AquariumGpuSensorPixelFormat
|
||||
{
|
||||
Unknown,
|
||||
Bgra8Unorm,
|
||||
Rgba8Unorm,
|
||||
R8Unorm,
|
||||
R16Unorm,
|
||||
R16Float,
|
||||
Rg8Unorm,
|
||||
LeapPackedMap,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumExternalGpuTexture(
|
||||
TextureHandle Handle,
|
||||
IntPtr SharedHandle,
|
||||
int Width,
|
||||
int Height,
|
||||
AquariumGpuSensorPixelFormat PixelFormat,
|
||||
long TimestampNs,
|
||||
int PlaneIndex = 0,
|
||||
string SharedHandleName = "");
|
||||
|
||||
public readonly record struct AquariumGpuSensorCamera(
|
||||
string SensorId,
|
||||
AquariumGpuSensorKind Kind,
|
||||
Matrix4x4 WorldFromSensor,
|
||||
Matrix4x4 SensorFromWorld,
|
||||
Vector4 Intrinsics,
|
||||
Vector4 Distortion01,
|
||||
Vector4 Distortion23,
|
||||
int Width,
|
||||
int Height,
|
||||
int FirstTextureIndex,
|
||||
int TextureCount,
|
||||
long TimestampNs);
|
||||
|
||||
public sealed class AquariumGpuSensorFrame
|
||||
{
|
||||
public static AquariumGpuSensorFrame Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumGpuSensorCamera> Cameras { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<AquariumExternalGpuTexture> ExternalTextures { get; init; } = [];
|
||||
|
||||
public float AccumulationWindowSeconds { get; init; }
|
||||
|
||||
public float PresentationDelaySeconds { get; init; }
|
||||
|
||||
public bool HasInput => Cameras.Count > 0 || ExternalTextures.Count > 0;
|
||||
}
|
||||
|
||||
public enum AquariumAcousticConstraintKind
|
||||
{
|
||||
Unknown,
|
||||
UltrasonicReflector,
|
||||
VoiceTdoa,
|
||||
SpeakerProbe,
|
||||
ClapImpact,
|
||||
}
|
||||
|
||||
public readonly record struct AquariumAcousticConstraint(
|
||||
string StableKey,
|
||||
AquariumAcousticConstraintKind Kind,
|
||||
Vector3 Position,
|
||||
Vector3 Velocity,
|
||||
float RadiusMeters,
|
||||
float Confidence,
|
||||
long TimestampNs);
|
||||
|
||||
public sealed class AquariumAcousticFieldFrame
|
||||
{
|
||||
public static AquariumAcousticFieldFrame Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumAcousticConstraint> Constraints { get; init; } = [];
|
||||
|
||||
public long TimingOracleNs { get; init; }
|
||||
|
||||
public float TimingConfidence { get; init; }
|
||||
|
||||
public float TimingUncertaintyMicroseconds { get; init; }
|
||||
|
||||
public float AccumulationWindowSeconds { get; init; }
|
||||
|
||||
public float PresentationDelaySeconds { get; init; }
|
||||
|
||||
public bool HasInput => Constraints.Count > 0;
|
||||
}
|
||||
|
||||
public readonly record struct AquariumClapCalibrationEvent(
|
||||
string StableKey,
|
||||
Vector3 Position,
|
||||
long AcousticOracleNs,
|
||||
long VisualObservedNs,
|
||||
float TimingUncertaintyMicroseconds,
|
||||
float VisualConfidence,
|
||||
float AcousticConfidence);
|
||||
|
||||
public sealed class AquariumCalibrationEventFrame
|
||||
{
|
||||
public static AquariumCalibrationEventFrame Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumClapCalibrationEvent> ClapEvents { get; init; } = [];
|
||||
|
||||
public bool HasInput => ClapEvents.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class AquariumGpuFusionField
|
||||
{
|
||||
public static AquariumGpuFusionField Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumGpuFusionSeed> Seeds { get; init; } = [];
|
||||
|
||||
public AquariumGpuFusionPointBuffer PointBuffer { get; init; }
|
||||
|
||||
public float AccumulationWindowSeconds { get; init; }
|
||||
|
||||
public float PresentationDelaySeconds { get; init; }
|
||||
|
||||
public bool HasInput => Seeds.Count > 0 || PointBuffer.HasInput;
|
||||
}
|
||||
|
||||
public readonly record struct AquariumGpuFusionPointBuffer(
|
||||
IntPtr Buffer,
|
||||
int Count,
|
||||
int StrideBytes)
|
||||
{
|
||||
// Caller owns the memory and must keep it stable until the render frame has uploaded it.
|
||||
public bool HasInput => Buffer != IntPtr.Zero && Count > 0 && StrideBytes > 0;
|
||||
}
|
||||
|
||||
public sealed class AquariumTemporalGaussianField
|
||||
{
|
||||
public static AquariumTemporalGaussianField Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumTemporalSdfGaussian> Gaussians { get; init; } = [];
|
||||
|
||||
public float AccumulationWindowSeconds { get; init; }
|
||||
|
||||
public float PresentationDelaySeconds { get; init; }
|
||||
}
|
||||
|
||||
public sealed class AquariumFractalReservoirField
|
||||
{
|
||||
public static AquariumFractalReservoirField Empty { get; } = new();
|
||||
|
||||
public int SplatCount { get; init; }
|
||||
|
||||
public int Depth { get; init; } = 8;
|
||||
|
||||
public uint Seed { get; init; } = 0xA17EA11u;
|
||||
|
||||
public int CandidatesPerReservoirUpdate { get; init; } = 2;
|
||||
|
||||
public int SplatUpdatesPerFrame { get; init; }
|
||||
|
||||
public int ReservoirUpdatesPerPass { get; init; }
|
||||
|
||||
public IReadOnlyList<AquariumPackedFractalIfsTransform> ProgramTransforms { get; init; } = [];
|
||||
|
||||
public Vector4 WorldCenterRadius { get; init; }
|
||||
|
||||
public Vector4 PriorityFocus { get; init; }
|
||||
|
||||
public bool HasInput =>
|
||||
SplatCount > 0 &&
|
||||
Depth > 0 &&
|
||||
CandidatesPerReservoirUpdate > 0 &&
|
||||
SplatUpdatesPerFrame > 0 &&
|
||||
SplatUpdatesPerFrame <= SplatCount &&
|
||||
ReservoirUpdatesPerPass > 0 &&
|
||||
ReservoirUpdatesPerPass <= SplatCount;
|
||||
}
|
||||
|
||||
public sealed class AquariumSceneState
|
||||
{
|
||||
public static AquariumSceneState Empty { get; } = new();
|
||||
|
||||
public bool TraceHeightFieldSurface { get; init; } = true;
|
||||
|
||||
public bool UseStarfieldBackground { get; init; }
|
||||
|
||||
public IReadOnlyList<AquariumHeightFieldBrush> HeightFieldBrushes { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<AquariumSdfObject> SdfObjects { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<AquariumSdfLight> SdfLights { get; init; } = [];
|
||||
|
||||
public AquariumTemporalGaussianField TemporalGaussianField { get; init; } = AquariumTemporalGaussianField.Empty;
|
||||
|
||||
public AquariumFractalReservoirField FractalReservoirField { get; init; } = AquariumFractalReservoirField.Empty;
|
||||
|
||||
public AquariumGpuSensorFrame GpuSensorFrame { get; init; } = AquariumGpuSensorFrame.Empty;
|
||||
|
||||
public AquariumAcousticFieldFrame AcousticFieldFrame { get; init; } = AquariumAcousticFieldFrame.Empty;
|
||||
|
||||
public AquariumCalibrationEventFrame CalibrationEventFrame { get; init; } = AquariumCalibrationEventFrame.Empty;
|
||||
|
||||
public AquariumGpuFusionField GpuFusionField { get; init; } = AquariumGpuFusionField.Empty;
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
namespace Aquarium.Engine.Ui;
|
||||
|
||||
public sealed class AquariumUiDocument
|
||||
{
|
||||
private readonly List<AquariumUiPanel> panels = [];
|
||||
private readonly List<AquariumConsoleCommand> commands = [];
|
||||
|
||||
public static AquariumUiDocument Empty { get; } = new();
|
||||
|
||||
public IReadOnlyList<AquariumUiPanel> Panels => panels;
|
||||
|
||||
public IReadOnlyList<AquariumConsoleCommand> Commands => commands;
|
||||
|
||||
public AquariumUiDocument Panel(string title, Action<AquariumUiPanelBuilder> compose)
|
||||
{
|
||||
return Panel(title, 18.0f, 82.0f, 360.0f, compose);
|
||||
}
|
||||
|
||||
public AquariumUiDocument Panel(string title, float left, float top, float width, Action<AquariumUiPanelBuilder> compose)
|
||||
{
|
||||
return Panel(title, left, top, width, fadeWhenMouseDistant: false, compose);
|
||||
}
|
||||
|
||||
public AquariumUiDocument Panel(string title, float left, float top, float width, bool fadeWhenMouseDistant, Action<AquariumUiPanelBuilder> compose)
|
||||
{
|
||||
var controls = new List<AquariumUiControl>();
|
||||
compose(new AquariumUiPanelBuilder(controls));
|
||||
panels.Add(new AquariumUiPanel(title, left, top, width, controls, fadeWhenMouseDistant));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiDocument Command(string name, Func<IReadOnlyList<string>, string> execute, string description = "")
|
||||
{
|
||||
commands.Add(new AquariumConsoleCommand(name, execute, description));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record AquariumUiPanel(
|
||||
string Title,
|
||||
float Left,
|
||||
float Top,
|
||||
float Width,
|
||||
IReadOnlyList<AquariumUiControl> Controls,
|
||||
bool FadeWhenMouseDistant = false);
|
||||
|
||||
public sealed class AquariumUiPanelBuilder(List<AquariumUiControl> controls)
|
||||
{
|
||||
public AquariumUiPanelBuilder Section(string title, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiSection(title, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Button(string label, Action action, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiButton(label, action, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Toggle(string label, Func<bool> read, Action<bool> write, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiToggle(label, read, write, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder TreeItem(string label, int depth, bool canExpand, Func<bool> isExpanded, Action<bool> setExpanded, Func<bool> isSelected, Action select, string? detail = null, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiTreeItem(label, Math.Max(0, depth), canExpand, isExpanded, setExpanded, isSelected, select, detail, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Slider(string label, Func<float> read, Action<float> write, float min, float max, string format = "0.###", string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiFloatSlider(label, read, write, min, max, format, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Slider(string label, Func<int> read, Action<int> write, int min, int max, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiIntSlider(label, read, write, min, max, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Options(string label, Func<int> read, Action<int> write, IReadOnlyList<AquariumUiOption> options, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiOptions(label, read, write, options, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Text(string label, Func<string> read, Action<string> write, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiText(label, read, write, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder TextBox(string label, Func<string> read, Action<string> write, int lines = 3, bool acceptsReturn = true, Action? submit = null, bool monospace = false, bool alignBottom = false, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiTextBox(label, read, write, Math.Max(1, lines), acceptsReturn, submit, monospace, alignBottom, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AquariumUiPanelBuilder Readout(string label, Func<string> read, string? tooltip = null, Func<bool>? isVisible = null)
|
||||
{
|
||||
controls.Add(new AquariumUiReadout(label, read, tooltip, isVisible));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct AquariumUiOption(int Value, string Label);
|
||||
|
||||
public sealed record AquariumConsoleCommand(string Name, Func<IReadOnlyList<string>, string> Execute, string Description = "");
|
||||
|
||||
public abstract record AquariumUiControl(string Label, string? Tooltip, Func<bool>? IsVisible)
|
||||
{
|
||||
public bool Visible => IsVisible?.Invoke() ?? true;
|
||||
}
|
||||
|
||||
public sealed record AquariumUiSection(string Label, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, null, IsVisible);
|
||||
|
||||
public sealed record AquariumUiButton(string Label, Action Action, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiToggle(string Label, Func<bool> Read, Action<bool> Write, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiTreeItem(string Label, int Depth, bool CanExpand, Func<bool> IsExpanded, Action<bool> SetExpanded, Func<bool> IsSelected, Action Select, string? Detail = null, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiFloatSlider(string Label, Func<float> Read, Action<float> Write, float Min, float Max, string Format = "0.###", string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiIntSlider(string Label, Func<int> Read, Action<int> Write, int Min, int Max, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiOptions(string Label, Func<int> Read, Action<int> Write, IReadOnlyList<AquariumUiOption> Options, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiText(string Label, Func<string> Read, Action<string> Write, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiTextBox(string Label, Func<string> Read, Action<string> Write, int Lines = 3, bool AcceptsReturn = true, Action? Submit = null, bool Monospace = false, bool AlignBottom = false, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
||||
public sealed record AquariumUiReadout(string Label, Func<string> Read, string? Tooltip = null, Func<bool>? IsVisible = null)
|
||||
: AquariumUiControl(Label, Tooltip, IsVisible);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine;
|
||||
|
||||
public readonly record struct ViewFrame(Vector2 Center, float Radius)
|
||||
{
|
||||
private const float MinimumRadius = 20.0f;
|
||||
private const float MaximumRadius = 80.0f;
|
||||
private const float CameraDistanceScale = 2.0f;
|
||||
|
||||
public static ViewFrame FromCamera(Vector2 target, float distance)
|
||||
{
|
||||
return new ViewFrame(target, Math.Clamp(distance * CameraDistanceScale, MinimumRadius, MaximumRadius));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aquarium.Engine.Contracts\Aquarium.Engine.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Brushes;
|
||||
|
||||
public readonly record struct FractalBrushEnvelope2D
|
||||
{
|
||||
private const double MinRadius = 1.0e-6;
|
||||
private const double MinFalloff = 1.0e-6;
|
||||
private const double MinShapePower = 1.0e-6;
|
||||
|
||||
public FractalBrushEnvelope2D(
|
||||
Vector2 center,
|
||||
double radiusX,
|
||||
double radiusY,
|
||||
double rotationRadians,
|
||||
double falloff,
|
||||
double shapePower = 1.0)
|
||||
{
|
||||
if (!double.IsFinite(radiusX) || radiusX <= MinRadius)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(radiusX), radiusX, "Envelope radius X must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(radiusY) || radiusY <= MinRadius)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(radiusY), radiusY, "Envelope radius Y must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(rotationRadians))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rotationRadians), rotationRadians, "Envelope rotation must be finite.");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(falloff) || falloff <= MinFalloff)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(falloff), falloff, "Envelope falloff must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(shapePower) || shapePower <= MinShapePower)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(shapePower), shapePower, "Envelope shape power must be positive and finite.");
|
||||
}
|
||||
|
||||
Center = center;
|
||||
RadiusX = radiusX;
|
||||
RadiusY = radiusY;
|
||||
RotationRadians = rotationRadians;
|
||||
Falloff = falloff;
|
||||
ShapePower = shapePower;
|
||||
}
|
||||
|
||||
public Vector2 Center { get; }
|
||||
|
||||
public double RadiusX { get; }
|
||||
|
||||
public double RadiusY { get; }
|
||||
|
||||
public double RotationRadians { get; }
|
||||
|
||||
public double Falloff { get; }
|
||||
|
||||
public double ShapePower { get; }
|
||||
|
||||
public double Evaluate(Vector2 point)
|
||||
{
|
||||
var normalizedRadiusSquared = NormalizedRadiusSquared(point);
|
||||
if (normalizedRadiusSquared >= 1.0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var edgeValue = Math.Exp(-Falloff);
|
||||
var gaussianValue = Math.Exp(-Falloff * normalizedRadiusSquared);
|
||||
var compactValue = (gaussianValue - edgeValue) / (1.0 - edgeValue);
|
||||
return Math.Pow(Math.Clamp(compactValue, 0.0, 1.0), ShapePower);
|
||||
}
|
||||
|
||||
public bool Contains(Vector2 point)
|
||||
{
|
||||
return NormalizedRadiusSquared(point) <= 1.0;
|
||||
}
|
||||
|
||||
public double SignedSupportDistanceEstimate(Vector2 point)
|
||||
{
|
||||
var normalizedRadius = Math.Sqrt(NormalizedRadiusSquared(point));
|
||||
return (normalizedRadius - 1.0) * Math.Min(RadiusX, RadiusY);
|
||||
}
|
||||
|
||||
public FractalBrushEnvelopeBounds2D AxisAlignedBounds()
|
||||
{
|
||||
var cos = Math.Cos(RotationRadians);
|
||||
var sin = Math.Sin(RotationRadians);
|
||||
var extentX = Math.Sqrt((RadiusX * cos * RadiusX * cos) + (RadiusY * sin * RadiusY * sin));
|
||||
var extentY = Math.Sqrt((RadiusX * sin * RadiusX * sin) + (RadiusY * cos * RadiusY * cos));
|
||||
var extents = new Vector2((float)extentX, (float)extentY);
|
||||
return new FractalBrushEnvelopeBounds2D(Center - extents, Center + extents);
|
||||
}
|
||||
|
||||
public FractalBrushEnvelope2DPacket ToPacket(double amplitude = 1.0)
|
||||
{
|
||||
var cos = Math.Cos(RotationRadians);
|
||||
var sin = Math.Sin(RotationRadians);
|
||||
return new FractalBrushEnvelope2DPacket(
|
||||
new Vector4(Center.X, Center.Y, (float)RadiusX, (float)RadiusY),
|
||||
new Vector4((float)cos, (float)sin, (float)Falloff, (float)ShapePower),
|
||||
new Vector4((float)amplitude, 0.0f, 0.0f, 0.0f));
|
||||
}
|
||||
|
||||
private double NormalizedRadiusSquared(Vector2 point)
|
||||
{
|
||||
var delta = point - Center;
|
||||
var cos = Math.Cos(RotationRadians);
|
||||
var sin = Math.Sin(RotationRadians);
|
||||
var localX = delta.X * cos + delta.Y * sin;
|
||||
var localY = -delta.X * sin + delta.Y * cos;
|
||||
var normalizedX = localX / RadiusX;
|
||||
var normalizedY = localY / RadiusY;
|
||||
return (normalizedX * normalizedX) + (normalizedY * normalizedY);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Brushes;
|
||||
|
||||
public readonly record struct FractalBrushEnvelopeBounds2D(Vector2 Min, Vector2 Max)
|
||||
{
|
||||
public bool Contains(Vector2 point)
|
||||
{
|
||||
return point.X >= Min.X && point.X <= Max.X && point.Y >= Min.Y && point.Y <= Max.Y;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Brushes;
|
||||
|
||||
public readonly record struct FractalBrushEnvelope2DPacket(
|
||||
Vector4 CenterRadii,
|
||||
Vector4 RotationFalloffShape,
|
||||
Vector4 Payload);
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
using Aquarium.Engine.Render;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Brushes;
|
||||
|
||||
public static class FractalHeightBrushCompiler
|
||||
{
|
||||
public static AquariumHeightFieldBrush Compile(AquariumBrushClaim claim)
|
||||
{
|
||||
return Compile(claim, null);
|
||||
}
|
||||
|
||||
public static AquariumHeightFieldBrush Compile(AquariumBrushClaim claim, AquariumFractalDomain? domain)
|
||||
{
|
||||
if (claim.PayloadKind != AquariumFractalPayloadKind.Height)
|
||||
{
|
||||
throw new ArgumentException($"Claim {claim.Key} carries {claim.PayloadKind}; only height claims can compile to height-field brushes.", nameof(claim));
|
||||
}
|
||||
|
||||
if (claim.Radii.X <= 0.0f || claim.Radii.Y <= 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(claim), claim.Radii, "Height claim radii must be positive.");
|
||||
}
|
||||
|
||||
return new AquariumHeightFieldBrush(
|
||||
claim.Center,
|
||||
claim.Radii.X,
|
||||
claim.ShapePower,
|
||||
claim.Amplitude,
|
||||
claim.WaveAmplitude,
|
||||
claim.WaveFrequency,
|
||||
claim.WaveSpeed,
|
||||
claim.WaveSinePower,
|
||||
RadiusY: claim.Radii.Y,
|
||||
RotationRadians: claim.RotationRadians,
|
||||
EnvelopeFalloff: claim.Falloff,
|
||||
DomainFace: domain is { Kind: AquariumFractalDomainKind.CubeSphereTile } ? domain.Value.Parameters0.X : -1.0f,
|
||||
DomainLevel: domain is { Kind: AquariumFractalDomainKind.CubeSphereTile } ? domain.Value.Parameters0.Y : 0.0f,
|
||||
DomainX: domain is { Kind: AquariumFractalDomainKind.CubeSphereTile } ? domain.Value.Parameters0.Z : 0.0f,
|
||||
DomainY: domain is { Kind: AquariumFractalDomainKind.CubeSphereTile } ? domain.Value.Parameters0.W : 0.0f);
|
||||
}
|
||||
|
||||
public static AquariumHeightFieldBrush[] CompileMany(IReadOnlyList<AquariumBrushClaim> claims)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(claims);
|
||||
|
||||
var brushes = new AquariumHeightFieldBrush[claims.Count];
|
||||
for (var index = 0; index < claims.Count; index++)
|
||||
{
|
||||
brushes[index] = Compile(claims[index]);
|
||||
}
|
||||
|
||||
return brushes;
|
||||
}
|
||||
|
||||
public static AquariumHeightFieldBrush[] CompileTree(FractalOwnershipTree tree)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tree);
|
||||
|
||||
var domains = tree.Domains.ToDictionary(domain => domain.Key, domain => domain);
|
||||
var brushes = new List<AquariumHeightFieldBrush>();
|
||||
for (var index = 0; index < tree.Claims.Count; index++)
|
||||
{
|
||||
var claim = tree.Claims[index];
|
||||
if (claim.PayloadKind != AquariumFractalPayloadKind.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
domains.TryGetValue(claim.DomainKey, out var domain);
|
||||
brushes.Add(Compile(claim, domain));
|
||||
}
|
||||
|
||||
return brushes.ToArray();
|
||||
}
|
||||
|
||||
public static AquariumHeightFieldBrush[] CompileSelectedTree(FractalOwnershipTree tree, IReadOnlyList<AquariumSelectedCut> selectedCut)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tree);
|
||||
ArgumentNullException.ThrowIfNull(selectedCut);
|
||||
|
||||
var selectedNodes = selectedCut.Select(cut => cut.NodeKey).ToHashSet();
|
||||
var domains = tree.Domains.ToDictionary(domain => domain.Key, domain => domain);
|
||||
var brushes = new List<AquariumHeightFieldBrush>();
|
||||
foreach (var node in tree.Nodes)
|
||||
{
|
||||
if (!selectedNodes.Contains(node.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var claimIndex = node.FirstClaimIndex; claimIndex < node.FirstClaimIndex + node.ClaimCount; claimIndex++)
|
||||
{
|
||||
var claim = tree.Claims[claimIndex];
|
||||
if (claim.PayloadKind != AquariumFractalPayloadKind.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
domains.TryGetValue(claim.DomainKey, out var domain);
|
||||
brushes.Add(Compile(claim, domain));
|
||||
}
|
||||
}
|
||||
|
||||
return brushes.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public enum CubeFace
|
||||
{
|
||||
PositiveX,
|
||||
NegativeX,
|
||||
PositiveY,
|
||||
NegativeY,
|
||||
PositiveZ,
|
||||
NegativeZ,
|
||||
}
|
||||
|
|
@ -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."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Aquarium.Engine.Fractal;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Debug;
|
||||
|
||||
public static class FractalDebugDump
|
||||
{
|
||||
public static string Build(
|
||||
FractalOwnershipTree tree,
|
||||
IReadOnlyList<AquariumFractalSummary> summaries,
|
||||
IReadOnlyList<AquariumSelectedCut> selectedCut)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tree);
|
||||
ArgumentNullException.ThrowIfNull(summaries);
|
||||
ArgumentNullException.ThrowIfNull(selectedCut);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
foreach (var domain in tree.Domains)
|
||||
{
|
||||
builder.AppendLine($"domain {domain.Key} kind={domain.Kind} parent={domain.ParentKey}");
|
||||
}
|
||||
|
||||
builder.AppendLine($"domain-path {string.Join(" -> ", tree.DomainGraph.GetPath(tree.Domain.Key).Select(domain => domain.Key.ToString()))}");
|
||||
builder.AppendLine($"nodes {tree.Nodes.Count} claims {tree.Claims.Count} summaries {summaries.Count} cut {selectedCut.Count}");
|
||||
foreach (var node in tree.Nodes)
|
||||
{
|
||||
builder.AppendLine($"node {node.Key} op={node.Operation} claims={node.ClaimCount} bounds={FormatBounds(node.BoundsMinMax)}");
|
||||
}
|
||||
|
||||
foreach (var claim in tree.Claims)
|
||||
{
|
||||
builder.AppendLine(
|
||||
string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"claim {claim.Key} payload={claim.PayloadKind} center=({claim.Center.X:0.###},{claim.Center.Y:0.###}) radii=({claim.Radii.X:0.###},{claim.Radii.Y:0.###}) amp={claim.Amplitude:0.###} tags={claim.Tags}"));
|
||||
}
|
||||
|
||||
foreach (var summary in summaries)
|
||||
{
|
||||
builder.AppendLine(
|
||||
string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"summary {summary.NodeKey} heightError={summary.MaxHeightError:0.###} cost={summary.EstimatedCost:0.###} descendants={summary.DescendantCount}"));
|
||||
}
|
||||
|
||||
foreach (var cut in selectedCut)
|
||||
{
|
||||
builder.AppendLine(
|
||||
string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"cut {cut.NodeKey} score={cut.Score:0.###} fade={cut.Fade:0.###} summary={cut.UsesSummary} requestChildren={cut.RequestedChildren}"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string FormatBounds(System.Numerics.Vector4 bounds)
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"({bounds.X:0.###},{bounds.Y:0.###})-({bounds.Z:0.###},{bounds.W:0.###})");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public sealed class FractalXorShiftRandom(uint seed) : IFractalRandom
|
||||
{
|
||||
private uint state = seed == 0 ? 0xA341316Cu : seed;
|
||||
|
||||
public double NextDouble()
|
||||
{
|
||||
var value = NextUInt32();
|
||||
return value / ((double)uint.MaxValue + 1.0);
|
||||
}
|
||||
|
||||
private uint NextUInt32()
|
||||
{
|
||||
var value = state;
|
||||
value ^= value << 13;
|
||||
value ^= value >> 17;
|
||||
value ^= value << 5;
|
||||
state = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalAffineIfsChaosGame
|
||||
{
|
||||
public static Vector2[] Generate(
|
||||
FractalAffineIfsDefinition definition,
|
||||
int count,
|
||||
int burnIn,
|
||||
IFractalRandom random)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(definition);
|
||||
ArgumentNullException.ThrowIfNull(random);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(burnIn);
|
||||
|
||||
var points = new Vector2[count];
|
||||
var point = definition.Start;
|
||||
var total = definition.Transforms.Sum(transform => MathF.Max(transform.Weight, 0.0f));
|
||||
if (total <= 0.0f)
|
||||
{
|
||||
throw new ArgumentException("Affine IFS transform weights must have positive total.", nameof(definition));
|
||||
}
|
||||
|
||||
for (var step = 0; step < burnIn + count; step++)
|
||||
{
|
||||
point = SelectTransform(definition.Transforms, total, (float)random.NextDouble()).Apply(point);
|
||||
if (step >= burnIn)
|
||||
{
|
||||
points[step - burnIn] = point;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static FractalAffineIfsTransform2D SelectTransform(
|
||||
IReadOnlyList<FractalAffineIfsTransform2D> transforms,
|
||||
float total,
|
||||
float unit)
|
||||
{
|
||||
var target = unit * total;
|
||||
var cumulative = 0.0f;
|
||||
for (var index = 0; index < transforms.Count; index++)
|
||||
{
|
||||
cumulative += MathF.Max(transforms[index].Weight, 0.0f);
|
||||
if (target <= cumulative)
|
||||
{
|
||||
return transforms[index];
|
||||
}
|
||||
}
|
||||
|
||||
return transforms[^1];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public readonly record struct FractalAffineIfsTransform2D(
|
||||
string Name,
|
||||
Vector4 Matrix,
|
||||
Vector2 Translation,
|
||||
float Weight)
|
||||
{
|
||||
public Vector2 Apply(Vector2 point)
|
||||
{
|
||||
return new Vector2(
|
||||
(Matrix.X * point.X) + (Matrix.Y * point.Y) + Translation.X,
|
||||
(Matrix.Z * point.X) + (Matrix.W * point.Y) + Translation.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FractalAffineIfsDefinition
|
||||
{
|
||||
public FractalAffineIfsDefinition(
|
||||
AquariumFractalKey key,
|
||||
AquariumFractalKey domainKey,
|
||||
string name,
|
||||
Vector2 start,
|
||||
int seed,
|
||||
string tags,
|
||||
IReadOnlyList<FractalAffineIfsTransform2D> transforms)
|
||||
{
|
||||
if (transforms.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Affine IFS definitions need at least one transform.", nameof(transforms));
|
||||
}
|
||||
|
||||
Key = key;
|
||||
DomainKey = domainKey;
|
||||
Name = name;
|
||||
Start = start;
|
||||
Seed = seed;
|
||||
Tags = tags;
|
||||
Transforms = transforms;
|
||||
}
|
||||
|
||||
public AquariumFractalKey Key { get; }
|
||||
|
||||
public AquariumFractalKey DomainKey { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Vector2 Start { get; }
|
||||
|
||||
public int Seed { get; }
|
||||
|
||||
public string Tags { get; }
|
||||
|
||||
public IReadOnlyList<FractalAffineIfsTransform2D> Transforms { get; }
|
||||
}
|
||||
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public sealed class FractalDomainGraph
|
||||
{
|
||||
private readonly Dictionary<AquariumFractalKey, int> indices;
|
||||
|
||||
public FractalDomainGraph(IReadOnlyList<AquariumFractalDomain> domains)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(domains);
|
||||
if (domains.Count == 0)
|
||||
{
|
||||
throw new FormatException("Fractal domain graph must contain at least one domain.");
|
||||
}
|
||||
|
||||
indices = new Dictionary<AquariumFractalKey, int>(domains.Count);
|
||||
for (var index = 0; index < domains.Count; index++)
|
||||
{
|
||||
var domain = domains[index];
|
||||
if (domain.Key.Value is null)
|
||||
{
|
||||
throw new FormatException($"Fractal domain at index {index} has no key.");
|
||||
}
|
||||
|
||||
if (!indices.TryAdd(domain.Key, index))
|
||||
{
|
||||
throw new FormatException($"Duplicate fractal domain key `{domain.Key}`.");
|
||||
}
|
||||
}
|
||||
|
||||
Domains = domains.ToArray();
|
||||
ValidateParents();
|
||||
}
|
||||
|
||||
public IReadOnlyList<AquariumFractalDomain> Domains { get; }
|
||||
|
||||
public bool Contains(AquariumFractalKey key)
|
||||
{
|
||||
return key.Value is not null && indices.ContainsKey(key);
|
||||
}
|
||||
|
||||
public AquariumFractalDomain GetRequired(AquariumFractalKey key)
|
||||
{
|
||||
if (key.Value is null || !indices.TryGetValue(key, out var index))
|
||||
{
|
||||
throw new FormatException($"Unknown fractal domain `{key}`.");
|
||||
}
|
||||
|
||||
return Domains[index];
|
||||
}
|
||||
|
||||
public IReadOnlyList<AquariumFractalDomain> GetPath(AquariumFractalKey leafKey)
|
||||
{
|
||||
var path = new List<AquariumFractalDomain>();
|
||||
var seen = new HashSet<AquariumFractalKey>();
|
||||
var current = GetRequired(leafKey);
|
||||
while (true)
|
||||
{
|
||||
if (!seen.Add(current.Key))
|
||||
{
|
||||
throw new FormatException($"Fractal domain graph contains a parent cycle at `{current.Key}`.");
|
||||
}
|
||||
|
||||
path.Add(current);
|
||||
if (current.ParentKey.Value is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = GetRequired(current.ParentKey);
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
private void ValidateParents()
|
||||
{
|
||||
foreach (var domain in Domains)
|
||||
{
|
||||
_ = GetPath(domain.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,598 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalDslCompiler
|
||||
{
|
||||
public static FractalOwnershipTree Compile(string source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var lines = source.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
|
||||
AquariumFractalDomain? domain = null;
|
||||
AquariumFractalKey rootKey = default;
|
||||
NodeDraft? currentNode = null;
|
||||
var domains = new List<AquariumFractalDomain>();
|
||||
var claims = new List<AquariumBrushClaim>();
|
||||
var nodes = new List<NodeDraft>();
|
||||
var affineIfsDrafts = new List<AffineIfsDraft>();
|
||||
AffineIfsDraft? currentAffineIfs = null;
|
||||
|
||||
for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
|
||||
{
|
||||
var line = StripComment(lines[lineIndex]).Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
switch (tokens[0])
|
||||
{
|
||||
case "domain":
|
||||
EnsureTokenCount(tokens, 4, lineIndex);
|
||||
domains.Add(ParseDomain(tokens, lineIndex));
|
||||
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 domainKey = FractalStableKeyBuilder.ForCubeTile(tile, tokens[5]);
|
||||
var parentKey = tokens.Length >= 7 ? new AquariumFractalKey(tokens[6]) : default;
|
||||
rootKey = FractalStableKeyBuilder.Child(domainKey, "root");
|
||||
domain = new AquariumFractalDomain(
|
||||
domainKey,
|
||||
AquariumFractalDomainKind.CubeSphereTile,
|
||||
parentKey,
|
||||
new Vector4((float)tile.Face, tile.Level, tile.X, tile.Y),
|
||||
Vector4.Zero);
|
||||
domains.Add(domain.Value);
|
||||
rootKey = FractalStableKeyBuilder.Child(domainKey, "root");
|
||||
currentNode = new NodeDraft(rootKey, domainKey, claims.Count);
|
||||
nodes.Add(currentNode);
|
||||
currentAffineIfs = null;
|
||||
break;
|
||||
case "affineifs":
|
||||
EnsureDomain(domain, lineIndex);
|
||||
EnsureNode(currentNode, lineIndex);
|
||||
EnsureTokenCount(tokens, 6, lineIndex);
|
||||
currentAffineIfs = new AffineIfsDraft(
|
||||
FractalStableKeyBuilder.Child(currentNode!.Key, $"affineifs/{tokens[1]}"),
|
||||
domain!.Value.Key,
|
||||
tokens[1],
|
||||
new Vector2(ParseFloat(tokens[2], lineIndex), ParseFloat(tokens[3], lineIndex)),
|
||||
ParseInt(tokens[4], lineIndex),
|
||||
tokens[5]);
|
||||
affineIfsDrafts.Add(currentAffineIfs);
|
||||
break;
|
||||
case "affinemap":
|
||||
EnsureAffineIfs(currentAffineIfs, lineIndex);
|
||||
EnsureTokenCount(tokens, 9, lineIndex);
|
||||
currentAffineIfs!.Transforms.Add(new FractalAffineIfsTransform2D(
|
||||
tokens[1],
|
||||
new Vector4(
|
||||
ParseFloat(tokens[2], lineIndex),
|
||||
ParseFloat(tokens[3], lineIndex),
|
||||
ParseFloat(tokens[5], lineIndex),
|
||||
ParseFloat(tokens[6], lineIndex)),
|
||||
new Vector2(ParseFloat(tokens[4], lineIndex), ParseFloat(tokens[7], lineIndex)),
|
||||
ParsePositiveFloat(tokens[8], lineIndex)));
|
||||
break;
|
||||
case "height":
|
||||
case "density":
|
||||
case "extinction":
|
||||
EnsureDomain(domain, lineIndex);
|
||||
EnsureNode(currentNode, lineIndex);
|
||||
EnsureTokenCount(tokens, 12, lineIndex);
|
||||
var fieldDomain = domain!.Value;
|
||||
claims.Add(ParseFieldClaim(tokens, fieldDomain.Key, currentNode!.Key, lineIndex, claims.Count - currentNode.FirstClaimIndex));
|
||||
currentNode.ClaimCount++;
|
||||
break;
|
||||
case "ripple":
|
||||
EnsureDomain(domain, lineIndex);
|
||||
EnsureNode(currentNode, lineIndex);
|
||||
EnsureTokenCount(tokens, 16, lineIndex);
|
||||
var rippleDomain = domain!.Value;
|
||||
claims.Add(ParseRippleClaim(tokens, rippleDomain.Key, currentNode!.Key, lineIndex, claims.Count - currentNode.FirstClaimIndex));
|
||||
currentNode.ClaimCount++;
|
||||
break;
|
||||
case "ifs":
|
||||
EnsureDomain(domain, lineIndex);
|
||||
EnsureNode(currentNode, lineIndex);
|
||||
EnsureTokenCount(tokens, 15, lineIndex);
|
||||
var ifsDomain = domain!.Value;
|
||||
var beforeIfs = claims.Count;
|
||||
AddIfsClaims(tokens, ifsDomain.Key, currentNode!.Key, lineIndex, claims);
|
||||
currentNode.ClaimCount += claims.Count - beforeIfs;
|
||||
break;
|
||||
case "flame":
|
||||
EnsureDomain(domain, lineIndex);
|
||||
EnsureNode(currentNode, lineIndex);
|
||||
EnsureTokenCount(tokens, 15, lineIndex);
|
||||
var flameDomain = domain!.Value;
|
||||
var beforeFlame = claims.Count;
|
||||
AddFlameClaims(tokens, flameDomain.Key, currentNode!.Key, lineIndex, claims);
|
||||
currentNode.ClaimCount += claims.Count - beforeFlame;
|
||||
break;
|
||||
default:
|
||||
throw new FormatException($"Unknown fractal DSL command `{tokens[0]}` at line {lineIndex + 1}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (domain is null)
|
||||
{
|
||||
throw new FormatException("Fractal DSL must declare a `tile <face> <level> <x> <y> <path>` line before claims.");
|
||||
}
|
||||
|
||||
return new FractalOwnershipTree(domain.Value, domains, BuildNodes(nodes, claims), claims, BuildAffineIfsDefinitions(affineIfsDrafts));
|
||||
}
|
||||
|
||||
private static AquariumFractalDomain ParseDomain(string[] tokens, int lineIndex)
|
||||
{
|
||||
if (!Enum.TryParse<AquariumFractalDomainKind>(tokens[1], ignoreCase: true, out var kind))
|
||||
{
|
||||
throw new FormatException($"Invalid domain kind `{tokens[1]}` at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
var parent = tokens[3] == "-" ? default : new AquariumFractalKey(tokens[3]);
|
||||
var parameters = new float[8];
|
||||
for (var index = 4; index < tokens.Length && index < 12; index++)
|
||||
{
|
||||
parameters[index - 4] = ParseFloat(tokens[index], lineIndex);
|
||||
}
|
||||
|
||||
return new AquariumFractalDomain(
|
||||
new AquariumFractalKey(tokens[2]),
|
||||
kind,
|
||||
parent,
|
||||
new Vector4(parameters[0], parameters[1], parameters[2], parameters[3]),
|
||||
new Vector4(parameters[4], parameters[5], parameters[6], parameters[7]));
|
||||
}
|
||||
|
||||
private static AquariumBrushClaim ParseFieldClaim(string[] tokens, AquariumFractalKey domainKey, AquariumFractalKey nodeKey, int lineIndex, int claimIndex)
|
||||
{
|
||||
var name = tokens[1];
|
||||
return FieldClaim(
|
||||
ParsePayloadKind(tokens[0], lineIndex),
|
||||
domainKey,
|
||||
nodeKey,
|
||||
name,
|
||||
claimIndex,
|
||||
new Vector2(ParseFloat(tokens[2], lineIndex), ParseFloat(tokens[3], lineIndex)),
|
||||
new Vector2(ParsePositiveFloat(tokens[4], lineIndex), ParsePositiveFloat(tokens[5], lineIndex)),
|
||||
ParseFloat(tokens[6], lineIndex),
|
||||
ParsePositiveFloat(tokens[7], lineIndex),
|
||||
ParsePositiveFloat(tokens[8], lineIndex),
|
||||
ParseFloat(tokens[9], lineIndex),
|
||||
ParseInt(tokens[10], lineIndex),
|
||||
tokens[11]);
|
||||
}
|
||||
|
||||
private static AquariumBrushClaim ParseRippleClaim(string[] tokens, AquariumFractalKey domainKey, AquariumFractalKey nodeKey, int lineIndex, int claimIndex)
|
||||
{
|
||||
var name = tokens[1];
|
||||
return FieldClaim(
|
||||
AquariumFractalPayloadKind.Height,
|
||||
domainKey,
|
||||
nodeKey,
|
||||
name,
|
||||
claimIndex,
|
||||
new Vector2(ParseFloat(tokens[2], lineIndex), ParseFloat(tokens[3], lineIndex)),
|
||||
new Vector2(ParsePositiveFloat(tokens[4], lineIndex), ParsePositiveFloat(tokens[5], lineIndex)),
|
||||
ParseFloat(tokens[6], lineIndex),
|
||||
ParsePositiveFloat(tokens[7], lineIndex),
|
||||
ParsePositiveFloat(tokens[8], lineIndex),
|
||||
ParseFloat(tokens[9], lineIndex),
|
||||
ParseInt(tokens[14], lineIndex),
|
||||
tokens[15],
|
||||
ParseFloat(tokens[10], lineIndex),
|
||||
ParsePositiveFloat(tokens[11], lineIndex),
|
||||
ParseFloat(tokens[12], lineIndex),
|
||||
ParsePositiveFloat(tokens[13], lineIndex));
|
||||
}
|
||||
|
||||
private static AquariumFractalPayloadKind ParsePayloadKind(string token, int lineIndex)
|
||||
{
|
||||
return token switch
|
||||
{
|
||||
"height" => AquariumFractalPayloadKind.Height,
|
||||
"density" => AquariumFractalPayloadKind.Density,
|
||||
"extinction" => AquariumFractalPayloadKind.Extinction,
|
||||
_ => throw new FormatException($"Invalid field payload `{token}` at line {lineIndex + 1}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddIfsClaims(string[] tokens, AquariumFractalKey domainKey, AquariumFractalKey nodeKey, int lineIndex, List<AquariumBrushClaim> claims)
|
||||
{
|
||||
var name = tokens[1];
|
||||
var levels = ParsePositiveInt(tokens[2], lineIndex);
|
||||
var branches = ParsePositiveInt(tokens[3], lineIndex);
|
||||
var center = new Vector2(ParseFloat(tokens[4], lineIndex), ParseFloat(tokens[5], lineIndex));
|
||||
var radii = new Vector2(ParsePositiveFloat(tokens[6], lineIndex), ParsePositiveFloat(tokens[7], lineIndex));
|
||||
var childScale = ParsePositiveFloat(tokens[8], lineIndex);
|
||||
var spread = ParsePositiveFloat(tokens[9], lineIndex);
|
||||
var rotationStep = ParseFloat(tokens[10], lineIndex);
|
||||
var falloff = ParsePositiveFloat(tokens[11], lineIndex);
|
||||
var shapePower = ParsePositiveFloat(tokens[12], lineIndex);
|
||||
var amplitude = ParseFloat(tokens[13], lineIndex);
|
||||
var seed = ParseInt(tokens[14], lineIndex);
|
||||
var tags = tokens.Length >= 16 ? tokens[15] : name;
|
||||
|
||||
if (childScale >= 1.0f)
|
||||
{
|
||||
throw new FormatException($"IFS child scale must be below 1 at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
EmitIfsLevel(domainKey, nodeKey, name, center, radii, levels, branches, childScale, spread, rotationStep, falloff, shapePower, amplitude, seed, tags, claims);
|
||||
}
|
||||
|
||||
private static void AddFlameClaims(string[] tokens, AquariumFractalKey domainKey, AquariumFractalKey nodeKey, int lineIndex, List<AquariumBrushClaim> claims)
|
||||
{
|
||||
var name = tokens[1];
|
||||
var levels = ParsePositiveInt(tokens[2], lineIndex);
|
||||
var branches = ParsePositiveInt(tokens[3], lineIndex);
|
||||
var center = new Vector2(ParseFloat(tokens[4], lineIndex), ParseFloat(tokens[5], lineIndex));
|
||||
var radii = new Vector2(ParsePositiveFloat(tokens[6], lineIndex), ParsePositiveFloat(tokens[7], lineIndex));
|
||||
var childScale = ParsePositiveFloat(tokens[8], lineIndex);
|
||||
var curl = ParseFloat(tokens[9], lineIndex);
|
||||
var spread = ParsePositiveFloat(tokens[10], lineIndex);
|
||||
var falloff = ParsePositiveFloat(tokens[11], lineIndex);
|
||||
var shapePower = ParsePositiveFloat(tokens[12], lineIndex);
|
||||
var amplitude = ParseFloat(tokens[13], lineIndex);
|
||||
var seed = ParseInt(tokens[14], lineIndex);
|
||||
var tags = tokens.Length >= 16 ? tokens[15] : name;
|
||||
|
||||
if (childScale >= 1.0f)
|
||||
{
|
||||
throw new FormatException($"Flame child scale must be below 1 at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
EmitFlameLevel(domainKey, nodeKey, name, center, radii, 0.0f, levels, branches, childScale, curl, spread, falloff, shapePower, amplitude, seed, tags, claims);
|
||||
}
|
||||
|
||||
private static void EmitIfsLevel(
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
string name,
|
||||
Vector2 center,
|
||||
Vector2 radii,
|
||||
int levelsRemaining,
|
||||
int branches,
|
||||
float childScale,
|
||||
float spread,
|
||||
float rotationStep,
|
||||
float falloff,
|
||||
float shapePower,
|
||||
float amplitude,
|
||||
int seed,
|
||||
string tags,
|
||||
List<AquariumBrushClaim> claims)
|
||||
{
|
||||
var level = claims.Count;
|
||||
claims.Add(FieldClaim(AquariumFractalPayloadKind.Height, domainKey, nodeKey, $"{name}/{level}", claims.Count, center, radii, rotationStep * level, falloff, shapePower, amplitude, seed + level, tags));
|
||||
|
||||
if (levelsRemaining <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nextRadii = radii * childScale;
|
||||
for (var branch = 0; branch < branches; branch++)
|
||||
{
|
||||
var phase = ((MathF.Tau * branch) / branches) + rotationStep * level + HashAngle(seed, level, branch);
|
||||
var offset = new Vector2(MathF.Cos(phase), MathF.Sin(phase)) * spread * MathF.Max(nextRadii.X, nextRadii.Y);
|
||||
EmitIfsLevel(
|
||||
domainKey,
|
||||
nodeKey,
|
||||
name,
|
||||
center + offset,
|
||||
nextRadii,
|
||||
levelsRemaining - 1,
|
||||
branches,
|
||||
childScale,
|
||||
spread,
|
||||
rotationStep,
|
||||
falloff,
|
||||
shapePower,
|
||||
amplitude * childScale,
|
||||
seed + 31 + branch,
|
||||
tags,
|
||||
claims);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitFlameLevel(
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
string name,
|
||||
Vector2 center,
|
||||
Vector2 radii,
|
||||
float spiralAngle,
|
||||
int levelsRemaining,
|
||||
int branches,
|
||||
float childScale,
|
||||
float curl,
|
||||
float spread,
|
||||
float falloff,
|
||||
float shapePower,
|
||||
float amplitude,
|
||||
int seed,
|
||||
string tags,
|
||||
List<AquariumBrushClaim> claims)
|
||||
{
|
||||
var level = claims.Count;
|
||||
var rotation = spiralAngle + curl * level;
|
||||
claims.Add(FieldClaim(AquariumFractalPayloadKind.Density, domainKey, nodeKey, $"{name}/{level}", claims.Count, center, radii, rotation, falloff, shapePower, amplitude, seed + level, tags));
|
||||
|
||||
if (levelsRemaining <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nextRadii = radii * childScale;
|
||||
var branchStep = MathF.Tau / branches;
|
||||
for (var branch = 0; branch < branches; branch++)
|
||||
{
|
||||
var turn = spiralAngle + curl + (branchStep * branch) + HashAngle(seed, level, branch) * 0.5f;
|
||||
var reach = spread * MathF.Max(nextRadii.X, nextRadii.Y) * (1.0f + branch * 0.17f);
|
||||
var offset = new Vector2(MathF.Cos(turn), MathF.Sin(turn)) * reach;
|
||||
EmitFlameLevel(
|
||||
domainKey,
|
||||
nodeKey,
|
||||
name,
|
||||
center + offset,
|
||||
nextRadii,
|
||||
turn,
|
||||
levelsRemaining - 1,
|
||||
branches,
|
||||
childScale,
|
||||
curl,
|
||||
spread,
|
||||
falloff,
|
||||
shapePower,
|
||||
amplitude * MathF.Sqrt(childScale),
|
||||
seed + 97 + (branch * 17),
|
||||
tags,
|
||||
claims);
|
||||
}
|
||||
}
|
||||
|
||||
private static AquariumBrushClaim FieldClaim(
|
||||
AquariumFractalPayloadKind payloadKind,
|
||||
AquariumFractalKey domainKey,
|
||||
AquariumFractalKey nodeKey,
|
||||
string name,
|
||||
int claimIndex,
|
||||
Vector2 center,
|
||||
Vector2 radii,
|
||||
float rotationRadians,
|
||||
float falloff,
|
||||
float shapePower,
|
||||
float amplitude,
|
||||
int seed,
|
||||
string tags,
|
||||
float waveAmplitude = 0.0f,
|
||||
float waveFrequency = 0.0f,
|
||||
float waveSpeed = 0.0f,
|
||||
float waveSinePower = 0.0f)
|
||||
{
|
||||
return new AquariumBrushClaim(
|
||||
FractalStableKeyBuilder.Child(nodeKey, $"claim/{claimIndex:0000}/{name}"),
|
||||
domainKey,
|
||||
nodeKey,
|
||||
payloadKind,
|
||||
center,
|
||||
radii,
|
||||
rotationRadians,
|
||||
falloff,
|
||||
shapePower,
|
||||
amplitude,
|
||||
seed,
|
||||
tags,
|
||||
waveAmplitude,
|
||||
waveFrequency,
|
||||
waveSpeed,
|
||||
waveSinePower);
|
||||
}
|
||||
|
||||
private static string StripComment(string line)
|
||||
{
|
||||
var index = line.IndexOf('#', StringComparison.Ordinal);
|
||||
return index < 0 ? line : line[..index];
|
||||
}
|
||||
|
||||
private static void EnsureTokenCount(string[] tokens, int minimum, int lineIndex)
|
||||
{
|
||||
if (tokens.Length < minimum)
|
||||
{
|
||||
throw new FormatException($"Command `{tokens[0]}` at line {lineIndex + 1} expected at least {minimum - 1} arguments.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureDomain(AquariumFractalDomain? domain, int lineIndex)
|
||||
{
|
||||
if (domain is null)
|
||||
{
|
||||
throw new FormatException($"Fractal DSL claim at line {lineIndex + 1} appears before a tile declaration.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureNode(NodeDraft? node, int lineIndex)
|
||||
{
|
||||
if (node is null)
|
||||
{
|
||||
throw new FormatException($"Fractal DSL claim at line {lineIndex + 1} appears before a tile root was created.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureAffineIfs(AffineIfsDraft? draft, int lineIndex)
|
||||
{
|
||||
if (draft is null)
|
||||
{
|
||||
throw new FormatException($"Affine IFS map at line {lineIndex + 1} appears before an affineifs declaration.");
|
||||
}
|
||||
}
|
||||
|
||||
private static FractalAffineIfsDefinition[] BuildAffineIfsDefinitions(IReadOnlyList<AffineIfsDraft> drafts)
|
||||
{
|
||||
var definitions = new FractalAffineIfsDefinition[drafts.Count];
|
||||
for (var index = 0; index < drafts.Count; index++)
|
||||
{
|
||||
var draft = drafts[index];
|
||||
definitions[index] = new FractalAffineIfsDefinition(
|
||||
draft.Key,
|
||||
draft.DomainKey,
|
||||
draft.Name,
|
||||
draft.Start,
|
||||
draft.Seed,
|
||||
draft.Tags,
|
||||
draft.Transforms.ToArray());
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static AquariumFractalNode[] BuildNodes(IReadOnlyList<NodeDraft> drafts, IReadOnlyList<AquariumBrushClaim> claims)
|
||||
{
|
||||
var nodes = new AquariumFractalNode[drafts.Count];
|
||||
for (var nodeIndex = 0; nodeIndex < drafts.Count; nodeIndex++)
|
||||
{
|
||||
var draft = drafts[nodeIndex];
|
||||
nodes[nodeIndex] = new AquariumFractalNode(
|
||||
draft.Key,
|
||||
draft.DomainKey,
|
||||
default,
|
||||
AquariumFractalOperation.Union,
|
||||
FirstChildIndex: 0,
|
||||
ChildCount: 0,
|
||||
draft.FirstClaimIndex,
|
||||
draft.ClaimCount,
|
||||
BoundsForClaims(claims, draft.FirstClaimIndex, draft.ClaimCount),
|
||||
Seed: 0);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static Vector4 BoundsForClaims(IReadOnlyList<AquariumBrushClaim> claims, int firstClaimIndex, int claimCount)
|
||||
{
|
||||
if (claimCount == 0)
|
||||
{
|
||||
return Vector4.Zero;
|
||||
}
|
||||
|
||||
var minX = float.PositiveInfinity;
|
||||
var minY = float.PositiveInfinity;
|
||||
var maxX = float.NegativeInfinity;
|
||||
var maxY = float.NegativeInfinity;
|
||||
|
||||
for (var index = firstClaimIndex; index < firstClaimIndex + claimCount; index++)
|
||||
{
|
||||
var claim = claims[index];
|
||||
var radius = MathF.Max(claim.Radii.X, claim.Radii.Y);
|
||||
minX = MathF.Min(minX, claim.Center.X - radius);
|
||||
minY = MathF.Min(minY, claim.Center.Y - radius);
|
||||
maxX = MathF.Max(maxX, claim.Center.X + radius);
|
||||
maxY = MathF.Max(maxY, claim.Center.Y + radius);
|
||||
}
|
||||
|
||||
return new Vector4(minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
private static CubeFace ParseFace(string value, int lineIndex)
|
||||
{
|
||||
if (Enum.TryParse<CubeFace>(value, ignoreCase: false, out var face))
|
||||
{
|
||||
return face;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid cube face `{value}` at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(string value, int lineIndex)
|
||||
{
|
||||
var parsed = ParseInt(value, lineIndex);
|
||||
if (parsed <= 0)
|
||||
{
|
||||
throw new FormatException($"Expected positive integer at line {lineIndex + 1}, got `{value}`.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static int ParseInt(string value, int lineIndex)
|
||||
{
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid integer `{value}` at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
private static float ParsePositiveFloat(string value, int lineIndex)
|
||||
{
|
||||
var parsed = ParseFloat(value, lineIndex);
|
||||
if (parsed <= 0.0f)
|
||||
{
|
||||
throw new FormatException($"Expected positive number at line {lineIndex + 1}, got `{value}`.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static float ParseFloat(string value, int lineIndex)
|
||||
{
|
||||
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid number `{value}` at line {lineIndex + 1}.");
|
||||
}
|
||||
|
||||
private static float HashAngle(int seed, int level, int branch)
|
||||
{
|
||||
var value = (seed * 1103515245) ^ (level * 374761393) ^ (branch * 668265263);
|
||||
value ^= value >> 13;
|
||||
value *= 1274126177;
|
||||
value ^= value >> 16;
|
||||
var normalized = (value & 0xFFFF) / 65535.0f;
|
||||
return (normalized - 0.5f) * 0.35f;
|
||||
}
|
||||
|
||||
private sealed class NodeDraft(AquariumFractalKey key, AquariumFractalKey domainKey, int firstClaimIndex)
|
||||
{
|
||||
public AquariumFractalKey Key { get; } = key;
|
||||
|
||||
public AquariumFractalKey DomainKey { get; } = domainKey;
|
||||
|
||||
public int FirstClaimIndex { get; } = firstClaimIndex;
|
||||
|
||||
public int ClaimCount { get; set; }
|
||||
}
|
||||
|
||||
private sealed class AffineIfsDraft(
|
||||
AquariumFractalKey key,
|
||||
AquariumFractalKey domainKey,
|
||||
string name,
|
||||
Vector2 start,
|
||||
int seed,
|
||||
string tags)
|
||||
{
|
||||
public AquariumFractalKey Key { get; } = key;
|
||||
|
||||
public AquariumFractalKey DomainKey { get; } = domainKey;
|
||||
|
||||
public string Name { get; } = name;
|
||||
|
||||
public Vector2 Start { get; } = start;
|
||||
|
||||
public int Seed { get; } = seed;
|
||||
|
||||
public string Tags { get; } = tags;
|
||||
|
||||
public List<FractalAffineIfsTransform2D> Transforms { get; } = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalFlameChaosGame
|
||||
{
|
||||
public static Vector2[] Generate(
|
||||
FractalFlameDefinition definition,
|
||||
int count,
|
||||
int burnIn,
|
||||
IFractalRandom random)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(definition);
|
||||
ArgumentNullException.ThrowIfNull(random);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(burnIn);
|
||||
|
||||
var points = new Vector2[count];
|
||||
var point = definition.Start;
|
||||
var total = definition.Transforms.Sum(transform => MathF.Max(transform.Weight, 0.0f));
|
||||
if (total <= 0.0f)
|
||||
{
|
||||
throw new ArgumentException("Flame transform weights must have positive total.", nameof(definition));
|
||||
}
|
||||
|
||||
for (var step = 0; step < burnIn + count; step++)
|
||||
{
|
||||
point = SelectTransform(definition.Transforms, total, (float)random.NextDouble()).Apply(point, random);
|
||||
if (step >= burnIn)
|
||||
{
|
||||
points[step - burnIn] = point;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static FractalFlameTransform2D SelectTransform(
|
||||
IReadOnlyList<FractalFlameTransform2D> transforms,
|
||||
float total,
|
||||
float unit)
|
||||
{
|
||||
var target = unit * total;
|
||||
var cumulative = 0.0f;
|
||||
for (var index = 0; index < transforms.Count; index++)
|
||||
{
|
||||
cumulative += MathF.Max(transforms[index].Weight, 0.0f);
|
||||
if (target <= cumulative)
|
||||
{
|
||||
return transforms[index];
|
||||
}
|
||||
}
|
||||
|
||||
return transforms[^1];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public readonly record struct FractalFlameVariationWeights(
|
||||
float Linear,
|
||||
float Spherical,
|
||||
float Bubble,
|
||||
float Disc = 0.0f,
|
||||
float Julian = 0.0f,
|
||||
float JulianPower = 2.0f,
|
||||
float JulianDist = 1.0f,
|
||||
float GaussianBlur = 0.0f)
|
||||
{
|
||||
public bool HasAny =>
|
||||
Linear != 0.0f ||
|
||||
Spherical != 0.0f ||
|
||||
Bubble != 0.0f ||
|
||||
Disc != 0.0f ||
|
||||
Julian != 0.0f ||
|
||||
GaussianBlur != 0.0f;
|
||||
|
||||
public Vector2 Apply(Vector2 point, IFractalRandom? random = null)
|
||||
{
|
||||
var result = Vector2.Zero;
|
||||
if (Linear != 0.0f)
|
||||
{
|
||||
result += point * Linear;
|
||||
}
|
||||
|
||||
var radiusSquared = Vector2.Dot(point, point);
|
||||
if (Spherical != 0.0f)
|
||||
{
|
||||
result += point * (Spherical / MathF.Max(radiusSquared, 0.000001f));
|
||||
}
|
||||
|
||||
if (Bubble != 0.0f)
|
||||
{
|
||||
result += point * (Bubble * 4.0f / (radiusSquared + 4.0f));
|
||||
}
|
||||
|
||||
if (Disc != 0.0f)
|
||||
{
|
||||
var radius = MathF.Sqrt(radiusSquared);
|
||||
var theta = MathF.Atan2(point.X, point.Y);
|
||||
var scaledTheta = Disc * theta / MathF.PI;
|
||||
result += new Vector2(
|
||||
MathF.Sin(MathF.PI * radius) * scaledTheta,
|
||||
MathF.Cos(MathF.PI * radius) * scaledTheta);
|
||||
}
|
||||
|
||||
if (Julian != 0.0f)
|
||||
{
|
||||
var power = MathF.Abs(JulianPower) < 1.0f ? 1.0f : JulianPower;
|
||||
var absPower = MathF.Max(MathF.Abs(power), 1.0f);
|
||||
var branch = MathF.Floor(NextUnit(random) * absPower);
|
||||
var angle = (MathF.Atan2(point.Y, point.X) + (2.0f * MathF.PI * branch)) / power;
|
||||
var radius = MathF.Pow(MathF.Max(MathF.Sqrt(radiusSquared), 0.000001f), JulianDist / power);
|
||||
result += new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * (Julian * radius);
|
||||
}
|
||||
|
||||
if (GaussianBlur != 0.0f)
|
||||
{
|
||||
var amount = GaussianBlur * (NextUnit(random) + NextUnit(random) + NextUnit(random) + NextUnit(random) - 2.0f);
|
||||
var angle = NextUnit(random) * 2.0f * MathF.PI;
|
||||
result += new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * amount;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static float NextUnit(IFractalRandom? random)
|
||||
{
|
||||
return random is null ? 0.5f : (float)random.NextDouble();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct FractalFlameTransform2D(
|
||||
string Name,
|
||||
Vector4 Matrix,
|
||||
Vector2 Translation,
|
||||
Vector4 PostMatrix,
|
||||
Vector2 PostTranslation,
|
||||
float Weight,
|
||||
float Color,
|
||||
FractalFlameVariationWeights Variations)
|
||||
{
|
||||
public Vector2 Apply(Vector2 point)
|
||||
{
|
||||
return Apply(point, random: null);
|
||||
}
|
||||
|
||||
public Vector2 Apply(Vector2 point, IFractalRandom? random)
|
||||
{
|
||||
var affine = new Vector2(
|
||||
(Matrix.X * point.X) + (Matrix.Y * point.Y) + Translation.X,
|
||||
(Matrix.Z * point.X) + (Matrix.W * point.Y) + Translation.Y);
|
||||
var varied = Variations.HasAny ? Variations.Apply(affine, random) : affine;
|
||||
return new Vector2(
|
||||
(PostMatrix.X * varied.X) + (PostMatrix.Y * varied.Y) + PostTranslation.X,
|
||||
(PostMatrix.Z * varied.X) + (PostMatrix.W * varied.Y) + PostTranslation.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FractalFlameDefinition
|
||||
{
|
||||
public FractalFlameDefinition(
|
||||
string name,
|
||||
Vector2 start,
|
||||
int seed,
|
||||
IReadOnlyList<FractalFlameTransform2D> transforms)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Flame name must not be empty.", nameof(name));
|
||||
}
|
||||
|
||||
if (transforms.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Flame definitions need at least one transform.", nameof(transforms));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
Start = start;
|
||||
Seed = seed;
|
||||
Transforms = transforms;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Vector2 Start { get; }
|
||||
|
||||
public int Seed { get; }
|
||||
|
||||
public IReadOnlyList<FractalFlameTransform2D> Transforms { get; }
|
||||
}
|
||||
|
|
@ -1,332 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalFlameFileParser
|
||||
{
|
||||
public static FractalFlameDefinition ParseFirst(string source, int seed = 0)
|
||||
{
|
||||
return ParseFirstWithReport(source, seed).Definition;
|
||||
}
|
||||
|
||||
public static FractalFlameParseResult ParseFirstWithReport(string source, int seed = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var document = XDocument.Parse(source);
|
||||
var flame = document.Descendants("flame").FirstOrDefault()
|
||||
?? throw new FormatException("Flame file does not contain a flame element.");
|
||||
var name = ((string?)flame.Attribute("name")) ?? "flame";
|
||||
var isJWildfireDialect = string.Equals(document.Root?.Name.LocalName, "Flames", StringComparison.Ordinal)
|
||||
|| flame.Elements("xform").Any(xform => xform.Element("variationGroup") is not null);
|
||||
var parsed = flame.Elements("xform").Select((xform, index) => ParseXform(xform, index, isJWildfireDialect)).ToArray();
|
||||
var definition = new FractalFlameDefinition(name, Vector2.Zero, seed, parsed.Select(row => row.Transform).ToArray());
|
||||
var report = new FractalFlameImportReport(
|
||||
name,
|
||||
isJWildfireDialect ? "JWildfire" : "Apophysis/FLAM3 subset",
|
||||
parsed.Length,
|
||||
BuildFlameFieldReports(flame),
|
||||
parsed.Select(row => row.Report).ToArray());
|
||||
return new FractalFlameParseResult(definition, report);
|
||||
}
|
||||
|
||||
private static ParsedXform ParseXform(XElement element, int index, bool isJWildfireDialect)
|
||||
{
|
||||
var coefs = ParseFloatList((string?)element.Attribute("coefs"), expectedCount: 6, "xform coefs");
|
||||
var variationGroup = element.Element("variationGroup");
|
||||
var variationSource = variationGroup ?? element;
|
||||
var accepted = new List<string> { "coefs", "weight", "color" };
|
||||
var approximated = new List<string>();
|
||||
var ignored = new List<string>();
|
||||
var rejected = new List<string>();
|
||||
if (element.Attribute("name") is not null)
|
||||
{
|
||||
accepted.Add("name");
|
||||
}
|
||||
|
||||
if (isJWildfireDialect)
|
||||
{
|
||||
accepted.Add("coefs: JWildfire xy layout");
|
||||
}
|
||||
|
||||
if (variationGroup is not null)
|
||||
{
|
||||
accepted.Add("variationGroup");
|
||||
}
|
||||
|
||||
var matrix = isJWildfireDialect
|
||||
? new Vector4(coefs[0], coefs[1], coefs[2], coefs[3])
|
||||
: new Vector4(coefs[0], coefs[1], coefs[3], coefs[4]);
|
||||
var translation = isJWildfireDialect
|
||||
? new Vector2(coefs[4], coefs[5])
|
||||
: new Vector2(coefs[2], coefs[5]);
|
||||
var post = ParseOptionalFloatList((string?)element.Attribute("post"), expectedCount: 6);
|
||||
var postMatrix = post is null
|
||||
? new Vector4(1.0f, 0.0f, 0.0f, 1.0f)
|
||||
: isJWildfireDialect
|
||||
? new Vector4(post[0], post[1], post[2], post[3])
|
||||
: new Vector4(post[0], post[1], post[3], post[4]);
|
||||
var postTranslation = post is null
|
||||
? Vector2.Zero
|
||||
: isJWildfireDialect
|
||||
? new Vector2(post[4], post[5])
|
||||
: new Vector2(post[2], post[5]);
|
||||
var variations = new FractalFlameVariationWeights(
|
||||
ParseOptionalFloat(variationSource, "linear", 0.0f)
|
||||
+ (isJWildfireDialect ? 0.0f : ParseOptionalFloat(variationSource, "normal", 0.0f)),
|
||||
ParseOptionalFloat(variationSource, "spherical", 0.0f),
|
||||
ParseOptionalFloat(variationSource, "bubble", 0.0f),
|
||||
ParseOptionalFloat(variationSource, "disc", 0.0f) + ParseOptionalFloat(variationSource, "jwf_disc", 0.0f),
|
||||
ParseOptionalFloat(variationSource, "julian", 0.0f) + ParseOptionalFloat(variationSource, "jwf_julian", 0.0f),
|
||||
ParseOptionalFloat(variationSource, "julian_power", ParseOptionalFloat(variationSource, "jwf_julian_power", 2.0f)),
|
||||
ParseOptionalFloat(variationSource, "julian_dist", ParseOptionalFloat(variationSource, "jwf_julian_dist", 1.0f)),
|
||||
ParseOptionalFloat(variationSource, "gaussian_blur", 0.0f) + ParseOptionalFloat(variationSource, "jwf_gaussian_blur", 0.0f));
|
||||
ClassifyVariationFields(variationSource, isJWildfireDialect, accepted, approximated, ignored, rejected);
|
||||
ClassifyXformFields(element, variationGroup, accepted, ignored, rejected);
|
||||
var transformName = ((string?)element.Attribute("name")) ?? $"xform/{index:0000}";
|
||||
|
||||
var transform = new FractalFlameTransform2D(
|
||||
transformName,
|
||||
matrix,
|
||||
translation,
|
||||
postMatrix,
|
||||
postTranslation,
|
||||
ParseOptionalFloat(element, "weight", 1.0f),
|
||||
ParseOptionalFloat(element, "color", 0.0f),
|
||||
variations);
|
||||
return new ParsedXform(transform, new FractalFlameTransformImportReport(
|
||||
index,
|
||||
transformName,
|
||||
accepted.Distinct().OrderBy(value => value, StringComparer.Ordinal).ToArray(),
|
||||
approximated.Distinct().OrderBy(value => value, StringComparer.Ordinal).ToArray(),
|
||||
ignored.Distinct().OrderBy(value => value, StringComparer.Ordinal).ToArray(),
|
||||
rejected.Distinct().OrderBy(value => value, StringComparer.Ordinal).ToArray()));
|
||||
}
|
||||
|
||||
private static float ParseOptionalFloat(XElement element, string name, float fallback)
|
||||
{
|
||||
var attribute = (string?)element.Attribute(name);
|
||||
return attribute is null ? fallback : float.Parse(attribute, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static float[] ParseFloatList(string? source, int expectedCount, string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
throw new FormatException($"{label} must not be empty.");
|
||||
}
|
||||
|
||||
var values = source
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(value => float.Parse(value, CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
if (values.Length != expectedCount)
|
||||
{
|
||||
throw new FormatException($"{label} expected {expectedCount} values, got {values.Length}.");
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static float[]? ParseOptionalFloatList(string? source, int expectedCount)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(source)
|
||||
? null
|
||||
: ParseFloatList(source, expectedCount, "optional float list");
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildFlameFieldReports(XElement flame)
|
||||
{
|
||||
var structural = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"name",
|
||||
"size",
|
||||
"center",
|
||||
"scale",
|
||||
"rotate",
|
||||
};
|
||||
return flame.Attributes()
|
||||
.Select(attribute => attribute.Name.LocalName)
|
||||
.Where(name => !structural.Contains(name))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static void ClassifyVariationFields(
|
||||
XElement variationSource,
|
||||
bool isJWildfireDialect,
|
||||
List<string> accepted,
|
||||
List<string> approximated,
|
||||
List<string> ignored,
|
||||
List<string> rejected)
|
||||
{
|
||||
var supported = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"linear",
|
||||
"spherical",
|
||||
"bubble",
|
||||
"disc",
|
||||
"jwf_disc",
|
||||
"julian",
|
||||
"jwf_julian",
|
||||
"julian_power",
|
||||
"jwf_julian_power",
|
||||
"julian_dist",
|
||||
"jwf_julian_dist",
|
||||
};
|
||||
foreach (var attribute in variationSource.Attributes())
|
||||
{
|
||||
var name = attribute.Name.LocalName;
|
||||
if (supported.Contains(name))
|
||||
{
|
||||
accepted.Add($"variation:{name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name is "normal")
|
||||
{
|
||||
if (isJWildfireDialect)
|
||||
{
|
||||
ignored.Add("variationGroup:normal");
|
||||
}
|
||||
else
|
||||
{
|
||||
approximated.Add("variation:normal -> linear");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name is "gaussian_blur" or "jwf_gaussian_blur")
|
||||
{
|
||||
approximated.Add($"variation:{name} -> stochastic gaussian_blur");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseFloat(attribute.Value, out var value) && value == 0.0f)
|
||||
{
|
||||
ignored.Add($"variation:{name}=0");
|
||||
continue;
|
||||
}
|
||||
|
||||
rejected.Add($"variation:{name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClassifyXformFields(
|
||||
XElement element,
|
||||
XElement? variationGroup,
|
||||
List<string> accepted,
|
||||
List<string> ignored,
|
||||
List<string> rejected)
|
||||
{
|
||||
var handled = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"name",
|
||||
"coefs",
|
||||
"weight",
|
||||
"color",
|
||||
};
|
||||
foreach (var attribute in element.Attributes())
|
||||
{
|
||||
var name = attribute.Name.LocalName;
|
||||
if (handled.Contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name is "post")
|
||||
{
|
||||
accepted.Add(IsIdentityPost(attribute.Value) ? "post=identity" : "post");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name is "chaos")
|
||||
{
|
||||
if (IsNeutralFloatList(attribute.Value, 1.0f))
|
||||
{
|
||||
ignored.Add("chaos=all-ones");
|
||||
}
|
||||
else
|
||||
{
|
||||
rejected.Add("chaos");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.StartsWith("wfield_", StringComparison.Ordinal))
|
||||
{
|
||||
rejected.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
ignored.Add(name);
|
||||
}
|
||||
|
||||
if (variationGroup is not null)
|
||||
{
|
||||
foreach (var child in element.Elements())
|
||||
{
|
||||
if (child != variationGroup)
|
||||
{
|
||||
ignored.Add(child.Name.LocalName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIdentityPost(string value)
|
||||
{
|
||||
var values = ParseFloatList(value, 6, "post");
|
||||
return values[0] == 1.0f
|
||||
&& values[1] == 0.0f
|
||||
&& values[2] == 0.0f
|
||||
&& values[3] == 1.0f
|
||||
&& values[4] == 0.0f
|
||||
&& values[5] == 0.0f;
|
||||
}
|
||||
|
||||
private static bool IsNeutralFloatList(string value, float neutral)
|
||||
{
|
||||
var values = value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return values.Length > 0 && values.All(item => TryParseFloat(item, out var parsed) && parsed == neutral);
|
||||
}
|
||||
|
||||
private static bool TryParseFloat(string source, out float value)
|
||||
{
|
||||
return float.TryParse(source, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
private readonly record struct ParsedXform(FractalFlameTransform2D Transform, FractalFlameTransformImportReport Report);
|
||||
}
|
||||
|
||||
public sealed record FractalFlameParseResult(
|
||||
FractalFlameDefinition Definition,
|
||||
FractalFlameImportReport Report);
|
||||
|
||||
public sealed record FractalFlameImportReport(
|
||||
string FlameName,
|
||||
string Dialect,
|
||||
int TransformCount,
|
||||
IReadOnlyList<string> IgnoredFlameAttributes,
|
||||
IReadOnlyList<FractalFlameTransformImportReport> Transforms)
|
||||
{
|
||||
public int AcceptedFieldCount => Transforms.Sum(transform => transform.AcceptedFields.Count);
|
||||
|
||||
public int ApproximatedFieldCount => Transforms.Sum(transform => transform.ApproximatedFields.Count);
|
||||
|
||||
public int IgnoredFieldCount => IgnoredFlameAttributes.Count + Transforms.Sum(transform => transform.IgnoredFields.Count);
|
||||
|
||||
public int RejectedFieldCount => Transforms.Sum(transform => transform.RejectedFields.Count);
|
||||
}
|
||||
|
||||
public sealed record FractalFlameTransformImportReport(
|
||||
int Index,
|
||||
string Name,
|
||||
IReadOnlyList<string> AcceptedFields,
|
||||
IReadOnlyList<string> ApproximatedFields,
|
||||
IReadOnlyList<string> IgnoredFields,
|
||||
IReadOnlyList<string> RejectedFields);
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public readonly record struct FractalFlameIterationState(
|
||||
Vector2 Point,
|
||||
uint RandomState,
|
||||
int Step)
|
||||
{
|
||||
public static FractalFlameIterationState Create(uint seed, int sampleIndex)
|
||||
{
|
||||
var state = Hash(seed ^ (uint)sampleIndex);
|
||||
return new FractalFlameIterationState(Vector2.Zero, state == 0 ? 0xA341316Cu : state, 0);
|
||||
}
|
||||
|
||||
private static uint Hash(uint value)
|
||||
{
|
||||
value ^= value >> 16;
|
||||
value *= 747796405u;
|
||||
value ^= value >> 16;
|
||||
value *= 2891336453u;
|
||||
value ^= value >> 16;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FractalFlameIterationStepper
|
||||
{
|
||||
public static FractalFlameIterationState Advance(
|
||||
FractalFlameDefinition definition,
|
||||
FractalFlameIterationState state,
|
||||
int iterations)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(definition);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(iterations);
|
||||
|
||||
var random = new StatefulRandom(state.RandomState);
|
||||
var point = state.Point;
|
||||
var total = definition.Transforms.Sum(transform => MathF.Max(transform.Weight, 0.0f));
|
||||
if (total <= 0.0f)
|
||||
{
|
||||
throw new ArgumentException("Flame transform weights must have positive total.", nameof(definition));
|
||||
}
|
||||
|
||||
for (var iteration = 0; iteration < iterations; iteration++)
|
||||
{
|
||||
point = SelectTransform(definition.Transforms, total, (float)random.NextDouble()).Apply(point, random);
|
||||
}
|
||||
|
||||
return new FractalFlameIterationState(point, random.State, checked(state.Step + iterations));
|
||||
}
|
||||
|
||||
private static FractalFlameTransform2D SelectTransform(
|
||||
IReadOnlyList<FractalFlameTransform2D> transforms,
|
||||
float total,
|
||||
float unit)
|
||||
{
|
||||
var target = unit * total;
|
||||
var cumulative = 0.0f;
|
||||
for (var index = 0; index < transforms.Count; index++)
|
||||
{
|
||||
cumulative += MathF.Max(transforms[index].Weight, 0.0f);
|
||||
if (target <= cumulative)
|
||||
{
|
||||
return transforms[index];
|
||||
}
|
||||
}
|
||||
|
||||
return transforms[^1];
|
||||
}
|
||||
|
||||
private sealed class StatefulRandom(uint state) : IFractalRandom
|
||||
{
|
||||
public uint State { get; private set; } = state == 0 ? 0xA341316Cu : state;
|
||||
|
||||
public double NextDouble()
|
||||
{
|
||||
var value = State;
|
||||
value ^= value << 13;
|
||||
value ^= value >> 17;
|
||||
value ^= value << 5;
|
||||
State = value;
|
||||
return value / ((double)uint.MaxValue + 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalHistogramResidualFocus
|
||||
{
|
||||
public static Vector4 Estimate(FractalPointHistogram reference, FractalPointHistogram candidate)
|
||||
{
|
||||
if (reference.Width != candidate.Width || reference.Height != candidate.Height)
|
||||
{
|
||||
throw new ArgumentException("Histogram dimensions must match.", nameof(candidate));
|
||||
}
|
||||
|
||||
if (reference.Bins.Length != candidate.Bins.Length)
|
||||
{
|
||||
throw new ArgumentException("Histogram bin counts must match.", nameof(candidate));
|
||||
}
|
||||
|
||||
var referenceTotal = Math.Max(reference.HitCount, 1);
|
||||
var candidateTotal = Math.Max(candidate.HitCount, 1);
|
||||
var span = new Vector2(reference.Bounds.Z - reference.Bounds.X, reference.Bounds.W - reference.Bounds.Y);
|
||||
var weighted = Vector2.Zero;
|
||||
var mass = 0.0f;
|
||||
|
||||
for (var y = 0; y < reference.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < reference.Width; x++)
|
||||
{
|
||||
var index = (y * reference.Width) + x;
|
||||
var delta = (reference.Bins[index] / (float)referenceTotal) - (candidate.Bins[index] / (float)candidateTotal);
|
||||
if (delta <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var world = BinCenter(reference, span, x, y);
|
||||
weighted += world * delta;
|
||||
mass += delta;
|
||||
}
|
||||
}
|
||||
|
||||
if (mass <= 0.0f)
|
||||
{
|
||||
return Vector4.Zero;
|
||||
}
|
||||
|
||||
var center = weighted / mass;
|
||||
var variance = 0.0f;
|
||||
for (var y = 0; y < reference.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < reference.Width; x++)
|
||||
{
|
||||
var index = (y * reference.Width) + x;
|
||||
var delta = (reference.Bins[index] / (float)referenceTotal) - (candidate.Bins[index] / (float)candidateTotal);
|
||||
if (delta <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var world = BinCenter(reference, span, x, y);
|
||||
variance += Vector2.DistanceSquared(world, center) * delta;
|
||||
}
|
||||
}
|
||||
|
||||
var minRadius = MathF.Max(span.X / MathF.Max(reference.Width, 1), span.Y / MathF.Max(reference.Height, 1)) * 1.5f;
|
||||
var maxRadius = MathF.Max(span.X, span.Y) * 0.75f;
|
||||
var radius = Math.Clamp(MathF.Sqrt(variance / mass) * 2.0f, minRadius, maxRadius);
|
||||
var strength = Math.Clamp(mass * 3.0f, 0.05f, 1.0f);
|
||||
return new Vector4(center.X, center.Y, radius, strength);
|
||||
}
|
||||
|
||||
private static Vector2 BinCenter(FractalPointHistogram histogram, Vector2 span, int x, int y)
|
||||
{
|
||||
return new Vector2(
|
||||
histogram.Bounds.X + ((x + 0.5f) / histogram.Width) * span.X,
|
||||
histogram.Bounds.Y + ((y + 0.5f) / histogram.Height) * span.Y);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public sealed class FractalOwnershipTree
|
||||
{
|
||||
public FractalOwnershipTree(
|
||||
AquariumFractalDomain domain,
|
||||
IReadOnlyList<AquariumFractalNode> nodes,
|
||||
IReadOnlyList<AquariumBrushClaim> claims)
|
||||
: this(domain, [domain], nodes, claims, [])
|
||||
{
|
||||
}
|
||||
|
||||
public FractalOwnershipTree(
|
||||
AquariumFractalDomain domain,
|
||||
IReadOnlyList<AquariumFractalDomain> domains,
|
||||
IReadOnlyList<AquariumFractalNode> nodes,
|
||||
IReadOnlyList<AquariumBrushClaim> claims)
|
||||
: this(domain, domains, nodes, claims, [])
|
||||
{
|
||||
}
|
||||
|
||||
public FractalOwnershipTree(
|
||||
AquariumFractalDomain domain,
|
||||
IReadOnlyList<AquariumFractalDomain> domains,
|
||||
IReadOnlyList<AquariumFractalNode> nodes,
|
||||
IReadOnlyList<AquariumBrushClaim> claims,
|
||||
IReadOnlyList<FractalAffineIfsDefinition> affineIfsDefinitions)
|
||||
{
|
||||
Domain = domain;
|
||||
DomainGraph = new FractalDomainGraph(domains);
|
||||
DomainGraph.GetRequired(domain.Key);
|
||||
Domains = DomainGraph.Domains;
|
||||
Nodes = nodes;
|
||||
Claims = claims;
|
||||
AffineIfsDefinitions = affineIfsDefinitions;
|
||||
}
|
||||
|
||||
public AquariumFractalDomain Domain { get; }
|
||||
|
||||
public IReadOnlyList<AquariumFractalDomain> Domains { get; }
|
||||
|
||||
public FractalDomainGraph DomainGraph { get; }
|
||||
|
||||
public IReadOnlyList<AquariumFractalNode> Nodes { get; }
|
||||
|
||||
public IReadOnlyList<AquariumBrushClaim> Claims { get; }
|
||||
|
||||
public IReadOnlyList<FractalAffineIfsDefinition> AffineIfsDefinitions { get; }
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalOwnershipTreeBuilder
|
||||
{
|
||||
public static FractalOwnershipTree BuildFlatUnion(
|
||||
AquariumFractalDomain domain,
|
||||
AquariumFractalKey rootKey,
|
||||
IReadOnlyList<AquariumBrushClaim> claims)
|
||||
{
|
||||
return BuildFlatUnion(domain, [domain], rootKey, claims);
|
||||
}
|
||||
|
||||
public static FractalOwnershipTree BuildFlatUnion(
|
||||
AquariumFractalDomain domain,
|
||||
IReadOnlyList<AquariumFractalDomain> domains,
|
||||
AquariumFractalKey rootKey,
|
||||
IReadOnlyList<AquariumBrushClaim> claims)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(claims);
|
||||
ArgumentNullException.ThrowIfNull(domains);
|
||||
|
||||
var bounds = BoundsForClaims(claims);
|
||||
var root = new AquariumFractalNode(
|
||||
rootKey,
|
||||
domain.Key,
|
||||
default,
|
||||
AquariumFractalOperation.Union,
|
||||
FirstChildIndex: 0,
|
||||
ChildCount: 0,
|
||||
FirstClaimIndex: 0,
|
||||
ClaimCount: claims.Count,
|
||||
bounds,
|
||||
Seed: 0);
|
||||
|
||||
return new FractalOwnershipTree(domain, domains, [root], claims);
|
||||
}
|
||||
|
||||
private static Vector4 BoundsForClaims(IReadOnlyList<AquariumBrushClaim> claims)
|
||||
{
|
||||
if (claims.Count == 0)
|
||||
{
|
||||
return Vector4.Zero;
|
||||
}
|
||||
|
||||
var minX = float.PositiveInfinity;
|
||||
var minY = float.PositiveInfinity;
|
||||
var maxX = float.NegativeInfinity;
|
||||
var maxY = float.NegativeInfinity;
|
||||
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
var radius = MathF.Max(claim.Radii.X, claim.Radii.Y);
|
||||
minX = MathF.Min(minX, claim.Center.X - radius);
|
||||
minY = MathF.Min(minY, claim.Center.Y - radius);
|
||||
maxX = MathF.Max(maxX, claim.Center.X + radius);
|
||||
maxY = MathF.Max(maxY, claim.Center.Y + radius);
|
||||
}
|
||||
|
||||
return new Vector4(minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public readonly record struct FractalPointHistogram(
|
||||
int Width,
|
||||
int Height,
|
||||
Vector4 Bounds,
|
||||
int[] Bins)
|
||||
{
|
||||
public int HitCount => Bins.Sum();
|
||||
|
||||
public int this[int x, int y] => Bins[y * Width + x];
|
||||
}
|
||||
|
||||
public static class FractalPointHistogramBuilder
|
||||
{
|
||||
public static FractalPointHistogram Build(
|
||||
IReadOnlyList<Vector2> points,
|
||||
int width,
|
||||
int height,
|
||||
Vector4 bounds)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height);
|
||||
|
||||
var bins = new int[width * height];
|
||||
var span = new Vector2(bounds.Z - bounds.X, bounds.W - bounds.Y);
|
||||
foreach (var point in points)
|
||||
{
|
||||
var u = (point.X - bounds.X) / MathF.Max(span.X, 0.000001f);
|
||||
var v = (point.Y - bounds.Y) / MathF.Max(span.Y, 0.000001f);
|
||||
if (u < 0.0f || u >= 1.0f || v < 0.0f || v >= 1.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var x = Math.Clamp((int)(u * width), 0, width - 1);
|
||||
var y = Math.Clamp((int)(v * height), 0, height - 1);
|
||||
bins[y * width + x]++;
|
||||
}
|
||||
|
||||
return new FractalPointHistogram(width, height, bounds, bins);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public readonly record struct FractalPpmImageReceipt(
|
||||
int Width,
|
||||
int Height,
|
||||
int MaxChannelValue,
|
||||
int PixelCount,
|
||||
int NonBlackPixelCount,
|
||||
ulong RgbChecksum,
|
||||
ulong LuminanceChecksum);
|
||||
|
||||
public static class FractalPpmImageReceiptBuilder
|
||||
{
|
||||
public static FractalPpmImageReceipt Build(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
var cursor = 0;
|
||||
var magic = ReadToken(bytes, ref cursor);
|
||||
if (!magic.SequenceEqual("P6"u8))
|
||||
{
|
||||
throw new FormatException("Only binary P6 PPM reference renders are supported.");
|
||||
}
|
||||
|
||||
var width = ParsePositiveInt(ReadToken(bytes, ref cursor), "width");
|
||||
var height = ParsePositiveInt(ReadToken(bytes, ref cursor), "height");
|
||||
var maxChannelValue = ParsePositiveInt(ReadToken(bytes, ref cursor), "max channel value");
|
||||
if (maxChannelValue > 255)
|
||||
{
|
||||
throw new FormatException("Only 8-bit PPM reference renders are supported.");
|
||||
}
|
||||
|
||||
ConsumeSingleWhitespace(bytes, ref cursor);
|
||||
var expectedBytes = checked(width * height * 3);
|
||||
if (bytes.Length - cursor < expectedBytes)
|
||||
{
|
||||
throw new FormatException("PPM reference render ended before all RGB pixels were present.");
|
||||
}
|
||||
|
||||
var rgbChecksum = 1469598103934665603UL;
|
||||
var luminanceChecksum = 1469598103934665603UL;
|
||||
var nonBlack = 0;
|
||||
var pixels = bytes.Slice(cursor, expectedBytes);
|
||||
for (var index = 0; index < pixels.Length; index += 3)
|
||||
{
|
||||
var r = pixels[index];
|
||||
var g = pixels[index + 1];
|
||||
var b = pixels[index + 2];
|
||||
rgbChecksum = Step(rgbChecksum, r);
|
||||
rgbChecksum = Step(rgbChecksum, g);
|
||||
rgbChecksum = Step(rgbChecksum, b);
|
||||
var luminance = (byte)(((r * 54) + (g * 183) + (b * 19)) >> 8);
|
||||
luminanceChecksum = Step(luminanceChecksum, luminance);
|
||||
if ((r | g | b) != 0)
|
||||
{
|
||||
nonBlack++;
|
||||
}
|
||||
}
|
||||
|
||||
return new FractalPpmImageReceipt(
|
||||
width,
|
||||
height,
|
||||
maxChannelValue,
|
||||
width * height,
|
||||
nonBlack,
|
||||
rgbChecksum,
|
||||
luminanceChecksum);
|
||||
}
|
||||
|
||||
private static ulong Step(ulong hash, byte value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 1099511628211UL;
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> ReadToken(ReadOnlySpan<byte> bytes, ref int cursor)
|
||||
{
|
||||
SkipWhitespaceAndComments(bytes, ref cursor);
|
||||
var start = cursor;
|
||||
while (cursor < bytes.Length && !IsWhitespace(bytes[cursor]) && bytes[cursor] != (byte)'#')
|
||||
{
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (cursor == start)
|
||||
{
|
||||
throw new FormatException("PPM reference render header ended unexpectedly.");
|
||||
}
|
||||
|
||||
return bytes[start..cursor];
|
||||
}
|
||||
|
||||
private static void SkipWhitespaceAndComments(ReadOnlySpan<byte> bytes, ref int cursor)
|
||||
{
|
||||
while (cursor < bytes.Length)
|
||||
{
|
||||
if (IsWhitespace(bytes[cursor]))
|
||||
{
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytes[cursor] != (byte)'#')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (cursor < bytes.Length && bytes[cursor] != (byte)'\n')
|
||||
{
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConsumeSingleWhitespace(ReadOnlySpan<byte> bytes, ref int cursor)
|
||||
{
|
||||
if (cursor >= bytes.Length || !IsWhitespace(bytes[cursor]))
|
||||
{
|
||||
throw new FormatException("PPM reference render header is missing the pixel-data separator.");
|
||||
}
|
||||
|
||||
cursor++;
|
||||
}
|
||||
|
||||
private static bool IsWhitespace(byte value)
|
||||
{
|
||||
return value is (byte)' ' or (byte)'\t' or (byte)'\r' or (byte)'\n';
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(ReadOnlySpan<byte> token, string name)
|
||||
{
|
||||
var value = 0;
|
||||
foreach (var digit in token)
|
||||
{
|
||||
if (digit is < (byte)'0' or > (byte)'9')
|
||||
{
|
||||
throw new FormatException($"PPM {name} is not an integer.");
|
||||
}
|
||||
|
||||
value = checked((value * 10) + digit - (byte)'0');
|
||||
}
|
||||
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new FormatException($"PPM {name} must be positive.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalStableKeyBuilder
|
||||
{
|
||||
public static AquariumFractalKey ForCubeTile(CubeTileKey tile, string grammarPath)
|
||||
{
|
||||
return new AquariumFractalKey($"cube/{tile.Face}/L{tile.Level:D2}/{tile.X}/{tile.Y}:{NormalizePath(grammarPath)}");
|
||||
}
|
||||
|
||||
public static AquariumFractalKey Child(AquariumFractalKey parent, string segment)
|
||||
{
|
||||
return new AquariumFractalKey($"{parent.Value}/{NormalizePath(segment)}");
|
||||
}
|
||||
|
||||
private static string NormalizePath(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Fractal key path segment must not be empty.", nameof(value));
|
||||
}
|
||||
|
||||
return value.Trim().Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
public static class FractalSummaryBuilder
|
||||
{
|
||||
public static AquariumFractalSummary[] Build(FractalOwnershipTree tree)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tree);
|
||||
|
||||
var summaries = new AquariumFractalSummary[tree.Nodes.Count];
|
||||
for (var nodeIndex = 0; nodeIndex < tree.Nodes.Count; nodeIndex++)
|
||||
{
|
||||
var node = tree.Nodes[nodeIndex];
|
||||
var maxHeightError = 0.0f;
|
||||
var estimatedCost = 0.0f;
|
||||
var formEncodingMask = AquariumFieldEncodingFlags.None;
|
||||
for (var claimIndex = node.FirstClaimIndex; claimIndex < node.FirstClaimIndex + node.ClaimCount; claimIndex++)
|
||||
{
|
||||
var claim = tree.Claims[claimIndex];
|
||||
if (IsFormPayload(claim.PayloadKind))
|
||||
{
|
||||
maxHeightError += MathF.Abs(claim.Amplitude);
|
||||
formEncodingMask |= EncodingFlagFor(claim.PayloadKind);
|
||||
}
|
||||
|
||||
estimatedCost += 1.0f;
|
||||
}
|
||||
|
||||
summaries[nodeIndex] = new AquariumFractalSummary(
|
||||
node.Key,
|
||||
node.BoundsMinMax,
|
||||
maxHeightError,
|
||||
MaxMaterialDelta: 0.0f,
|
||||
estimatedCost,
|
||||
DescendantCount: node.ChildCount + node.ClaimCount,
|
||||
formEncodingMask);
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private static bool IsFormPayload(AquariumFractalPayloadKind kind)
|
||||
{
|
||||
return kind is AquariumFractalPayloadKind.Height
|
||||
or AquariumFractalPayloadKind.SignedDistance
|
||||
or AquariumFractalPayloadKind.Density
|
||||
or AquariumFractalPayloadKind.Extinction;
|
||||
}
|
||||
|
||||
private static AquariumFieldEncodingFlags EncodingFlagFor(AquariumFractalPayloadKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
AquariumFractalPayloadKind.Height => AquariumFieldEncodingFlags.Height,
|
||||
AquariumFractalPayloadKind.SignedDistance => AquariumFieldEncodingFlags.SignedDistance,
|
||||
AquariumFractalPayloadKind.Density => AquariumFieldEncodingFlags.Density,
|
||||
AquariumFractalPayloadKind.Extinction => AquariumFieldEncodingFlags.Extinction,
|
||||
_ => AquariumFieldEncodingFlags.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public interface IFractalClock
|
||||
{
|
||||
double TimeSeconds { get; }
|
||||
|
||||
ulong FrameIndex { get; }
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public interface IFractalDebugSink
|
||||
{
|
||||
void Record(string channel, string key, double value);
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal;
|
||||
|
||||
public interface IFractalRandom
|
||||
{
|
||||
double NextDouble();
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
using Aquarium.Engine.Fractal.Temporal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public sealed class FractalContributionCache
|
||||
{
|
||||
private readonly FractalOccupancyGraph occupancy = new();
|
||||
private uint frameIndex;
|
||||
|
||||
public FractalContributionReservoirSnapshot LastFrameReservoir { get; private set; }
|
||||
|
||||
public FractalResourcePlan PlanFrame(
|
||||
IReadOnlyList<AquariumFractalSummary> summaries,
|
||||
Func<AquariumFractalSummary, float> projectedPixelsPerWorldUnit,
|
||||
FractalResourceBudget budget,
|
||||
IFractalRandom random,
|
||||
IFractalPayloadStore payloadStore)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(summaries);
|
||||
ArgumentNullException.ThrowIfNull(projectedPixelsPerWorldUnit);
|
||||
ArgumentNullException.ThrowIfNull(random);
|
||||
ArgumentNullException.ThrowIfNull(payloadStore);
|
||||
|
||||
frameIndex++;
|
||||
var states = new AquariumContributionState[summaries.Count];
|
||||
var scores = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < summaries.Count; index++)
|
||||
{
|
||||
var summary = summaries[index];
|
||||
states[index] = occupancy.GetOrCreate(summary.NodeKey).Contribution;
|
||||
var score = FractalProjectedErrorScorer.Score(summary, projectedPixelsPerWorldUnit(summary));
|
||||
scores[summary.NodeKey.Value] = score;
|
||||
var updateProbability = FractalStochasticUpdateScheduler.UpdateProbability(states[index], score, frameIndex);
|
||||
occupancy.MarkVisible(summary.NodeKey, score, frameIndex, updateProbability);
|
||||
}
|
||||
|
||||
var plan = FractalResourceBudgetPlanner.Plan(
|
||||
summaries,
|
||||
states,
|
||||
projectedPixelsPerWorldUnit,
|
||||
state => scores.TryGetValue(state.NodeKey.Value, out var score) ? score : 0.0f,
|
||||
budget,
|
||||
random,
|
||||
payloadStore,
|
||||
frameIndex);
|
||||
|
||||
var reservoir = new ResampledImportanceReservoir<FractalContributionCandidate>();
|
||||
var resident = plan.Residency.ResidentNodes.Select(key => key.Value).ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var nodeKey in plan.UpdateNodes)
|
||||
{
|
||||
if (!scores.TryGetValue(nodeKey.Value, out var observedScore))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = occupancy.GetOrCreate(nodeKey).Contribution;
|
||||
var updateProbability = FractalStochasticUpdateScheduler.UpdateProbability(state, observedScore, frameIndex);
|
||||
var isResident = resident.Contains(nodeKey.Value);
|
||||
var candidate = FractalContributionCandidateGenerator.Build(
|
||||
nodeKey,
|
||||
observedScore,
|
||||
updateProbability,
|
||||
frameIndex,
|
||||
isResident);
|
||||
reservoir.Add(candidate, random.NextDouble());
|
||||
occupancy.Observe(nodeKey, observedScore, frameIndex, isResident);
|
||||
}
|
||||
|
||||
occupancy.DecayUnobserved(frameIndex);
|
||||
LastFrameReservoir = FractalContributionCandidateGenerator.Snapshot(reservoir);
|
||||
return plan;
|
||||
}
|
||||
|
||||
public bool TryGet(AquariumFractalKey nodeKey, out AquariumContributionState state)
|
||||
{
|
||||
if (occupancy.TryGet(nodeKey, out var node))
|
||||
{
|
||||
state = node.Contribution;
|
||||
return true;
|
||||
}
|
||||
|
||||
state = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetOccupancy(AquariumFractalKey nodeKey, out FractalOccupancyNodeState state)
|
||||
{
|
||||
return occupancy.TryGet(nodeKey, out state);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
using Aquarium.Engine.Fractal.Temporal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public readonly record struct FractalContributionCandidate(
|
||||
AquariumFractalKey NodeKey,
|
||||
float ObservedContribution,
|
||||
float UpdateProbability,
|
||||
uint FrameIndex,
|
||||
bool Resident);
|
||||
|
||||
public readonly record struct FractalContributionReservoirSnapshot(
|
||||
bool HasSample,
|
||||
AquariumFractalKey SelectedNodeKey,
|
||||
float SelectedContribution,
|
||||
float SelectedTarget,
|
||||
float WeightSum,
|
||||
int CandidateCount,
|
||||
float ContributionWeight);
|
||||
|
||||
public static class FractalContributionCandidateGenerator
|
||||
{
|
||||
public static ResampledImportanceCandidate<FractalContributionCandidate> Build(
|
||||
AquariumFractalKey nodeKey,
|
||||
float observedContribution,
|
||||
double updateProbability,
|
||||
uint frameIndex,
|
||||
bool resident)
|
||||
{
|
||||
var target = MathF.Max(observedContribution, 0.0f);
|
||||
var sourcePdf = MathF.Max((float)updateProbability, 1.0e-6f);
|
||||
var sample = new FractalContributionCandidate(
|
||||
nodeKey,
|
||||
target,
|
||||
sourcePdf,
|
||||
frameIndex,
|
||||
resident);
|
||||
|
||||
return new ResampledImportanceCandidate<FractalContributionCandidate>(
|
||||
sample,
|
||||
target,
|
||||
sourcePdf);
|
||||
}
|
||||
|
||||
public static FractalContributionReservoirSnapshot Snapshot(
|
||||
ResampledImportanceReservoir<FractalContributionCandidate> reservoir)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reservoir);
|
||||
if (!reservoir.HasSample)
|
||||
{
|
||||
return new FractalContributionReservoirSnapshot(
|
||||
HasSample: false,
|
||||
SelectedNodeKey: default,
|
||||
SelectedContribution: 0.0f,
|
||||
SelectedTarget: 0.0f,
|
||||
WeightSum: reservoir.WeightSum,
|
||||
CandidateCount: reservoir.CandidateCount,
|
||||
ContributionWeight: 0.0f);
|
||||
}
|
||||
|
||||
var selected = reservoir.SelectedSample;
|
||||
return new FractalContributionReservoirSnapshot(
|
||||
HasSample: true,
|
||||
SelectedNodeKey: selected.NodeKey,
|
||||
SelectedContribution: selected.ObservedContribution,
|
||||
SelectedTarget: reservoir.SelectedTarget,
|
||||
WeightSum: reservoir.WeightSum,
|
||||
CandidateCount: reservoir.CandidateCount,
|
||||
ContributionWeight: reservoir.ContributionWeight);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public static class FractalContributionEstimator
|
||||
{
|
||||
public static AquariumContributionState Observe(
|
||||
AquariumContributionState state,
|
||||
float observedContribution,
|
||||
uint frameIndex,
|
||||
bool resident)
|
||||
{
|
||||
var sampleCount = state.SampleCount + 1;
|
||||
var delta = observedContribution - state.MeanContribution;
|
||||
var mean = state.MeanContribution + (delta / sampleCount);
|
||||
var variance = sampleCount <= 1
|
||||
? 0.0f
|
||||
: (((sampleCount - 2) * state.Variance) + (delta * (observedContribution - mean))) / (sampleCount - 1);
|
||||
var confidence = Math.Clamp(sampleCount / 16.0f, 0.0f, 1.0f);
|
||||
|
||||
return state with
|
||||
{
|
||||
MeanContribution = mean,
|
||||
Variance = MathF.Max(variance, 0.0f),
|
||||
Confidence = confidence,
|
||||
SampleCount = sampleCount,
|
||||
LastSampledFrame = frameIndex,
|
||||
Resident = resident,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public sealed class FractalContributionTable
|
||||
{
|
||||
private readonly Dictionary<string, AquariumContributionState> states = new(StringComparer.Ordinal);
|
||||
|
||||
public bool TryGet(AquariumFractalKey nodeKey, out AquariumContributionState state)
|
||||
{
|
||||
return states.TryGetValue(nodeKey.Value, out state);
|
||||
}
|
||||
|
||||
public AquariumContributionState GetOrCreate(AquariumFractalKey nodeKey)
|
||||
{
|
||||
if (states.TryGetValue(nodeKey.Value, out var state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = new AquariumContributionState(
|
||||
nodeKey,
|
||||
MeanContribution: 0.0f,
|
||||
Variance: 1.0f,
|
||||
Confidence: 0.0f,
|
||||
SampleCount: 0,
|
||||
LastSampledFrame: 0,
|
||||
Resident: false);
|
||||
states.Add(nodeKey.Value, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
public void Store(AquariumContributionState state)
|
||||
{
|
||||
states[state.NodeKey.Value] = state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public static class FractalDensitySplat3DCompiler
|
||||
{
|
||||
public static AquariumFractalDensitySplat3D FromProbe(
|
||||
FractalProbeSample probe,
|
||||
float depthRadius,
|
||||
float densityScale = 1.0f,
|
||||
float extinctionScale = 1.0f,
|
||||
float falloff = 4.0f,
|
||||
float shapePower = 1.0f)
|
||||
{
|
||||
if (probe.Layer != AquariumFieldLayer.Form ||
|
||||
(probe.Encoding != AquariumFieldEncoding.Density && probe.Encoding != AquariumFieldEncoding.Extinction))
|
||||
{
|
||||
throw new ArgumentException("3D density splat lowering only accepts Form/Density or Form/Extinction probes.", nameof(probe));
|
||||
}
|
||||
|
||||
if (!float.IsFinite(probe.BoundRadius) || probe.BoundRadius <= 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(probe), probe.BoundRadius, "Probe bound radius must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(depthRadius) || depthRadius <= 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(depthRadius), depthRadius, "3D density splat depth radius must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(densityScale) || densityScale < 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(densityScale), densityScale, "Density scale must be finite and non-negative.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(extinctionScale) || extinctionScale < 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(extinctionScale), extinctionScale, "Extinction scale must be finite and non-negative.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(falloff) || falloff <= 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(falloff), falloff, "3D density splat falloff must be positive and finite.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(shapePower) || shapePower <= 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(shapePower), shapePower, "3D density splat shape power must be positive and finite.");
|
||||
}
|
||||
|
||||
var contribution = Math.Clamp(probe.TargetContribution, 0.0f, 1.0f);
|
||||
var density = probe.Encoding == AquariumFieldEncoding.Density
|
||||
? contribution * densityScale
|
||||
: contribution;
|
||||
var extinction = probe.Encoding == AquariumFieldEncoding.Extinction
|
||||
? contribution * extinctionScale
|
||||
: contribution * densityScale * extinctionScale;
|
||||
|
||||
return new AquariumFractalDensitySplat3D(
|
||||
new AquariumFractalDensitySplat3DKey(probe.DomainKey, probe.NodeKey, probe.PayloadHandle),
|
||||
probe.LocalCenter,
|
||||
Quaternion.Identity,
|
||||
new Vector3(probe.BoundRadius, probe.BoundRadius, depthRadius),
|
||||
falloff,
|
||||
shapePower,
|
||||
Math.Clamp(density, 0.0f, 1.0f),
|
||||
Math.Clamp(extinction, 0.0f, 1.0f),
|
||||
contribution);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public static class FractalGpuProgramCompiler
|
||||
{
|
||||
public static AquariumPackedFractalIfsTransform[] CompileFlame2D(
|
||||
FractalFlameDefinition flame,
|
||||
int maxTransformCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(flame);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTransformCount);
|
||||
|
||||
var transforms = new AquariumPackedFractalIfsTransform[Math.Min(maxTransformCount, flame.Transforms.Count)];
|
||||
for (var index = 0; index < transforms.Length; index++)
|
||||
{
|
||||
transforms[index] = PackFlame(flame.Transforms[index]);
|
||||
}
|
||||
|
||||
return transforms;
|
||||
}
|
||||
|
||||
public static AquariumPackedFractalIfsTransform[] CompileSelectedTree(
|
||||
FractalOwnershipTree tree,
|
||||
IReadOnlyList<AquariumSelectedCut> selectedCut,
|
||||
int maxTransformCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tree);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTransformCount);
|
||||
|
||||
if (selectedCut.Count == 0 || tree.Claims.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var selected = selectedCut.Select(cut => cut.NodeKey).ToHashSet();
|
||||
var domains = tree.Domains.ToDictionary(domain => domain.Key);
|
||||
var transforms = new List<AquariumPackedFractalIfsTransform>(Math.Min(maxTransformCount, tree.Claims.Count));
|
||||
foreach (var node in tree.Nodes)
|
||||
{
|
||||
if (!selected.Contains(node.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var claimEnd = Math.Min(node.FirstClaimIndex + node.ClaimCount, tree.Claims.Count);
|
||||
for (var claimIndex = node.FirstClaimIndex; claimIndex < claimEnd && transforms.Count < maxTransformCount; claimIndex++)
|
||||
{
|
||||
transforms.Add(Pack(tree.Claims[claimIndex], domains));
|
||||
}
|
||||
|
||||
if (transforms.Count >= maxTransformCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return transforms.ToArray();
|
||||
}
|
||||
|
||||
private static AquariumPackedFractalIfsTransform PackFlame(FractalFlameTransform2D transform)
|
||||
{
|
||||
return new AquariumPackedFractalIfsTransform(
|
||||
transform.Matrix,
|
||||
new Vector4(transform.Translation, transform.Variations.Linear, transform.Variations.Spherical),
|
||||
new Vector4(transform.Variations.Bubble, MathF.Max(transform.Weight, 0.0f), transform.Color, transform.Variations.JulianDist),
|
||||
new Vector4(transform.Variations.Disc, transform.Variations.Julian, transform.Variations.JulianPower, transform.Variations.GaussianBlur),
|
||||
transform.PostMatrix,
|
||||
new Vector4(transform.PostTranslation, 0.0f, 0.0f));
|
||||
}
|
||||
|
||||
private static AquariumPackedFractalIfsTransform Pack(
|
||||
AquariumBrushClaim claim,
|
||||
IReadOnlyDictionary<AquariumFractalKey, AquariumFractalDomain> domains)
|
||||
{
|
||||
var radius = MathF.Max(claim.Radii.X, claim.Radii.Y);
|
||||
var material = StableUnit(claim.Tags, claim.Seed);
|
||||
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);
|
||||
var rotationCos = MathF.Cos(claim.RotationRadians);
|
||||
var rotationSin = MathF.Sin(claim.RotationRadians);
|
||||
|
||||
return new AquariumPackedFractalIfsTransform(
|
||||
new Vector4(claim.Center, radius, claim.Amplitude),
|
||||
new Vector4(claim.Radii, claim.RotationRadians, claim.Falloff),
|
||||
new Vector4(material, claim.Seed, rotationCos, rotationSin),
|
||||
tileAddress,
|
||||
new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
|
||||
new Vector4(0.0f, 0.0f, (float)EncodingFor(claim.PayloadKind), 0.0f));
|
||||
}
|
||||
|
||||
private static AquariumFieldEncoding EncodingFor(AquariumFractalPayloadKind payloadKind)
|
||||
{
|
||||
return payloadKind switch
|
||||
{
|
||||
AquariumFractalPayloadKind.Height => AquariumFieldEncoding.Height,
|
||||
AquariumFractalPayloadKind.SignedDistance => AquariumFieldEncoding.SignedDistance,
|
||||
AquariumFractalPayloadKind.Density => AquariumFieldEncoding.Density,
|
||||
AquariumFractalPayloadKind.Extinction => AquariumFieldEncoding.Extinction,
|
||||
AquariumFractalPayloadKind.Material => AquariumFieldEncoding.Material,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(payloadKind), payloadKind, "Unknown fractal payload kind."),
|
||||
};
|
||||
}
|
||||
|
||||
private static float StableUnit(string tags, int seed)
|
||||
{
|
||||
var hash = unchecked((uint)seed);
|
||||
foreach (var ch in tags)
|
||||
{
|
||||
hash ^= ch;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
|
||||
return (hash & 0x00FF_FFFFu) / 16777215.0f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public enum FractalGpuReservoirPassKind
|
||||
{
|
||||
SdfEnvelope,
|
||||
PbrMaterial,
|
||||
Radiosity,
|
||||
}
|
||||
|
||||
public readonly record struct FractalGpuReservoirPassPlan(
|
||||
FractalGpuReservoirPassKind Kind,
|
||||
AquariumFieldLayer Layer,
|
||||
AquariumFieldEncoding Encoding,
|
||||
int ResidentEntries,
|
||||
int UpdatesThisFrame,
|
||||
int CandidatesPerUpdate,
|
||||
double ExpectedFullCoverageFrames,
|
||||
ulong ResidentBytes,
|
||||
ulong CandidateEvaluationsThisFrame);
|
||||
|
||||
public sealed class FractalGpuReservoirFramePlan
|
||||
{
|
||||
public FractalGpuReservoirFramePlan(
|
||||
int splatCount,
|
||||
int splatStrideBytes,
|
||||
ulong splatBytes,
|
||||
IReadOnlyList<FractalGpuReservoirPassPlan> passes)
|
||||
{
|
||||
SplatCount = splatCount;
|
||||
SplatStrideBytes = splatStrideBytes;
|
||||
SplatBytes = splatBytes;
|
||||
Passes = passes;
|
||||
}
|
||||
|
||||
public int SplatCount { get; }
|
||||
|
||||
public int SplatStrideBytes { get; }
|
||||
|
||||
public ulong SplatBytes { get; }
|
||||
|
||||
public IReadOnlyList<FractalGpuReservoirPassPlan> Passes { get; }
|
||||
|
||||
public ulong ReservoirBytes => Passes.Aggregate(0UL, (sum, pass) => sum + pass.ResidentBytes);
|
||||
|
||||
public ulong TotalResidentBytes => SplatBytes + ReservoirBytes;
|
||||
|
||||
public ulong CandidateEvaluationsThisFrame => Passes.Aggregate(0UL, (sum, pass) => sum + pass.CandidateEvaluationsThisFrame);
|
||||
}
|
||||
|
||||
public static class FractalGpuReservoirBudgetPlanner
|
||||
{
|
||||
public static FractalGpuReservoirFramePlan Plan(
|
||||
int splatCount,
|
||||
int reservoirUpdatesPerPass,
|
||||
int candidatesPerUpdate,
|
||||
int splatStrideBytes = 80,
|
||||
int reservoirStrideBytes = 64)
|
||||
{
|
||||
if (splatCount <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(splatCount), splatCount, "Splat count must be positive.");
|
||||
}
|
||||
|
||||
if (reservoirUpdatesPerPass <= 0 || reservoirUpdatesPerPass > splatCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(reservoirUpdatesPerPass), reservoirUpdatesPerPass, "Reservoir updates must be positive and no larger than the resident splat count.");
|
||||
}
|
||||
|
||||
if (candidatesPerUpdate <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(candidatesPerUpdate), candidatesPerUpdate, "Candidates per update must be positive.");
|
||||
}
|
||||
|
||||
if (splatStrideBytes <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(splatStrideBytes), splatStrideBytes, "Splat stride must be positive.");
|
||||
}
|
||||
|
||||
if (reservoirStrideBytes <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(reservoirStrideBytes), reservoirStrideBytes, "Reservoir stride must be positive.");
|
||||
}
|
||||
|
||||
var splatBytes = checked((ulong)splatCount * (ulong)splatStrideBytes);
|
||||
var expectedCoverageFrames = splatCount / (double)reservoirUpdatesPerPass;
|
||||
var passes = new[]
|
||||
{
|
||||
Pass(FractalGpuReservoirPassKind.SdfEnvelope),
|
||||
Pass(FractalGpuReservoirPassKind.PbrMaterial),
|
||||
Pass(FractalGpuReservoirPassKind.Radiosity),
|
||||
};
|
||||
|
||||
return new FractalGpuReservoirFramePlan(
|
||||
splatCount,
|
||||
splatStrideBytes,
|
||||
splatBytes,
|
||||
passes);
|
||||
|
||||
FractalGpuReservoirPassPlan Pass(FractalGpuReservoirPassKind kind)
|
||||
{
|
||||
return new FractalGpuReservoirPassPlan(
|
||||
kind,
|
||||
LayerFor(kind),
|
||||
EncodingFor(kind),
|
||||
splatCount,
|
||||
reservoirUpdatesPerPass,
|
||||
candidatesPerUpdate,
|
||||
expectedCoverageFrames,
|
||||
checked((ulong)splatCount * (ulong)reservoirStrideBytes),
|
||||
checked((ulong)reservoirUpdatesPerPass * (ulong)candidatesPerUpdate));
|
||||
}
|
||||
}
|
||||
|
||||
private static AquariumFieldLayer LayerFor(FractalGpuReservoirPassKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
FractalGpuReservoirPassKind.SdfEnvelope => AquariumFieldLayer.Form,
|
||||
FractalGpuReservoirPassKind.PbrMaterial => AquariumFieldLayer.Appearance,
|
||||
FractalGpuReservoirPassKind.Radiosity => AquariumFieldLayer.Transport,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown reservoir pass kind."),
|
||||
};
|
||||
}
|
||||
|
||||
private static AquariumFieldEncoding EncodingFor(FractalGpuReservoirPassKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
FractalGpuReservoirPassKind.SdfEnvelope => AquariumFieldEncoding.SignedDistance,
|
||||
FractalGpuReservoirPassKind.PbrMaterial => AquariumFieldEncoding.Material,
|
||||
FractalGpuReservoirPassKind.Radiosity => AquariumFieldEncoding.Radiance,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown reservoir pass kind."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public sealed unsafe class FractalNativeBuffer<T> : IDisposable
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly nuint byteCount;
|
||||
private bool disposed;
|
||||
|
||||
public FractalNativeBuffer(int length, nuint alignment = 64)
|
||||
{
|
||||
if (length < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), length, "Native buffer length must not be negative.");
|
||||
}
|
||||
|
||||
if (alignment == 0 || (alignment & (alignment - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(alignment), alignment, "Native buffer alignment must be a power of two.");
|
||||
}
|
||||
|
||||
Length = length;
|
||||
byteCount = checked((nuint)length * (nuint)sizeof(T));
|
||||
Pointer = byteCount == 0
|
||||
? null
|
||||
: (T*)NativeMemory.AlignedAlloc(byteCount, alignment);
|
||||
if (Pointer is null && byteCount > 0)
|
||||
{
|
||||
throw new OutOfMemoryException($"Could not allocate {byteCount} bytes for native buffer.");
|
||||
}
|
||||
|
||||
if (Pointer is not null)
|
||||
{
|
||||
NativeMemory.Clear(Pointer, byteCount);
|
||||
}
|
||||
}
|
||||
|
||||
public int Length { get; }
|
||||
|
||||
public T* Pointer { get; private set; }
|
||||
|
||||
public Span<T> Span
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new Span<T>(Pointer, Length);
|
||||
}
|
||||
}
|
||||
|
||||
public ref T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if ((uint)index >= (uint)Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, "Native buffer index is outside the allocated range.");
|
||||
}
|
||||
|
||||
return ref Pointer[index];
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
if (Pointer is not null)
|
||||
{
|
||||
NativeMemory.AlignedFree(Pointer);
|
||||
Pointer = null;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~FractalNativeBuffer()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(FractalNativeBuffer<T>));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public readonly record struct FractalOccupancyNodeState(
|
||||
AquariumContributionState Contribution,
|
||||
uint LastVisibleFrame,
|
||||
float LastObservedScore,
|
||||
double UpdateProbability);
|
||||
|
||||
public sealed class FractalOccupancyGraph
|
||||
{
|
||||
private const float ConfidenceDecayPerFrame = 0.985f;
|
||||
private const float StaleVarianceGrowth = 0.035f;
|
||||
private readonly Dictionary<string, FractalOccupancyNodeState> nodes = new(StringComparer.Ordinal);
|
||||
|
||||
public FractalOccupancyNodeState GetOrCreate(AquariumFractalKey nodeKey)
|
||||
{
|
||||
if (nodes.TryGetValue(nodeKey.Value, out var state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = new FractalOccupancyNodeState(
|
||||
CreateEmptyContributionState(nodeKey),
|
||||
LastVisibleFrame: 0,
|
||||
LastObservedScore: 0.0f,
|
||||
UpdateProbability: 0.0);
|
||||
nodes.Add(nodeKey.Value, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
public bool TryGet(AquariumFractalKey nodeKey, out FractalOccupancyNodeState state)
|
||||
{
|
||||
return nodes.TryGetValue(nodeKey.Value, out state);
|
||||
}
|
||||
|
||||
public void MarkVisible(AquariumFractalKey nodeKey, float observedScore, uint frameIndex, double updateProbability)
|
||||
{
|
||||
var state = GetOrCreate(nodeKey);
|
||||
nodes[nodeKey.Value] = state with
|
||||
{
|
||||
LastVisibleFrame = frameIndex,
|
||||
LastObservedScore = MathF.Max(observedScore, 0.0f),
|
||||
UpdateProbability = Math.Clamp(updateProbability, 0.0, 1.0),
|
||||
};
|
||||
}
|
||||
|
||||
public AquariumContributionState Observe(AquariumFractalKey nodeKey, float observedContribution, uint frameIndex, bool resident)
|
||||
{
|
||||
var state = GetOrCreate(nodeKey);
|
||||
var contribution = FractalContributionEstimator.Observe(
|
||||
state.Contribution,
|
||||
observedContribution,
|
||||
frameIndex,
|
||||
resident);
|
||||
nodes[nodeKey.Value] = state with
|
||||
{
|
||||
Contribution = contribution,
|
||||
LastVisibleFrame = frameIndex,
|
||||
LastObservedScore = MathF.Max(observedContribution, 0.0f),
|
||||
};
|
||||
return contribution;
|
||||
}
|
||||
|
||||
public void DecayUnobserved(uint frameIndex)
|
||||
{
|
||||
foreach (var key in nodes.Keys.ToArray())
|
||||
{
|
||||
var state = nodes[key];
|
||||
if (state.Contribution.LastSampledFrame == frameIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var age = frameIndex >= state.Contribution.LastSampledFrame
|
||||
? frameIndex - state.Contribution.LastSampledFrame
|
||||
: 0;
|
||||
if (age == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var staleGrowth = MathF.Min(age * StaleVarianceGrowth, 4.0f);
|
||||
nodes[key] = state with
|
||||
{
|
||||
Contribution = state.Contribution with
|
||||
{
|
||||
Confidence = MathF.Max(0.0f, state.Contribution.Confidence * ConfidenceDecayPerFrame),
|
||||
Variance = MathF.Max(state.Contribution.Variance, state.Contribution.Variance + staleGrowth),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static AquariumContributionState CreateEmptyContributionState(AquariumFractalKey nodeKey)
|
||||
{
|
||||
return new AquariumContributionState(
|
||||
nodeKey,
|
||||
MeanContribution: 0.0f,
|
||||
Variance: 1.0f,
|
||||
Confidence: 0.0f,
|
||||
SampleCount: 0,
|
||||
LastSampledFrame: 0,
|
||||
Resident: false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Aquarium.Engine.Fractal.Grammar;
|
||||
using Aquarium.Engine.Fractal.Temporal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public readonly record struct FractalProbeSample(
|
||||
AquariumFractalKey DomainKey,
|
||||
AquariumFractalKey NodeKey,
|
||||
Vector3 LocalCenter,
|
||||
float BoundRadius,
|
||||
float TargetContribution,
|
||||
float SourcePdf,
|
||||
float MaterialDelta,
|
||||
int PayloadHandle,
|
||||
AquariumFieldLayer Layer = AquariumFieldLayer.Form,
|
||||
AquariumFieldEncoding Encoding = AquariumFieldEncoding.SignedDistance)
|
||||
{
|
||||
public ResampledImportanceCandidate<FractalProbeSample> ToReservoirCandidate()
|
||||
{
|
||||
return new ResampledImportanceCandidate<FractalProbeSample>(
|
||||
this,
|
||||
MathF.Max(TargetContribution, 0.0f),
|
||||
MathF.Max(SourcePdf, 1.0e-6f));
|
||||
}
|
||||
}
|
||||
|
||||
public enum FractalProbeReuseRejection
|
||||
{
|
||||
None,
|
||||
UnknownDomain,
|
||||
DifferentLineage,
|
||||
ExcessiveLocalShift,
|
||||
InvalidBounds,
|
||||
FieldMismatch,
|
||||
ExcessiveCameraMotion,
|
||||
Disoccluded,
|
||||
MaterialMismatch,
|
||||
NotVisible,
|
||||
}
|
||||
|
||||
public readonly record struct FractalProbeReuseResult(
|
||||
bool CanReuse,
|
||||
FractalProbeReuseRejection Rejection,
|
||||
float LocalShift);
|
||||
|
||||
public readonly record struct FractalProbeTemporalValidation(
|
||||
float CameraMotionPixels,
|
||||
float MaxCameraMotionPixels,
|
||||
float DisocclusionConfidence,
|
||||
float MinDisocclusionConfidence,
|
||||
float MaterialDelta,
|
||||
float MaxMaterialDelta,
|
||||
float VisibilityConfidence,
|
||||
float MinVisibilityConfidence);
|
||||
|
||||
public static class FractalProbeReuseValidator
|
||||
{
|
||||
public static FractalProbeReuseResult Validate(
|
||||
FractalProbeSample source,
|
||||
FractalProbeSample target,
|
||||
FractalDomainGraph domains,
|
||||
float maxLocalShift)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(domains);
|
||||
if (maxLocalShift < 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxLocalShift), maxLocalShift, "Maximum local shift must not be negative.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(source.BoundRadius) || !float.IsFinite(target.BoundRadius) || source.BoundRadius <= 0.0f || target.BoundRadius <= 0.0f)
|
||||
{
|
||||
return new FractalProbeReuseResult(false, FractalProbeReuseRejection.InvalidBounds, 0.0f);
|
||||
}
|
||||
|
||||
if (!domains.Contains(source.DomainKey) || !domains.Contains(target.DomainKey))
|
||||
{
|
||||
return new FractalProbeReuseResult(false, FractalProbeReuseRejection.UnknownDomain, 0.0f);
|
||||
}
|
||||
|
||||
if (source.Layer != target.Layer || source.Encoding != target.Encoding)
|
||||
{
|
||||
return new FractalProbeReuseResult(false, FractalProbeReuseRejection.FieldMismatch, 0.0f);
|
||||
}
|
||||
|
||||
if (!ShareLineage(source.DomainKey, target.DomainKey, domains))
|
||||
{
|
||||
return new FractalProbeReuseResult(false, FractalProbeReuseRejection.DifferentLineage, 0.0f);
|
||||
}
|
||||
|
||||
var localShift = Vector3.Distance(source.LocalCenter, target.LocalCenter);
|
||||
var allowedShift = MathF.Min(maxLocalShift, source.BoundRadius + target.BoundRadius);
|
||||
if (!float.IsFinite(localShift) || localShift > allowedShift)
|
||||
{
|
||||
return new FractalProbeReuseResult(false, FractalProbeReuseRejection.ExcessiveLocalShift, localShift);
|
||||
}
|
||||
|
||||
return new FractalProbeReuseResult(true, FractalProbeReuseRejection.None, localShift);
|
||||
}
|
||||
|
||||
public static FractalProbeReuseResult ValidateTemporal(
|
||||
FractalProbeSample source,
|
||||
FractalProbeSample target,
|
||||
FractalDomainGraph domains,
|
||||
float maxLocalShift,
|
||||
FractalProbeTemporalValidation temporal)
|
||||
{
|
||||
var spatial = Validate(source, target, domains, maxLocalShift);
|
||||
if (!spatial.CanReuse)
|
||||
{
|
||||
return spatial;
|
||||
}
|
||||
|
||||
if (!float.IsFinite(temporal.CameraMotionPixels) || temporal.CameraMotionPixels > MathF.Max(temporal.MaxCameraMotionPixels, 0.0f))
|
||||
{
|
||||
return spatial with { CanReuse = false, Rejection = FractalProbeReuseRejection.ExcessiveCameraMotion };
|
||||
}
|
||||
|
||||
if (!float.IsFinite(temporal.DisocclusionConfidence) || temporal.DisocclusionConfidence < Math.Clamp(temporal.MinDisocclusionConfidence, 0.0f, 1.0f))
|
||||
{
|
||||
return spatial with { CanReuse = false, Rejection = FractalProbeReuseRejection.Disoccluded };
|
||||
}
|
||||
|
||||
if (!float.IsFinite(temporal.MaterialDelta) || temporal.MaterialDelta > MathF.Max(temporal.MaxMaterialDelta, 0.0f))
|
||||
{
|
||||
return spatial with { CanReuse = false, Rejection = FractalProbeReuseRejection.MaterialMismatch };
|
||||
}
|
||||
|
||||
if (!float.IsFinite(temporal.VisibilityConfidence) || temporal.VisibilityConfidence < Math.Clamp(temporal.MinVisibilityConfidence, 0.0f, 1.0f))
|
||||
{
|
||||
return spatial with { CanReuse = false, Rejection = FractalProbeReuseRejection.NotVisible };
|
||||
}
|
||||
|
||||
return spatial;
|
||||
}
|
||||
|
||||
private static bool ShareLineage(AquariumFractalKey sourceDomain, AquariumFractalKey targetDomain, FractalDomainGraph domains)
|
||||
{
|
||||
var sourcePath = domains.GetPath(sourceDomain).Select(domain => domain.Key).ToArray();
|
||||
var targetPath = domains.GetPath(targetDomain).Select(domain => domain.Key).ToArray();
|
||||
var shorterLength = Math.Min(sourcePath.Length, targetPath.Length);
|
||||
for (var index = 0; index < shorterLength; index++)
|
||||
{
|
||||
if (!sourcePath[index].Equals(targetPath[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return shorterLength == sourcePath.Length || shorterLength == targetPath.Length;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
using Aquarium.Engine.Fractal;
|
||||
|
||||
namespace Aquarium.Engine.Fractal.Lod;
|
||||
|
||||
public static class FractalProjectedErrorScorer
|
||||
{
|
||||
public static float Score(AquariumFractalSummary summary, float projectedPixelsPerWorldUnit)
|
||||
{
|
||||
if (projectedPixelsPerWorldUnit <= 0.0f || summary.MaxHeightError <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
var cost = MathF.Max(summary.EstimatedCost, 1.0f);
|
||||
return summary.MaxHeightError * projectedPixelsPerWorldUnit / cost;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue