Compare commits
671 changed files with 4679 additions and 170082 deletions
3
.gitattributes
vendored
3
.gitattributes
vendored
|
|
@ -38,6 +38,3 @@ packages/cultmesh-ts/native/MANIFEST.txt text eol=lf
|
|||
# LF-configured or Linux clone builds the same bytes as a Windows one. The index
|
||||
# stays LF; only the checkout changes.
|
||||
packages/cultmath/src/** text eol=crlf
|
||||
|
||||
# Stores the Python typed-leaf tests write for the C# reference to open.
|
||||
tests/vectors/**/*.cc binary
|
||||
|
|
|
|||
91
.github/workflows/cultmesh-portability.yml
vendored
91
.github/workflows/cultmesh-portability.yml
vendored
|
|
@ -9,6 +9,44 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
getting-started:
|
||||
name: ${{ matrix.os }} one-command multiplayer onboarding
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out CultLib
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: CultLib
|
||||
|
||||
- name: Check out the matching Eve contract
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: GameCult/Eve
|
||||
# Eve main is the contract this sample is written against; the codex
|
||||
# branch this used to pin was never merged and stopped on 2026-08-25.
|
||||
ref: ${{ vars.EVE_INTEGRATION_REF || 'main' }}
|
||||
path: Eve
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Run the documented one-command checkpoint
|
||||
shell: pwsh
|
||||
run: ./CultLib/scripts/verify-eve-getting-started.ps1 -EveRoot ./Eve
|
||||
|
||||
reactive-and-resource-lifetime:
|
||||
name: ${{ matrix.os }} reactive semantics and leases
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
|
@ -16,8 +54,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# The forge runner is Ubuntu-only: Windows legs run on workflow_dispatch (GitHub or a Windows runner), Ubuntu gates push and PR.
|
||||
os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && '["windows-latest", "ubuntu-latest"]' || '["ubuntu-latest"]') }}
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out CultLib
|
||||
|
|
@ -64,42 +101,23 @@ jobs:
|
|||
# pull requests: cultnet-interop.yml is windows-latest and main-only, so
|
||||
# until these steps existed the shared verifier's tests never ran on
|
||||
# Linux at all, and the browser client's never ran in CI on any OS.
|
||||
# packages/cultnet-ts verifies both vector files (content.test.ts judges both
|
||||
# C# content files, the Rust one and its own); cultmesh-browser proves
|
||||
# the browser client reads the shared rules rather than its own, and that its content reader releases only verified
|
||||
# bytes, requesting the same URLs as the C# HTTPS connector.
|
||||
# No workflow ran cultcache-ts's tests, and its test script listed two files by hand while a third sat compiled
|
||||
# and unrun. The wiring check fails when any TypeScript test file is run by no script, and the cache's own suite
|
||||
# runs here beside the packages built on it.
|
||||
- name: Check that every TypeScript test file is run by its package
|
||||
run: node scripts/check-typescript-tests-wired.mjs
|
||||
|
||||
- name: Verify the CultCache document owner with the TypeScript cache
|
||||
run: npm run test --workspace packages/cultcache-ts
|
||||
|
||||
# Packs the four TypeScript packages and installs them into a clean consumer, so a package that builds but does
|
||||
# not install or load fails here.
|
||||
- name: Verify the packed TypeScript packages install and load from a clean consumer
|
||||
run: npm run test:pack:ts
|
||||
|
||||
# packages/cultnet-ts verifies both vector files; cultmesh-browser proves
|
||||
# the browser client reads the shared rules rather than its own.
|
||||
- name: Verify the CultMesh authority rules with the TypeScript verifier
|
||||
run: npm run test --workspace packages/cultnet-ts
|
||||
|
||||
# cultmesh-browser's test compiles test/interop, which imports cultmesh-ts/content, so cultmesh-ts builds first,
|
||||
# as in scripts/run-typescript-workspaces.mjs.
|
||||
# The realtime frame codec and the HTTP content chunk provider (content.test.ts,
|
||||
# its answers over node:http). The codec's byte parity rests on two vector files,
|
||||
- name: Verify the browser client against the shared TypeScript verifier
|
||||
run: npm run test --workspace packages/cultmesh-browser
|
||||
|
||||
# The realtime frame codec. Its byte parity rests on two vector files,
|
||||
# contracts/cultmesh/realtime-frame-vectors.json written by the C#
|
||||
# reference and decoded by this step, and realtime-frame-vectors.ts-written.json
|
||||
# written by this codec and decoded by the C# step below. Neither half ran
|
||||
# in CI on any OS until these two steps existed, so a codec that agreed
|
||||
# only with itself would have gone green.
|
||||
- name: Verify the realtime frame codec and the content provider with the TypeScript runtime
|
||||
- name: Verify the realtime frame codec with the TypeScript codec
|
||||
run: npm run test --workspace packages/cultmesh-ts
|
||||
|
||||
- name: Verify the browser client against the shared TypeScript verifier
|
||||
run: npm run test --workspace packages/cultmesh-browser
|
||||
|
||||
# Both directions of the CultMesh authority vectors: the C#-written
|
||||
# file (contracts/cultmesh/authority-route-vectors.json) and the
|
||||
# TypeScript-signed file (authority-route-vectors.ts-signed.json) are
|
||||
|
|
@ -120,20 +138,3 @@ jobs:
|
|||
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
|
||||
--filter FullyQualifiedName~RealtimeWireProtocol
|
||||
--logger "console;verbosity=minimal"
|
||||
|
||||
# The content plane: the reference writes the manifest and record-key
|
||||
# vectors and judges the manifests the TypeScript packer wrote
|
||||
# (content-vectors.ts-written.json). This suite also holds the C#/Rust
|
||||
# content parity, which no CI step ran before this one. CULTNET_WRITE_VECTORS
|
||||
# is never set in CI: the committed files are the contract.
|
||||
- name: Verify the content vectors with the C# reference
|
||||
run: >-
|
||||
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
|
||||
--filter FullyQualifiedName~CultMeshContentVectorTests
|
||||
--logger "console;verbosity=minimal"
|
||||
|
||||
# The cross-process HTTPS content parity: the C# transfer service over the unmodified HTTPS connector and the
|
||||
# TypeScript reader fetch from the TypeScript provider in its own process. Both refuse bytes tampered in flight,
|
||||
# and the provider refuses a corrupt store.
|
||||
- name: Verify HTTPS content parity across C#, the TypeScript provider and the TypeScript reader
|
||||
run: npm run test:interop --workspace packages/cultmesh-browser
|
||||
|
|
|
|||
7
.github/workflows/cultnet-interop.yml
vendored
7
.github/workflows/cultnet-interop.yml
vendored
|
|
@ -1,9 +1,10 @@
|
|||
name: CultNet cross-runtime interop
|
||||
|
||||
# workflow_dispatch only: this job is Windows-only (pwsh Kotlin build, setup steps), and the forge
|
||||
# runner is Ubuntu-only, so a push or pull_request trigger would queue a job that can never run.
|
||||
# Run it from GitHub or any Windows runner.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
21
.github/workflows/publish-packages.yml
vendored
21
.github/workflows/publish-packages.yml
vendored
|
|
@ -8,26 +8,15 @@ name: Publish packages
|
|||
# directly, which is already how Ghostlight, Epiphany, CodexConnector and Odin
|
||||
# consume the core stack.
|
||||
#
|
||||
# Tag a Python release to publish, for example `cultnet-py-v0.1.0`. The tag's
|
||||
# version must match the version in the package manifest;
|
||||
# scripts/release-after-suite.mjs refuses the release otherwise rather than
|
||||
# shipping a mislabelled artifact.
|
||||
#
|
||||
# npm publishing of @gamecult/cultcache-ts is deliberately off: `cultcache-ts-v*`
|
||||
# is not a tag trigger until it is ruled on (the cultcache-ts job below runs from
|
||||
# workflow_dispatch only, which publishes nothing). A tag run uses the workflow
|
||||
# file at the tagged commit, not the one on main, so a tag on an older commit
|
||||
# (`cultcache-ts-v0.15.0` sits on 7d292085, before the guard) still runs that
|
||||
# commit's triggers and jobs. Read this file at the tagged sha before pushing a tag.
|
||||
# Tag a release to publish, for example `cultcache-ts-v0.13.6` or
|
||||
# `cultnet-py-v0.1.0`. The tag's version must match the version in the package
|
||||
# manifest; scripts/release-after-suite.mjs refuses the release otherwise rather
|
||||
# than shipping a mislabelled artifact.
|
||||
|
||||
on:
|
||||
push:
|
||||
# cultcache-ts-v* is deliberately not a trigger: publishing @gamecult/cultcache-ts is disabled
|
||||
# until it is ruled on, and whether NPM_TOKEN is set is unknown, so a pushed tag must not reach
|
||||
# `npm publish`. The cultcache-ts jobs below still run from workflow_dispatch, which publishes nothing.
|
||||
# (The comment sits above `tags:` because the tag-prefix pin in scripts/check-changelog-semver.test.mjs
|
||||
# reads the list as `tags:` followed directly by its items.)
|
||||
tags:
|
||||
- "cultcache-ts-v*"
|
||||
- "cultcache-py-v*"
|
||||
- "cultnet-py-v*"
|
||||
- "cultmesh-py-v*"
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -322,10 +322,6 @@ node_modules/
|
|||
!packages/cultmesh-browser/**
|
||||
!packages/cultmesh-kotlin/
|
||||
!packages/cultmesh-kotlin/**
|
||||
!packages/heimdall-delegation/
|
||||
!packages/heimdall-delegation/**
|
||||
!packages/heimdall-stake/
|
||||
!packages/heimdall-stake/**
|
||||
packages/**/node_modules/
|
||||
packages/**/dist/
|
||||
packages/**/dist-test/
|
||||
|
|
|
|||
47
CultLib.sln
47
CultLib.sln
|
|
@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking.WebSock
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking.WebSockets.Tests", "tests\GameCult.Networking.WebSockets.Tests\GameCult.Networking.WebSockets.Tests.csproj", "{B9A441EC-DADE-4500-B092-C635F3EB296A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EveBrowserNetworkSample", "samples\eve-browser-network\EveBrowserNetworkSample.csproj", "{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cultmath", "cultmath", "{B48EAE30-152F-01E3-6E31-82B350494D3C}"
|
||||
|
|
@ -45,10 +49,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath.Tests", "packages\
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.ConsumerBuild", "tests\GameCult.Caching.ConsumerBuild\GameCult.Caching.ConsumerBuild.csproj", "{D3138F3C-9531-4906-A5D3-74BD857D0685}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.GeneratorConsumer", "tests\GameCult.Caching.GeneratorConsumer\GameCult.Caching.GeneratorConsumer.csproj", "{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.OutsideStoreTests", "tests\GameCult.Caching.OutsideStoreTests\GameCult.Caching.OutsideStoreTests.csproj", "{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -227,6 +227,18 @@ Global
|
|||
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -263,30 +275,6 @@ Global
|
|||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -306,11 +294,10 @@ Global
|
|||
{21F26329-2119-4037-93CC-DD1801A1AB78} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
{7F32D96B-AB5D-4A72-9217-B5DCCFEFC7E5} = {E733CBFF-42AE-4B6A-B5B9-0071ECFB45F1}
|
||||
{B9A441EC-DADE-4500-B092-C635F3EB296A} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
|
||||
{B48EAE30-152F-01E3-6E31-82B350494D3C} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
|
||||
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
|
||||
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
|
||||
{D3138F3C-9531-4906-A5D3-74BD857D0685} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -1,7 +1,5 @@
|
|||
# CultLib
|
||||
|
||||
**New here? Start at [`docs/start`](docs/start/README.md)**: a language x package matrix with tested first-call snippets.
|
||||
|
||||
CultLib is GameCult's shared state, transport, and runtime substrate. It started
|
||||
as reusable C# game infrastructure, but the useful thing now is larger: typed
|
||||
`.cc` persistence, schema-stable wire formats, ergonomic reliable-UDP
|
||||
|
|
@ -32,30 +30,6 @@ bespoke JSON bridge at every boundary. A frontend is just another CultMesh
|
|||
client: the daemon publishes a typed Eve/CultUI surface, and any compatible
|
||||
runtime can lower that surface where it needs to live.
|
||||
|
||||
## Entry Points
|
||||
|
||||
- C# reference runtime: `src/` (`GameCult.Caching`, `GameCult.Networking`,
|
||||
`GameCult.Mesh`, `GameCult.Geometry`, `GameCult.Unity`). Build and test with
|
||||
`dotnet build CultLib.sln` and `dotnet test CultLib.sln`.
|
||||
- Other runtimes, wire-compatible with the C# CultCache: `packages/cultcache-ts`,
|
||||
`cultcache-py`, `cultcache-rs`, and the CultNet and CultMesh packages beside
|
||||
them under `packages/`; portable math in `packages/cultmath`.
|
||||
- Smallest working path: `src/GameCult.Mesh/docs/durable-node-quickstart.md`.
|
||||
Application path: `src/GameCult.Mesh/docs/getting-started/README.md` and
|
||||
`samples/eve-two-runtime`.
|
||||
|
||||
## Live And Planned
|
||||
|
||||
Live: typed CultCache persistence, CultNet schema-v0 transports and native RUDP,
|
||||
CultMesh nodes with Verse discovery, shard replication and authority leases,
|
||||
the geometry documents, and CultUI for Unity. The browser client boundary
|
||||
(`packages/cultmesh-browser`, `GameCult.Networking.WebSockets`) is proven on
|
||||
loopback under an explicit `local-development` trust policy by
|
||||
`samples/eve-browser-network`.
|
||||
|
||||
Planned: a complete public Verse proof over deployed infrastructure; the
|
||||
remaining gates are listed in `src/GameCult.Mesh/docs/browser-verse-transport.md`.
|
||||
|
||||
## Which Package Do I Want?
|
||||
|
||||
If the job is shared game state, start here before inventing a fourth drawer
|
||||
|
|
@ -98,8 +72,7 @@ start with the maintained
|
|||
series. Its first executable package-consumer checkpoint is
|
||||
[`samples/eve-two-runtime`](samples/eve-two-runtime/README.md).
|
||||
The real-process browser continuation is
|
||||
[`samples/eve-browser-network`](samples/eve-browser-network/README.md); it
|
||||
needs a sibling Eve checkout (`--eve-root`) and is not in `CultLib.sln`.
|
||||
[`samples/eve-browser-network`](samples/eve-browser-network/README.md).
|
||||
One documented command runs both layers:
|
||||
|
||||
```powershell
|
||||
|
|
@ -124,6 +97,7 @@ The solution includes:
|
|||
- `GameCult.Logging`: common logging abstraction plus console and file implementations
|
||||
- `GameCult.Caching`: `DatabaseEntry`-based cache, indexes, global entries, and backing-store abstractions
|
||||
- `GameCult.Caching.MessagePack`: MessagePack-backed persistence for the cache
|
||||
- `GameCult.Caching.NewtonsoftJson`: Newtonsoft.Json-backed persistence for the cache
|
||||
- `GameCult.Math` / CultMath: portable numeric primitives, deterministic random, noise, and shader-parity helpers
|
||||
- `GameCult.Geometry` / CultGeometry: engine-neutral geometry algorithms plus CultCache-native domain, selected-cut, and mesh artifact documents
|
||||
- `GameCult.Networking`: encrypted login/register/verify flows, schema-v0 contracts, transport adapters, and native RUDP sessions
|
||||
|
|
@ -151,6 +125,7 @@ src/
|
|||
GameCult.Logging/
|
||||
GameCult.Caching/
|
||||
GameCult.Caching.MessagePack/
|
||||
GameCult.Caching.NewtonsoftJson/
|
||||
GameCult.Geometry/
|
||||
GameCult.Networking/
|
||||
GameCult.Networking.WebSockets/
|
||||
|
|
@ -369,6 +344,7 @@ Each subproject has a local README with package-specific detail:
|
|||
- [GameCult.Logging](src/GameCult.Logging/README.md)
|
||||
- [GameCult.Caching](src/GameCult.Caching/README.md)
|
||||
- [GameCult.Caching.MessagePack](src/GameCult.Caching.MessagePack/README.md)
|
||||
- [GameCult.Caching.NewtonsoftJson](src/GameCult.Caching.NewtonsoftJson/README.md)
|
||||
- [GameCult.Geometry](src/GameCult.Geometry/README.md)
|
||||
- [GameCult.Networking](src/GameCult.Networking/README.md)
|
||||
- [GameCult.Mesh](src/GameCult.Mesh/README.md)
|
||||
|
|
|
|||
|
|
@ -1,702 +0,0 @@
|
|||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
32,
|
||||
32,
|
||||
77,
|
||||
105,
|
||||
120,
|
||||
101,
|
||||
100,
|
||||
32,
|
||||
67,
|
||||
97,
|
||||
115,
|
||||
101,
|
||||
47,
|
||||
80,
|
||||
97,
|
||||
116,
|
||||
104,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"versionUnits": [
|
||||
32,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
"recordKey": "mesh:cdn:artifact:mixed-case-path:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
304,
|
||||
115,
|
||||
116,
|
||||
97,
|
||||
110,
|
||||
98,
|
||||
117,
|
||||
108
|
||||
],
|
||||
"versionUnits": [
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
46,
|
||||
48
|
||||
],
|
||||
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
|
||||
"recordKey": "mesh:cdn:artifact:\u0130stanbul:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
913,
|
||||
931
|
||||
],
|
||||
"versionUnits": [
|
||||
86,
|
||||
50,
|
||||
32,
|
||||
66,
|
||||
101,
|
||||
116,
|
||||
97
|
||||
],
|
||||
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:\u03B1\u03C3:v2-beta:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
65279,
|
||||
98,
|
||||
111,
|
||||
109
|
||||
],
|
||||
"versionUnits": [
|
||||
252
|
||||
],
|
||||
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:bom:\u00FC:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
133,
|
||||
110,
|
||||
101,
|
||||
108,
|
||||
133
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
|
||||
"recordKey": "mesh:cdn:artifact:nel:unversioned:\u0130\u03C3\uD801\uDC28"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
26085,
|
||||
26412,
|
||||
35486,
|
||||
47,
|
||||
36039,
|
||||
29987
|
||||
],
|
||||
"versionUnits": [
|
||||
32,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"contentHash": "sha256:",
|
||||
"recordKey": "mesh:cdn:artifact:\u65E5\u672C\u8A9E-\u8CC7\u7523:unversioned:"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
55357,
|
||||
56898,
|
||||
32,
|
||||
115,
|
||||
109,
|
||||
105,
|
||||
108,
|
||||
101
|
||||
],
|
||||
"versionUnits": [
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
46,
|
||||
48
|
||||
],
|
||||
"contentHash": "\u017Fha256:abc",
|
||||
"recordKey": "mesh:cdn:artifact:smile:1.0.0:\u017Fha256:abc"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
45,
|
||||
45,
|
||||
45
|
||||
],
|
||||
"versionUnits": [
|
||||
86,
|
||||
50,
|
||||
32,
|
||||
66,
|
||||
101,
|
||||
116,
|
||||
97
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:cb3f91d54eee30e53e35b2b99905f70f169ed549fd78909d3dac2defc9ed8d3b:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
97,
|
||||
55296,
|
||||
98
|
||||
],
|
||||
"versionUnits": [
|
||||
252
|
||||
],
|
||||
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
"recordKey": "mesh:cdn:artifact:a-b:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
55296
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
|
||||
"recordKey": "mesh:cdn:artifact:83d544ccc223c057d2bf80d3f2a32982c32c3c0db8e2674820da5064783fb097:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
120,
|
||||
178,
|
||||
121
|
||||
],
|
||||
"versionUnits": [
|
||||
32,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:x-y:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
1635,
|
||||
100
|
||||
],
|
||||
"versionUnits": [
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
46,
|
||||
48
|
||||
],
|
||||
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:\u0663d:1.0.0:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
55297,
|
||||
56320,
|
||||
65
|
||||
],
|
||||
"versionUnits": [
|
||||
86,
|
||||
50,
|
||||
32,
|
||||
66,
|
||||
101,
|
||||
116,
|
||||
97
|
||||
],
|
||||
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
|
||||
"recordKey": "mesh:cdn:artifact:a:v2-beta:\u0130\u03C3\uD801\uDC28"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
32,
|
||||
32,
|
||||
45,
|
||||
45,
|
||||
45,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"versionUnits": [
|
||||
252
|
||||
],
|
||||
"contentHash": "sha256:",
|
||||
"recordKey": "mesh:cdn:artifact:fcd2a2fb27957aa9a10d2d9eca16ccbc9c35a76933b03f470211d327f6946c3f:\u00FC:"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
9398
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "\u017Fha256:abc",
|
||||
"recordKey": "mesh:cdn:artifact:7d9633ae307dae04a8b362df41dca4b38a653823d69f27a259e7b6928508d3a6:unversioned:\u017Fha256:abc"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
8555
|
||||
],
|
||||
"versionUnits": [
|
||||
32,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:8c0e55c55f47de02406174ec976279a977e2e73ea5aec8ab90f94c40a6ab2d60:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [
|
||||
32,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [
|
||||
49,
|
||||
46,
|
||||
48,
|
||||
46,
|
||||
48
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [
|
||||
86,
|
||||
50,
|
||||
32,
|
||||
66,
|
||||
101,
|
||||
116,
|
||||
97
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [
|
||||
252
|
||||
],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u0130\u03C3\uD801\uDC28"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "sha256:",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:"
|
||||
},
|
||||
{
|
||||
"artifactIdUnits": [
|
||||
101,
|
||||
117,
|
||||
114,
|
||||
101,
|
||||
107,
|
||||
97,
|
||||
47,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
116,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
45,
|
||||
118,
|
||||
101,
|
||||
99,
|
||||
116,
|
||||
111,
|
||||
114
|
||||
],
|
||||
"versionUnits": [],
|
||||
"contentHash": "\u017Fha256:abc",
|
||||
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u017Fha256:abc"
|
||||
}
|
||||
],
|
||||
"chunks": [
|
||||
{
|
||||
"chunkHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"chunkHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"chunkHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
|
||||
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"chunkHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"chunkHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"chunkHash": "Sha256:\u0130\u03A3\uD801\uDC00",
|
||||
"recordKey": "mesh:cdn:chunk:\u0130\u03C3\uD801\uDC28"
|
||||
},
|
||||
{
|
||||
"chunkHash": "sha256:",
|
||||
"recordKey": "mesh:cdn:chunk:"
|
||||
},
|
||||
{
|
||||
"chunkHash": "\u017Fha256:abc",
|
||||
"recordKey": "mesh:cdn:chunk:\u017Fha256:abc"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -168,22 +168,22 @@
|
|||
{
|
||||
"label": "manifest",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQgA=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQgA=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_out_of_order",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOnMTMxMDcyMJXZQGUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzbOAAQAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazplMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2pjI2MjE0NJXZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAypzEwNDg1NzaV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiQgA=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY5TZQDMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmUAzgAEAADZT21lc2g6Y2RuOmNodW5rOjMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmWU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOU2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NpTZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAylNlAYzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZs4ACAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmM3ZjFiZGU5ZTFlZmE3ZDEzODVkNTFiMDc3ZWE4N2IwYzM4NjBkMjhkYTQzZDVkNmYyMjFlODQ1YzRjMjM2MGaQgA=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_metadata_order",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_empty_body",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NaEwkIA="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZTZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NZCA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,22 +138,22 @@
|
|||
{
|
||||
"label": "manifest",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQgA=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQgA=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_out_of_order",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOnMTMxMDcyMJXZQGUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzbOAAQAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazplMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2pjI2MjE0NJXZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAypzEwNDg1NzaV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiQgA=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY5TZQDMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmUAzgAEAADZT21lc2g6Y2RuOmNodW5rOjMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmWU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOU2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NpTZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAylNlAYzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZs4ACAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmM3ZjFiZGU5ZTFlZmE3ZDEzODVkNTFiMDc3ZWE4N2IwYzM4NjBkMjhkYTQzZDVkNmYyMjFlODQ1YzRjMjM2MGaQgA=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_metadata_order",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
|
||||
},
|
||||
{
|
||||
"label": "manifest_empty_body",
|
||||
"kind": "manifest",
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NaEwkIA="
|
||||
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZTZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NZCA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
{
|
||||
"vectors": [
|
||||
{
|
||||
"label": "ts multi-chunk body",
|
||||
"kind": "manifest",
|
||||
"input": {
|
||||
"artifactId": "Pixel Heroes/Hüt ☃",
|
||||
"kind": "asset",
|
||||
"version": "",
|
||||
"mimeType": "application/x-cultnet-ts-vector",
|
||||
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
|
||||
"chunkSizeBytes": 131072,
|
||||
"tags": [
|
||||
"a",
|
||||
"b"
|
||||
],
|
||||
"metadata": [
|
||||
[
|
||||
"10",
|
||||
"x"
|
||||
],
|
||||
[
|
||||
"alpha",
|
||||
"y"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"z"
|
||||
]
|
||||
],
|
||||
"body": {
|
||||
"length": 777777,
|
||||
"multiplier": 7,
|
||||
"modulus": 256
|
||||
}
|
||||
},
|
||||
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:cf8a453a36981321977355448d24df49bcefee22be6e3730b9140e87c29013f9",
|
||||
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBjZjhhNDUzYTM2OTgxMzIxOTc3MzU1NDQ4ZDI0ZGY0OWJjZWZlZTIyYmU2ZTM3MzBiOTE0MGU4N2MyOTAxM2Y5zgAL3jG/YXBwbGljYXRpb24veC1jdWx0bmV0LXRzLXZlY3RvctkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwlpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDgAzgACAADZT21lc2g6Y2RuOmNodW5rOjM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDihMJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAIAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjEzMTA3MpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAQAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjI2MjE0NJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAYAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjM5MzIxNpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAgAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjUyNDI4OJXZQGEwYmI2OWY0NDg2MWViM2RiNTYwMjZhMWFiYmY5MWZmYTYyMTMyNTA4ZDVhMDZlMGQwZGRjNWNjMjZiZTMzZmPOAAoAAM4AAd4x2U9tZXNoOmNkbjpjaHVuazphMGJiNjlmNDQ4NjFlYjNkYjU2MDI2YTFhYmJmOTFmZmE2MjEzMjUwOGQ1YTA2ZTBkMGRkYzVjYzI2YmUzM2ZjpjY1NTM2MJKhYaFig6IxMKF4pWFscGhhoXmhMqF6"
|
||||
},
|
||||
{
|
||||
"label": "ts empty body",
|
||||
"kind": "manifest",
|
||||
"input": {
|
||||
"artifactId": "Pixel Heroes/Hüt ☃",
|
||||
"kind": "asset",
|
||||
"version": "",
|
||||
"mimeType": "application/x-cultnet-ts-vector",
|
||||
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
|
||||
"chunkSizeBytes": 131072,
|
||||
"tags": [
|
||||
"a",
|
||||
"b"
|
||||
],
|
||||
"metadata": [
|
||||
[
|
||||
"10",
|
||||
"x"
|
||||
],
|
||||
[
|
||||
"alpha",
|
||||
"y"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"z"
|
||||
]
|
||||
],
|
||||
"body": {
|
||||
"length": 0,
|
||||
"multiplier": 7,
|
||||
"modulus": 256
|
||||
}
|
||||
},
|
||||
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1AL9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQAA2U9tZXNoOmNkbjpjaHVuazplM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1oTCSoWGhYoOiMTCheKVhbHBoYaF5oTKheg=="
|
||||
},
|
||||
{
|
||||
"label": "ts single chunk, versioned",
|
||||
"kind": "manifest",
|
||||
"input": {
|
||||
"artifactId": "Pixel Heroes/Hüt ☃",
|
||||
"kind": "asset",
|
||||
"version": "V1 Beta",
|
||||
"mimeType": "application/x-cultnet-ts-vector",
|
||||
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
|
||||
"chunkSizeBytes": 4194304,
|
||||
"tags": [],
|
||||
"metadata": [],
|
||||
"body": {
|
||||
"length": 1000,
|
||||
"multiplier": 7,
|
||||
"modulus": 256
|
||||
}
|
||||
},
|
||||
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:v1-beta:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
|
||||
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSnVjEgQmV0YdlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMgDNA+jZT21lc2g6Y2RuOmNodW5rOjg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzKhMJCA"
|
||||
},
|
||||
{
|
||||
"label": "ts whitespace version, padded kind and mime",
|
||||
"kind": "manifest",
|
||||
"input": {
|
||||
"artifactId": "Pixel Heroes/Hüt ☃",
|
||||
"kind": " build ",
|
||||
"version": " ",
|
||||
"mimeType": " text/plain; charset=utf-8 ",
|
||||
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
|
||||
"chunkSizeBytes": 4194304,
|
||||
"tags": [],
|
||||
"metadata": [],
|
||||
"body": {
|
||||
"length": 1000,
|
||||
"multiplier": 7,
|
||||
"modulus": 256
|
||||
}
|
||||
},
|
||||
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
|
||||
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOnIGJ1aWxkIKIgINlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L0gIHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTggINkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQDg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzIAzQPo2U9tZXNoOmNkbjpjaHVuazo4OWY0ZmY1NmEyNWRkMWRiMDZhNGNlNjAzMzYwMzc3NWQ3MDVmYjk2ZjMwZjg2OTM3MzNmZWY2MDJhMWNhNTMyoTCQgA=="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -111,10 +111,8 @@ Not implemented yet:
|
|||
not guess. Adding a code is therefore a coordinated change across every runtime, made together: the schema (both
|
||||
copies), the C# `CultNetErrorMessage` factory, the Rust `CultNetErrorCode`, and a captured-bytes case in
|
||||
`packages/cultnet-rs/tests/error_message.rs` for it. TypeScript reads the schema. The current codes are
|
||||
`selection_invalid`, `cursor_stale`, `cursor_invalid`, `reference_outside_target`, `variant_unsupported`,
|
||||
`unowned_schema` (a write, or a remote put or delete, for a schema and key no shard owns),
|
||||
`write_not_authorized` (a peer's put or delete its server's remote write authority did not grant) and
|
||||
`unsupported_schema_version` (a message whose `schemaVersion` the peer does not speak; `details` names `schemaVersion` and the offered value).
|
||||
`selection_invalid`, `cursor_stale`, `cursor_invalid`, `reference_outside_target`, `variant_unsupported` and
|
||||
`unowned_schema` (a write, or a remote put or delete, for a schema and key no shard owns).
|
||||
|
||||
## Live Invariants
|
||||
|
||||
|
|
@ -124,9 +122,6 @@ copies), the C# `CultNetErrorMessage` factory, the Rust `CultNetErrorCode`, and
|
|||
- One primary owns writes for a shard until a stronger policy exists.
|
||||
- Non-owner writes are rejected or explicitly forwarded. They are not silently
|
||||
applied.
|
||||
- A peer's put or delete is applied or forwarded only when the server's
|
||||
`RemoteWriteAuthority` grants it; none configured refuses every remote write.
|
||||
The connection key admits a peer to the transport and is never a grant.
|
||||
- Raw snapshot/put/delete messages must pass through `CultNetDatabase`, not
|
||||
bypass shard policy.
|
||||
- The accepted shard log is stored in replica catch-up wire form. Object logs
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"code": {
|
||||
"description": "Machine-readable refusal code (R-N). Additive: a v0 peer that does not know this field ignores it. The set is a closed enum: Rust and TypeScript refuse a message whose code is not listed, so adding a code is one coordinated change across every runtime (this schema, the C# factory, the Rust CultNetErrorCode, and a captured-bytes case for each).",
|
||||
"type": ["string", "null"],
|
||||
"enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema", "write_not_authorized", "unsupported_schema_version"]
|
||||
"enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema"]
|
||||
},
|
||||
"details": {
|
||||
"description": "Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
|
||||
|
|
@ -36,8 +36,8 @@
|
|||
"additionalProperties": false,
|
||||
"required": ["field", "value", "asOf", "current"],
|
||||
"properties": {
|
||||
"field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\". write_not_authorized: \"recordKey\". unsupported_schema_version: \"schemaVersion\".", "type": ["string", "null"] },
|
||||
"value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended. unowned_schema: the schema id no shard owns. write_not_authorized: the key of the refused write. unsupported_schema_version: the schemaVersion the peer does not speak.", "type": ["string", "null"] },
|
||||
"field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\".", "type": ["string", "null"] },
|
||||
"value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended. unowned_schema: the schema id no shard owns.", "type": ["string", "null"] },
|
||||
"asOf": { "description": "cursor_stale: the cursor's own minted asOf.", "type": ["integer", "null"], "minimum": 0 },
|
||||
"current": { "description": "cursor_stale: the answering server's current asOf.", "type": ["integer", "null"], "minimum": 0 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@
|
|||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"role": { "type": "string", "minLength": 1 },
|
||||
"schemaId": { "type": "string", "minLength": 1 },
|
||||
"schemaVersion": { "type": "string", "minLength": 1 },
|
||||
|
|
@ -75,7 +74,6 @@
|
|||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"role": { "type": "string", "minLength": 1 },
|
||||
"uri": { "type": "string", "minLength": 1 },
|
||||
"mediaType": { "type": "string", "minLength": 1 },
|
||||
|
|
@ -94,7 +92,6 @@
|
|||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"stage": { "type": "string", "minLength": 1 },
|
||||
"startedAt": { "type": "string", "minLength": 1 },
|
||||
"completedAt": { "type": "string", "minLength": 1 },
|
||||
|
|
|
|||
|
|
@ -1,346 +0,0 @@
|
|||
{
|
||||
"vectors": [
|
||||
{
|
||||
"name": "snapshot_request_v1_defaults",
|
||||
"schemaVersion": "cultnet.snapshot_request.v1",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9uu2N1bHRuZXQuc25hcHNob3RfcmVxdWVzdC52MaltZXNzYWdlSWShbalzZWxlY3Rpb26Jp3NjaGVtYXPApGtleXPApmZpZWxkc8ClY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywKdzaGFyZElkwKpzaGFyZEVwb2NowA==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.snapshot_request.v1",
|
||||
"messageId": "m",
|
||||
"selection": {
|
||||
"schemas": null,
|
||||
"keys": null,
|
||||
"fields": null,
|
||||
"cites": null,
|
||||
"cited": null,
|
||||
"projection": "header",
|
||||
"descending": false,
|
||||
"limit": null,
|
||||
"cursor": null
|
||||
},
|
||||
"shardId": null,
|
||||
"shardEpoch": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot_request_v1_full_selection_and_shard",
|
||||
"schemaVersion": "cultnet.snapshot_request.v1",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9uu2N1bHRuZXQuc25hcHNob3RfcmVxdWVzdC52MaltZXNzYWdlSWSmbS1mdWxsqXNlbGVjdGlvbomnc2NoZW1hc5KpbGVhZl9hLnY52UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqRrZXlzkqRhLWVxqPCfmIAtcm93pmZpZWxkc5KEpWluZGV4pGtpbmSib3CmYW55X29mpnZhbHVlc5Kmd2VhcG9upHRvb2ymbnVtYmVywISlaW5kZXikbWFzc6JvcKJnZaZ2YWx1ZXPApm51bWJlcqstMjIzMzc1OS4yNaVjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26lY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqhkb2N1bWVudKpkZXNjZW5kaW5nw6VsaW1pdMzIpmN1cnNvcq1vcGFxdWUtY3Vyc29yp3NoYXJkSWSnc2hhcmQtMapzaGFyZEVwb2NoBw==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.snapshot_request.v1",
|
||||
"messageId": "m-full",
|
||||
"selection": {
|
||||
"schemas": [
|
||||
"leaf_a.v9",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff"
|
||||
],
|
||||
"keys": [
|
||||
"a-eq",
|
||||
"\uD83D\uDE00-row"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"index": "kind",
|
||||
"op": "any_of",
|
||||
"values": [
|
||||
"weapon",
|
||||
"tool"
|
||||
],
|
||||
"number": null
|
||||
},
|
||||
{
|
||||
"index": "mass",
|
||||
"op": "ge",
|
||||
"values": null,
|
||||
"number": "-2233759.25"
|
||||
}
|
||||
],
|
||||
"cites": {
|
||||
"target": {
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"recordKey": "a-eq"
|
||||
},
|
||||
"role": "Design"
|
||||
},
|
||||
"cited": {
|
||||
"role": "Design",
|
||||
"exists": false
|
||||
},
|
||||
"projection": "document",
|
||||
"descending": true,
|
||||
"limit": 200,
|
||||
"cursor": "opaque-cursor"
|
||||
},
|
||||
"shardId": "shard-1",
|
||||
"shardEpoch": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "database_subscribe_v1_defaults",
|
||||
"schemaVersion": "cultnet.database_subscribe.v1",
|
||||
"messagePackBase64": "iK1zY2hlbWFWZXJzaW9uvWN1bHRuZXQuZGF0YWJhc2Vfc3Vic2NyaWJlLnYxqW1lc3NhZ2VJZKFtrnN1YnNjcmlwdGlvbklko3N1YqlzZWxlY3Rpb26Jp3NjaGVtYXPApGtleXPApmZpZWxkc8ClY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywK9pbmNsdWRlU25hcHNob3TDsWNvbnN1bWVyUnVudGltZUlkwKdib2R5SWRzwLdzdXBwb3J0ZWRCb2R5VHJhbnNwb3J0c8A=",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.database_subscribe.v1",
|
||||
"messageId": "m",
|
||||
"subscriptionId": "sub",
|
||||
"selection": {
|
||||
"schemas": null,
|
||||
"keys": null,
|
||||
"fields": null,
|
||||
"cites": null,
|
||||
"cited": null,
|
||||
"projection": "header",
|
||||
"descending": false,
|
||||
"limit": null,
|
||||
"cursor": null
|
||||
},
|
||||
"includeSnapshot": true,
|
||||
"consumerRuntimeId": null,
|
||||
"bodyIds": null,
|
||||
"supportedBodyTransports": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "database_subscribe_v1_full",
|
||||
"schemaVersion": "cultnet.database_subscribe.v1",
|
||||
"messagePackBase64": "iK1zY2hlbWFWZXJzaW9uvWN1bHRuZXQuZGF0YWJhc2Vfc3Vic2NyaWJlLnYxqW1lc3NhZ2VJZKZtLWZ1bGyuc3Vic2NyaXB0aW9uSWSoc3ViLWZ1bGypc2VsZWN0aW9uiadzY2hlbWFzkqlsZWFmX2EudjnZR3NoYTI1NjpmZjM1ZjNkOWQ2ZGUzZjEwNjZiOTM2OWU3M2ZlNmM3NjQ4ZTg4MzY4ZDE4ZWZjMjI3NWM5OTQ3MzBjZDg0Y2ZmpGtleXOSpGEtZXGo8J\u002BYgC1yb3emZmllbGRzkoSlaW5kZXika2luZKJvcKZhbnlfb2amdmFsdWVzkqZ3ZWFwb26kdG9vbKZudW1iZXLAhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyqy0yMjMzNzU5LjI1pWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmkYS1lcaRyb2xlpkRlc2lnbqVjaXRlZIKkcm9sZaZEZXNpZ26mZXhpc3Rzwqpwcm9qZWN0aW9uqGRvY3VtZW50qmRlc2NlbmRpbmfDpWxpbWl0zMimY3Vyc29yrW9wYXF1ZS1jdXJzb3KvaW5jbHVkZVNuYXBzaG90wrFjb25zdW1lclJ1bnRpbWVJZKhjb25zdW1lcqdib2R5SWRzkaZib2R5LWG3c3VwcG9ydGVkQm9keVRyYW5zcG9ydHOSpHJ1ZHCkcXVpYw==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.database_subscribe.v1",
|
||||
"messageId": "m-full",
|
||||
"subscriptionId": "sub-full",
|
||||
"selection": {
|
||||
"schemas": [
|
||||
"leaf_a.v9",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff"
|
||||
],
|
||||
"keys": [
|
||||
"a-eq",
|
||||
"\uD83D\uDE00-row"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"index": "kind",
|
||||
"op": "any_of",
|
||||
"values": [
|
||||
"weapon",
|
||||
"tool"
|
||||
],
|
||||
"number": null
|
||||
},
|
||||
{
|
||||
"index": "mass",
|
||||
"op": "ge",
|
||||
"values": null,
|
||||
"number": "-2233759.25"
|
||||
}
|
||||
],
|
||||
"cites": {
|
||||
"target": {
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"recordKey": "a-eq"
|
||||
},
|
||||
"role": "Design"
|
||||
},
|
||||
"cited": {
|
||||
"role": "Design",
|
||||
"exists": false
|
||||
},
|
||||
"projection": "document",
|
||||
"descending": true,
|
||||
"limit": 200,
|
||||
"cursor": "opaque-cursor"
|
||||
},
|
||||
"includeSnapshot": false,
|
||||
"consumerRuntimeId": "consumer",
|
||||
"bodyIds": [
|
||||
"body-a"
|
||||
],
|
||||
"supportedBodyTransports": [
|
||||
"rudp",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot_response_raw_v1_headers",
|
||||
"schemaVersion": "cultnet.snapshot_response_raw.v1",
|
||||
"messagePackBase64": "i61zY2hlbWFWZXJzaW9u2SBjdWx0bmV0LnNuYXBzaG90X3Jlc3BvbnNlX3Jhdy52MaltZXNzYWdlSWShbadtYXRjaGVkAqRhc09mBaRuZXh0q25leHQtY3Vyc29yp2hlYWRlcnOSiqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZaZsZWFmX2Gtc2NoZW1hVmVyc2lvbqExsXNjaGVtYUNvbnRlbnRIYXNoo2FiY6lyZWNvcmRLZXmkYS1lcahzdG9yZWRBdLgxOTcwLTAxLTAxVDAwOjAwOjAwLjAwMFqvc291cmNlUnVudGltZUlkonJ0rXNvdXJjZUFnZW50SWSlYWdlbnSqc291cmNlUm9sZaRyb2xlpHRhZ3PAiqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZaZsZWFmX2Gtc2NoZW1hVmVyc2lvbqExsXNjaGVtYUNvbnRlbnRIYXNoo2FiY6lyZWNvcmRLZXmkYS1sb6hzdG9yZWRBdLgxOTcwLTAxLTAxVDAwOjAwOjAwLjAwMFqvc291cmNlUnVudGltZUlkonJ0rXNvdXJjZUFnZW50SWSlYWdlbnSqc291cmNlUm9sZaRyb2xlpHRhZ3OSpWFscGhhpGJldGGpZG9jdW1lbnRzwKVlZGdlc8Cnc2hhcmRJZKdzaGFyZC0xqnNoYXJkRXBvY2gDsHNoYXJkTG9nU2VxdWVuY2Up",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.snapshot_response_raw.v1",
|
||||
"messageId": "m",
|
||||
"matched": 2,
|
||||
"asOf": 5,
|
||||
"next": "next-cursor",
|
||||
"headers": [
|
||||
{
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"schemaName": "leaf_a",
|
||||
"schemaVersion": "1",
|
||||
"schemaContentHash": "abc",
|
||||
"recordKey": "a-eq",
|
||||
"storedAt": "1970-01-01T00:00:00.000Z",
|
||||
"sourceRuntimeId": "rt",
|
||||
"sourceAgentId": "agent",
|
||||
"sourceRole": "role",
|
||||
"tags": null
|
||||
},
|
||||
{
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"schemaName": "leaf_a",
|
||||
"schemaVersion": "1",
|
||||
"schemaContentHash": "abc",
|
||||
"recordKey": "a-lo",
|
||||
"storedAt": "1970-01-01T00:00:00.000Z",
|
||||
"sourceRuntimeId": "rt",
|
||||
"sourceAgentId": "agent",
|
||||
"sourceRole": "role",
|
||||
"tags": [
|
||||
"alpha",
|
||||
"beta"
|
||||
]
|
||||
}
|
||||
],
|
||||
"documents": null,
|
||||
"edges": null,
|
||||
"shardId": "shard-1",
|
||||
"shardEpoch": 3,
|
||||
"shardLogSequence": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snapshot_response_raw_v1_documents_and_edges",
|
||||
"schemaVersion": "cultnet.snapshot_response_raw.v1",
|
||||
"messagePackBase64": "i61zY2hlbWFWZXJzaW9u2SBjdWx0bmV0LnNuYXBzaG90X3Jlc3BvbnNlX3Jhdy52MaltZXNzYWdlSWShbadtYXRjaGVkAaRhc09mBaRuZXh0wKdoZWFkZXJzwKlkb2N1bWVudHORjKhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZcCtc2NoZW1hVmVyc2lvbsCxc2NoZW1hQ29udGVudEhhc2jAqXJlY29yZEtleaRhLWVxqHN0b3JlZEF0uDE5NzAtMDEtMDFUMDA6MDA6MDAuMDAwWq9wYXlsb2FkRW5jb2RpbmerbWVzc2FnZXBhY2uncGF5bG9hZMQEgaFrAa9zb3VyY2VSdW50aW1lSWTArXNvdXJjZUFnZW50SWTAqnNvdXJjZVJvbGXApHRhZ3PApWVkZ2VzkoWkZnJvbYKoc2NoZW1hSWTZR3NoYTI1Njo5ZDBiYzlhZWJjMzYxOWIwN2FhYmY1NjRmOTg2NjdhZmRlNDE0NDRjMWI4NjY2NjFkNzc3Njg3ZDQ0MzYwOGRjqXJlY29yZEtleadjaXRlci0xpHJvbGWmRGVzaWduonRvgqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGvcGF5bG9hZEVuY29kaW5nwKdwYXlsb2FkwIWkZnJvbYKoc2NoZW1hSWTZR3NoYTI1Njo5ZDBiYzlhZWJjMzYxOWIwN2FhYmY1NjRmOTg2NjdhZmRlNDE0NDRjMWI4NjY2NjFkNzc3Njg3ZDQ0MzYwOGRjqXJlY29yZEtleadjaXRlci0xpHJvbGWmRGVzaWduonRvgqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGvcGF5bG9hZEVuY29kaW5nq21lc3NhZ2VwYWNrp3BheWxvYWTEAwECA6dzaGFyZElkwKpzaGFyZEVwb2NowLBzaGFyZExvZ1NlcXVlbmNlwA==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.snapshot_response_raw.v1",
|
||||
"messageId": "m",
|
||||
"matched": 1,
|
||||
"asOf": 5,
|
||||
"next": null,
|
||||
"headers": null,
|
||||
"documents": [
|
||||
{
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"schemaName": null,
|
||||
"schemaVersion": null,
|
||||
"schemaContentHash": null,
|
||||
"recordKey": "a-eq",
|
||||
"storedAt": "1970-01-01T00:00:00.000Z",
|
||||
"payloadEncoding": "messagepack",
|
||||
"payload": "gaFrAQ==",
|
||||
"sourceRuntimeId": null,
|
||||
"sourceAgentId": null,
|
||||
"sourceRole": null,
|
||||
"tags": null
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from": {
|
||||
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
|
||||
"recordKey": "citer-1"
|
||||
},
|
||||
"role": "Design",
|
||||
"to": {
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"recordKey": "a-eq"
|
||||
},
|
||||
"payloadEncoding": null,
|
||||
"payload": null
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
|
||||
"recordKey": "citer-1"
|
||||
},
|
||||
"role": "Design",
|
||||
"to": {
|
||||
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
|
||||
"recordKey": "a-eq"
|
||||
},
|
||||
"payloadEncoding": "messagepack",
|
||||
"payload": "AQID"
|
||||
}
|
||||
],
|
||||
"shardId": null,
|
||||
"shardEpoch": null,
|
||||
"shardLogSequence": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "error_plain",
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KkYm9vbatyb3V0aW5nSGludMCkY29kZcCnZGV0YWlsc8A=",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "boom",
|
||||
"routingHint": null,
|
||||
"code": null,
|
||||
"details": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "error_selection_invalid",
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3LZOnNlbGVjdGlvbl9pbnZhbGlkOiBmaWVsZHNbMF0uaW5kZXggImtpbmQiIGlzIG5vdCBkZWNsYXJlZC6rcm91dGluZ0hpbnTApGNvZGWxc2VsZWN0aW9uX2ludmFsaWSnZGV0YWlsc4SlZmllbGSvZmllbGRzWzBdLmluZGV4pXZhbHVlpGtpbmSkYXNPZsCnY3VycmVudMA=",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "selection_invalid: fields[0].index \u0022kind\u0022 is not declared.",
|
||||
"routingHint": null,
|
||||
"code": "selection_invalid",
|
||||
"details": {
|
||||
"field": "fields[0].index",
|
||||
"value": "kind",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "error_cursor_stale",
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KzY3Vyc29yX3N0YWxlOiBzdGFsZatyb3V0aW5nSGludMCkY29kZaxjdXJzb3Jfc3RhbGWnZGV0YWlsc4SlZmllbGTApXZhbHVlwKRhc09mA6djdXJyZW50CQ==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "cursor_stale: stale",
|
||||
"routingHint": null,
|
||||
"code": "cursor_stale",
|
||||
"details": {
|
||||
"field": null,
|
||||
"value": null,
|
||||
"asOf": 3,
|
||||
"current": 9
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "error_cursor_invalid_no_details",
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KzY3Vyc29yX2ludmFsaWQ6IGJhZKtyb3V0aW5nSGludMCkY29kZa5jdXJzb3JfaW52YWxpZKdkZXRhaWxzwA==",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "cursor_invalid: bad",
|
||||
"routingHint": null,
|
||||
"code": "cursor_invalid",
|
||||
"details": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "error_write_not_authorized",
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3K0d3JpdGVfbm90X2F1dGhvcml6ZWSrcm91dGluZ0hpbnTApGNvZGW0d3JpdGVfbm90X2F1dGhvcml6ZWSnZGV0YWlsc4SlZmllbGSpcmVjb3JkS2V5pXZhbHVloWukYXNPZsCnY3VycmVudMA=",
|
||||
"expected": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "write_not_authorized",
|
||||
"routingHint": null,
|
||||
"code": "write_not_authorized",
|
||||
"details": {
|
||||
"field": "recordKey",
|
||||
"value": "k",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
{
|
||||
"provenance": "Captured from MessagePack C# 3.1.7 (CultNetErrorMessage/CultNetErrorDetails, Options = Standard.WithSecurity(UntrustedData)); pinned byte-for-byte by packages/cultnet-rs/tests/error_message.rs, whose inline hex equals messagePackHex here (test error_vectors_file_matches_the_inline_csharp_captures). Rust encodes to these exact bytes, so C#-written and Rust-written bytes are one set. Every key is written on the wire, nil included.",
|
||||
"vectors": [
|
||||
{
|
||||
"name": "selection_invalid_schemas_null",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92473656C656374696F6E5F696E76616C69643A206D757374206E6F7420626520656D707479AB726F7574696E6748696E74C0A4636F6465B173656C656374696F6E5F696E76616C6964A764657461696C7384A56669656C64A7736368656D6173A576616C7565C0A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "selection_invalid: must not be empty",
|
||||
"routingHint": null,
|
||||
"code": "selection_invalid",
|
||||
"details": {
|
||||
"field": "schemas",
|
||||
"value": null,
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "selection_invalid_field_value",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92273656C656374696F6E5F696E76616C69643A20626C616E6B206B657920656E747279AB726F7574696E6748696E74C0A4636F6465B173656C656374696F6E5F696E76616C6964A764657461696C7384A56669656C64A46B657973A576616C7565A3202020A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "selection_invalid: blank key entry",
|
||||
"routingHint": null,
|
||||
"code": "selection_invalid",
|
||||
"details": {
|
||||
"field": "keys",
|
||||
"value": " ",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cursor_stale",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72BD637572736F725F7374616C653A20637572736F72206973207374616C65AB726F7574696E6748696E74C0A4636F6465AC637572736F725F7374616C65A764657461696C7384A56669656C64C0A576616C7565C0A461734F6601A763757272656E7402",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "cursor_stale: cursor is stale",
|
||||
"routingHint": null,
|
||||
"code": "cursor_stale",
|
||||
"details": {
|
||||
"field": null,
|
||||
"value": null,
|
||||
"asOf": 1,
|
||||
"current": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cursor_invalid",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D923637572736F725F696E76616C69643A2063616E6E6F7420706172736520637572736F72AB726F7574696E6748696E74C0A4636F6465AE637572736F725F696E76616C6964A764657461696C73C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "cursor_invalid: cannot parse cursor",
|
||||
"routingHint": null,
|
||||
"code": "cursor_invalid",
|
||||
"details": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reference_outside_target",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D9317265666572656E63655F6F7574736964655F7461726765743A206F757473696465206465636C6172656420746172676574AB726F7574696E6748696E74C0A4636F6465B87265666572656E63655F6F7574736964655F746172676574A764657461696C73C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "reference_outside_target: outside declared target",
|
||||
"routingHint": null,
|
||||
"code": "reference_outside_target",
|
||||
"details": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "variant_unsupported_subscription_ended",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72B876617269616E7420756E737570706F727465642068657265AB726F7574696E6748696E74C0A4636F6465B376617269616E745F756E737570706F72746564A764657461696C7384A56669656C64AE737562736372697074696F6E4964A576616C7565A57375622D31A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "variant unsupported here",
|
||||
"routingHint": null,
|
||||
"code": "variant_unsupported",
|
||||
"details": {
|
||||
"field": "subscriptionId",
|
||||
"value": "sub-1",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "variant_unsupported_no_subscription",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72B876617269616E7420756E737570706F727465642068657265AB726F7574696E6748696E74C0A4636F6465B376617269616E745F756E737570706F72746564A764657461696C73C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "variant unsupported here",
|
||||
"routingHint": null,
|
||||
"code": "variant_unsupported",
|
||||
"details": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unowned_schema",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D95C756E6F776E65645F736368656D613A204E6F207368617264206F776E7320736368656D612027736F6D652E736368656D612E763127206B65792027736F6D653A6B6579273B207468652077726974652077617320726566757365642EAB726F7574696E6748696E74C0A4636F6465AE756E6F776E65645F736368656D61A764657461696C7384A56669656C64A8736368656D614964A576616C7565AE736F6D652E736368656D612E7631A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "unowned_schema: No shard owns schema 'some.schema.v1' key 'some:key'; the write was refused.",
|
||||
"routingHint": null,
|
||||
"code": "unowned_schema",
|
||||
"details": {
|
||||
"field": "schemaId",
|
||||
"value": "some.schema.v1",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_not_authorized",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92177726974655F6E6F745F617574686F72697A65643A206E6F74206772616E746564AB726F7574696E6748696E74C0A4636F6465B477726974655F6E6F745F617574686F72697A6564A764657461696C7384A56669656C64A97265636F72644B6579A576616C7565A8736F6D653A6B6579A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "write_not_authorized: not granted",
|
||||
"routingHint": null,
|
||||
"code": "write_not_authorized",
|
||||
"details": {
|
||||
"field": "recordKey",
|
||||
"value": "some:key",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unsupported_schema_version",
|
||||
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92C756E737570706F727465645F736368656D615F76657273696F6E3A2063756C746E65742E68656C6C6F2E7631AB726F7574696E6748696E74C0A4636F6465BA756E737570706F727465645F736368656D615F76657273696F6EA764657461696C7384A56669656C64AD736368656D6156657273696F6EA576616C7565B063756C746E65742E68656C6C6F2E7631A461734F66C0A763757272656E74C0",
|
||||
"message": {
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"error": "unsupported_schema_version: cultnet.hello.v1",
|
||||
"routingHint": null,
|
||||
"code": "unsupported_schema_version",
|
||||
"details": {
|
||||
"field": "schemaVersion",
|
||||
"value": "cultnet.hello.v1",
|
||||
"asOf": null,
|
||||
"current": null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,441 +0,0 @@
|
|||
# C2a land map
|
||||
|
||||
What C2a (`hands/variants-c2a`, a5c7954e) must still prove to merge to main, measured
|
||||
on 2026-10-22 against main 7a8933b0 and the 2.0.0 release candidate
|
||||
`hands/cultlib-unity-release-2.0.0` (5b5d8573, which carries f91e30ed). The land cut
|
||||
is the merge plus the gate. Everything else is classified below. Governing ruling:
|
||||
`cultlib-gaps:ruling:release-now-c2a-bumps-format`.
|
||||
|
||||
## Cut sequence to main (2026-10-27, branch head 7c7cc799)
|
||||
|
||||
Landed since the facts below: `land-c2a` (h1), `land-c2a-merge-api-docs` (h1-h6), the 2.0.1 and
|
||||
cultcache-rs 0.3.1 merges (5baa02fa, f54472e5), `land-c2a-gate` r4 (h1), `store-gate-one-owner`
|
||||
(h1-h7, Soul s6) and `restamp-on-write` (h1-h2, 8ee66714..7c7cc799, Soul s2 holds). One static `Owns`
|
||||
decides ownership for cache and cacheless writers, and the High finding
|
||||
`cut-land-c2a-gate.s1.reflist-record-read-only-at-head` is Fixed by 7c7cc799. origin/main is 73e684b1:
|
||||
21 commits past the merge base 02cf04b5, all docs and research (15 files, +1879, none under src/,
|
||||
tests/ or packages/, none on a path the branch changed). What stands between the branch and main, in
|
||||
order:
|
||||
|
||||
1. **claimant-one-owner** (spec r1 at 7c7cc799, Hands then Soul). Restamp's Soul s2 left four Lows
|
||||
on the landing path. Which entry reads a persisted entry is chosen twice (`Claimant` at
|
||||
CultCache.cs:703-750, `ClaimantEntry` at :797-799), and so a cacheless writer handed registry {A,B}
|
||||
overwrites a record that a cache with {A,B} holds read-only (probe X4). The cut keeps one `Claimant`
|
||||
precedence over a by-id/by-name lookup that the registry or a single entry supplies. A cacheless
|
||||
writer with a registry then takes its claimant from that registry. The cut also pins the
|
||||
unpublished-id case and fixes the stale test comment. It closes `claimant-selection-duplicated`,
|
||||
`cacheless-inverts-registry-claimant`, `unpublished-id-guard-unpinned` and `stale-flag-copy-comment`.
|
||||
It is kept apart from the gate so that Soul can falsify it alone. Its files (CultCache.cs, the
|
||||
persistence contract, RestampOnWriteTests.cs) are disjoint from the gate's, so it can run before or
|
||||
beside step 2. The second cut to land rebases.
|
||||
2. **land-c2a-gate** (spec r7 at 7c7cc799, Hands h2 then Soul). The ref-list gate does the same write
|
||||
at 2.0.1 and at head and compares schema ids. The committed vectors are pinned byte for byte under a
|
||||
fixed clock, and every stage counts its cases. r7 drops r6's head-only restamp assertions, which
|
||||
`RestampOnWriteTests.TheCommitted1060VectorsListRecordIsWrittenUnderTheHoldersOwnId` already pins.
|
||||
It also drops the red-at-193cf1c3 run, because restamp is measured. The cut closes
|
||||
`single-vector-hand-edit-passes`, `gate-green-with-no-refusal-probes` and follow-up
|
||||
`gate-reads-1x-written-store`.
|
||||
3. **Ledger** (Self, no Hands; can run now, beside steps 1 and 2). Each of these is in force though
|
||||
its fix has landed:
|
||||
- The seven `cut-land-c2a.s1.*` findings. `land-c2a-merge-api-docs` carried their pins (source reads
|
||||
2026-10-27: GameCult.Caching grants InternalsVisibleTo only to `.Tests` and `.MessagePack`, the
|
||||
residual being follow-up `caching-messagepack-ivt`; PreCut2StoreFormatTests pins "with a suffix
|
||||
of"). Close each one against its commit, or defer it. `stale-unity-plugin-dlls` defers to the
|
||||
Unity 3.0.0 release cut (the plugins are 2.0.1 builds, 1eeb1a01).
|
||||
- `cut-c2a-write-set.s1.untouched-dropped-slot-resurrects` and follow-up `slot-reuse-contract-doc`.
|
||||
Ruling `slot-reuse-documented` decided the behaviour, and the paragraph is in
|
||||
cultcache-schema-compatibility.md:64-66.
|
||||
- `cut-restamp-on-write.s1.websockets-tests-unreported`, Fixed by h2 (s2 promise
|
||||
`websockets-tests-reported` holds).
|
||||
- Follow-up `gate-bound-test-tighten`, carried by restamp r2 (50362447, a768b821). Close it if
|
||||
restamp's Soul passes measured it; otherwise route it.
|
||||
- After steps 1 and 2 hold: the four s2 Lows, the two remaining `cut-land-c2a-gate.s1` Lows and
|
||||
`gate-reads-1x-written-store`.
|
||||
4. **Consumer pins** (ruling `unpinned-consumers-at-land-3`, follow-up `consumer-pins-before-land`).
|
||||
Brokkr, Aquarium, Ymir, AquaSynth and Mimir have no `CultLibRevision`/`VerifyCultLibRevision` guard,
|
||||
and each builds against the sibling `..\CultLib` checkout. In its own repo, each adopts Aetheria's
|
||||
guard at the 2.0.0 tag, with one Hands per repo, before the merge reaches main. The pins touch no
|
||||
CultLib file, so they can start now and run in parallel with steps 1-3. Eve and Gjallar get no pin
|
||||
(ruled). The step is done when each repo's guard fails a build against a CultLib checkout at
|
||||
another revision and passes at the tag, and the follow-up is resolved.
|
||||
5. **Main into the branch, then full verification.** After steps 1, 2 and 4, `git merge origin/main`
|
||||
into `hands/variants-c2a`. It should be clean, because main's commits since 02cf04b5 are docs and
|
||||
research only. Re-check that before merging (`git diff --stat 02cf04b5 origin/main -- src tests
|
||||
packages unity`). Then run every suite at the merged head on Yggdrasil: Caching, Mesh, Networking,
|
||||
Geometry, WebSockets, ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts.
|
||||
Run `old-reader-refusal.sh gate` for csharp, typescript, python and rust. If main gained code by then,
|
||||
a Soul pass covers the merge.
|
||||
6. **Land.** The land is `git merge --no-ff`, never a squash, so the SHAs that every report and verdict cites stay reachable from main (skip the branch's non-building commits with `git bisect --first-parent`). Steps 1 and 2 closed at 723b7996 (claimant-one-owner, land-c2a-gate); step 5 closed at the merge commit 3157d4b3 (Soul `cut-land-c2a-main.s1` holds). Step 3 closed except `cut-land-c2a.s1.stale-unity-plugin-dlls` and `cut-land-c2a-gate.s2.mutation-table-reads-gate-as-zero`, which wait on the 3.0.0 release cut and the Eureka tools respectively; step 4 closed with `consumer-pins-before-land` (pins since 2026-10-22). Run `git merge --no-ff hands/variants-c2a` onto main, with an empty diff against the
|
||||
verified head. There is no tag: tags, the Unity plugin rebuild and `declared-range-gate-own-version`
|
||||
belong to the 3.0.0 release cut. Open follow-ups `c2a-load-slower` (load 2 to 2.3 times slower than
|
||||
main on a synthetic Aetheria catalog) and `drift-write-ownership-leaves` do not say "before land".
|
||||
`c2a-load-slower` still needs a reason or a fix before the 3.0.0 release, under
|
||||
`cultlib-gaps:ruling:database-shaped-built-for-load`.
|
||||
|
||||
## Facts
|
||||
|
||||
**1. The merge.** A trial `git merge origin/main` on a5c7954e gives 12 conflicted files and
|
||||
33 hunks of about 950 marker lines (CultCache.cs 17 hunks/196; cult-cache.test.ts 337;
|
||||
single-file-messagepack-backing-store.ts 4/106; rs lib.rs 2/51; VariantTests 51; three
|
||||
CHANGELOG add/adds; store-composition contract 32; Directory store 14; InteropPeer 21).
|
||||
Main moved 4.8k lines in the cache runtimes since base 1a12bc2d. Three semantic collisions
|
||||
are not textual:
|
||||
|
||||
- *Durable record identity has two owners.* Main added `CultStoredDocument.PersistedSchemaId`
|
||||
and `CacheBackingStore.MarkWritten` (73d438db, afd799ba, 3f6dda8b). Conditional commit
|
||||
compares the persisted schema id plus storedAt. C2a already owns this as
|
||||
`StoredSchemaId` + `StoredVersion` (SHA-256 of the stored bytes, which names the schema
|
||||
id and storedAt) set by `CultCache.Wrote`. After the merge, 9 references in
|
||||
ConditionalCommitTests and the contract name main's owner. The version is the owner.
|
||||
PersistedSchemaId and MarkWritten are deleted, and main's +309 test lines are ported as
|
||||
pins against StoredVersion. 2.0.0 ships MarkWritten as protected API, and C2a also
|
||||
removes `SerializeSnapshot`/`DeserializeSnapshot` overrides and changes `ConditionsHold`.
|
||||
C2a is therefore a major release (3.0.0), which semver forces.
|
||||
- *TS store commit moved.* Main's `store-file.ts` (0e6452c9, 0010ee92; 391 lines) now owns
|
||||
commit, replace and the `<path>.lock`. C2a's v3 read and keep-v3-on-rewrite
|
||||
(`STORE_FORMAT_ELEMENT_IDS`) lives in the old store file and must move into store-file.ts.
|
||||
- *Rust decode door.* Main's cultcache-rs 0.3.0 grown decoder (`decode_stack`, fc630d3b)
|
||||
must sit under C2a's `readable_store_format` and v3 `decode_store_file`.
|
||||
|
||||
**2. The gate already holds; it needs a test, not a format change.** C2a stamps
|
||||
`cultcache.store.v3` (single file) or `...v5.directory-content-addressed-pages` exactly when
|
||||
a record holds an id (`HeaderFor`, CultCache.cs:3543). It also marks when a copied record
|
||||
decodes holding one (`CopiedHoldIds`, :4285). The 2.0 C# reader accepts only v1/v2
|
||||
(CultDocumentMessagePackSerialization.cs:229) and directory v4 (Directory store :452).
|
||||
Main's TS, Rust and Python readers accept only v1 (store-format.ts `isV1Snapshot`, lib.rs:1274,
|
||||
stores.py:192). Probe on Yggdrasil at 5b5d8573 plus C2a's `v3-base.msgpack` (job
|
||||
`gate1.log`): 4/4 pass. The single file is refused naming v3, and a later flush leaves it
|
||||
byte-identical. A v5-stamped directory store is refused naming v5, and its 4 files are
|
||||
byte-identical after a later add and flush. **The pin:** C2a already ships
|
||||
`tests/vectors/document-variants-c2a/old-reader-refusal.sh`, which stages an old revision
|
||||
plus probes on a scratch ref and runs them per runtime. The land cut sets its
|
||||
`OLD_READER_REV` default to the tag `cultlib-unity-v2.0.0`. It also adds the never-rewritten
|
||||
assertion and the v5 directory case to the C# probe, and runs all four runtimes at the
|
||||
tag. If the tag is absent, the land waits for it. The tag, not a package, is the pin: the tag's
|
||||
tree holds every runtime's released reader.
|
||||
|
||||
*Outside C2a's reach (release session):* in 2.0, `CultMesh.WriteSingleFileDocument`/
|
||||
`WriteSingleFileDocumentPayload` (CultMeshSingleFileDocuments.cs:211-245 at 7a8933b0) build
|
||||
a fresh one-record v1 snapshot and replace the file without reading it. A 2.0 Mesh write
|
||||
to a C2a v3 path therefore discards it whole, ids and other records included, and no header
|
||||
gate can stop a writer that never reads. C2a's version reads under the lock and refuses.
|
||||
Fix it in 2.0.0 before tagging, or record it.
|
||||
|
||||
**5. Consumers.** Aetheria pins a CultLib revision (`VerifyCultLibRevision`) and Unity tags.
|
||||
Ghostlight, Eve's Rust crates, Huginn and the Brokkr daemon pin cultcache-rs by git rev.
|
||||
None of them moves when main moves. Two C# consumers build against an unpinned sibling
|
||||
`..\CultLib` checkout: Eve `GameCult.Eve.Surface.csproj:16` and Brokkr's three csprojs.
|
||||
Eve.Surface has `[MessagePackObject]` element arrays with no `[CultElementId]`
|
||||
(`EveAssetCatalogEntry[]`, `EveEntitySoaBuffer[]`, `EveInputActionDocument[]`,
|
||||
`IReadOnlyList<EveCommandTemplate>`, 0 ids in the repo), so C2a registration refuses its
|
||||
documents as soon as the checkout pulls the land. Grep finds no object lists in Brokkr's
|
||||
`[CultDocument]` files (not build-verified). Removed C2a API (`Options`, `AsSchemaAlias`) is
|
||||
unused by Eve and Brokkr. Fork: `variants:question:unpinned-consumers-at-land`.
|
||||
|
||||
## Classification
|
||||
|
||||
| Item | Class | Why |
|
||||
|---|---|---|
|
||||
| c2a-format-version-gate | **a** | The gate test above. No new header: v3/v5 already refuse. |
|
||||
| slot-reuse-contract-doc | **a** | The ruled consequence of the write set C2a lands. One paragraph in cultcache-schema-compatibility.md, absent at a5c7954e (grep). |
|
||||
| foreign-ids-under-v1-header | **b** | Obsoleted by fb065f0d (write-time `CopiedHoldIds`, after the finding) for every C# writer with the type. The residual (non-C# or opaque writers) is what `non-csharp-writers-mark-ids-2` ruled to carry, and the contract states it (persistence-format.md:434-439). The gate changes neither side, so no fork remains. Resolve as Superseded by that ruling. |
|
||||
| custom-resolver-element-ids | **b** | Fixed by b091d21f (19:52Z, after the 18:27Z follow-up): `ListElementsClaimedByAResolverAreRefused`, `AResolverClaimingTheListTypeOfAnObjectListIsRefused`. s6 holds on the claim loop. |
|
||||
| codec-door-proven-mismatch | **b** | Done in h6 57fdf70c (s6 `only-proven-mismatch` Holds; Geometry 76/0). |
|
||||
| consumer-element-id-sweep-r2 | **c** | Each repo's sweep is its own work (q3). Only the unpinned trackers matter at land, and that is the question below. Aetheria bumps a pin on purpose (C4). |
|
||||
| schema-discovery-holder-recogniser | **c** | `TryGetDictionaryRefTypes` is identical on main (CultCache.cs:1288). It describes record-ref dictionaries and is not on the id path. Changing it moves schema hashes, which is its own epoch event. |
|
||||
| copy-check-memo | **c** | Performance only (about 3x flush on unmarked stores of 20k records). Correct as is. |
|
||||
| string-key-field-and-shadow-pins | **c** | No wrong behaviour at 64ec14f9. These are test pins and may ride the land cut if Hands touches the file. |
|
||||
| declared-range-gate-own-version, range-check-in-ci | **c** | The script exists only on C2a and runs in no CI. It guards a TS publish, not a merge. Fix both before the first TS release from main after the land. |
|
||||
| s6 Unproven: Unity/IL2CPP identity, other MessagePack versions | **c** | The merge reaches no Unity build. Unity sees C2a only at the 3.0.0 tag, so measure it in the Unity release cut (operator Unity check, C3). |
|
||||
| multi-store-commit-atomicity, store-replace-cut, reader-refuses-duplicate-keys, soft-flush-all-runtimes, writer-dangling-link-tolerance, unique-index-cut, py-shared-write-lock, rust-lock-open-waits, ts-lock-test-hardening, c0/legacy/directory vectors, culture-test, wire-id-cuts, remaining-variant-cuts, golden-path, paged store | **c** | None says "before land". Each is pre-existing on main or C2a-independent. None lets a released reader strip ids or corrupt a C2a store. |
|
||||
|
||||
## The land cut's shape (transcribe once the question is ruled)
|
||||
|
||||
- **Base.** Merge main at or after the `cultlib-unity-v2.0.0` tag into `hands/variants-c2a`.
|
||||
Versions stay main's. CHANGELOGs get Unreleased entries, and the Unity entry says major.
|
||||
- **Deletes and collapses.** Delete `PersistedSchemaId`, `PersistedIdentity`,
|
||||
`CultStoredDocument.MarkWritten` and `CacheBackingStore.MarkWritten` (main, about 60 lines),
|
||||
and the store-composition contract's MarkWritten paragraph. C2a's `StoredVersion`/`Wrote`
|
||||
is the one owner. In TS, delete C2a's header handling in
|
||||
single-file-messagepack-backing-store.ts and move it into store-file.ts. In Rust, C2a's v3
|
||||
decode goes through the grown decoder.
|
||||
- **Adds.** Retarget old-reader-refusal.sh to the tag. Add the C# probe's never-rewritten
|
||||
assertion and the v5 directory case (as in gate1.log). Add the slot-reuse paragraph.
|
||||
Port main's ConditionalCommitTests to the version identity.
|
||||
- **Verification.** Full suites: Caching, Mesh, Networking, Geometry, WebSockets,
|
||||
ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts, all on
|
||||
Yggdrasil. Run old-reader-refusal.sh for all four runtimes at the tag, and it must refuse.
|
||||
Negative greps over the merged tree: `PersistedSchemaId|MarkWritten`, conflict markers.
|
||||
Mutation: drop the v3/v5 stamp in `HeaderFor` and the gate probe must fail.
|
||||
- **Estimate.** About 950 conflict lines resolved. Net -60 to -150 from the identity
|
||||
collapse and TS de-duplication, +80 to +120 tests and docs. One Hands pass of 100-150
|
||||
calls, then one Soul pass.
|
||||
- **Precondition (question).** Eve.Surface, and Brokkr if confirmed, are pinned or swept
|
||||
before the merge reaches main.
|
||||
|
||||
## Store gate: attachment is the attach's hold (store-gate-one-owner r2)
|
||||
|
||||
Spec `variants:cut_spec:cut-store-gate-one-owner.r2` (base ec94ab98), question
|
||||
`variants:question:pulled-variant-codec`. It closes Soul s3's
|
||||
`attach-window-cacheless-write` (Medium) and `codec-loan-pinned-by-reflection` (Low).
|
||||
|
||||
**Facts (source reads at ec94ab98 and a probe, 2026-10-25).**
|
||||
|
||||
- The window. AddBackingStore holds the cache's gate, then the store's gate through
|
||||
`InStoreHold`, lends the registry and codec, sets `Loaded`, adds the store to `_stores`,
|
||||
pulls, and only then sets `store.Cache` (CultCache.cs 2268-2312). `InStoreHold` lets any
|
||||
call on the same store's gate re-enter it (2825-2831). So a journal the pull's Admit runs
|
||||
on the attaching thread can call `store.WriteDocument`: `Gate(cacheless)` guesses null,
|
||||
re-enters the store's gate, finds `Cache == null` and runs the write, and its `finally`
|
||||
calls `UseRegistry(null, null)` (Soul s3 P1, full1.log lines 49-53). Other threads are not
|
||||
in the window: they queue on the store's gate and see either the published cache or a
|
||||
store that the failed attach put back.
|
||||
- The codec loan has no reader on the attach path. A pulled variant's codec is read only
|
||||
by `CultStoredDocument.HoldsIds` (543-557). Header decisions read `HoldsIds` only on
|
||||
staged entries (single-file 4318, 4447 and 4472; directory store 362), and a staged entry
|
||||
is always the document the cache pushed (`Push` sets `Entries[key] = entry`, 4211-4215).
|
||||
A single-file `PullAll` returns without loading while the store is dirty (4157-4159), so
|
||||
a pulled copy is never staged. A refused write unstages its keys before it adopts the
|
||||
reload. The directory store refuses variants (ruling `q6-directory-and-cultnet-refuse`;
|
||||
DirectoryMessagePackBackingStore.cs 139-144 and 642-643). Admit re-resolves every pulled
|
||||
variant with the cache's codec (CultCache.cs 3259). Soul's N4 mutant (the attach lends
|
||||
no codec) was caught only by the reflection test, and h4's M2 only by a bare-store test
|
||||
that sets `Loaded` and calls `UseRegistry` by hand.
|
||||
- Probe (scratch clone `imagination-store-gate-r2/clone`, commits 4663934e and c099b9e8 on
|
||||
ec94ab98; Yggdrasil t1.log, t2.log and t3.log; 2026-10-25 around 20:50 CEST). The
|
||||
probe did three things. `InStoreHold` refuses a cacheless call that re-enters the same
|
||||
store's gate. The `_codec` field and the two-argument `UseRegistry` are deleted. The pull
|
||||
builds variants with a null codec. Results:
|
||||
- Mesh is 342/0/4.
|
||||
- In the Caching suite, the only failures were two probe tests, both from authoring
|
||||
errors: a key `AddAsync` minted, and an element id that was not 12 hex characters.
|
||||
No existing test failed, `UserCodeInACachelessCallMayCallItsOwnStore` included.
|
||||
- Soul's P1 as a test: the journal's write is refused with the new message. The attach
|
||||
keeps its registry and the file holds only `a`. After the attach, a cache write, a
|
||||
flush and `store.PullAll()` all succeed.
|
||||
- A cacheless write nested in another on the same store is refused, the outer write
|
||||
fails, and no file is written.
|
||||
- Through a real attach, a store holding a name-override variant flushes an unrelated
|
||||
commit with a `cultcache.store.v2` header. With no codec loan, the pulled copy does not
|
||||
mark the store.
|
||||
- Mutant `return cacheless` changed to `return false`, which is ec94ab98's behaviour,
|
||||
fails both new gate tests. In that mutant the nested write succeeds and the file exists.
|
||||
|
||||
**Rationale.**
|
||||
|
||||
- Attachment has one fact again. While the attach runs, the store's gate is the attach's
|
||||
hold, and `Cache` is published when that hold ends. A cacheless write is a write the
|
||||
store owns outright, so it must be the outermost hold of its store's gate. One more
|
||||
argument to the existing owner of per-thread gate facts (`InStoreHold`) refuses it. No
|
||||
field mirrors `Cache`, and `Loaded` and `_registry` are not read. Precedent:
|
||||
`ConcurrentHashMap.computeIfAbsent` refuses a mapping function that updates the same
|
||||
map with `IllegalStateException("Recursive update")` since Java 9. That is from
|
||||
memory, not fetched this pass; the 2026-10-23 prior-art note covers the documented
|
||||
restriction in its counter-positions. Non-cacheless store calls from user code still
|
||||
re-enter: `UserCodeInACachelessCallMayCallItsOwnStore` stays green.
|
||||
- The late publish stays. It is what bounds `Gate` at two passes (s2's finding). Publishing
|
||||
early would bring back the failure-rate-bounded retry.
|
||||
- The codec loan goes: it is a value nothing on the attach path reads. A pulled variant
|
||||
carries no codec and counts as holding ids (the documented conservative answer), and
|
||||
the cache's resolved copy is what any header decision sees. This reverses the codec
|
||||
half of Defaulted ruling `late-publish-needs-attach-codec`, so it goes to Self as
|
||||
`variants:question:pulled-variant-codec`. The spec is written for the recommended option.
|
||||
- Scar check: `git log -S` over CultCache.cs for `reentrant`, `outermost` and `_attaching`
|
||||
finds nothing. `Cache?.Codec` appears in 1468a83a (h4) and 619e089f only. No earlier
|
||||
re-entry refusal was built and deleted.
|
||||
- Behaviour change, stated in the CHANGELOG: a cacheless write made from user code that
|
||||
another cacheless write of the same store runs used to succeed, and its `finally` took
|
||||
the outer call's registry. It is now refused.
|
||||
|
||||
## Store gate: Cache is the one attachment fact (store-gate-one-owner r3)
|
||||
|
||||
Spec `variants:cut_spec:cut-store-gate-one-owner.r3` (base 0fa836cb), superseding r2. It
|
||||
closes Soul s4's `attach-window-held-store-writes` (Medium), `cacheless-refusal-order-unpinned`
|
||||
(Low) and `interop-consumer-suite-unrun` (Low). This is the third time the attach window has
|
||||
beaten the rule (s3 from the cacheless side, s4 from the held side), so r3 moves the
|
||||
decision instead of patching the next entry point.
|
||||
|
||||
**Facts: every entry point that reads or decides attachment, at 0fa836cb.** Source reads of
|
||||
CultCache.cs, DirectoryMessagePackBackingStore.cs and CultMeshSingleFileDocuments.cs. The
|
||||
window column is a journal on the attaching thread, through a real attach of a store holding
|
||||
`a`. Probe: scratch clone `imagination-store-gate-r3/clone`, commit b2330b6d (test R3Probes),
|
||||
Yggdrasil `probe-base.log`, 2026-10-25 around 23:00 CEST. "Disagree" means that after the
|
||||
attach, a commit of `later` and a flush, the attaching cache holds `a,later` and a fresh cache
|
||||
over the same file reads something else.
|
||||
|
||||
| Entry point (line) | Fact it reads | In the attach window at 0fa836cb |
|
||||
| --- | --- | --- |
|
||||
| Single-file `PullAll` (4156) | `Gate`: `Cache` guessed unlocked, then re-read under the re-entered store gate | Runs as detached; its `Loaded` re-enters the cache and is refused as a journal write. Agrees. |
|
||||
| Single-file `Push` (4212) | same | Runs as detached and stages `b`. Disagree: fresh reads `a,b,later`. |
|
||||
| Single-file `Delete` (4223) | same | Runs as detached and stages a removal the cache never made (dirty after the attach). |
|
||||
| Single-file `PushAll` (4239), and `Dispose` (3722) through it | same, then in-body `Cache == null` (4531, 4543, 4588, 4603, 4611, 4661) | Runs as a cacheless writer. Push then PushAll writes `b` to the file inside the attach (disagree). Delete then PushAll is refused with the cacheless writer's text. On a variant store it is refused as "a writer with no CultCache" (Soul s4). |
|
||||
| Single-file `CommitBatch` (4256) | same | Runs as detached and commits `b`. Disagree. |
|
||||
| Single-file `JudgeChecks` (4568), `RefuseRemovalOfUndeclared` and `StageChecks` (4558, 3915) | same | Run as detached. Agree in the probe, but under the wrong gate's rules. |
|
||||
| Single-file `ReadStoreSnapshot` (4687) | same | Runs as detached and reads the file. |
|
||||
| `WriteRecord` (4342), `WriteDocument` (4366) | `InStoreHold`'s per-thread hold (cacheless re-entry), before `Cache` | Refused with the re-entry text (h5). Agree. |
|
||||
| Directory `PullAll` (48) | `Gate`, as above | Refused as a journal write. Agree. |
|
||||
| Directory `Push` (147), `Delete` (160), `CommitBatch` (172) | same | Run as detached. Disagree: fresh reads `a,b,later` after Push or CommitBatch, and `later` after Delete. |
|
||||
| Directory `PushAll` (235) | same | Runs as detached. Push then PushAll and Delete then PushAll disagree. |
|
||||
| Directory `JudgeChecks` (269), `RefuseRemovalOfUndeclared` (259) | same | Run as detached. Agree. |
|
||||
| In-body reads: `StoredAtOver` clock (4028), refusal text and `DurableAt` (4531, 4543), declares and cacheless refusal (4588, 4603, 4611), `Wrote` (4661) | `Cache`, under the gate `Gate` chose | On the attaching thread they read null while the thread holds the cache's gate, so a held call runs as a cacheless writer. |
|
||||
| `AddBackingStore` (2259-2314) | writes `Cache` under both gates, after the pull | The window's author: the store is attached for the pull (registry lent, `Loaded` set, in `_stores`) while `Cache` is null. |
|
||||
| Mesh `CultMeshSingleFileDocuments` writes and reads (99, 143, 174, 226) | none of the attached store's | `StoreAt` (239) builds a new store per call, so they never touch an attached store object. A file it shares with an attached store is the file lock's and the write set's business. |
|
||||
| `IsDirty`, `HasStagedChecks`, `ForeignRecords`, `LastSchemaMigrationReports`, `HeaderFor`, `HeaderForWrite` | none | Ungated values or static decisions. They read no attachment fact. |
|
||||
|
||||
So during an attach, cacheless entry points decide attachment from the hold, and every held
|
||||
entry point, plus every in-body read, decides it from `Cache`, which is still null. These are
|
||||
two facts.
|
||||
|
||||
**Probe of the chosen shape** (commit 4695390c on b2330b6d; `proto-full.log`). `AddBackingStore`
|
||||
publishes `Cache` before the pull and takes it back in the same hold if the pull fails. `Gate`
|
||||
becomes straight-line code: guess, then the store's gate, where `Cache` is settled, then the
|
||||
settled cache's gate. Results:
|
||||
|
||||
- All 21 probe cases are refused. The cacheless writes get the re-entry text and every other
|
||||
case gets "A journal entered the cache it journals". After the flush, the attaching cache
|
||||
and a fresh cache both read `a,later`.
|
||||
- Caching: the only failures are the two tests that pin the late publish:
|
||||
`AnAttachLendsTheRegistryAndPublishesTheCacheAfterThePull` (asserts no cache during the
|
||||
pull) and `ACallWhoseGateChangesTwiceUnderItThrowsInsteadOfLooping` (asserts the old message
|
||||
text; the tampering reader still throws). The churn, queued-reader and failed-attach tests
|
||||
pass unchanged.
|
||||
- Mesh and Networking are green.
|
||||
- The cultcache-ts suite, which builds and drives the C# InteropPeer, passes on this shape:
|
||||
175 tests, 171 pass, 0 fail, 4 skipped. The C#, Rust and Python interop tests ran, the C#
|
||||
edit-in-place one included (`proto-ts2.log`, commit f5f02053). Recipe `clone/r3ts.sh`:
|
||||
image node:24-bookworm, apt python3-msgpack, dotnet-install for 10.0, rustup minimal.
|
||||
|
||||
**Shapes weighed (rationale).**
|
||||
|
||||
- (a) Publish `Cache` before the pull, as a616431e did. Chosen. What s2 found was not that an
|
||||
early publish is unbounded. It was that `Gate` re-read `Cache` *unlocked* after a stale
|
||||
guess, so a churn of failing attaches could hand it a new stale guess every pass. The fix is
|
||||
where `Gate` reads on its second step. Under the store's gate, `Cache` is settled: an attach
|
||||
publishes and takes it back inside one hold of that gate, and no other thread can hold that
|
||||
gate during the attach. A settled cache is permanent, because there is no detach. So `Gate`
|
||||
needs at most three acquisitions, with no loop and no counter: the guess's gate, the store's
|
||||
gate, and then the settled cache's gate, which cannot miss. This deletes a fact rather than
|
||||
reconciling two. During the attach the store *is* attached, so every entry point, cacheless
|
||||
or held, and every in-body read sees one answer. A journal's call is then refused by the rule
|
||||
journals already have (AddJournal: "any hold on this cache while a journal runs throws"),
|
||||
exactly as it is after the attach. Prior art: `ConcurrentHashMap.computeIfAbsent` installs a
|
||||
ReservationNode in the bin *before* running the mapping function, so a recursive update on
|
||||
the same thread finds the owner and throws, and on failure the reservation is removed (see
|
||||
`docs/research/2026-10-23-store-lock-user-code-prior-art.md` item 4 on main: the re-check
|
||||
after locking, and the ReservationNode on the empty-bin path). The validate-after-acquire in
|
||||
`Gate` is the same item.
|
||||
- (b) Keep the late publish and make the attach's hold the one fact for every entry point.
|
||||
Rejected. `InStoreHold` knows that this thread holds the store's gate, not *whose* hold it is.
|
||||
Refusing every same-store re-entry kills `UserCodeInACachelessCallMayCallItsOwnStore` (r2's
|
||||
R5), so (b) needs an "attaching" marker beside `Cache`. That is the second fact that s3 and
|
||||
s4 rule out, plus every in-body `Cache == null` read would need to consult it as well.
|
||||
- (c) Run no user code on the attaching thread under the store's hold during a pull: defer the
|
||||
pull's journals to after the hold (open calls). Rejected as insufficient on its own. Getters
|
||||
and constructors still run during the pull's decode, so the window stays open to any user
|
||||
code that is not a journal. It also moves journal timing, which is a documented contract
|
||||
("right after an admission is applied").
|
||||
- Scar check: `git log -S` over CultCache.cs finds `store.Cache = null` and `MaxStaleGuesses`
|
||||
only in ba658965 and 1468a83a. The early publish was not removed for a correctness reason of
|
||||
its own: 1468a83a removed it to bound `Gate` and to lend the codec, and the codec loan is gone
|
||||
(`pulled-variant-codec`). No settled read under the store's gate was built before. The
|
||||
precedent recipe (no root filter) finds no same decision.
|
||||
- Not a product fork. A journal's store call during an attach now throws, as it already does
|
||||
after the attach. Cacheless writes on the attaching thread stay refused, with the same text.
|
||||
Getters and constructors during the pull see an attached store, as they do during any held
|
||||
call. It changes the late-publish clause of Defaulted ruling `variants:ruling:pulled-variant-codec`,
|
||||
so Self defaults a superseding ruling (recommended label `cache-published-for-the-pull`)
|
||||
before dispatch.
|
||||
- Residual, not in r3. `Push`, `Delete`, `PushAll` and `CommitBatch` are public, so user code
|
||||
that is not a journal (another thread, or a getter during any held call) can stage records
|
||||
behind an attached store's cache. Soul s4 shows this with `other-thread-push`: file `a,b`,
|
||||
cache `a`. That is true before, during and after an attach, so it is not the window's
|
||||
two-fact problem. Closing it changes public API (a store's writes taken only from its
|
||||
cache), so it is a follow-up and an operator question for Self, not part of this cut.
|
||||
|
||||
## Body facts: claimant-one-owner r2 (Imagination, 2026-10-27, base fc74cf6f)
|
||||
|
||||
Why a revision: Soul beat the same rule three times (s1 claimant choice, s2 compatible ids, s3 shape). Each time a
|
||||
cacheless write lent a registry let the registry's entry for the written schema id decide ownership beside the entry
|
||||
the caller passed. Two descriptions of one type reached `Owns`; each fix (h2 schema-id compare, h3 subset of compatible
|
||||
ids) compared one more facet of the two and left the rest open. r2 removes the second description from the lookup.
|
||||
|
||||
### Where the two descriptions met (source read, fc74cf6f)
|
||||
|
||||
- `OwnsOnDisk` cacheless arm (`CultCache.cs:4725-4740`): with a lent registry it called
|
||||
`lent.Owns(id, catalog, declared, written)`, whose lookup is the registry's `_indexes` and whose `entryOf` is
|
||||
`descriptor.ToCatalogEntry()` (`:824-825`). The claimant for the written schema id was the registry's descriptor,
|
||||
so `ReadsWhole(persisted, claimantEntry)` (`:848`) judged shape against the registry's entry, and the claimant
|
||||
precedence (`:756-806`) walked the registry's compatible ids. `written` (`:4734-4736`) then compared the two entries
|
||||
by schema id plus a subset of compatible ids.
|
||||
- `WriteDocument` (`:4495-4524`) takes its entry from the registry (`resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`),
|
||||
so its two descriptions are always identical. Only `WriteRecord(record, catalogEntry, holdsIds, registry)` (`:4471`) can
|
||||
pass an entry that disagrees with the registry's entry for the same id.
|
||||
- `CopiedHoldIds` (`:4600-4630`) also reads `AttachedRegistry`, to decide the v3 header from copied records. It is a
|
||||
positive-only header decision, not ownership, and out of this cut.
|
||||
|
||||
### Consumer audit (2026-10-27)
|
||||
|
||||
- `WriteRecord` and `WriteDocument` are new in the Unreleased 3.0.0 entry; `main` has neither (`git show main:src/GameCult.Caching/CultCache.cs`).
|
||||
- Per-repo `rg` over every git repo directly under `F:\Projects` for `.WriteRecord(`, `.WriteDocument(`, `WriteSingleFileDocument`
|
||||
(Library, obj, bin, PackageCache excluded): hits only in CultLib and in vendored CultLib copies under Heimdall and StreamPixels
|
||||
(`vendor\CultLib`, older pins without these methods). No consumer outside CultLib calls either method.
|
||||
- Inside CultLib at fc74cf6f: Mesh `WriteSingleFileDocument` calls `WriteDocument(key, document, storedAt, registry)`;
|
||||
Mesh's raw-payload path calls `WriteRecord(..., registry: null)` (`CultMeshSingleFileDocuments.cs:129, 226`). The only
|
||||
`WriteRecord` calls with a registry are tests: `CachelessSingleFileTests.cs:453, 660` (a registry `{IdDeck}` that does
|
||||
not hold the written entry `One` at all, used for the copied-record header), `RestampOnWriteTests.cs:595, 599`, and
|
||||
Soul's probes. Packages: no TS, Python or Rust counterpart takes a registry (cultnet-rs `write_record` is an example helper).
|
||||
- Consequence: the registry's documented role in `WriteRecord` is to describe the other records in the store. A shape
|
||||
that derives the written entry from the registry (or refuses an entry the registry does not hold) breaks
|
||||
`CachelessSingleFileTests.cs:660` and removes that role.
|
||||
|
||||
### Candidate shapes weighed
|
||||
|
||||
| Shape | One description? | Public API | Kills s2/s3 | Cost |
|
||||
|---|---|---|---|---|
|
||||
| A. Lent registry's entry decides; refuse a passed entry that disagrees | yes, after a door check | signature kept; new refusal | yes | an equality check over a redundant parameter; breaks the registry-describes-others use (`:660`) when the registry lacks the written type unless it falls back to the passed entry, which is two descriptions again |
|
||||
| B. Passed entry is the only description of its schema id; the registry supplies other types only | yes, by construction | signature kept; doc only | yes (probed) | one lookup class gains the registry; one registry overload deleted |
|
||||
| C. Overloads: `WriteRecord(record, catalogEntry, holdsIds)` and `WriteRecord(record, registry, holdsIds)` | yes, by type | changes a 3.0.0 signature | yes | loses the registry-describes-others use |
|
||||
| D. Delete `WriteRecord`'s registry parameter | yes | changes a 3.0.0 signature | yes | loses the copied-record header decision for raw writes |
|
||||
|
||||
Picked B. It is the only shape that keeps every documented use of the registry, needs no public signature change,
|
||||
and deletes the comparison instead of extending it. It is ordinary scoping (an inner binding shadows the outer one
|
||||
for its name), applied to schema ids.
|
||||
|
||||
### Probes (Starfire, local filtered runs, 2026-10-27)
|
||||
|
||||
Scratch clone of fc74cf6f at `<scratchpad>/imagination-claimant-r2/clone`, Soul's `SoulClaimantProbe.cs`,
|
||||
`SoulClaimant2Probe.cs`, `SoulClaimant3Probe.cs` copied into `tests/GameCult.Caching.Tests`.
|
||||
|
||||
Base fc74cf6f (`probe-base.log`):
|
||||
- P7 (registry {N lists own, A, B}; passed entry N with the list varied; record k under O, named like N): lent R / no
|
||||
registry C for rows b, b2, e, g, h, k, l, l2; lent C for a, c, d, f, i, j. Confirms `subset-refuses-own-drift`.
|
||||
- P9 (registry {NW: Name, Value, Colour}; passed NW minus Colour): lent C (restamped, Colour lost), no registry R.
|
||||
Confirms `lent-shape-widens-passed-entry`.
|
||||
- P6 (bare X1 entry, registry {X1} listing OX, record under OX): lent R, no registry R, cache C. h3 closed the id axis.
|
||||
- P4: 12 states x 8 registries, violations=0. P1d: a cache {N,L} commits N over L's listed old id (`cache-replaces-other-types-record`).
|
||||
|
||||
Prototype B (scratch commit 349ea3f3 on the clone, not for landing; `probe-protoB2.log`): `CatalogEntryLookup` takes
|
||||
the written entries and an optional registry and drops every registry descriptor whose schema id a written entry has;
|
||||
the registry's `Owns(schemaId, catalog, declared, mayClaim)` overload is deleted; the cacheless arm always asks
|
||||
`CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), entry => entry,
|
||||
claimant => writes has claimant's schema id)`.
|
||||
- P7: lent == no registry on all 14 rows (all C).
|
||||
- P9: lent R, no registry R, wide entry C.
|
||||
- P6: unchanged (R, R, C). P4: identical table to base, violations=0. P1a-f, P2a-d: identical to base.
|
||||
- `RestampOnWriteTests`, `CachelessSingleFileTests` and the probes: 139/139 passed locally.
|
||||
- Yggdrasil, 349ea3f3, `ygg-protoB-full.log`: GameCult.Caching.Tests 1180 passed / 0 failed / 2 skipped;
|
||||
GameCult.Mesh.Tests 342 / 0 / 4.
|
||||
|
||||
Negative greps, each tested at fc74cf6f (each matches there, and none may match after the cut, src and tests):
|
||||
`claimant.CompatibleSchemaIds.All(` (1, the subset), `lent.Owns(` (1), `Func<CultSchemaCatalogEntry, bool> mayClaim) =>`
|
||||
(1, the deleted overload), `CatalogEntryLookup(writes)` (3, one-argument construction). CHANGELOG and docs:
|
||||
`never widens what the passed entry declares` (1), `with no compatible id the passed entry does not list` (1).
|
||||
|
||||
### Not settled by r2
|
||||
|
||||
- `cache-replaces-other-types-record` (s2, PreExisting, cache arm): r2 touches only the cacheless lookup, so it does
|
||||
not settle whether a cache may replace another registered type's record at a shared key.
|
||||
- Whether a registry-less `WriteDocument` should consult `CultDocumentRegistry.Shared` for claimants at all (s1
|
||||
`shared-decides-registryless-write`, closed Fixed by documentation in h2). r2 keeps today's answer.
|
||||
- `variants:finding:cut-restamp-on-write.s2.cacheless-inverts-registry-claimant` is still in force although r1 was
|
||||
mapped to close it; Self should close or re-triage it after r2's Soul pass.
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
# CultCache Union Parity: Map
|
||||
|
||||
Campaign `cultlib-gaps`, follow-up `cultlib-gaps:follow_up:union-parity-and-unknown-arms` (from
|
||||
`thing2:follow_up:cultcache-union-parity`), under operator ruling `thing2:ruling:contract-decode-tolerance`
|
||||
("CultCache refuses unknown union arms in every runtime, the C# reference included"). This file holds body facts,
|
||||
the model page and rationale; the cut specs and the question are documents in the mind and cite this file by
|
||||
section and fact number.
|
||||
|
||||
Pinned base: `origin/main` at `aaf0d7f6` (2026-10-10). Imagination pass `imagination-cl-union-parity`, session
|
||||
`self-2026-10-10c-eureka`. Probes live under the session scratchpad, `imagination-cl-union-parity/` (`cs` payload
|
||||
probe, `cs2` store probe, `rs` payload probe, `py` store walkers, `il` decompiled MessagePack-CSharp). None of it is
|
||||
committed. Earlier evidence is not repeated here: Eve `docs/thing2-map.md` P12-P13 (the cross-runtime probe) and
|
||||
`docs/research/cultcache-union-decode-tolerance-prior-art.md` (prior art).
|
||||
|
||||
## Cut order
|
||||
|
||||
1. `union-refusal-cs`: the C# reference refuses an unknown union arm on read and a subtype the union does not name
|
||||
on write. Ruled; nothing blocks it.
|
||||
2. `union-golden`: the recursive-union golden and the arm refusal goldens in `tests/vectors/union-parity/`,
|
||||
written by the C# reference and held by its tests.
|
||||
3. `union-derive-rs` and `union-codec-ts`, in either order or together: cultcache-rs and cultcache-ts gain a
|
||||
union (and, in TS, a slot record) codec that writes the reference shape and decodes the golden byte-identically.
|
||||
`union-codec-py` follows the same golden for cultcache-py.
|
||||
4. `slot-tolerance` (C# reference), then `slot-readonly-parity` (Rust, TypeScript, Python): a record carrying a
|
||||
slot past its reader's declared slots, at any depth, is read-only (ruling
|
||||
`cultlib-gaps:ruling:extra-trailing-slots`; the refinement is settled by section S). Both change a slot rule the
|
||||
cuts in step 3 build, so they wait for 1-3 rather than reopening them.
|
||||
5. `union-parity-release`: changelogs and tags, Breaking where `docs/semver-policy.md` says so.
|
||||
|
||||
Thing2's `cut-contract-rs` waits on step 3 (Rust) only: its first step pins cultcache-rs at the commit that lands
|
||||
`union-derive-rs` and re-runs P12 or the `union-golden` vector both ways.
|
||||
|
||||
Why this order: the C# reference is the wire authority, so its refusal and its golden come first and the other
|
||||
runtimes are held to bytes C# wrote, as `document-variants-c1` was (`docs/runtime-parity-scope.md`). Arms are
|
||||
ruled and slots are not, so the slot rule is its own cut rather than a blocked step inside every runtime's cut.
|
||||
|
||||
## Model page (step 0b)
|
||||
|
||||
| Kind | What names it | Over time | Who decides |
|
||||
|---|---|---|---|
|
||||
| Union arm | An integer key, `[Union(k, typeof(Arm))]` on the abstract base (C#), assigned by the union's author. The wire carries `[k, armSlots]`; the arm's type name never travels. `nil` is a null reference, not an arm. | **Added:** a key never used before. **Retired:** the attribute is removed or commented out and the key is never reused (Aetheria `Behaviors.cs:160,173`; Aetheria-legacy `Behaviors.cs:37-38`). **Renamed type:** free, the key is the identity. **Unknown to a reader** (a newer writer's arm, or a retired arm in an old store): refused, naming the union and the key (ruling). | The union's author assigns keys. The C# reference defines the shape; every runtime keeps parity with it (doctrine, `runtime-parity-scope.md`). |
|
||||
| Arm subtype on write | The value's runtime type must be one the union names. | A subtype the union does not name is today written as `nil` (U4). After cut 1 it is refused. | The union's author, by naming the arm. |
|
||||
| Arm slots, record slots | `[Key(n)]` integers, base-class keys included (an arm is a flat array). Gaps are written `nil`. | **Added:** appended at the next free key. **Retired:** kept as a gap, never reused (Aetheria `ItemInstance.cs:35`, `EntitySerializer.cs` key 5). **Missing in an older store:** the reader's default (C# field initializer; Rust and Python need a declared default). **Extra past the reader's last key, at any depth:** the record is read and is read-only; every write or removal of it is refused, and it is persisted from its original bytes (ruling, S). | The type's author. The slot rule is CultCache's (`src/GameCult.Caching/Contracts/cultcache-schema-compatibility.md`). |
|
||||
| Serde-enum member (Rust) | serde's external tag: the variant **name** (`{"Name": fields}`, a unit variant the bare string). | Persisted already by Rust consumers (section C). They keep their bytes: the union derive is opt-in and serde enums are untouched. | The Rust consumer. Not a cross-runtime shape. |
|
||||
|
||||
## U. Mechanism facts (C# reference, Rust, TS)
|
||||
|
||||
- **U1. The miss branch is `Skip` then `null`, in both union formatters MessagePack-CSharp 3.1.7 can hand out.**
|
||||
`DynamicUnionResolver.BuildDeserialize` (decompiled from `MessagePack.dll` 3.1.7 net9.0 with ilspycmd): after
|
||||
`ReadArrayHeader() == 2` (else `InvalidOperationException "Invalid Union data was detected"`) and `ReadInt32`, a
|
||||
non-zero-start key set is mapped through `keyToJumpMap.TryGetValue`, a miss sets the index to `-1`, the `switch`
|
||||
default calls `reader.Skip()`, and the result local stays `null`. The source generator's `UnionTemplate`
|
||||
(`MessagePackAnalyzer` 3.1.7, `MessagePack.SourceGenerator.dll`) emits the same: `default: reader.Skip(); break;`
|
||||
with `result = null`. Its serializer writes `nil` when `typeToKeyAndJumpMap` has no entry for `value.GetType()`.
|
||||
- **U2. The C# reference decodes an unknown arm to null and does not re-encode to its bytes.** Probe `cs`
|
||||
(payload built by hand with `MessagePackWriter`, decoded with `CultDocumentMessagePackSerialization.OptionsFor`):
|
||||
unknown key 9 in a union field gives `Root=NULL`, in a list `Items=[TextNode,NULL]`; re-encode equal: False in
|
||||
both. The formatter the reference composite hands out for the union is `MessagePack.Formatters.NodeFormatter1`
|
||||
(DynamicUnionResolver's emitted type; the probe assembly carries no generated resolver).
|
||||
- **U3. Missing and extra trailing slots, C# payload level.** Same probe: a payload one slot short decodes with
|
||||
the field initializer (`Added=42`); a payload one slot long decodes with the extra slot skipped. Neither
|
||||
re-encodes to its bytes.
|
||||
- **U4. The reference writes an unnamed subtype as nil.** Same probe: `Root = new RogueNode()` (a subclass the
|
||||
union does not list) serializes to `94 a2 64 32 c0 90 2a`: the root slot is `nil`, and it reads back `Root=NULL`.
|
||||
Silent loss on the write side, the mirror of U2.
|
||||
- **U5. Refusal can live at the CultCache layer; no MessagePack-CSharp fork.** Same probe, `strict` options: a
|
||||
resolver placed ahead of `StandardResolver` returns, for any abstract or interface type with `[Union]`s, a
|
||||
formatter that peeks (`CreatePeekReader`) the `[key, ...]` header, throws
|
||||
`MessagePackSerializationException("union Node has no arm 9")` for a key the union does not list, refuses to
|
||||
serialize an unnamed subtype, and otherwise delegates to the formatter `StandardResolver` would have handed out.
|
||||
Baseline round-trips byte-identical; both unknown-arm cases and U4 are refused; slot behaviour is unchanged.
|
||||
Every C# decode path reaches the composite through `CultDocumentMessagePackSerialization.OptionsFor`
|
||||
(`CultCacheMessagePack.CreateCodec`, `DirectoryMessagePackBackingStore.cs:345`, `CultMesh.cs:2779,3493,3499`,
|
||||
`CultMeshPrimitives.cs:1158,1230,2357`), so one resolver covers the cache, the directory store and CultMesh.
|
||||
Consumer resolvers named by `[CultCacheFormatterResolver]` sit ahead of `CultDocumentResolver` today
|
||||
(`CultDocumentMessagePackSerialization.cs:58-62`); the guard goes first and delegates to the rest of the
|
||||
composite, so a consumer formatter still encodes the arm while the key check still runs.
|
||||
- **U6. A top-level extra slot is dropped on write-back.** Probe `cs2`: a C#-written store, mutated as a newer
|
||||
writer would (catalog member slot 2 added, payload slot 2 `"newer writer data"`), is pulled, the record edited
|
||||
and flushed. The read succeeds, and the written payload is `['n1', 'edited by the older build']`: the newer
|
||||
writer's slot is gone. (The schema id was left unchanged, so the migration report said `Kind=Exact`; with a
|
||||
changed id the documented drift path reports `IgnoredExtraSlots`, `CultCache.cs:840-924`, but writes back the
|
||||
same way.)
|
||||
- **U7. Rust at `aaf0d7f6`, before the union cuts.** Probe `rs` (`cultcache-rs` at `aaf0d7f6`, payloads built with `rmpv`, decoded with
|
||||
`rmp_serde::from_slice`): a serde enum's unknown variant is refused (`unknown variant 'Exploded'`); the C# arm
|
||||
shape `[0, [...]]` is refused (`expected variant identifier`); a non-nil value in a gap slot is skipped; a missing
|
||||
trailing slot is refused (`missing field 'added'`) unless the field is `#[cultcache(default)]`; an extra trailing
|
||||
slot is skipped (`cultcache-rs-derive/src/lib.rs`, the `IgnoredAny` loop after the slot loop). The derive
|
||||
accepts named structs only (`lib.rs:51-67`) and writes an empty member list into the catalog
|
||||
(`src/lib.rs:2706`).
|
||||
- **U8. TypeScript at `aaf0d7f6`, before the union cuts.** No typed codec: with no `formatter` a document encodes as a MessagePack **map** of the
|
||||
zod-parsed object and decodes raw (`cult-cache.ts:679-692`; `@msgpack/msgpack` `encode({id:'d',version:7})` is
|
||||
`82 a2 69 64 ...`, a fixmap, where the reference writes an array); positional slots exist only in hand-written
|
||||
formatters (`test/cult-cache.test.ts:1263-1310`). P13 measured that a hand codec matches whichever encoding its
|
||||
author chose.
|
||||
- **U9. Python at `aaf0d7f6`, before the union cuts.** `define_database_entry_type` (`packages/cultcache-py/src/cultcache_py/documents.py:185-276`)
|
||||
is a slot codec: gaps written `nil`, extra slots ignored, a missing slot defaulted or refused. No union codec.
|
||||
- **U10. The written contracts.** `cultcache-schema-compatibility.md:75-91` classifies "persisted slot is missing
|
||||
locally and can be ignored" as soft-migratable drift, reported as `IgnoredExtraSlots`;
|
||||
`cultcache-persistence-format.md:215-218` says a store reader "never skips a slot it does not understand" for
|
||||
record envelope slots, and every runtime refuses `extra-slot-full-payload.msgpack`
|
||||
(`tests/vectors/document-variants-c0`). `docs/persisted-record-encoding.md` says consumers verify canonicality
|
||||
by re-encoding and comparing bytes. The two contracts disagree about payload slots; the question below is
|
||||
about that disagreement.
|
||||
- **U11. Fixture home.** `tests/vectors/<name>/`: committed bytes every runtime's tests read
|
||||
(`document-variants-c0` by Python `generate.py`, `document-variants-c1` written by C#), consumed by
|
||||
`tests/GameCult.Caching.Tests/PreCut2StoreFormatTests.cs`, `packages/cultcache-rs/src/lib.rs`,
|
||||
`packages/cultcache-ts/test/cult-cache.test.ts` and `packages/cultcache-py/tests/test_cultcache.py`. The CultNet
|
||||
precedent for a reference-written vector is `CULTNET_WRITE_VECTORS=1` (`docs/cultnet-error-contract-cut.md:170`).
|
||||
|
||||
## S. Slot read-only probe (any depth)
|
||||
|
||||
Imagination pass `imagination-cl-slot-readonly` (session `self-2026-10-10c-eureka`), under ruling
|
||||
`cultlib-gaps:ruling:extra-trailing-slots` ("b, and probe the any-depth refinement"). Probes live under the session
|
||||
scratchpad, `imagination-cl-slot-readonly/` (`cs` C# synthetic, `aeth` C# on Aetheria's game database, `rs`, `py`,
|
||||
`ts`, `il` decompiled MessagePack-CSharp 3.1.7). None is committed. The ruling's test: every runtime's decoder
|
||||
carries a "slot skipped" mark to the record cheaply, on the one decode path, with no second pass and no per-type
|
||||
consumer code.
|
||||
|
||||
- **S1. Where C# skips, and why a resolver sees it.**
|
||||
`DynamicObjectTypeBuilder.BuildDeserializeInternalDeserializeLoopIntKey` emits the slot loop's `switch` default as
|
||||
a direct `MessagePackReader.Skip()` call; the source generator's `FormatterTemplate` writes
|
||||
`default: reader.Skip(); break;`. `MessagePackReader` is a struct, and neither path raises an event or reads an
|
||||
option, so the skip itself cannot be observed without a fork. Both fetch every member formatter from
|
||||
`options.Resolver` (the generated code's `formatterResolver = options.Resolver`), and the union formatters fetch arm
|
||||
formatters the same way (U1). A resolver ahead of the composite is therefore handed every nested object, list
|
||||
element and union arm, and can read each one's array header before delegating.
|
||||
- **S2. The mark arrives in C#.** Probe `cs`: a guard resolver returns, for each concrete int-keyed
|
||||
`[MessagePackObject]` type, a formatter that reads the array count (from `NextCode` for a fixarray, a peek for
|
||||
array16 and array32), compares it with the highest `[Key]` plus one (base keys included, MessagePack's member
|
||||
visibility rule), notes a skip into a `[ThreadStatic]` scope the decode's caller opened, and delegates to the
|
||||
formatter the rest of the composite hands out. An extra slot at depth 0 (record), depth 1 (nested object), depth 2
|
||||
(object in an object), in a union arm and in a list element each arrives as one mark naming the type and both
|
||||
counts; the clean payload gives none. The guard needs `CompositeResolver.Create` around it so formatters are
|
||||
cached (uncached, it was 40 times slower).
|
||||
- **S3. C# cost.** Aetheria `GameData/Aetheria.cc` (12.1 MB; 237 of 238 records decodable with Aetheria.Shared's
|
||||
types; decoded with `OptionsFor` that assembly; best of 9, three rounds): plain 165-209 ms, a second plain composite
|
||||
(A/A) 174-193 ms, guarded 179-196 ms. That is 3.7-8.9% over the best plain run, inside the A/A spread. Worst case,
|
||||
synthetic tiny objects (120,000 records, 23.1 MB, about 24 objects each): plain 618-637 ms with A/A within 4%,
|
||||
guarded 704-705 ms: 11-19%, about 30 ns per decoded object.
|
||||
- **S4. A live case in Aetheria.** The same probe, run with the AetherDb release binaries (built 2026-09-30) against
|
||||
today's `Aetheria.cc`, marks 25 records: `WeaponItemData` whose `BehaviorData` arms (`AutoWeaponData`,
|
||||
`ChargedWeaponData` and others) carry 34 slots where that build declares 21 or 33. The extra slot is
|
||||
`WeaponData.Tracking` (key 33, Aetheria `8cd71ed6`, 2026-10-07): inside a union arm, inside a list. Today that tool
|
||||
reads these records and drops `Tracking` from all 25 at its next landing (S5). Refusing nested extra slots would
|
||||
stop it opening `Aetheria.cc` at all; read-only lets it open the store and refuses writes to those 25.
|
||||
- **S5. A C# flush re-encodes every held record, not only the edited one.** `PushAll` and an unconditional
|
||||
`CommitBatch` write `ToPersistedRecord(entry, SerializePayload)` for every entry (`CultCache.cs:3191-3201`, `3355`,
|
||||
`3384`), under the local descriptor's schema id and catalog entry. So U6's loss reaches every held record with
|
||||
unread slots at the next flush of anything. Rust (holds payload bytes and decodes on `get`, `lib.rs:2280-2293`),
|
||||
Python (holds envelopes and decodes at load, `cache.py:257-285`) and TypeScript (holds payload bytes) carry an
|
||||
untouched record's bytes. In C#, read-only therefore also means persisted from the original record (bytes, schema
|
||||
id, catalog entry), never from the decoded document.
|
||||
- **S6. Rust.** Probe `rs`: the derive's slot loop, with the trailing `IgnoredAny` loop counting into a
|
||||
`thread_local`, marks depth 1, depth 2 and a list element. Depth 0 is the `DatabaseEntry` derive's own trailing
|
||||
loop (`cultcache-rs-derive/src/lib.rs:258`), the same edit. A plain serde struct nested in a record already refuses
|
||||
an extra element (`array had incorrect length, expected 2`), so only derived slot types skip. Cost (120,000
|
||||
records, 21.4 MB): inside the A/A spread (A/A from -3% to +9%; marked against plain from -14% to +13% over six
|
||||
runs). The added code is one branch per object, never taken on a clean payload. Rust decodes on `get`, so whether
|
||||
a held record is writable is answered by decoding its held bytes through the same derive, on the write path only.
|
||||
- **S7. Python and TypeScript.** Probes `py` and `ts`: a slot codec tree passing a context argument (the shape
|
||||
`union-codec-py` and `union-codec-ts` build) marks all five positions. Python (40,000 records, 11.1 MB, msgpack
|
||||
unpack included): +14% against an A/A spread of 8%. TypeScript (120,000 decoded records, codec walk only; no
|
||||
msgpack in the scratch copy): +20% against an A/A spread of 18%. The check is one length comparison per object.
|
||||
- **S8. Kotlin has no library slot codec.** `CultDocumentCodec<T>.decode` is written per type by the consumer
|
||||
(`packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt:31-36`), and the package's own Eve
|
||||
decoders skip extras by hand at 8 sites (`repeat((count - N).coerceAtLeast(0)) { reader.skip() }` in
|
||||
`eve/EveDocuments.kt`). No slot rule, refusal or read-only, can be held by the library there until it owns slot
|
||||
decoding. That is a follow-up, not a per-runtime rule.
|
||||
- **S9. CultMesh decode paths** (`CultMesh.cs:2779,3493,3499`, `CultMeshPrimitives.cs:1158,1230,2357`) go through
|
||||
`OptionsFor`, so through the guard, with no scope open: unchanged. Whether a Mesh replica that re-publishes a
|
||||
decoded document drops slots is a follow-up.
|
||||
- **S10. The refinement is in.** Every runtime with a library slot decoder (C#, Rust, Python, and the TypeScript
|
||||
codec being built) carries the mark to the record on its one decode path, with no second pass and no consumer
|
||||
code. The cost on Aetheria's real store is inside the A/A noise (S3); the worst synthetic case is 11-19% in C#.
|
||||
A nested extra slot makes the record read-only; it is not refused.
|
||||
|
||||
## A. Aetheria (C#, the only consumer persisting `[Union]`s)
|
||||
|
||||
Aetheria consumes the reference through Unity packages pinned by tag (`Packages/manifest.json:55-57`:
|
||||
`caching-unity-v1.4.0`, `cultlib-unity-v1.0.60`), so a CultLib change reaches it only when it bumps the pin.
|
||||
Its only consumer resolver is `MathResolver` (`AssemblyInfo.cs:4`), which supplies no union formatter.
|
||||
|
||||
- **A1. Unions inside persisted documents** (Aetheria `aac2cf91`, `Assets/Scripts/ServerShared`):
|
||||
`BehaviorData` (`Behaviors/Behaviors.cs:147-191`, keys 0-11, 15, 16, 18, 20-24, 28, 31-40; 12 and 26 retired,
|
||||
14, 17, 19, 25, 27, 29, 30 commented out) in `List<BehaviorData> Behaviors` of the item data documents
|
||||
(`ItemData.cs:346,368`); `WeaponData` (`Behaviors/Weapon.cs:15-18`); `EntityPack` (`EntitySerializer.cs:172-173`)
|
||||
in `aetheria.savedzone` contents and its own `Children`; `ItemInstance` and `CraftedItemInstance`
|
||||
(`ItemInstance.cs:18-31`, a union whose arms are also a union) in entity cargo and docking bays
|
||||
(`EntitySerializer.cs:186-187`); `Provenance` (`Provenance.cs:96-98`) in `aetheria.provenanceledger`
|
||||
(`Provenance.cs:11-15`, `Lot.Origin`); `SavedActionBarBinding` (`SavedGame.cs:221-223`) in `aetheria.savedgame`;
|
||||
the input layout rows and keys (`InputLayout.cs:29-50`) in `aetheria.inputlayout`. `Brush`
|
||||
(`Environment.cs:73`) was not traced to a persisted document.
|
||||
- **A2. `GameData/Aetheria.cc` carries no unknown arm.** Walker `py/store_unions.py` over the git-tracked game
|
||||
database (12.7 MB, `cultcache.store.v1`, 238 records, 13 catalog entries): `Behaviors` arm keys
|
||||
{0:3, 1:2, 2:10, 3:5, 4:2, 5:2, 6:3, 8:7, 9:23, 11:2, 16:4, 23:7, 24:5, 31:1, 32:1, 34:1, 35:2, 37:4, 38:10, 39:2,
|
||||
40:1}; none retired or commented out; no `nil` element.
|
||||
- **A3. The run save carries no unknown arm, and does carry missing trailing slots.** Walker `py/run_unions.py`
|
||||
over `GameData/run.cc` (2026-09-30): `EntityPack` arms 0 and 1 (3 each), `ItemInstance` arm 2 (12),
|
||||
`Provenance` arm 0 (64), `SavedActionBarBinding` 9 `nil` (empty bar slots). The `EntityPack` arms carry 21 slots
|
||||
(orbital) and 20 (ship) against today's 22: the `Boss` slot (key 21, added 2026-10-07 in `db4617d8`) is missing,
|
||||
so today's Aetheria reads this save only because a missing trailing slot defaults. `py/gaps.py`: no non-nil
|
||||
retired slot (ItemInstance 2, 9, 10; EntityPack 5). `GameData/stale-2026-09-22/run.cc` is the same shape;
|
||||
`player.cc` holds only `aetheria.playersettings`. `py/slots.py`: every top-level record matches its own store's
|
||||
catalog exactly.
|
||||
- **A4. What changes for Aetheria.** After it adopts a CultLib with cut 1, its current stores load unchanged
|
||||
(A2, A3). A store written later by a build that has added an arm is refused by a build that lacks it, naming the
|
||||
union and key, where today the arm silently becomes `null` (a gear item quietly loses a behaviour, a lot loses
|
||||
its provenance) and the next save writes the loss back. Builds already pinned to 1.0.60 keep the null path; a
|
||||
shipped binary cannot change. Aetheria-legacy does not use CultCache and has no store.
|
||||
- **A5. Other C# stores.** `%LOCALAPPDATA%Low/GameCult/Aetheria/aetheria-unity.cc` (2026-08-08, 1.2 KB) is a
|
||||
`cultcache.store.v2.directory-indexed` header from an older layout, not a union carrier.
|
||||
|
||||
## C. Rust, TypeScript and other consumers
|
||||
|
||||
Eyes audit for this pass (session `self-2026-10-10c-eureka`), `git grep` at each repo's `origin/HEAD`, Rust reach
|
||||
by a lexical script (it can over-count; it does not under-count named fields). Aetheria and CultLib's tests excluded.
|
||||
|
||||
- **C1. Rust: no `DatabaseEntry` record crosses runtimes as a union, and none uses the reference arm shape.**
|
||||
Records that reach serde enums, each keeping its bytes because the derive is opt-in:
|
||||
Huginn `epiphany.pipeline.resolution.v2` (`ResolutionOutcome`, external tag, `eureka-pipeline/src/lib.rs:746`,
|
||||
live on Yggdrasil); Idunn `idunn.deployment_transaction` and `idunn.admitted_generation` (v4 and legacy forms;
|
||||
internally tagged `#[serde(tag = ..., deny_unknown_fields)]` enums in `control_plane.rs`, `deployment_plan.rs`,
|
||||
and two `#[serde(untagged)]` enums kept so older transactions decode, `drivers.rs:259,387`; live at
|
||||
`/var/lib/gamecult/idunn/control.cc`); CodexConnector `gamecult.codex.replay_record.v1` (`CodexReplayState`,
|
||||
`lib.rs:1017`); Epiphany's Atlas, model-request, persona and receipt records (internal, adjacent, external and
|
||||
untagged representations; `.cc` files under `F:\Projects\.epiphany-runtime\` and on Yggdrasil). Unit-only serde
|
||||
enums (a bare string) are widespread in Idunn, Huginn (`Faculty`, the pipeline's unit enums), CodexConnector and
|
||||
Epiphany. No enum reaches any record in Odin, Muninn, Ghostlight, Huginn `mind-body`, or cultnet-rs and
|
||||
cultmesh-rs production records. None of these is read by another runtime as a union today, so none is affected
|
||||
by the arm refusal; the derive changes nothing for them.
|
||||
- **C2. TypeScript: no consumer encodes a tagged union.** The only hand formatters are StreamPixels
|
||||
`apps/service/src/realtime/overlay-state-documents.ts:190,232,277` (positional arrays, no union). Bifrost,
|
||||
Heimdall, Hermodr, Odin scripts, Stonks, Vili, VoidBot (vendored `cultcache-ts`) and weksa have none.
|
||||
- **C3. C# outside Aetheria: StreamPixels' overlay declares unions; its tracked store holds none of them.**
|
||||
`apps/overlay-unity/Assets/Scripts/Assembly-CSharp/` (`8d40541`): `ActivationCondition` (10 arms),
|
||||
`BattleEffect` (2), `StatusEffect` (4), `ItemInstance` (3), `EquippableItemInstance` (2), reachable from
|
||||
`streampixels.status`, `streampixels.action_item`, `streampixels.skill` and gear documents. Walker
|
||||
`py/sp_catalog.py` over `apps/overlay-unity/Data/catalog.cc` (`cultcache.store.v1`, 20 records): only
|
||||
`outfit`, `skin_color`, `hair_style`, `emote` and `hair_color` records, and no `[int, array]` pair at any depth.
|
||||
Its only consumer resolver is `MathResolver` (`Catalog/CatalogAssembly.cs:6`). CultLib's own
|
||||
`GameCult.Networking` `Message` union (`Messages.cs:10`) is a CultNet wire message, decoded with
|
||||
`MessageSerialization.Options` (`MessageSerialization.cs:7`, `DefaultOptions`), outside `OptionsFor`: the arm
|
||||
guard does not reach it (follow-up).
|
||||
- **C4. Python and Kotlin: no union-shaped record.** repixelizer uses `define_database_entry_type` with flat
|
||||
slots; `cultmesh-kotlin` documents are `data class`es with no sealed type.
|
||||
|
||||
So the stores the arm refusal can affect today are Aetheria's (A2-A4, none carrying an unknown arm) and
|
||||
StreamPixels' overlay catalog (C3, carrying no arm). Every other runtime's persisted enums are serde shapes that
|
||||
the opt-in derive leaves alone.
|
||||
|
||||
## D. Payload depth bound (body at `c028c511`)
|
||||
|
||||
One bound, 256 nested MessagePack arrays and maps, on read and on write (ruling
|
||||
`cultlib-gaps:ruling:payload-depth-limit-256`; prior art `docs/research/payload-depth-limit.md`). The C# reference
|
||||
landed in `ead5260c`, Python in `5618d8dd`, Rust in `c028c511`. TypeScript is not landed (spec
|
||||
`cultlib-gaps:cut_spec:cut-union-bounds-ts.r2`, blocked on question `cultlib-gaps:question:ts-payload-opaque-type`).
|
||||
|
||||
| Runtime | Owner of the decision (constant and walker) | Doors routed through it |
|
||||
|---|---|---|
|
||||
| C# | `CultPayloadDepth.Check` (`src/GameCult.Caching.MessagePack/CultPayloadDepth.cs`), limit `CultMessagePackSecurity.MaximumPayloadDepth`. MessagePack's own graph counter is set to the same value as a backstop only. | Write: `CultDocumentMessagePackSerialization.SerializeUntyped`. Read: `DeserializeUntyped`. The cache store (`SerializePayload`/`DeserializePayload`), the directory store, `CultMesh` and single-file documents all call those two; C# has no separate raw-envelope or prepared-batch door. |
|
||||
| Rust | `cultcache_rs::check_payload_depth` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-rs/src/lib.rs`). | Read: `decode_payload` (get, list, `validate_envelope`/`put_envelope`, `finish_prepared_entry`). Write: `put`/`prepare_entry`/`prepare_entry_named` reach it through `finish_prepared_entry`, which decodes what it just encoded. Raw envelope: `put_raw_envelope`, the door CultMesh network puts use (`cultmesh-rs` `lib.rs`, two call sites). Prepared batch: `put_prepared_batch` via `require_payload_depth`. cultnet-rs: every free-form or skippable wire payload decodes through `cultcache_rs::decode_payload` (message decode in `contracts.rs`, `provider_session`, `media_stream_wire`, `replication`, `service_identity`). |
|
||||
| Python | `_refuse_past_depth_limit` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-py/src/cultcache_py/documents.py`); a msgpack `StackError` on read maps to the same error. | `DocumentDefinition.encode_payload` (write) and `decode_payload` (read). `CultCache.put`, `put_envelope`, `put_envelopes` and load-time hydration go through them; there is no raw-envelope door. cultmesh-py round trips call the same pair. |
|
||||
|
||||
- **Doors deliberately not routed.** cultnet-rs `idunn_lifecycle_brake`, `provider_health_contracts` and
|
||||
`runtime_authority_contracts` decode records whose positional array length is checked exactly before decoding. A
|
||||
fixed shape has no undeclared slot to skip and no free-form content, so the derive's depth is its shape (commit
|
||||
`84bb1e28`). The five media records have no length check and are held by the test instead. C# `CultCacheMessagePack.CreateCodec`
|
||||
serialises member values and deserialises member types without the document guard, by design (codec comment); only
|
||||
the document type goes through the guard.
|
||||
- **Forbidden writers.** No second walker and no second constant: a runtime's decoders, stores and wire layers call
|
||||
the owner above, never re-count depth or restate 256. An added payload door routes through the owner or names
|
||||
itself in the exact-length list.
|
||||
- **Shared path.** `tests/vectors/union-parity/` (`bounds-outcomes.msgpack`, `depth-*.msgpack`; table in its
|
||||
`README.md`), written by the C# reference and held by `tests/GameCult.Caching.Tests/UnionParityVectorTests.cs` and
|
||||
`PayloadDepthTests.cs`, `packages/cultcache-rs/tests/union_parity.rs`, `packages/cultcache-py/tests/test_union_parity.py`.
|
||||
- **Cut line.** `packages/cultnet-rs/tests/payload_depth.rs` and the `service_identity`/`cultmesh-rs` tests pin the
|
||||
Rust wire doors. cultnet-py and cultmesh-py message and replication decoders call `msgpack.unpackb` directly and
|
||||
are outside the Python cut; they hold only msgpack's own stack limit.
|
||||
- **Verification layer.** Vector held by each runtime's own suite (limit 256 accepted, 257 refused, skipped content
|
||||
counted, non-recursive walk); Rust wire doors by the cultnet-rs test above.
|
||||
|
||||
## Rationale
|
||||
|
||||
- **Why a guard formatter, not a fork or a re-implementation.** U5 shows a guard that only checks the key and the
|
||||
subtype and delegates the encoding is enough. It keeps MessagePack-CSharp's union encoding as the one encoder,
|
||||
costs one small file, and covers every path that already goes through `OptionsFor`. Forking the generated
|
||||
template or vendoring `DynamicUnionResolver` would put a second encoder in CultLib.
|
||||
- **Why the Rust derive is opt-in.** Serde enums already persisted by Rust consumers keep their bytes
|
||||
(`docs/persisted-record-encoding.md`); a changed default would make every such store unreadable or
|
||||
non-canonical. A new derive with explicit arm keys is how a Rust type says "this crosses runtimes".
|
||||
- **Why arms use newtype variants over slot structs.** A C# arm is a subclass whose slots include the base class's
|
||||
keys. A Rust newtype variant over a struct with keyed slots is the same thing, and it needs the slot code the
|
||||
`DatabaseEntry` derive already has, factored so both derives share one slot generator.
|
||||
- **Why the slot rule is a separate, asked cut.** The ruling settles arms and asks for extra trailing slots to be
|
||||
"settled against the same re-encode rule". Two written contracts disagree (U10), and the current behaviour drops
|
||||
data silently (U6), so the choice is the operator's and it should change every runtime at once.
|
||||
- **The recommendation on extra slots was refusal; the ruling chose read-only, and S settles its depth.** An extra trailing slot is the slot-level twin of an unknown
|
||||
arm: data from a newer writer that the reader's types do not describe. The operator ruled refusal for arms
|
||||
knowing it costs an older reader the newer record; the same reasoning gives refusal for slots, and record
|
||||
envelope slots are already refused in every runtime (U10). The alternative with precedent is
|
||||
`variants:ruling:undeclared-read-only` (a record a type can read but does not declare loads read-only): it would
|
||||
let an older reader see a newer record without overwriting it, but only where a catalog shows the extra slot
|
||||
(top level), while most evolution here is nested (A3's `Boss` slot sits inside a union arm).
|
||||
- **Missing trailing slots stay tolerated.** The reader knows a missing slot and its default, so it carries no
|
||||
bytes it does not understand, and Aetheria's live run save depends on it (A3). A store whose guard compares
|
||||
re-encoded bytes (Idunn's) refuses such a record by its own stricter rule, which is that guard's purpose.
|
||||
|
|
@ -1,827 +0,0 @@
|
|||
# CultLib Gaps: Map
|
||||
|
||||
Campaign `cultlib-gaps` (target: `docs/cultlib-gaps-target.md`). This file holds body facts, probes and
|
||||
rationale for the cuts the mind's `cut_spec`s name; the specs cite it by section and fact number.
|
||||
|
||||
Pinned base: `origin/main` at `6bdc23c9` (2026-10-10). Imagination pass `imagination-cl-gaps-cultmath`, session
|
||||
`self-2026-10-10-ag`. Probes live under the session scratchpad, `imagination-cl-gaps-cultmath/` (`probe1` traced
|
||||
copy of the solver and the warm/cold sweep, `suite` a scratch copy of `packages/cultmath` with the candidate fix,
|
||||
`probe2` the sweep against that copy, `probe3` the overload probe). None of it is committed.
|
||||
|
||||
## Cut order
|
||||
|
||||
1. `bls-release-stop`: BoundedLeastSquares stops on a release only when the release is not real.
|
||||
2. `bls-input-field`: a validation helper names the invalid input (follow-up `bls-invalid-input-field`). Blocked on
|
||||
question `cultlib-gaps:question:bls-input-error-shape`.
|
||||
3. `cultmath-release-0-4-1`: release `org.gamecult.cultmath` with cuts 1 and 2. Version follows question
|
||||
`cultlib-gaps:question:bls-fix-release-lane`.
|
||||
4. `cultmath-rotation-algebra`: quaternion algebra, swing-twist and joint clamps (ruling
|
||||
`aetheria-release:ruling:solver-in-gamecult-animation`). Ships in the CultMath release that the first
|
||||
GameCult.Animation release pins; that release is mapped with GameCult.Animation.
|
||||
5. `py-msgpack-default` revision 4 (section P, "Revision 4"): the fix batch (h4) on `hands/py-msgpack-default`
|
||||
at `a59bbc7d`. `migrate_payloads` is replaced by `convert` (ruling `py-migration-shape-ruled`), and Soul s3's
|
||||
open codec Lows are fixed. Its merge to main waits on the Brokkr cut (follow-up `brokkr-msgpack-payloads-r4`).
|
||||
5a. Not mapped: the Python store takes the cross-process sidecar lock the C# and Rust stores take (follow-up
|
||||
`py-ts-store-cross-process-lock`). It is a store parity cut of its own; `convert` does not need it (P15).
|
||||
6. Not mapped: the C# leaf-vectors revision (`leaf-vectors-next`) gains the P2 map-key rows, so the Python
|
||||
key tests read C#-written bytes instead of literals.
|
||||
7. `decode-stack-safety` (section D): cultcache-rs's one decode door grows its stack at every container level
|
||||
(ruling `decode-stack-mechanism-ruled`). Independent of 5 and 6; thing2's contract-rs merge waits on it.
|
||||
8. `expect-persisted-schema` (section E): an observed entry keeps the schema id it was persisted under and
|
||||
conditional commit compares that (ruling `aetheria-release:ruling:expect-fix-in-cultcache`). C# only. The
|
||||
release that carries it to Aetheria follows question `cultlib-gaps:question:expect-fix-release-lane`; the
|
||||
Aetheria pin bump is an Aetheria follow-on, not this cut.
|
||||
9. `aot-record-ref-generator` (section A, AOT cut 1): a consumer with MessagePack's source generator on can declare
|
||||
`CultRecordRef<T>` members and dictionary keys. Base: the C2a branch head `a768b821`; Hands starts after C2a merges
|
||||
to main or rebases onto it. Serves `record-ref-breaks-msgpack-generator`, the first cut of
|
||||
`cs-aot-safe-registration` (operator 2026-10-27: "I'm not a fan of requiring reflection. ... AoT is a nice thing to
|
||||
support anyway.").
|
||||
10. Not mapped (AOT cut 2): the generator reaches consumers that reference only `GameCult.Caching.MessagePack` (today its
|
||||
analyzer is `PrivateAssets="all"`, so they run every document on Reflection.Emit).
|
||||
11. Not mapped (AOT cut 3): registration descriptors are generated instead of reflected (`BuildDescriptor`,
|
||||
`DiscoverMembers`, `CultDocumentResolver`, `CultMessagePackSecurity`), and C2a's element-id walk stops reflecting
|
||||
(A6), so a NativeAOT consumer round-trips a ref-keyed dictionary.
|
||||
|
||||
## A. AOT cut 1: CultRecordRef under MessagePack's source generator (cut `aot-record-ref-generator`)
|
||||
|
||||
Imagination pass `imagination-aot-cut1`, session `self-2026-10-27-eureka`, pinned to `origin/hands/variants-c2a` at
|
||||
`a768b821` (C2a's head; merge base with main `02cf04b5`). Starting evidence: `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`
|
||||
(main `eb6a8f2c`). Probes: the scratch console from that note, copied to the session scratchpad
|
||||
`imagination-aot-cut1/aot-probe/` and committed only as dangling commits on top of `a768b821` (`a0f06b2c`, `6d382961`,
|
||||
`dc6cf9dc`, `99cae039`, `07b89982`), run on Yggdrasil in `mcr.microsoft.com/dotnet/sdk:10.0` (MessagePack 3.1.7, .NET
|
||||
10.0.12). Logs `job1.log` to `job5.log` beside the probe. Variants: G = `CultRecordRef<T>` member only, H = ref-keyed
|
||||
`Dictionary<CultRecordRef<T>, float>` only, B = both. Every variant references `MessagePack` directly (generator on).
|
||||
|
||||
### Body facts
|
||||
|
||||
- A1. The generator does not discover `CultRecordRefFormatter<T>`. For a generic type it does not know, it collects
|
||||
the type's definition as an object and names the formatter it would generate, `<Type>Formatter<T>`, nested in
|
||||
`GeneratedMessagePackResolver`. For a type from another assembly it then generates nothing, so the resolver names
|
||||
`GeneratedMessagePackResolver.GameCult.Caching.CultRecordRefFormatter<T>`, which does not exist (CS0426). The match
|
||||
with our formatter's name is a coincidence. Evidence (job1): `[ExcludeFormatterFromSourceGeneratedResolver]` on the
|
||||
formatter, and an `internal` formatter, both still fail G, H and B with the same line. Upstream issue
|
||||
MessagePack-CSharp#2133 (open, no maintainer reply) reports the same failure for any open-generic type with a
|
||||
formatter in another package.
|
||||
- A2. Consumer MessagePack 3.1.12, the newest stable, fails G, H and B the same way (job1). Upgrading does not fix it.
|
||||
- A3. `[MessagePackFormatter(typeof(CultRecordRefFormatter<>))]` on the `CultRecordRef<T>` struct stops the
|
||||
generator naming a formatter for it (the mangled line is gone), but the generator then reports `MsgPack006` (error)
|
||||
in every consumer: it checks `typeof(X<>)`'s interfaces, which an unbound generic does not list. Adding the
|
||||
non-generic `IMessagePackFormatter` marker to the formatter does not help (job2 `ta2`). With `MsgPack006`
|
||||
suppressed by the consumer, G, H and B build and run (job2). Rejected: it needs the formatter moved into
|
||||
`GameCult.Caching`, which then needs the whole `MessagePack` package instead of `MessagePack.Annotations`, and every
|
||||
generator consumer still has to suppress an error.
|
||||
- A4. `[assembly: MessagePackAssumedFormattable(typeof(CultRecordRef<>))]` in the consumer assembly fixes G, H and B
|
||||
with no library change (job2 `base@ASSUMED`). The generator stops collecting `CultRecordRef<T>`; the generated
|
||||
document formatter asks the resolver, and `CultDocumentResolver` serves the ref. This is the attribute MessagePack
|
||||
documents for "a formatter added through a custom resolver at runtime". The same attribute as an MSBuild
|
||||
`AssemblyAttribute` item in the consumer project (`_Parameter1_IsLiteral`), which is what a package's
|
||||
`buildTransitive` targets injects, does the same (job3 MG, MH, MB). The attribute declared in the library assembly
|
||||
does not reach the consumer's generator (job4 LIBATTR: CS0426).
|
||||
- A5. Generated and dynamic formatters write the same bytes. RefDoc (B) payload is `93A172A17081A170CA3F000000` from
|
||||
the Reflection.Emit fallback with no generator (job3 BASE) and from the generated formatter (job3 MB). The document
|
||||
formatter is `GeneratedMessagePackResolver+Probe+RefDocFormatter` (`dynamic=False`); the ref formatter is the
|
||||
library's `CultRecordRefFormatter<PlainDoc>` through `CultDocumentResolver`.
|
||||
- A6. NativeAOT (`PublishAot`, probe assembly rooted, linux-x64): G round-trips (payload, upsert, flush, reopen) with
|
||||
payload `92A172A170`, identical to JIT (job4 G0). B fails, in C2a code, not in the generator path: upsert fails in
|
||||
`CultElementIds.MapEntries`, whose `MakeGenericMethod(key, value)` has no native code for a struct key (job4 B0). With
|
||||
that walk reading the map through non-generic `IDictionary` instead, upsert passes under AOT and JIT is unchanged
|
||||
(job5 B1), but the payload and flush still throw `TargetInvocationException` from a second reflective call on the
|
||||
serialize path. By source read the candidate is `ResolverClaim`'s `GetFormatterMethod.MakeGenericMethod(type)`
|
||||
(`CultDocumentMessagePackSerialization.cs:84`), not proven. Both are runtime reflection over element-id paths, which is
|
||||
AOT cut 3's ground, so cut 1 proves AOT on the member shape and leaves the key shape's AOT round trip to cut 3.
|
||||
- A7. `GameCult.Geometry` already runs the generator (it references `MessagePack` directly) and carries the per-member
|
||||
workaround on three members (`CultGeometryDocuments.cs:164,239,328`). Under A4 those attributes are redundant.
|
||||
|
||||
### Shape the cut lands, and why
|
||||
|
||||
`GameCult.Caching.MessagePack` ships `buildTransitive/GameCult.Caching.MessagePack.targets`, which adds the A4 assembly
|
||||
attribute to every project that consumes the package. NuGet consumers need no line of their own, which is what a
|
||||
consumer expects from a library whose type breaks the generator. Unity consumers and in-repo `ProjectReference`
|
||||
consumers do not get `buildTransitive`: Unity consumers write the one attribute line (README), and in-repo consumers
|
||||
import the targets file, so they exercise the file that ships. The attribute type comes from `MessagePack.Annotations`,
|
||||
which `GameCult.Caching` already brings, so the injection compiles in a consumer with or without the generator. Wire
|
||||
bytes do not change: no formatter changes, only which code path asks for it (A5).
|
||||
|
||||
Rejected: a type-level formatter attribute (A3); excluding or hiding the formatter (A1); upgrading MessagePack (A2);
|
||||
declaring the attribute in the library (A4); keeping per-member attributes, which cannot reach dictionary keys.
|
||||
|
||||
## B. BoundedLeastSquares stopping
|
||||
|
||||
Finding `aetheria-release:finding:cut-thrust-allocator-core.s1.solver-converged-early` (Soul, Medium): a cold start
|
||||
reports `Converged` at a non-optimal point, so Aetheria's thrust allocator (tolerance 1e-12) answers differently
|
||||
warm and cold, by up to .078 throttle.
|
||||
|
||||
### Body facts
|
||||
|
||||
- **B1. One runtime ships the solver.** `BoundedLeastSquares` exists only in
|
||||
`packages/cultmath/src/CultMath/BoundedLeastSquares.cs` (C#, netstandard2.1 and net8.0). The type doc says there is
|
||||
no HLSL mirror; grep over `packages/*` finds no Rust, TypeScript, Python or Kotlin sibling, and no other CultLib
|
||||
project calls it. Its consumer outside CultLib is Aetheria's `ThrustAllocator`.
|
||||
- **B2. The defect reproduces against CultMath at base, outside Aetheria.** `probe1` rebuilds Soul's allocator
|
||||
matrix (Soul's `ProbeAllocator2`, constants H=100, Wy=1000, W=.3, kappa=10, rho=.1, EMax=2) for hull
|
||||
`Synthetic(8, 7005)` and the 8th intent of `System.Random(5)`, (-0.8505, 0.6739, 0.5587): 15 rows, 12 columns.
|
||||
Real `BoundedLeastSquares.Solve` at tolerance 1e-12 from x = 0: `Converged`, 13 iterations, cost 36.0313737,
|
||||
x7 = 1. Re-solving from that answer: 1 iteration, cost 36.0312608, x7 = 0.9222145, which is also the warm answer.
|
||||
The same literals (dumped with `"R"` floats, `probe1 dump`) reproduce it at tolerance 1e-9 and 1e-12, and not at
|
||||
the default 1e-6, where the cold solve stops at iteration 10 inside the tolerance and the re-solve takes 0
|
||||
iterations.
|
||||
- **B2a. The W5 problem as literals** (`probe1 dump`, float `"R"` round trip; m = 15, n = 12, row-major; the
|
||||
regression fixture for `bls-release-stop`). Nonzero entries of A:
|
||||
- row 0: a[0,2] = 100f, a[0,8] = -100f, a[0,9] = 100f
|
||||
- row 1: 13.882518f, 20.86695f, 0, 14.8965845f, 12.38046f, 22.115599f, -13.706654f, 15.85789f, 0, 0, -100f, 100f
|
||||
- row 2: -187.72859f, 239.31168f, 158.87828f, -270.31165f, 235.38467f, -14.704258f, 121.61232f, 244.81305f, 0, 0,
|
||||
0, 0
|
||||
- rows 3-6: a[3+k, 8+k] = 0.3f (k = 0..3); rows 7-14: a[7+i, i] = 0.1f (i = 0..7)
|
||||
- b = {0, 67.385445f, 558.70917f, -3f, -3f, -3f, -3f, 0 x 8}; lo = 0; hi = 1f for columns 0-7, 2f for 8-11.
|
||||
At base, tolerance 1e-12 from x = 0: `Converged`, 13 iterations, x[7] = 1, cost 36.031373704487095; a re-solve
|
||||
from that answer: 1 iteration, x[7] = 0.92221445f, cost 36.031260771352414.
|
||||
- **B3. The stopping rule that fires is the progress rule, after a release** (`BoundedLeastSquares.cs:273-278`). A
|
||||
traced copy of the solver, bit-equal to the real one on this problem, shows the last two passes:
|
||||
- iteration 12: full step lands; column 2 sits at its lower bound with gradient -0.00205 (violation 2.05e-3,
|
||||
tolerance 1.5e-7); the solver releases it and records fMark = f.
|
||||
- iteration 13: the re-solve moves x2 from 0 to 2.05e-7. The cost falls by about 2.1e-10, and the stall band
|
||||
`1e-15 * fScale` is 2.11e-10, so `f >= fMark - band` holds and the solver reports `Converged`. Column 7 is then at
|
||||
its upper bound with gradient +0.00145, ten thousand times the tolerance; releasing it is worth 1.1e-4 of cost.
|
||||
- **B4. Why the progress rule cannot judge a release.** Near a minimiser, a cost difference resolves only about the
|
||||
square root of what a gradient comparison resolves: a real step of size dx against gradient g lowers the cost by
|
||||
about g*dx, while the cost itself is a sum of terms of size fScale. Here g*dx (2e-10) sat exactly at the cost's
|
||||
rounding band. The gradient comparison is far from its own rounding: for column 7, the bound on the double
|
||||
gradient's rounding error, `(n+1) * eps * (|A^T b|_7 + sum_k |(A^T A)_7k x_k|)` (Higham's inner-product bound,
|
||||
gamma_(n+1)), is 9.3e-10, and the violation is 1.56e6 times that.
|
||||
- **B5. What the progress rule is for** (commit 43035bc8, and the tests at `BoundedLeastSquaresTests.cs:774-942`).
|
||||
It stops two kinds of pass that can never reach the tolerance: a free-set re-solve whose residual is rounding on a
|
||||
near-singular free set, and a release whose violation is itself rounding (kktTol at or below the noise, including
|
||||
kktTol = 0 when the box point nearest the origin is stationary). Deleting it outright breaks both: with a noise
|
||||
gate on every test and no progress rule, 8 of 66 BLS tests fail (`wide`, `integer-tie`, `collinear`, `cond` spin to
|
||||
the cap; `NearSingularFreeSetsConfirmOnceAndStop` sees 0 confirmations). Exempting releases from the progress rule
|
||||
with no noise gate fails one: `StalledPassesConvergeInsteadOfSpinningToTheCap("plain")` seed 141, b = 0,
|
||||
kktTol = 0, cycles release 3 (violation 1.7e-18) / stationarity miss / block on 3 until the cap.
|
||||
- **B6. The candidate fix, measured.** In a scratch copy of the package (`suite`, diff `suite/clean.diff`, 44 lines):
|
||||
(a) a bound column is a release candidate only if its violation exceeds both kktTol and the rounding bound of B4;
|
||||
(b) the progress rule judges only passes that release nothing; a pass that releases sets fMark to +Inf, so a
|
||||
release is never measured against a cost recorded before it. Results:
|
||||
- all 66 tests in `BoundedLeastSquaresTests` pass, including `DefaultSolvesAreBitIdenticalToTheirGoldenBits` and
|
||||
every stall and noise-floor test; the same holds with the bound's factor at 1, 16 and 256 in place of n+1;
|
||||
- the W5 problem: cold `Converged` in 14 iterations at the warm answer (x7 = 0.92221, cost 36.03126077);
|
||||
- 400 synthetic hulls x 60 random intents (24,000 calls, warm and cold) at tolerance 1e-12: every call `Converged`,
|
||||
max iterations 45, warm and cold agree within 6e-8 on every call (the original: one call off by .078 in the first
|
||||
2,400). At the default tolerance warm and cold still differ, by more than .1 in 10,273 of 24,000 calls: that is
|
||||
the documented resolution of 1e-6 times the gradient scale, pinned by
|
||||
`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance` (spread >= 0.5), and not this cut's subject.
|
||||
- **B7. Released columns the factor drops.** With the fix, 266 releases in the suite land on a column that
|
||||
`SolveFreeSet` marks dependent (p = 0). The next pass is then a stationarity miss with no release, and the
|
||||
progress rule stops it, as before. No release in the suite or the 24,000 allocator calls produced a step that moves
|
||||
the released column outward. A Lawson-Hanson style rejection guard (re-bind the column, try the next candidate)
|
||||
was prototyped and is not needed by any measured case; it is not in the cut.
|
||||
|
||||
### Prior art, and which one the solver follows
|
||||
|
||||
- The solver is a primal active-set method on the normal equations: solve the free set, step to the first bound
|
||||
crossed, release the most violated bound only after a full step lands. That is the outer/inner loop of
|
||||
Lawson-Hanson NNLS (1974) generalised to two-sided bounds, which is the shape of Stark-Parker BVLS (1995) and of
|
||||
scipy's `lsq_linear(method='bvls')`. It differs in factorisation (Cholesky of `A^T A`, not QR of `A`) and in
|
||||
stopping.
|
||||
- Lawson-Hanson stop on the dual test only (netlib `NNLS`, label 350: `IF (WMAX .le. ZERO)`), and guard roundoff
|
||||
per candidate: a column whose new norm is negligible against rounding (`DIFF(UNORM+...)`) or whose new
|
||||
coefficient is not positive (`ZTEST .gt. ZERO`) is rejected with `W(J)=ZERO` and the next candidate tried. There is
|
||||
no cost-based stop.
|
||||
- scipy's BVLS (`scipy/optimize/_lsq/bvls.py`, main) measures KKT exactly as CultMath's status doc states
|
||||
(`g * on_bound` for bound variables, `|g|` for free ones, max over components) and has two stops: optimality below
|
||||
`tol` (status 1) and a relative cost change below `tol` (status 2), reported as different statuses.
|
||||
- CultMath reported its cost stop as `Converged`, which its status doc defines as "the KKT conditions hold". The fix
|
||||
follows Lawson-Hanson: a release is judged on the gradient against its rounding (the role of their `DIFF` test),
|
||||
and the cost stop is kept only where no gradient test can decide (a free-set residual at rounding level), which is
|
||||
where scipy's status 2 also lives.
|
||||
|
||||
### Rejected alternatives
|
||||
|
||||
- Remove the progress rule and stop on the gradient alone: B5, 8 failures.
|
||||
- Report the cost stop as a new status (scipy's status 2): the W5 answer would still be wrong, just labelled, and a
|
||||
new enum member is a breaking change for callers that switch on the status.
|
||||
- Measure progress from the step (the exact model decrease `t(1-t/2)|L^-1 g|^2`) instead of differencing the cost:
|
||||
exact, but a noise-level gradient still yields a tiny positive decrease, so it needs the same noise threshold, on a
|
||||
quantity further from the KKT test than the gradient itself.
|
||||
|
||||
## I. Naming the invalid input
|
||||
|
||||
Follow-up `cultlib-gaps:follow_up:bls-invalid-input-field` (from Soul finding
|
||||
`cut-bls-kkt-tolerance.s1.invalid-input-names-no-field`): every validation failure returns the same `InvalidInput`.
|
||||
|
||||
- **I1.** Validation today is three sites in `Solve`: tolerance (`:147-148`), bounds (`:149-151`), and finiteness of
|
||||
A and b folded into the `A^T b` build (`:153-161`), which cannot tell A from b.
|
||||
- **I2.** Precedent: LAPACK reports a bad argument as `INFO = -i`, naming the argument and never its value. .NET
|
||||
throws `ArgumentException` with a parameter name, which the solver's no-throw, zero-allocation contract rules out.
|
||||
- Recommended shape (question `bls-input-error-shape`, option `validate-helper`): a public
|
||||
`BoundedLeastSquares.Validate(m, n, a, b, lo, hi, kktRelativeTolerance)` returning a new enum
|
||||
`BoundedLeastSquaresInput { Valid, InvalidTolerance, InvalidBounds, NonFiniteMatrix, NonFiniteTarget }`, the first
|
||||
failure in that order. `Solve` calls it and returns `InvalidInput` for anything but `Valid`, so validation has
|
||||
one owner and the folded finiteness check in the `A^T b` loop is deleted. Additive: `InvalidInput` and every
|
||||
existing result stay.
|
||||
|
||||
## R. Release 0.4.1
|
||||
|
||||
- **R1.** `org.gamecult.cultmath` is at 0.4.0 (`package.json:4`, `CultMath.csproj:13`, tag
|
||||
`cultmath-unity-v0.4.0`). `scripts/check-changelog-semver.mjs:97-101`: in 0.y.z a minor bump is the breaking lane;
|
||||
additions and fixes take the patch lane (0.2.4 added `erf` as a patch).
|
||||
- **R2.** Cut 1 changes results only where the old solver stopped after a release whose violation was above both the
|
||||
caller's tolerance and the gradient's rounding, that is, where it reported `Converged` in breach of its status doc.
|
||||
The default path's golden bits are unchanged (B6). The 0.4.0 precedent (ruling `cultmath-0-4-0-breaking-r2`)
|
||||
declared the snoise tie fix Breaking because two valid functions gave different values; here the old value broke
|
||||
the documented contract. Question `bls-fix-release-lane` asks which lane; the recommendation is `fixed-patch`
|
||||
(0.4.1, `### Fixed`).
|
||||
- The release procedure is the 0.4.0 cut's (`cultlib-gaps:cut_spec:cut-cultmath-release.r1`): Windows-only DLL rebuild
|
||||
on Starfire, tests on Yggdrasil, public-surface diff, Unity import smoke, Soul before the tag.
|
||||
|
||||
## Q. Rotation algebra
|
||||
|
||||
Ruling `aetheria-release:ruling:solver-in-gamecult-animation`: CultMath gains quaternion algebra, swing-twist and
|
||||
joint clamps; the aim-chain solve lives in a new package GameCult.Animation. The interface the consumer needs is the
|
||||
Aetheria map's section "The rig's interface to GameCult.Animation and CultMath" (Aetheria `origin/master`
|
||||
`30d02719`, `docs/aetheria-release-map.md:7559-7601`).
|
||||
|
||||
### Body facts
|
||||
|
||||
- **Q1. What exists.** `quaternion` (`src/CultMath/quaternion.cs`, 95 lines): public float fields x, y, z, w;
|
||||
`identity`; `LookRotation(float3 forward, float3 up)`; implicit conversions to and from `float4`; scalar `==`.
|
||||
`math.normalize(quaternion)` (`math.cs:225-235`) returns identity below length 1e-20. No mul, rotate, conjugate,
|
||||
inverse, axis-angle, slerp, swing-twist or clamp exists. `math` is a partial class (`math.cs`,
|
||||
`math.Interval.cs`, `math.Affine.cs`, `math.Phacelle.cs`); `math.rotate(float2, float)` exists (`math.cs:240`).
|
||||
- **Q2. Serialization.** `GameCult.Caching.MessagePack` serialises `quaternion` as the float array [x, y, z, w]
|
||||
(`CultMathResolver.cs:170-171`) and as an xyzw object in JSON (`CultMathJson.cs:468`). No other runtime
|
||||
(`cultcache-rs`, `-ts`, `-py`) has a quaternion type. The cut adds static functions only: no field, layout or
|
||||
formatter changes, so no wire change and no cross-runtime fixture.
|
||||
- **Q3. Runtimes.** The algebra is C#-only, like `LookRotation`: shaders carry rotations as `float4` (the type's doc),
|
||||
the mirror tests map shader functions to C# and never the reverse (`HlslSourceCompatibilityTests.cs:114-117`), and
|
||||
the native core (`native/cultmath-core`) is a Voronoi tone helper. The Unity bridge already converts `quaternion`
|
||||
(`UnityConversions.cs:16,23`); it needs no change.
|
||||
- **Q4. An overload trap, probed.** `quaternion` converts implicitly from `float4`, and CultMath has no
|
||||
`mul(float4, float4)`. `probe3` declares `mul(quaternion, quaternion)` beside scalar and matrix `mul` overloads and
|
||||
calls it with two `float4`: it compiles and returns the quaternion product. HLSL's `mul(vector, vector)` is the
|
||||
inner product, and Unity.Mathematics 1.3.3 (the copy in Aetheria's PackageCache) defines `mul(float4 a, float4 b)`
|
||||
as such (`matrix.gen.cs:129`), so the same call there is a dot product. The cut therefore adds HLSL's vector
|
||||
`mul(float2|3|4, float2|3|4)` (dot) beside the quaternion `mul`, so a `float4` pair keeps its shader meaning.
|
||||
- **Q5. Unity.Mathematics as reference, not template.** Its signatures for `mul`, `rotate`, `conjugate`, `inverse`,
|
||||
`AxisAngle` and `slerp` (`quaternion.cs:99,493,502,630,651,678`) are the consumer's stated convention ("`mul(a, b)`
|
||||
applies `b` first"). CultMath's design rule is to state semantics in code and not copy Unity.Mathematics
|
||||
(`docs/design.md:465-469`), so the formulas are written from the definitions below.
|
||||
|
||||
### Semantics the cut lands
|
||||
|
||||
Quaternions are Hamilton, stored xyzw, unit for rotation; angles are radians, right-handed about the axis.
|
||||
|
||||
- `math.mul(quaternion a, quaternion b)`: the Hamilton product; `rotate(mul(a, b), v) == rotate(a, rotate(b, v))`.
|
||||
- `math.rotate(quaternion q, float3 v)`: `t = 2 cross(q.xyz, v); v + q.w t + cross(q.xyz, t)`; q assumed unit.
|
||||
- `math.conjugate(q)`: (-x, -y, -z, w). `math.inverse(q)`: `conjugate(q) / dot(q, q)`, no guard (zero in, non-finite
|
||||
out, as HLSL division).
|
||||
- `quaternion.AxisAngle(float3 unitAxis, float radians)`: (axis sin(r/2), cos(r/2)); the axis is not normalised.
|
||||
- `math.slerp(a, b, t)`: shortest arc (negate b when `dot(a, b) < 0`); normalised lerp when the quaternions are
|
||||
nearly parallel; t is not clamped.
|
||||
- `math.swing_twist(q, unitTwistAxis, out swing, out twist)`: twist is q's projection onto the axis, normalised
|
||||
(identity when that projection vanishes, a half-turn swing); `swing = mul(q, conjugate(twist))`, so
|
||||
`q == mul(swing, twist)` (twist applied first).
|
||||
- `math.clamp_twist(twist, unitAxis, min, max)`: angle `2 atan2(dot(twist.xyz, axis), twist.w)` wrapped to (-pi, pi];
|
||||
inside [min, max] the input is returned unchanged, otherwise `AxisAngle(axis, clamp(angle, min, max))`.
|
||||
- `math.clamp_hinge(q, unitAxis, min, max)`: `clamp_twist` of q's twist about the axis; the swing is discarded.
|
||||
- `math.clamp_cone(swing, unitAxis, maxAngle)`: the angle between `rotate(swing, axis)` and `axis`; inside the cone
|
||||
the input is returned unchanged, otherwise the rotation by `maxAngle` about `normalize(cross(axis,
|
||||
rotate(swing, axis)))` (at a half turn, where that cross product vanishes, about the swing's own axis).
|
||||
- Preconditions (`min <= max`, unit axes) are documented, not checked, as everywhere in `math`.
|
||||
|
||||
### What GameCult.Animation will need from this cut (not mapped here)
|
||||
|
||||
- From cut 4: `mul`, `rotate`, `AxisAngle` for `AimChain.Pose` and `EffectorDirection`; `conjugate` or `inverse` for
|
||||
frame changes; `swing_twist` and the clamps if its joints ever carry cone or twist limits (the presenter's per-axis
|
||||
boxes do not).
|
||||
- From cut 1: start-independent `BoundedLeastSquares` at a tight tolerance. `AimChainSolver.Solve` is warm-started
|
||||
from `current`; without cut 1 its answer would depend on the previous frame exactly as the allocator's did.
|
||||
- A CultMath release that carries cut 4, which `org.gamecult.animation`'s `package.json` pins. That release is
|
||||
mapped with GameCult.Animation, so one Soul-verified release pair ships both.
|
||||
|
||||
## P. cultcache-py payload format (py-msgpack-default, revisions 2 to 4)
|
||||
|
||||
Imagination pass `imagination-py-msgpack-r2`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
|
||||
at `7f2c7477`. Probes ran from the session scratchpad and were removed afterwards; their results are recorded here (`jobK` ran
|
||||
the C# reference on Yggdrasil at `7f2c7477`; `p1`-`p3` ran Python 3.13.15 with msgpack 1.2.2 on Starfire). Revision 1's facts
|
||||
are in `docs/research/py-msgpack-default/README.md`, and Soul's are in verdict `cut-py-msgpack-default.s1`.
|
||||
|
||||
### Body facts
|
||||
|
||||
P1. **What decides a payload's format at 7f2c7477.** A search of every `packb`, `unpackb`, `json.loads` and
|
||||
`json.dumps` call in cultcache-py, cultnet-py and cultmesh-py found five places:
|
||||
- (a) `PayloadCodec.encode`/`decode`, through `DocumentDefinition.encode_payload`/`decode_payload`.
|
||||
- (b) `_check_payload_tree` (documents.py:75), which holds the map-key rule. `MESSAGEPACK_PAYLOAD_CODEC.encode`
|
||||
does not apply that rule, so a direct call writes `{1: 'a'}` and its own `decode` refuses the bytes.
|
||||
- (c) `stores.py:217`, `_infer_schema_version_from_payload`. A v1 record whose schema id is missing from the
|
||||
catalog is read by unpacking its payload as MessagePack to find a schema stamp.
|
||||
- (d) `replication.py:186`, the same stamp reader copied into cultnet-py behind a lazy `import msgpack`.
|
||||
- (e) `stores.py:257`. A legacy envelope whose payload is an inline map or array is repacked as MessagePack.
|
||||
|
||||
The other JSON sites are wire messages, schema JSON, CLI output and the cultmesh ready file. None of them is a
|
||||
document payload.
|
||||
|
||||
P2. **The C# reference's object slot, map keys** (`jobK`, `NumericLeafVectorTests.AnySlot` under
|
||||
`CultDocumentMessagePackSerialization.OptionsFor`, MessagePack `UntrustedData` security):
|
||||
- Accepted: str, int at every width (negative fixint, u64 max, i64 min), float32, float64, bool, and timestamp
|
||||
ext. The reference rewrites each one at its own width, for example `9181ff01` becomes `9181d0ffcc01`.
|
||||
- Refused: nil (inner `ArgumentNullException`); bin, array and map keys (inner `TypeAccessException`, because
|
||||
UntrustedData has no collision-resistant comparer for them).
|
||||
- Refused as duplicates (inner `ArgumentException`): `{1:1,1:2}`, `{"a":1,"a":2}`, and `{01:1, cc01:2}`, which
|
||||
is the same value written at two widths.
|
||||
- Accepted as two keys: `{1:1, 1.0:2}` and `{1:1, true:2}`. The CLR types differ, so the keys are distinct.
|
||||
- An `object` value of u64 max or i64 min is accepted and rewritten byte-identical.
|
||||
|
||||
P3. **msgpack-python with `strict_map_key=False`** (`p2`, `p3`). Every key kind decodes: nil becomes None, bin
|
||||
becomes bytes, and a timestamp ext becomes `msgpack.Timestamp`. An array or map key raises TypeError
|
||||
(unhashable) without a hook. Duplicates are silently last-wins: `{1:1,1:2}`, `{"a":1,"a":2}`, `{01, cc01}`,
|
||||
`{1, 1.0}` and `{1, true}` each decode to a single key. An `object_pairs_hook` sees every key kind, lists and
|
||||
dicts included. An exception it raises propagates with `__context__` None. On 100k small maps the hook costs
|
||||
2.2x the decode time (0.087 s becomes 0.200 s). `packb` raises an untyped `OverflowError` for 2**64 and
|
||||
-(2**63)-1.
|
||||
|
||||
P4. **Hash flooding with non-str keys** (`p1`). This is msgpack-python's reason for `strict_map_key=True`.
|
||||
Within the 64-bit MessagePack integer range, at most 13 integers share one CPython hash. The full-hash collision
|
||||
attack on int keys therefore needs unbounded integers, and MessagePack cannot carry them. Keys crafted to share
|
||||
low hash bits (`k << 43`) took 0.44 s for 200k keys and 1.34 s for 400k. 200k str keys take 0.25 s. The cost is
|
||||
mildly superlinear, not quadratic. The reference defends itself with UntrustedData's collision-resistant
|
||||
comparers. Python's dict has no such comparer, and the measured bound makes one unnecessary.
|
||||
|
||||
P5. **Schema-stamp recovery.** The C# store deleted its stamp recovery in `a3cc3942` (2026-09-13), so a persisted
|
||||
schema id that is absent from the embedded catalog is refused ("Persisted schema ... is not present in the
|
||||
embedded catalog", `CultCache.cs:504-507`). The C# CultNet registry keeps payload-stamp routing for raw wire
|
||||
records (`CultNetDocumentRegistry.ResolveDescriptorForRawDocument`, `TryResolveDescriptorByPayloadSchema`).
|
||||
cultcache-py's store copy (P1c) is therefore a divergence from the reference. cultnet-py's copy (P1d) mirrors
|
||||
the reference. The TS store (`single-file-messagepack-backing-store.ts:336`) also keeps the store copy.
|
||||
|
||||
P6. **Legacy envelope arrays.** The C# reference does not read them. TS (`normalizePayload`, line 490) and Python
|
||||
both repack an inline payload with MessagePack `encode`. A legacy inline payload is a subtree of a MessagePack
|
||||
document, so it is MessagePack by construction. No JSON-payload writer ever produced one: the 0.3 JSON default
|
||||
wrote bytes, which land as bin.
|
||||
|
||||
P7. **Brokkr** (read-only, `f97d4dc`). The bridge imports cultcache-py from `F:/Projects/CultLib/packages`
|
||||
(`blender_target.py:19`) and defines 8 types with `define_document_type` (630-637). Merging to that checkout's
|
||||
main is therefore the switch itself: Soul s1 measured that the next `_open_node` pull refuses the whole Blender
|
||||
store. The daemon writes JSON into the Blender store and the Unity store with `put_json_document`
|
||||
(`main.rs:1424`, called at 790-1392 and in tests from 2710). It also has `put_messagepack_document` (1435) and
|
||||
reads JSON first, then MessagePack (`decode_payload`, 1447). The daemon is Rust and cannot call cultcache-py.
|
||||
|
||||
### Authority, and why
|
||||
|
||||
- **Payload format.** The definition's `PayloadCodec` owns its bytes: the encoding, and which maps and integers
|
||||
the format can carry. On read, `MESSAGEPACK_PAYLOAD_CODEC` refuses key kinds and duplicates through its
|
||||
`object_pairs_hook`; that is the only place they are visible, because a dict cannot show a collapsed duplicate
|
||||
afterwards. On write, its encode walks the tree first, so a direct `codec.encode` call cannot write bytes
|
||||
its own `decode` refuses (Soul's single path).
|
||||
- **What the definition owns.** `DocumentDefinition.encode_payload`/`decode_payload` own the cross-codec rules
|
||||
(depth 256) and the translation of codec faults into the typed refusals, naming the type. `PayloadCodec.map_keys`
|
||||
is deleted, and `_check_payload_tree` goes back to checking depth only.
|
||||
- **The key rule is P2's.** It is not `strict_map_key`'s. Duplicates are refused as the reference refuses them.
|
||||
- **Residual.** Keys Python holds as one but the reference holds as two (`1` and `1.0`, `1` and `true`) are
|
||||
refused, because a dict cannot hold both. The refusal is typed (`duplicate_map_key`) and pinned so a change is
|
||||
seen. Matching the reference here would need a decoded map type that is not a dict. No consumer is known to
|
||||
write such maps.
|
||||
- **Prior art for refusing duplicates** (from knowledge, not fetched this pass): CBOR's RFC 8949 section 5.6
|
||||
expects decoders to reject duplicate keys; I-JSON (RFC 7493) requires the same of JSON; MessagePack-CSharp's
|
||||
`UntrustedData` refuses duplicates and keys it cannot hash safely (P2). msgpack-python's `strict_map_key` exists
|
||||
for hash flooding, which P4 bounds.
|
||||
- **Integer range.** Beyond 64 bits is a format limit, so the MessagePack codec's write walk refuses it with the
|
||||
code `integer_out_of_range`. The value is never named, and neither is a map key on its path, because in a
|
||||
free-form tree a key is payload data. Entry slots keep their leaf refusals, which name the slot and the field.
|
||||
A bigint was never wire-parity data: MessagePack has no integer wider than 64 bits (P3), and the 0.3 JSON
|
||||
payloads that could hold one were unreadable by the reference (`py-default-payload-json`).
|
||||
- **The stamp readers.** P1c is deleted and its record is refused as the reference refuses it (P5). P1d stays as
|
||||
cultnet-py's raw-record router, mirroring C#. It reads a stamp and decides no format: the resolved definition's
|
||||
codec decodes the payload or refuses it. It loses its lazy import.
|
||||
- **Legacy envelopes.** P1e is demoted and stays (P6). It extracts the MessagePack bytes the inline subtree
|
||||
already is. A definition whose codec is not MessagePack refuses the record like any other foreign record.
|
||||
- **The default codec.** The `DocumentDefinition` field is its only author. `define_document_type`'s parameter
|
||||
defaults to None and forwards only a codec it was given.
|
||||
- **Refusal text.** The JSON remedy sentence is dropped from the read refusal: it was false for every entry
|
||||
definition. Each refusal carries a `code` (`not_one_value`, `map_key_kind`, `duplicate_map_key`,
|
||||
`integer_out_of_range`) and names the kind or the fault class. It is raised after the `except` block, so
|
||||
`__context__` holds no decoder exception and no payload.
|
||||
|
||||
### Migration: what CultLib owns
|
||||
|
||||
Revision 1's README procedure (read under JSON, then put each value) was per record and not atomic. Soul s1
|
||||
stranded a store with it. Revision 2 replaces the procedure with
|
||||
`migrate_payloads(store, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int` in cultcache-py:
|
||||
|
||||
1. It pulls every envelope from the store.
|
||||
2. For each record whose type is among `definitions`, it decodes under that definition with `source_codec` and
|
||||
encodes under the definition itself. That is the whole definition path, validation and every refusal included.
|
||||
3. Any refusal stops the migration before the store is touched. The error names the type, the record key and
|
||||
the refusal's code, never the value.
|
||||
4. Otherwise one `push_all` writes the store whole. `SingleFileMessagePackBackingStore` and
|
||||
`JsonLinesBackingStore` each do that as a temp file and a rename.
|
||||
5. Records of other types keep their bytes and `stored_at`. The return value is the count rewritten.
|
||||
|
||||
It never sniffs, and it cannot leave a store half-migrated. Revision 2 also claimed that a second run is refused;
|
||||
Soul s2 falsified that (P10), and a concurrent writer strands the store (P11). Revision 3 replaced this shape with a
|
||||
backup-and-swap (P12), which Soul s3 broke three more ways. Revision 4 deletes the in-place migration: CultLib owns
|
||||
only `convert`, which writes a new file and never writes its source (P14).
|
||||
|
||||
Rejected alternatives:
|
||||
- A sniffing reader that accepts both formats: P1's one-digit payloads are valid in both formats, and r1's map
|
||||
forbade readers that try one format and then another.
|
||||
- A consumer-run loop over `put`: that is the procedure that stranded the store.
|
||||
|
||||
### Brokkr landing contract (follow-up `brokkr-msgpack-payloads-r4`)
|
||||
|
||||
The merge of `hands/py-msgpack-default` to main waits on three things:
|
||||
1. Soul holds on the fix batch.
|
||||
2. A Brokkr branch, Soul-held, is ready in its own campaign. On it, the daemon writes MessagePack
|
||||
(`put_messagepack_document`) into both stores. The JSON-first read becomes a read of exactly one MessagePack
|
||||
value with a trailing-bytes check, and the bridge's 8 definitions are unchanged. The Blender store is converted
|
||||
by `cultcache_py.convert` over those 8 definitions into a new file beside it, because those payloads came from
|
||||
cultcache-py's old default (P14). The daemon-written JSON receipts in the Unity store are the daemon's own, and
|
||||
it rewrites them, also into a new file.
|
||||
3. An operator window: close Blender and stop sync, Self merges CultLib and Brokkr together, build the daemon,
|
||||
run both conversions, open each new file, swap it into place, then restart. The JSON originals are kept under
|
||||
another name for as long as the operator wants them.
|
||||
|
||||
### Revision 3
|
||||
|
||||
Imagination pass `imagination-py-msgpack-r3`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
|
||||
at `57f85acd`. Python probes ran on Starfire (Python 3.13, msgpack 1.2.2) over a `git archive` of the cultcache-py
|
||||
and cultnet-py sources at that head, and were removed afterwards. The C# facts are Soul s2's (`jobB`, Yggdrasil),
|
||||
not rerun here.
|
||||
|
||||
P8. **What still decides map keys at 57f85acd.** One search found three MessagePack readers that see document
|
||||
payload maps. (a) The codec's `_unpack_tree` applies the reference rule through `_checked_map`. (b) The store's
|
||||
whole-file `msgpack.unpackb(data, raw=False)` (`stores.py:98`) applies msgpack's `strict_map_key=True`. A legacy
|
||||
envelope file whose inline payload is `{1: 'a'}` is refused with "int is not allowed for map key". Read through
|
||||
`_unpack_tree` instead, the same file yields the payload `8101a161`, and a v1 snapshot's framing decodes
|
||||
identically either way. (c) cultnet-py's stamp router (`replication.py:192`) applies `strict_map_key=True`. Read
|
||||
through the codec, `['x.v1', {1: 'a'}]` and `{'schemaVersion': 'x.v1', 'v': {1: 'a'}}` both route to `x.v1` (C#
|
||||
routes both, Soul s2 STAMPPROBE). A nil-keyed payload gives `None`, as it does today and in C#. Every other
|
||||
`unpackb` in cultnet-py and cultmesh-py reads wire messages, cursor files or fixed-slot protocol records, whose
|
||||
maps are frames rather than payload data. Their bound is follow-up `py-wire-decoders-unbounded`. Unmeasured: C#
|
||||
reads a map stamp as `IReadOnlyDictionary<string, object>`, so a top-level non-str key may stop C# routing where
|
||||
Python routes. The resolved definition decides the record either way.
|
||||
|
||||
P9. **The two depth counters.** `_check_writable` (documents.py:184) counts every node, scalars included, to
|
||||
`_PACK_DEPTH_LIMIT = 511`. `_check_payload_tree` counts containers to 256. Commit `eef1cb15` moved the codec
|
||||
from 256 to 511 so that `test_union_parity.py:650` could build its 257-deep read fixture with
|
||||
`document.payload_codec.encode(past)`. The fixture needs bytes that are 257 deep, not a codec that writes them:
|
||||
`msgpack.packb(past, use_bin_type=True)` and `json.dumps(past).encode()` build them, and both writers nest to 511
|
||||
or further. One container walk, bounded at `MAX_PAYLOAD_DEPTH`, serves both the definition and the codec. It also
|
||||
ends a cycle, because a cycle reaches 257.
|
||||
|
||||
P10. **Rerun.** No byte in a CultCache store records a payload format, and no wire change is wanted for one. A
|
||||
migration that rewrites the live store in place therefore cannot tell its own output from its input when a
|
||||
payload is valid in both formats. `b'5'` is the JSON 5 or the MessagePack 53, and Soul s2 saw 53 become 5. No
|
||||
check on the payload content can separate the two, so the rerun guard has to come from outside the payload.
|
||||
|
||||
P11. **Concurrency in the store.** cultcache-py's file stores hold a per-object `threading.RLock` only, so every
|
||||
`push` is a read-modify-write that loses another process's write. The C# store opens a `.lock` sidecar with
|
||||
`FileShare.None` (`CultCache.cs:3489-3509`), and cultcache-rs takes an `fs2` lock on its sidecar (`lib.rs:853-859`).
|
||||
cultcache-ts has none (a search of `packages/cultcache-ts/src` finds no lock). The migration race is one case of a
|
||||
store that lacks the reference's lock.
|
||||
|
||||
P12. **Prior art and the shape chosen** (primary docs fetched 2026-10-12). SQLite `VACUUM INTO`: "the original
|
||||
database file is unchanged", and the target "must not previously exist, or else it must be an empty file". LMDB
|
||||
`mdb_env_copy`: the target directory "must already exist and be writable but must otherwise be empty". Realm is
|
||||
the counter-example to Self's hypothesis. It migrates in place on open, and a rerun is safe because the file
|
||||
records its schema version, which CultCache's payload format does not (P10). The chosen shape follows `VACUUM
|
||||
INTO`, with the destination role taken by a mandatory backup:
|
||||
|
||||
`migrate_payloads(store, definitions, *, backup, source_codec=JSON_PAYLOAD_CODEC) -> int`
|
||||
|
||||
1. Copy the live store's bytes to `backup` with exclusive create (`open(backup, "xb")`). If the backup exists,
|
||||
the call is refused before anything is read. Every rerun is refused this way, whatever the payloads hold.
|
||||
2. Read the records from the backup, the frozen copy, and rewrite the listed ones. Any refusal of any exception
|
||||
class raises `CultCacheError` naming the type, the record key and the code or class.
|
||||
3. Compare the live bytes with the copied bytes. A difference means a writer ran during the migration, and the
|
||||
call refuses and says to stop every writer.
|
||||
4. Replace the live file in one `_replace_all` (temp file and rename).
|
||||
5. On any failure after step 1, the backup is removed and the live store is unchanged. A count of zero also
|
||||
removes it.
|
||||
|
||||
Probe (`probe2`, this shape over the 57f85acd sources). The first run over `b'53'`, `b'5'` and an unlisted type
|
||||
gives 53 and 5, keeps the unlisted bytes, and keeps the JSON in the backup. A second identical call raises
|
||||
`FileExistsError` and leaves the live bytes unchanged. A push between the read and the swap (the s2 interleave)
|
||||
is refused, with the backup removed and the live store holding the writer's JSON record. A validate that raises
|
||||
`AssertionError` is named by class and the backup is removed.
|
||||
|
||||
What remains and is stated: the window between the compare and the rename, closed by the lock in `5a`. The
|
||||
precondition is to stop every writer, as it is for SQLite's offline tools. If someone deletes the backup and then
|
||||
reruns the migration on its own output, P10 recurs. The README says to delete the backup only after the
|
||||
migrated store has been opened, and never to rerun after that.
|
||||
|
||||
P13. **Float32 keys.** `Float32(0.1).value` is `0.1`, so a `Float32` key is not rounded until it is packed.
|
||||
Decoded, it is the float32-rounded value. Writing `{1, F32(1.0)}`, `{True, F32(1.0)}`, `{1.5, F32(1.5)}` or
|
||||
`{0, F32(-0.0)}` produces bytes that the codec's own decode refuses as `duplicate_map_key`. `{0.1, F32(0.1)}` and
|
||||
two NaN keys read back as two keys. The rule that matches decode exactly compares each key's decoded image (a
|
||||
`Float32` key's image is `struct.unpack('>f', struct.pack('>f', v))[0]`) and refuses a collision on write as
|
||||
`duplicate_map_key`. The reference reads all four collision maps as two keys (Soul s2 KEYPROBE). Python cannot,
|
||||
for the same reason it cannot read `{1, 1.0}`, which is the residual already recorded. Refusing on write keeps
|
||||
Python inside what it can read back, and it is the write half of that residual.
|
||||
|
||||
### Revision 4
|
||||
|
||||
Imagination pass `imagination-py-msgpack-r4`, session `self-2026-10-14-eureka`, against `hands/py-msgpack-default`
|
||||
at `a59bbc7d`, under ruling `py-migration-shape-ruled` (convert-to-new-file). Source reads only; nothing was run.
|
||||
|
||||
P14. **The shape ruled.** `convert(source, destination, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int`
|
||||
replaces `migrate_payloads`. It creates `destination` with exclusive create (`open(path, "xb")`) before it reads
|
||||
anything, so an existing file (the source itself, an earlier output, anything) is refused and left alone. It reads
|
||||
the source once through the source store's `pull_all`, rewrites the listed records in memory, writes the store
|
||||
bytes into the file it created, and fsyncs it. Any failure after the create removes that file. It never opens the
|
||||
source for writing. The destination is the same store kind as the source, and its bytes come from the same
|
||||
per-kind encoder `_replace_all` uses, so the two writers cannot drift. The caller stops writers, converts, opens
|
||||
the new file and swaps it in. This follows SQLite `VACUUM INTO` and LMDB `mdb_env_copy` (P12) without the backup
|
||||
role that r3 added to them.
|
||||
|
||||
P15. **What the shape makes impossible, and what stays.** The library no longer writes a live store during a
|
||||
format change. That leaves no code to fix for s2's `migrate-races-concurrent-writer` or s3's
|
||||
`inflight-writer-strands-migration`, `rerun-guard-is-per-backup-path` and `stopped-migration-has-no-recovery`,
|
||||
because the backup, the compare and the swap are deleted. The library does not lose a write made during
|
||||
`convert`. The write lands in the source, and only the caller's swap can drop it. The README makes stopping
|
||||
writers the caller's precondition, as SQLite's offline tools do. A killed run leaves the source intact and possibly
|
||||
a partial destination; recovery is to delete the destination and run again. A partial JSON Lines destination can
|
||||
parse as a smaller store, so the README says the destination is complete only once `convert` has returned.
|
||||
Converting a store that is already MessagePack can still produce wrong values in the destination (P10: `b'5'`),
|
||||
but the source is untouched. The cross-process store lock (follow-up `py-ts-store-cross-process-lock`) is still a
|
||||
store parity gap, but nothing in this cut waits on it.
|
||||
|
||||
P16. **The codec decodes only what it writes.** At `a59bbc7d`, `MESSAGEPACK_PAYLOAD_CODEC.decode` is `_unpack_tree`
|
||||
alone. It returns trees 257 or more deep, `ExtType` values, and timestamps outside .NET `DateTime` (Soul s3); the
|
||||
definition or the reference refuses every one of them. Revision 4 also runs the write check over the decoded tree
|
||||
(`_check_carried`, renamed from `_check_writable`). That gives one rule in both directions. The depth walk, the
|
||||
integer range and the key kinds apply as before. No `ExtType` node is carried (code `value_kind`, kind `ext`).
|
||||
A timestamp's seconds must lie within `-62135596800..253402300799` (code `timestamp_out_of_range`). That interval
|
||||
is `DateTime.MinValue` to `DateTime.MaxValue`. It is derived from MessagePack-CSharp's timestamp reader, which adds
|
||||
the seconds to the Unix epoch and then the nanoseconds as ticks. It was not measured at the seam here, and a
|
||||
C#-written seam row belongs to follow-up `leaf-vectors-next`. The store's whole-file read keeps `_unpack_tree`
|
||||
alone, because a store frame is not a payload. The `any` leaf keeps its own ext refusal, which is the slot's rule
|
||||
for a value handed to it directly.
|
||||
|
||||
P17. **NaN keys.** The reference holds a map's keys in a `Dictionary` keyed by CLR type and value, where a NaN
|
||||
equals a NaN of the same width. It therefore refuses two float64 NaN keys and accepts a float64 NaN beside a
|
||||
float32 NaN (Soul s3). Python reads both widths as one `float` and cannot tell them apart. Revision 4 gives
|
||||
`_key_image` one NaN image, used by `_checked_map` on read and by `_check_carried` on write. Any two NaN keys are
|
||||
refused both ways as `duplicate_map_key`. On the mixed-width pair this refuses something the reference accepts,
|
||||
the same kind of residual as `1` beside `1.0`. The other choice would read two float64 NaN keys that the
|
||||
reference refuses.
|
||||
|
||||
Revision 5 records this as a deliberate exception to ruling `leaf-acceptance-follows-reference`, because Python
|
||||
cannot tell the two widths apart (msgpack-python decodes both to one `float`) and no ruling lets the codec read a
|
||||
pair the reference refuses. Soul s4 probed the C# behaviour: `Dictionary<object,object>{float.NaN, double.NaN}`
|
||||
serialises as `82 ca ffc00000 01 cb fff8000000000000 02`, the reference reads it back as two keys, and cultcache-py
|
||||
refuses it on read as `duplicate_map_key` (base `a59bbc7d` accepted that read). Every same-width NaN pair, with
|
||||
equal or different payload bits and with +NaN beside -NaN, is refused by both runtimes. The exception lives where a
|
||||
consumer reads it: the cultcache-py README's Wire Parity section, pinned by a test vector (the bytes above).
|
||||
|
||||
## D. cultcache-rs decode stack safety (cut `decode-stack-safety`)
|
||||
|
||||
Pinned base: `origin/main` at `ae6ef5a8` (2026-10-14). Imagination pass `imagination-decode-stack`, session
|
||||
`self-2026-10-14-eureka`. Ruling `decode-stack-mechanism-ruled` (grow-stack); prior art
|
||||
`docs/research/decode-stack-safety.md`. Probes live under the session scratchpad, `imagination-decode-stack/`:
|
||||
`probe` decodes through a scratch copy of cultcache-rs at the base whose derive calls a hook per derived level
|
||||
(off, observe the stack used between levels, or `stacker::maybe_grow`), against Eve's `thing-contract` at
|
||||
`2467e1b`; `wasmprobe` is the wasm run. Yggdrasil rust image (rustc 1.95.0, rmp-serde 1.3.1, serde_stacker
|
||||
0.1.14, stacker 0.1.25, psm 0.1.32), jobs `job1` to `job5`. None of it is committed.
|
||||
|
||||
Shapes: `box` is a thing-contract Box chain (3 containers a level, the bound admits 83), `repeat` a Repeat chain
|
||||
(2 a level, admits 125), `fat` a 32-slot record of `[u64; 32]` with an `Option<Box<Self>>` slot (1 a level,
|
||||
admits 255, so the leaf arrays sit at depth 256), `json` a one-slot record holding a `serde_json::Value` nested
|
||||
255 arrays, `ignored` a one-slot record whose extra trailing slot nests 255 arrays (skipped as `IgnoredAny`).
|
||||
|
||||
### Body facts
|
||||
|
||||
D1. **Today's abort, reproduced.** `decode_payload` at the bound, first level that aborts (process abort, no
|
||||
exit code, every time):
|
||||
|
||||
| Profile, thread | box (83) | repeat (125) | fat (255) | json, 64 KiB | ignored, 32 KiB |
|
||||
|---|---|---|---|---|---|
|
||||
| release, 1 MiB | 44 | 56 | 13 | 115 | 24 |
|
||||
| release, 2 MiB | ok | 114 | 25 | | |
|
||||
| debug, 1 MiB | 11 | 14 | 5 | 15 | 8 |
|
||||
| debug, 2 MiB | 23 | 30 | 10 | | |
|
||||
|
||||
One level past the bound is refused typed (`PayloadTooDeep { depth: 257, limit: 256 }`) for every shape and
|
||||
every variant, before any recursion.
|
||||
|
||||
D2. **Recursion points.** Per container level a typed decode passes through: the derive's generated
|
||||
`Deserialize::deserialize` (`cultcache-rs-derive/src/lib.rs:294-330` for slots, `534-684` for unions), which
|
||||
calls `deserialize_tuple`; rmp-serde's `Deserializer::any_inner` (`decode.rs:530`), which calls
|
||||
`visitor.visit_seq`/`visit_map`/`visit_newtype_struct` under `depth_count!` (566, 583, 616); the derive's
|
||||
`visit_seq`, whose frame holds one `Option<T>` per slot; and `next_element`, which re-enters either a derived
|
||||
type, serde's own impls (`Vec`, `Option`, `Box`) or another crate's (`serde_json::Value`, `rmpv::Value`).
|
||||
rmp-serde 1.3.1 has no skip of its own: `IgnoredAny` goes through `any_inner` and a visitor like any value.
|
||||
`check_payload_depth` is iterative.
|
||||
|
||||
D3. **Where the grow must sit.** A `maybe_grow` in the derive's `deserialize` covers derived levels only: `json`
|
||||
on a 64 KiB debug thread still aborts at 15 with a 32 KiB red zone, and passes only when the first derived
|
||||
level happens to hand it a whole new segment. A `serde_stacker::Deserializer` over rmp-serde at the door checks
|
||||
at every `deserialize_*` call it forwards (`maybe_grow` at `de.rs:476, 489, 502, 511, 520`), so
|
||||
rmp-serde's recursion, serde's impls, foreign `Value` types and `IgnoredAny` skips all re-enter through a check:
|
||||
every shape passes in both profiles (D4). Consumers decode non-derived recursive types through the door today:
|
||||
cultnet-rs `replication.rs:525` (`serde_json::Value`) and `contracts.rs:607` (`rmpv::Value`).
|
||||
|
||||
D4. **The frame between checks, measured.** Stack used from one derived `deserialize` to the next nested one,
|
||||
at the bound: release box 13,168 B, repeat 12,192 B, fat 84,944 B; debug box 52,064 B, repeat 49,760 B, fat
|
||||
207,504 B. The door wrapper checks more often, so its gap is smaller: release, every shape passes at a red zone
|
||||
of 32 KiB on 128 KiB, 1 MiB and 2 MiB threads; debug, box and repeat pass at 32 KiB, fat fails at 128 KiB and
|
||||
passes at 144 KiB through 256 KiB (1 MiB thread). So the worst gap constructed is between 128 and 144 KiB,
|
||||
the debug `visit_seq` of the 32-slot record. serde_stacker's defaults (red zone 64 KiB, segment 2 MiB,
|
||||
`param.rs:10-11`) fail debug fat. The cut sets 256 KiB and 2 MiB: 1.8x the worst measured gap. A type whose
|
||||
single `visit_seq` frame exceeds the red zone can still overflow; that is documented, not prevented.
|
||||
|
||||
D5. **Same values, same errors.** Through the wrapper, box 83, repeat 125 and a 1,000-node surface decode equal
|
||||
to the plain decode. The wrapper forwards `is_human_readable` (`de.rs:313`) and returns rmp-serde's own error
|
||||
type, so `opaque_decode_error` and the derive's refusal texts are unchanged.
|
||||
|
||||
D6. **Cost.** No growth (release, repeat 125 on an 8 MiB thread): 322-339 us through the wrapper against 320 us
|
||||
plain. A shallow 27 KB, 1,000-node surface on 1 MiB: 1.57 ms plain, 1.61-1.80 ms wrapped (run-to-run noise is
|
||||
about that wide). When the decode does grow, it pays for faulting in fresh segment pages: repeat 125 on a 1 MiB
|
||||
thread 1.5-1.7 ms; fat 255 on 1 MiB 25 ms against 9.3 ms on a 64 MiB thread. A 1 MiB and a 2 MiB segment cost
|
||||
the same.
|
||||
|
||||
D7. **Windows x86_64: source read, not run.** stacker 0.1.25 does not use psm on Windows: `backends/windows.rs`
|
||||
grows by fibers (`ConvertThreadToFiber`, `CreateFiber`, `SwitchToFiber`, `SetThreadStackGuarantee`), reads the
|
||||
limit with `VirtualQuery`, and links a C helper built by `cc`. psm's README marks its own Windows stack switching
|
||||
disabled; that is the other layer, so the research's conflict (C) is two layers, not a contradiction. Yggdrasil
|
||||
has no Windows: the rust image carries `x86_64-pc-windows-gnu` (cross-compile, no run). Whether it actually grows
|
||||
on Starfire is question `decode-stack-windows-check`.
|
||||
|
||||
D8. **wasm32: stacker is not a no-op.** psm's `build.rs:53` ships a precompiled `wasm32.o` and sets
|
||||
`switchable_stack`; stacker then allocates segments from the global allocator with no guard page
|
||||
(`alloc_stack_restore_guard.rs`). `remaining_stack()` is `None` on wasm, so the first check always grows and
|
||||
the limit is known from then on. Under wasmtime 48.0.5 (wasm32-wasip1, release), a nested 9-slot record: plain
|
||||
decodes 48 levels and traps at 64 ("out of bounds memory access" at `0xffffe510`: the stack-first shadow stack
|
||||
wrapped below zero); wrapped decodes 64, 255, 400 and 600 levels (linear memory 1,088 KiB before, 8,896 KiB
|
||||
after 255) and traps at 1,000 with "call stack exhausted", the engine's own stack, which stacker cannot grow.
|
||||
`wasm32-unknown-unknown` builds. This corrects the ruling's premise that wasm keeps the abort: the shadow stack
|
||||
grows and the engine stack is the host's setting, so the documentation says that instead.
|
||||
|
||||
D9. **Dependency.** `serde_stacker` 0.1.14 (dtolnay), pulling `stacker` 0.1.25 and `psm` 0.1.32 (rust-lang),
|
||||
all `MIT OR Apache-2.0`; plus `cfg-if`, `libc`, `cc` (build) and `windows-sys` on Windows. psm assembles and
|
||||
stacker compiles C at build time, so a C toolchain becomes a build requirement (gcc in the rust image, MSVC on
|
||||
Starfire). cultcache-rs is MIT. Written by hand, the wrapper would be serde_stacker's ~650 lines of forwarding.
|
||||
|
||||
### Body facts, revision 2 (verdict `cut-decode-stack-safety.s1`)
|
||||
|
||||
Imagination pass `imagination-decode-stack-r2`, session `self-2026-10-14-eureka`, on branch
|
||||
`hands/decode-stack-safety` at `2505c0c4`. Probes live under the session scratchpad,
|
||||
`imagination-decode-stack-r2/` (`gen.py`, `gen2.py`, `driver*.rs.in`, jobs `jobP1` to `jobP4`), run on Yggdrasil
|
||||
in the rust image (rustc 1.95.0). None of it is committed. `jobP1` patches a copy of serde_stacker 0.1.14 so
|
||||
each of its five `maybe_grow` calls (`de.rs:476-520`, the wrapper visitor's container entries) records the
|
||||
stack pointer, and reads the door's red zone and segment from the environment. Shapes: `wide` is the committed
|
||||
`Wide32` (32 slots of `[u64; 32]`, 8 KiB inline); `bigN` is 32 slots of an N-array record (32·N KiB inline:
|
||||
32, 64, 128, 256 KiB), each with an `Option<Box<Self>>` slot; `lim` is 31 slots of a 2-array record (15,880 B).
|
||||
A caller is one `#[inline(never)]` function holding the `Result<T>`.
|
||||
|
||||
D10. **The gap between checks is linear in the record's inline size.** Stack used between two consecutive
|
||||
checks, the worst over a 6-level decode on a 1 GiB thread (every gap in a shape is the same value):
|
||||
|
||||
| Shape (inline) | wide (8 KiB) | big4 (32) | big8 (64) | big16 (128) | big32 (256) |
|
||||
|---|---|---|---|---|---|
|
||||
| debug gap | 115 KiB | 451 | 899 | 1,795 | 3,587 |
|
||||
| debug frames before the first check | 131 KiB | 515 | 1,027 | 2,051 | 4,099 |
|
||||
| release gap | 48 KiB | 192 | 384 | 768 | 1,536 |
|
||||
| release frames before the first check | 48 KiB | 192 | 384 | 768 | 1,536 |
|
||||
|
||||
So the gap is 14·S + 3 KiB in debug and 6·S in release, for S = `size_of` of the record entered: the frames
|
||||
between a container check and the next one each hold the record being built or a `Result` of it (rmp-serde's
|
||||
`any_inner`, the wrapper visitor, the derive's `visit_seq` with one `Option` per slot, the seed). D4's 128-144
|
||||
KiB debug gap for `fat` is this line at 8 KiB plus a margin. The stack touched below the deepest check is 6 KiB
|
||||
in debug and 0 in release, so the excursions between checks are the gaps above.
|
||||
|
||||
D11. **Why the thread size decided, in two ways.** (a) Before the first check the decode has already used the
|
||||
second row of D10: `rmp-serde`'s `deserialize_tuple`, `any_inner` and the wrapper's frames sit between
|
||||
`decode_payload` and the wrapper's first `maybe_grow`, and each holds `Result<T>`. A thread smaller than that
|
||||
row aborts whatever the red zone: `wide` aborts on every thread up to 144 KiB in debug. (b) Mid-decode, a check
|
||||
with between R and the gap remaining lets the next gap overrun; after the first grow the positions in each
|
||||
segment are fixed, so which check first grows (set by the thread size) decides, which is the non-monotonic
|
||||
pattern Soul measured. Thread sweeps at the bound (threads from 64 KiB in steps of a 64th of the span, plus
|
||||
1, 1.5, 2, 3, 4 and 64 MiB):
|
||||
|
||||
| Profile, shape | R = 256 KiB | R = gap - 48 KiB | R = gap rounded up + 64 KiB |
|
||||
|---|---|---|---|
|
||||
| debug wide | 6 of 65 sweep threads abort (64-144 KiB); none at 1-64 MiB | all 65; and at 1-4 MiB | 6 (64-144 KiB); none at 1-64 MiB |
|
||||
| debug big4 | 39 of 65; and at 1.5, 2, 3, 4, 64 MiB | 29; and at 2, 64 MiB | 24, the lowest from 64 KiB; none at 1-64 MiB |
|
||||
| release wide | none | 14, spread from 112 to 864 KiB; and at 1, 4 MiB | none |
|
||||
| release big4 | 9 (64-192 KiB) | 18, including 448-480 KiB | 9 (64-192 KiB) |
|
||||
| release big8 | 40; and at 1.5, 2, 3, 64 MiB | 20; and at 1.5 MiB | 17, the lowest from 64 KiB; none at 1-64 MiB |
|
||||
|
||||
With R at least the gap, the abort counts match the threads smaller than the frames before the first check
|
||||
(D10's second row plus the harness) in every shape and profile, big16 and big32 included (segments 4·R); with
|
||||
R below the gap, threads well above those frames abort mid-decode.
|
||||
|
||||
D12. **Checking at the top of the door** (`jobP4`). `decode_payload` patched so its whole body runs inside
|
||||
`stacker::maybe_grow(R, segment, ..)` before the walk and the wrapper, R = 512 KiB, segment 2 MiB. Every
|
||||
thread from 24 KiB to 1 MiB in 8 KiB steps, then 1.5, 2, 3, 4, 8 and 64 MiB:
|
||||
|
||||
| Profile | wide (8 KiB), 1 level | wide, 255 levels | lim (15,880 B), 1 level | lim, 254 levels |
|
||||
|---|---|---|---|---|
|
||||
| debug: smallest thread that decodes | 40 KiB | 40 KiB | 72 KiB | 72 KiB |
|
||||
| release: smallest thread that decodes | 24 KiB | 24 KiB | 40 KiB | 40 KiB |
|
||||
|
||||
No thread at or above the smallest one aborts, in any of the four columns, in either profile. lim's gap is
|
||||
220 KiB in debug and 93 KiB in release (D10's lines give 220 and 93), so R = 512 KiB is 2.3 times the debug
|
||||
gap of a record at 16 KiB. What a thread needs is now the caller's own frames, which hold the returned value
|
||||
and do not depend on the payload's depth.
|
||||
|
||||
D13. **No red zone or placement removes the bound.** Every frame that holds a value of type T sits between the
|
||||
check before T's container and the check inside it, and serde's own impls add frames that hold T before T's
|
||||
first check: `Box<T>`'s `Deserialize` holds the `Result<T>` it boxes, and the `Option` check before it sized
|
||||
nothing larger than a pointer. A per-type red zone (a forked wrapper sized by `size_of::<V::Value>()`, or a
|
||||
grow in the derive sized by `size_of::<Self>()`) still leaves that frame of about one to three T outside any
|
||||
sized check. So the stack a decode needs between checks is bounded only by bounding the inline size of the
|
||||
records it builds. The derive is the one place that sees every record type it produces: a `const` assertion on
|
||||
`size_of::<Self>()` refuses an oversized record at compile time. A field of a type the derive did not produce
|
||||
(a fixed array of arrays, a large tuple, a plain serde struct) behind a `Box` or `Vec` is not reached by it.
|
||||
|
||||
D14. **Build closure and wasm, measured by Soul (`soul-decode-stack-s1` jobD, `cargo tree -e normal,build`
|
||||
at `2505c0c4` against `ae6ef5a8`).** Added on `x86_64-unknown-linux-gnu`: serde_stacker 0.1.14, stacker 0.1.25,
|
||||
psm 0.1.32, cc 1.8.0, find-msvc-tools 0.1.14, shlex 2.0.1, object 0.39.1 (`Apache-2.0 OR MIT`), all
|
||||
`MIT OR Apache-2.0` except ar_archive_writer 0.5.3 (`Apache-2.0 WITH LLVM-exception`, build-only, via psm).
|
||||
On `x86_64-pc-windows-msvc` the same plus libc 0.2.190 and windows-sys 0.61.2 (`MIT OR Apache-2.0`, beside the
|
||||
crate's own windows-sys 0.59). `fs2 0.4` (8 call sites in `lib.rs`, the store's file locks) has no wasi
|
||||
backend, so cultcache-rs does not build for `wasm32-wasip1`; D8 measured serde_stacker in a separate crate.
|
||||
|
||||
### Authority, and why
|
||||
|
||||
The door owns stack safety as it owns the bound: `decode_payload` runs the bound walk, then rmp-serde through
|
||||
`serde_stacker::Deserializer` with two private constants. The ruling's "every container level the derive and the
|
||||
door descend through" is met there, because every derived level is entered through the wrapper (D3); a
|
||||
`maybe_grow` in the derive would be a second owner of the same check, needs a red zone of 128 KiB in release
|
||||
and 256 KiB in debug for fat where the wrapper needs 32 and 144 KiB (D4), and still misses foreign recursive types. The derive is unchanged.
|
||||
|
||||
Revision 2 (D10-D13): the door's first act is a check, so no frame holding T precedes it, and the red zone is
|
||||
set against the gap of the largest record the derive may produce. That size is a public constant the derive
|
||||
asserts at compile time, because nothing at decode time can bound a frame that holds a T before T's own check
|
||||
(D13). The thread's size then decides nothing beyond the caller's own frames, which hold the returned value
|
||||
whatever the payload's depth (D12). Envelope and store decodes enter through the same grown decoder, without
|
||||
the payload walk, so what they accept is unchanged. rmp-serde's 1024 counter stays a backstop that never decides. Callers that decode a payload with
|
||||
`rmp_serde::from_slice` directly bypass the door, the bound and the grow alike; cultnet-rs re-decodes
|
||||
door-checked `rmpv::Value`s that way (follow-up `rs-redecode-outside-door-r2`).
|
||||
|
||||
Rejected inside the ruling: the derive placement (above), and writing the wrapper by hand (D9: established
|
||||
tooling, one owner upstream).
|
||||
|
||||
## E. Expect under compatible schema drift (cut `expect-persisted-schema`)
|
||||
|
||||
Pinned base: `origin/main` at `b549d731` (2026-10-21). Imagination pass `imagination-expect`, session
|
||||
`self-2026-10-19-ag7`. Probe `probe1` (`ProbeExpectDrift.cs`, written into the clone by the job command, never
|
||||
committed) ran on Yggdrasil, dotnet sdk 10.0, against `b549d731`. Fixture: a record written under
|
||||
`DriftWide` (`tests.variant_drift.v1`) and read by a cache whose registry holds only `DriftNarrow` (v2) loads
|
||||
as CompatibleDrift.
|
||||
|
||||
### Body facts
|
||||
|
||||
- **E1. The defect.** `CultCommitRequest.ConditionsHold` (`CultCache.cs:1677-1696`) compares the durable
|
||||
record's `SchemaId` with the observed entry's `Descriptor.SchemaId` (`Expect` captures it at `:1772`;
|
||||
`ExpectUnchanged` reads it at `:1694`). `ToStoredDocument` (`:3212-3268`) resolves a persisted record to the
|
||||
local descriptor and drops `record.SchemaId`. So the observed side holds the local id and the durable side
|
||||
the persisted id. Probe: unchanged drifted record, `Expect` + `Upsert` commits **False** on the single-file
|
||||
store (P1) and on the directory store (P7). Same code at Aetheria's pin `45c2f40` (`CultCache.cs:1147-1207`).
|
||||
- **E2. What must still refuse, at base.** Another cache commits a changed value (new minted `storedAt`):
|
||||
Expect False (P4). Another cache reading under v3 (`DriftReused`) flushes the file, rewriting the record under
|
||||
v3 with the **same** `storedAt`: Expect False (P5).
|
||||
- **E3. A whole-file write rewrites every record under its local schema and keeps `storedAt`.**
|
||||
`ToPersistedRecord` (`:3194-3204`) writes `entry.Descriptor.SchemaId` with `entry.StoredAt`, and both the
|
||||
single-file `PushAll` (`:3361-3375`) and an unconditional or dirty `CommitBatchCore` (`:3393-3399`) write the
|
||||
whole view. So the contract sentence "every write to a key mints a later storedAt"
|
||||
(`Contracts/cultcache-store-composition.md:229-231`) is false for a flush: the schema id is the half of the
|
||||
identity that sees it. The directory store writes only staged keys, each freshly stamped
|
||||
(`DirectoryMessagePackBackingStore.cs:240-293`), so it never normalises a clean record.
|
||||
- **E4. Lifetime at base.** After the cache's own `PushAll`, Expect commits True (P2): disk and descriptor now
|
||||
agree. After *another* cache's normalising flush to the observer's own local schema, Expect commits True (P3);
|
||||
after a re-pull, True (P6).
|
||||
- **E5. Re-reads.** Both pull filters skip a record whose `storedAt` and local descriptor id are unchanged
|
||||
(`CultCache.cs:3323-3327`, `DirectoryMessagePackBackingStore.cs:110-114`). A record normalised P to L by
|
||||
another writer keeps `storedAt` and the local id, so with the persisted id on the entry the filter must
|
||||
compare the persisted id, or a re-read never refreshes it and Expect stays a Mismatch forever.
|
||||
- **E6. Other runtimes.** cultcache-rs `SingleFileMessagePackBackingStore::compare_exchange` and the snapshot
|
||||
form (`packages/cultcache-rs/src/lib.rs:1352-1393`, `:1427-1450`) compare whole raw envelopes as read from
|
||||
disk (`schema_id: Some(record.1)`, `:3189`); the expectation *is* the persisted envelope, and the crate has
|
||||
no compatible-drift resolution, so the split cannot occur. Unchanged. cultcache-ts and cultcache-py have no
|
||||
conditional commit (`cultcache-persistence-format.md:67-69`; grep finds none); nor do the cultnet and
|
||||
cultmesh packages or cultmesh-kotlin. Not added here.
|
||||
- **E7. Release path.** Aetheria pins `org.gamecult.cultlib#cultlib-unity-v1.0.60` and
|
||||
`org.gamecult.caching.unity#caching-unity-v1.4.0`, both locked at `45c2f400` (2026-09-17). The CultCache
|
||||
runtime ships as DLLs in `unity/org.gamecult.cultlib` (built by `scripts/build-unity-package.ps1`). Main is
|
||||
1363 commits past the pin with a `### Breaking` section under `[Unreleased]`
|
||||
(`unity/org.gamecult.cultlib/CHANGELOG.md`). `release/cultlib-unity-1.0.x` is the pin plus one commit
|
||||
(1.0.61, Win64 QUIC plugins).
|
||||
|
||||
### Authority, and why
|
||||
|
||||
The backing store that read or wrote a record's durable bytes owns the schema id it is persisted under. It is set
|
||||
when the store builds the entry from a persisted record (`ToStoredDocument`), defaults to the descriptor's id
|
||||
for an entry this cache stamps (it is about to be written under that id), and is reset to the descriptor's id,
|
||||
under the hold, after a whole-file write the store completed. The commit check, `Expect` and the pull filters
|
||||
only read it. `Descriptor.SchemaId` stays what the entry decodes as; it is no longer the record's durable identity.
|
||||
|
||||
Consequence, recorded rather than ruled: after another writer's whole-file flush rewrites a drifted record
|
||||
under a schema id this cache did not read it under, an `Expect` captured before is a Mismatch until the cache
|
||||
re-reads (E4's P3 flips). That is the ruling's "compares that persisted id" applied to a rewrite that kept
|
||||
`storedAt`; it is fail-closed, the standard retry (re-read, retry) recovers, and it is what refuses E2's P5.
|
||||
|
||||
## Observed, not owned by these cuts
|
||||
|
||||
- At the default tolerance the solver's answer on allocator-shaped problems depends on the start by up to the whole
|
||||
box (B6). That is documented and pinned (`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance`); a
|
||||
consumer that needs one answer passes a tight tolerance, as Aetheria does.
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# CultLib Gaps: Target
|
||||
|
||||
Date: 2026-10-09. Campaign `cultlib-gaps` in the Eureka mind.
|
||||
|
||||
## Why
|
||||
|
||||
Consumers of CultLib keep finding capabilities a reasonable consumer of a general
|
||||
library would expect and that CultLib lacks. Doctrine says a gap is filled in its
|
||||
owner, never worked around in the consumer. This campaign is the standing home for
|
||||
those gaps, so each one lands in CultLib as a small cut instead of a local helper in
|
||||
whichever project hit it.
|
||||
|
||||
Known gaps at opening (each a follow-up in the mind, owned by the consumer campaign
|
||||
that found it):
|
||||
|
||||
- CultMath `BoundedLeastSquares.Solve` has a fixed KKT tolerance (1e-6); Aetheria's
|
||||
thrust allocator needs a tighter one (`aetheria-release:follow_up:cultmath-bls-kkt-tolerance`).
|
||||
- `cultcache-rs` has no decimal type, its snapshot reads follow symlinks, and its
|
||||
single-file store stages files with the process umask and no mode option
|
||||
(`eureka-body:follow_up:cultcache-rs-decimal-and-nofollow`).
|
||||
|
||||
## Invariants
|
||||
|
||||
- `owner-fills-gap`: a capability a consumer needs from CultLib is added to CultLib, with the consumer adopting it; no consumer-local copy survives the cut that adds it.
|
||||
- `defaults-unchanged`: a new option defaults to today's behaviour; existing callers and stored bytes do not change.
|
||||
- `wire-parity`: a CultCache wire change keeps byte parity across runtimes against the C# reference, with a cross-runtime fixture.
|
||||
- `semver-measured`: a public API change bumps the package version by measured semver and ships as a tagged release consumers can pin.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Growing CultCache into a general database (memory `cultcache-scope-restraint`).
|
||||
- Any consumer's adoption code beyond the pin bump; that stays in the consumer's campaign.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,9 @@
|
|||
# CultMath tapes: interval-pruned field evaluation on CPU and GPU
|
||||
|
||||
Status: steps 2 and 4 (graph, C# evaluator, HLSL interpreter) unparked on
|
||||
2026-10-14 (ruling tape-unparked-for-trophic-graphs, which supersedes
|
||||
tape-target-unparks-when-asura-stable); their model page and cut sequence are
|
||||
`docs/cultmath-interval-ground-cut.md`, "Pass 7". Steps 3 and 5 (pruning,
|
||||
meshing) stay parked. The affine-forms
|
||||
piece was unparked on 2026-10-03 (ruling affine-forms-unparked) and is on main
|
||||
(156782da), alongside step 1's intervals; its map and as-built record are in
|
||||
`docs/cultmath-interval-ground-cut.md`. This note records the direction for the
|
||||
parked steps so they can be picked up without re-deriving it.
|
||||
Status: **parked**, 2026-10-02. The operator wants Asura working first. This
|
||||
note records the direction so it can be picked up without re-deriving it. It is
|
||||
not a cut map: no Imagination pass has run, and nothing here is ruled except
|
||||
that the campaign exists and waits.
|
||||
|
||||
## Intent
|
||||
|
||||
|
|
@ -108,18 +103,15 @@ source.
|
|||
- Asura does not need this campaign. Its field is noise-dominated, and its
|
||||
`A_max` shell and band-limiting already give the cheap culling.
|
||||
|
||||
## Decided since the unpark, and what stays open
|
||||
## Open before unparking
|
||||
|
||||
- The spike-or-(c) choice was settled by the operator's unpark: the tape
|
||||
unparked for Aetheria's trophic species graphs (ruling
|
||||
`cultmath-tapes:ruling:tape-unparked-for-trophic-graphs`).
|
||||
- Interval representation: affine forms for noise and warp, unparked on
|
||||
2026-10-03 (ruling `cultmath-tapes:ruling:affine-forms-unparked`): one
|
||||
shared symbol, mapped in `docs/cultmath-interval-ground-cut.md`, "Pass 5".
|
||||
- Register model: the HLSL interpreter keeps its registers in groupshared
|
||||
memory, a slice per thread, linear-scan allocated at lowering (ruling
|
||||
`cultmath-tapes:ruling:tape-register-model-ruled`; research in
|
||||
`docs/research/tape-register-model.md`).
|
||||
- Open: whether the brush-grammar station project lives in Aetheria or in
|
||||
its own repo (follow-up `cultmath-tapes:follow_up:brush-grammar-station-repo`,
|
||||
raised as an operator question when a station cut reaches the frontier).
|
||||
- Run the (a) spike first, or go straight to (c)?
|
||||
- Pick the interval representation (plain intervals or affine arithmetic) and
|
||||
the noise bound strategy.
|
||||
Affine forms for noise and warp were unparked on 2026-10-03 (ruling
|
||||
`cultmath-tapes:ruling:affine-forms-unparked`): one shared symbol, mapped in
|
||||
`docs/cultmath-interval-ground-cut.md`, "Pass 5".
|
||||
- Choose the bytecode register model for the HLSL interpreter, given GPU
|
||||
register pressure.
|
||||
- Decide whether the brush-grammar station project lives in Aetheria or in
|
||||
its own repo.
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
# CultMesh garden path: map
|
||||
|
||||
Campaign `cultmesh-garden-path`, stage 1 (server-authoritative garden path for the
|
||||
orbs .io game). Target: `docs/cultmesh-garden-path-target.md` (typed as
|
||||
`cultmesh-garden-path:target:r2`). Rulings, questions and follow-ups are documents
|
||||
in the mind; this page holds the model page, body facts and the stage 1 cut lines.
|
||||
|
||||
Written by Imagination (`imagination-gp-0b`, session `self-2026-10-27-eureka`)
|
||||
against CultLib `d1bf21b3`. Refreshed by `imagination-gp-stage1` (session
|
||||
`self-2026-10-27c-eureka`) against `51ca81a1`: every fork the page waited on is
|
||||
ruled, and the cells below carry the rulings.
|
||||
|
||||
## Model page
|
||||
|
||||
One row per kind. "Store" says where it lives: a CultCache document in the room's
|
||||
store, or transient per-tick data that never enters a store. Proposed values are
|
||||
marked as such; they are the design this page offers for the cuts, not landed
|
||||
behaviour. A cell that waits on an operator question names it.
|
||||
|
||||
The governing split, from `state-is-cultcache` and the CultCache scope restraint:
|
||||
every value that crosses a boundary is CultCache-encoded (one serialisation model,
|
||||
no JSON), but only state that outlives a tick is a record in a store. Inputs, ticks
|
||||
and snapshots are typed CultNet message payloads, not store records. The store
|
||||
receives exactly one write set per tick (ruled `tick-state-commit`: tick-batch).
|
||||
|
||||
| Kind | Store | Identity (what names it) | Lifecycle (what happens to it) | Authority (who decides; forbidden writers) |
|
||||
|---|---|---|---|---|
|
||||
| **Game definition** (documents, systems, authority declaration, tick rate) | Proposed: one CultCache document per server process (`gamecult.mesh.game_definition`), read-only to everyone but the server's startup. | Game id string from code (`"orbs"`), namespaced by Verse id. The id alone is not injective (two builds share it), so proposed identity is `(game id, definition hash)`, the hash derived from the lowered definition: document schema ids and versions, system ids, order and marks (`[Predicted]`, later `[Adjudicable]`), tick rate, authority declaration. Schema ids (`orbs.player`) are global strings; the game prefix is convention, not enforced (body fact F9). | Created at server startup from code; never revised at runtime; a new build supersedes it with a new hash. Clients carry their own compiled hash (C# in wasm, ruling `browser-sim-runtime`) and are refused at join on mismatch. A declaration a mode cannot honour is refused at startup (`modes-do-not-leak`). Never replayed. | The game's code, through the builder, at startup; the runtime validates. Forbidden: clients, any runtime mutation, Odin (Odin may read and index it, never write it). |
|
||||
| **Game document** (Player, Pellet, Shot) | CultCache documents in the room store. | `(schema id, record key)`. Today the key is a free string the caller picks; `[CultIndex] Id` in the illustration is an index, not the key. Proposed: the runtime mints keys. Player key derives from the session principal (one Player per principal per room). Spawned documents (Shot, Pellet) key on `(spawner key, tick, spawn ordinal within that tick)`, so a client's predicted spawn and the server's authoritative spawn derive the same key and reconcile instead of duplicating. That is injective as long as spawn order within a system step is deterministic for the predicting client, which the predicted system's own code gives under one runtime (C# both sides). The developer's `Id` field becomes redundant; whether to keep it as a mirror of the key is an API call for the cut, not an operator fork. | Created by a system during a tick, or by the join system (Player). Revised at most once per tick, in that tick's commit. Deleted by a system (pellet eaten) or by the game's leave system after a session ends. Across reconnect within the grace window the Player record is unchanged; only the session's connection binding moves. Across a server restart: gone (ruled `room-durability`: ephemeral; a restart is a new room epoch at tick 0). Replayed only on the client, during reconciliation; the server never replays in stage 1. | The server tick runner on the room's primary is the one writer of committed state, and systems write only through the tick's working set. Forbidden writers: client document puts through `CultNetDatabaseServer.HandlePutAsync` (today accepted from any peer, F1); `PutPredictedAsync` and `CultMeshGameSession.PredictAsync` on any networked store; game code calling database APIs; the simulation-fact committer (F7). |
|
||||
| **Player input** | Transient. A CultCache-encoded payload on a CultNet message; never a store record in stage 1. | `(session principal, input sequence)`, the sequence monotonic per session; each input also carries the tick it targets. Namespace: the room. Injective given the principal. The input type is a schema'd CultCache type, so the schema catalog publishes it like any document. | Sampled each client tick (devices read by the TS shell, handed to wasm); sent with the last few unacknowledged inputs repeated, so one loss is absorbed; buffered by the server per session against its target tick; consumed exactly once by the matching tick (proposed policy for late inputs: applied to the next tick and counted); acknowledged in the snapshot as "last processed sequence"; kept by the client until acknowledged, for replay; then discarded. Stage 4 changes this: inputs become signed, hash-chained and retained for a dispute window. | The client's principal authors the content; the tick runner decides which tick consumes it. Forbidden: server code altering input content; other clients; any input that arrives without a bound session. |
|
||||
| **Tick** | Transient counter; proposed: the last committed tick is written into the room record in each tick's batch, so a snapshot or checkpoint knows it. | `(room id, room epoch, tick number)`, the number starting at 0 when the room epoch starts. Injective with the epoch. | Advanced only by the server tick runner at the one declared rate (ruled `tick-model`: one-fixed-rate); never revised. The client estimates the server tick from snapshots and runs its predicted tick ahead by its round trip plus a buffer; on reconnect it re-syncs from the next full snapshot. | Server tick runner. Forbidden: client clocks, transport arrival time, wall-clock reads inside systems (systems receive the tick, not the time). Today no tick scheduler exists anywhere in the C# body (F6). |
|
||||
| **Predicted state and reconciliation** | Client-side only. In the browser they live in the C# wasm CultCache store; the TS renderer reads a per-frame view (ruled `browser-client-store`: wasm-owns-store). | Same `(schema id, record key)` as the authoritative document, plus the client tick it was predicted for. | Each client tick: predicted systems run over owned documents and the client's own spawns. On a snapshot for tick T acknowledging input sequence S: predicted documents are replaced by the authoritative ones at T, then inputs after S are replayed through the predicted systems up to the current client tick; predicted spawns are matched by derived key, and an unmatched one is removed once the acknowledgement passes its tick. Today `Reconciled` means "the authoritative put replaced the predicted copy" with no replay (F3); the replay loop does not exist. | Predicted state owns nothing outside the client. It is derived from the last authoritative snapshot plus unacknowledged inputs plus the predicted systems. The snapshot owns truth. Forbidden: sending predicted documents to the server; `CultNetClientAuthorityScope` in server mode (F4); the renderer writing. |
|
||||
| **Session and the player's identity** | Proposed: session membership as a small CultCache document in the room store (`gamecult.mesh.game_session`, low churn, readable by systems and present in snapshots). The connection handle is transient process state. | Principal: the fingerprint of a browser-held P-256 key, proved by signing a join challenge (ruled `player-identity`: browser-keypair). Connection: a server-minted id per transport connection (ephemeral). Logical session: `(principal, room)`, bound across reconnects. Today the client's identity is a self-asserted `runtimeId` string (browser client options; `SourceRuntimeId` on operation envelopes); CultMesh's session identity proves the server to the client and not the reverse (F5). The body still has no authenticated client principal (F5, F13); the session cut builds it. | Connect, then join (definition hash check, principal proof), then the session record is created and the join system spawns the Player. Disconnect suspends the session for a grace window; resume binds a new connection to the same session; expiry ends the session, systems see the leave, and the game's leave system removes the Player. On server restart sessions end (ruled `room-durability`). | The server's join and leave handler on the room primary decides membership; the principal proves itself by signing the join challenge. Forbidden: a client-asserted `runtimeId` as identity; transport peer address; peer catalogs. |
|
||||
| **Authority declaration and the lease it lowers to** | Declaration: inside the game definition. Lease: proposed as a CultCache document (`gamecult.mesh.authority_lease`); today a plain class in an in-memory dictionary (F2). | Declaration: per document type, `(writer, checker, arbiter)`. Lease: `LeaseId`, carrying Verse, peer, roles, shard ids, issuer, validity window, epoch and signature (`CultMeshAuthorityLease`). Namespace: the Verse. | Declaration fixed per build. At startup `Authority.Server` lowers to: the server holds a `ShardPrimary` lease on the room shard, and clients hold no write scope at all. Renewed before expiry; revocation or expiry stops the tick runner from committing; a new primary takes a new epoch. Proposed issuer in stage 1: the server's own deployment key, which clients pin through the browser trust policy, so an indie deployment needs no Odin. Odin-issued leases stay possible. | The game definition decides the declaration; the issuer issues leases; `CultMeshAuthorityResolver` decides whether a peer holds a role on a shard. Today the database's write gate (`CultNetShardDescriptor.IsPrimary`, `CultNetClientAuthorityScope`, both set from options) never consults the resolver: two authority structures (F2, F4). Proposed: the write gate is derived from lease resolution. First step (cut `remote-write-authority`): a peer's put or delete reaches the database only when `CultNetDatabaseServerOptions.RemoteWriteAuthority` grants it; none configured refuses every remote write (F1, GP1). The lease-backed grant needs an authenticated principal bound to the connection, so it follows the session cut. `CultNetClientAuthorityScope` is no longer an owner; it is derived from the declaration, and in stage 1 it is empty. |
|
||||
| **Shard / room** (stage 1: one) | `CultNetShardDescriptor` (configuration); the room's state is its CultCache store. | Room id `(Verse, game id, room name)`, for example `orbs/main`, carried as the shard id plus the shard epoch. | Created at server start (epoch 1; on restart a new epoch, ruled `room-durability`); never moved in stage 1; ended at shutdown. Join means "join the one room"; matchmaking is out of scope. Interest management is stage 5 per the target, so in stage 1 every client receives the whole room. | The runtime holding the `ShardPrimary` lease. Forbidden: a second primary; clients. |
|
||||
| **Snapshot sent to clients** | Transient. A CultNet message whose payload is CultCache-encoded documents; never stored. The server keeps per-client baselines in memory for deltas. | `(room, epoch, tick, recipient session, baseline tick)`. | Derived after a tick commit at the send rate; a delta against the recipient's last acknowledged snapshot, or a full snapshot on join and resume; carries the last processed input sequence for that session. Discarded once sent; a baseline is dropped when a newer one is acknowledged. Today the nearest things are per-record change pushes from `CultNetDatabaseSubscriptionServer` (no tick, no acknowledgement, no input sequence) and shard catch-up snapshots in `CultMeshSnapshots.cs` (F8). | The snapshot builder decides nothing; it is derived from the committed tick. Forbidden: the per-record subscription stream acting as a second game replication path. That stream stays for non-game observers. |
|
||||
| **Simulation fact / witness observation** (found) | `CultMeshSimulationFact` is a CultCache document; observations are hub memory. | `(shard, epoch, frame, subject, kind)`. | Stage 1: not used. The target replaces it at stage 4. | Today `CultMeshGameSession` serves observations by default (`ServeSimulationObservations = true`) and commits quorum facts through the primary (F7). In server mode there is no checker, so the observation server and the committer must not be wired. Forbidden writer in stage 1. |
|
||||
| **Shard mutation log entry** (found) | `CultNetDatabase` in-memory `SortedList` per shard, plus an optional durable store. | `(shard id, sequence)`. | Today every primary write appends one entry, and the in-memory list is never trimmed (F10). Per-record writes at tick rate would grow it without bound. In stage 1 the tick's write set either bypasses the per-record log or lands as one bounded entry per tick. Ruled: the tick's write set lands as one batch per tick (`tick-state-commit`), in memory (`room-durability`); the bounded per-tick entry is the tick-runner cut's to specify. | `CultNetDatabase` on the primary. Forbidden: per-system-write appends from the tick runner. |
|
||||
|
||||
### Cells filled by rulings (2026-10-27)
|
||||
|
||||
- Session principal: `player-identity` (browser-keypair). Not yet in the body (F5, F13).
|
||||
- Browser documents and the renderer's read: `browser-client-store` (wasm-owns-store).
|
||||
- Room state across restarts: `room-durability` (ephemeral).
|
||||
- Tick rate model: `tick-model` (one-fixed-rate).
|
||||
- How a tick reaches the store: `tick-state-commit` (tick-batch; the tick-runner cut times `CultCacheBatch` at tick rate first).
|
||||
- Stage 1 transport: `browser-transport` (WebSocket behind a port carrying only inputs and snapshots; a WebTransport server in parallel, `webtransport-server-layer`: managed C# over the MsQuic bridge).
|
||||
- Still unprobed: `CultCacheBatch` commit cost at tick rate for about 50 players plus pellets (wasm measuring probe and tick-runner cut).
|
||||
|
||||
## Body facts
|
||||
|
||||
All are source reads at CultLib `d1bf21b3` on 2026-10-27 by `imagination-gp-0b`. No
|
||||
runtime probe was run in this pass. Each fact names the read that would falsify it.
|
||||
|
||||
- **F1. The primary applies any peer's document put.** `CultNetDatabaseServer.HandlePutAsync(message, peer)` (`src/GameCult.Networking/CultNetDatabaseServer.cs:307`) calls `ApplyPutAsync(message)` (`CultNetDatabase.cs:1022`), which checks only that this runtime is primary for the shard and the epoch matches (`EnsurePrimary`, `:2003`). `peer` is used only to send errors. Any connected client can overwrite any game document on the server.
|
||||
- **F2. Leases are not typed state and do not gate writes.** `CultMeshAuthorityLease` is a plain class (no `[CultDocument]`); `CultMeshAuthorityLeaseCatalog` is a `Dictionary` (`CultMeshAuthority.cs:242`). The only `[CultDocument]` types in `GameCult.Mesh` are body publication, CDN chunk and manifest, content transfer state, discovery state, entity prefab package and simulation fact. `GameCult.Networking` (the database) references no lease type; the one Networking file naming `Lease` is `CultNetSchemaMessages.cs`.
|
||||
- **F3. Reconciliation is replacement, not replay.** `CultNetDatabase.cs:1247-1252`: a predicted put records a prediction key; the next non-predicting write to that key turns the change into `Reconciled`. No code re-applies inputs after an authoritative state.
|
||||
- **F4. Client write scope is a runtime-id string match from options.** `CultNetClientAuthorityScope.Matches` compares `OwnerRuntimeId` with the database's own `_runtimeId` (`CultNetDatabase.cs:111-153`, `EnsureClientAuthority` `:1934`). It gates local prediction only and has no relation to leases.
|
||||
- **F5. No authenticated client principal.** `CultMeshSessionIdentity.cs` verifies transports for `(verse, authorityRuntimeId, protocol, routeGeneration)`, which proves the server to the client. The browser client's `runtimeId` (`packages/cultmesh-browser/src/index.ts:182`) and the operation envelope's `SourceRuntimeId` (`CultNetOperationServer.cs:39`) are self-asserted.
|
||||
- **F6. No tick scheduler.** A grep of `GameCult.Networking` and `GameCult.Mesh` for tick or timestep names finds none. `Frame` exists only as a witness-asserted number in `CultNetSimulationConsensus` and `CultMeshSimulationFact`.
|
||||
- **F7. The game session serves the witness path by default.** `CultMeshGameSession` constructs `CultNetSimulationObservationServer` when `ServeSimulationObservations` (default `true`) and always constructs a `CultMeshSimulationFactCommitter` (`CultMeshGameSession.cs:70-104`). `PredictAsync` is `PutPredictedAsync` (`:161`).
|
||||
- **F8. The browser can watch and invoke, not put.** The `cultmesh-browser` README says it "does not expose direct document writes". The client offers `leaseRawDocument` (`index.ts:349`) and `invoke` of typed operations (`:370`), over one WebSocket. On C#: `CultNetDatabaseSubscriptionServer` and `CultNetOperationServer` take `ICultNetSchemaServer`, so they run over `CultNetWebSocketSchemaServer`; `CultNetDatabaseServer` (puts) takes the concrete UDP `Server` (`CultNetDatabaseServer.cs:36`). Operations are request and response with receipts and idempotency keys, which is a poor fit for an input stream at tick rate.
|
||||
- **F9. Schema-id namespacing is convention.** `[CultDocument("orbs.player", ...)]` ids are global strings; nothing ties a schema id to a game id.
|
||||
- **F10. The in-memory mutation log is never trimmed.** `RecordMutationLogEntry` (`CultNetDatabase.cs:1610`) inserts into a per-shard `SortedList`; no `Remove` or trim on that list exists in the file. Compaction applies to the durable store and to burned sequences only.
|
||||
- **F11. CultCache has a write-set batch; CultNetDatabase does not expose one.** `CultCacheBatch` with `Commit` and `TryCommit` (`GameCult.Caching/CultCache.cs:1796`, `:2212`) is the 2.0.0 write set (`variants:ruling:write-set-content-version-declared-ownership`). `CultNetDatabase` has no batch method: every write is a per-record door.
|
||||
- **F12. A reflection site on the mesh put path.** `CultMesh.cs:3502` `PutDatabaseDocumentAsync` picks `PutAsync` or `PutPredictedAsync` by `GetMethods` and `MakeGenericMethod`, which matters to the AOT prerequisite (`cultlib-gaps:follow_up:cs-aot-safe-registration`; probe in `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`).
|
||||
|
||||
- **GP1 (runtime probe, 2026-10-28, `imagination-gp-stage1`).** Scratch NUnit test at `51ca81a1`, run on Yggdrasil (`ygg-verify.sh`, `mcr.microsoft.com/dotnet/sdk:10.0`, filter `ImagGpProbeTests`, never pushed to origin). The server writes `gp:player` = "server-owned" through `CultNetDatabase.PutAsync` on a primary shard. `CultNetDatabaseServer._putHandler`, invoked with an uninitialized `CultNetServerPeer` (no field set: no endpoint, no transport, no identity) and a put of "client-overwrote", leaves the record "client-overwrote", and `Server.CanProcessBeforeVerification(put)` returns `true`. Output: `GP1 after=client-overwrote canProcessBeforeVerification=True`, Passed 1. This confirms F1 at runtime: nothing about the peer is consulted, and puts and deletes pass before the legacy login verification (`Server.cs:562-575`).
|
||||
- **F13. A server peer carries no identity.** `CultNetServerPeer` (`Server.cs:829`) holds the LiteNetLib `NetPeer`, its endpoint and transport stats. The only principal on `Server` is the legacy username and password login (`PlayerData.PlayerId`, `IsVerified` `:697`), which the database door never reads. `ServerSecurityOptions` is one shared `ConnectionKey`: transport admission, not authority.
|
||||
- **F14. A new refusal code is a coordinated change.** The `cultnet.error.v0` `code` is a closed enum (`contracts/cultnet/cultnet.error.schema.json:28-31`, with a byte-identical copy under `packages/cultnet-rs/contracts/`). Rust `CultNetErrorCode` (`packages/cultnet-rs/src/contracts.rs:242-273`) refuses unknown codes, `packages/cultnet-rs/tests/error_message.rs` decodes captured C# bytes per code, and TypeScript imports the root schema (`packages/cultnet-ts/src/contracts.ts:10`). `unowned_schema` was added this way and is the pattern.
|
||||
- **F15. Forwarding re-sends as a new client.** `TryForwardPutAsync` (`CultNetDatabaseServer.cs:654`) forwards a `not_primary` put through `ICultNetShardWriteForwarder`, and `CultNetSchemaWriteForwarder` opens its own client connection (`CultNetShardWriteForwarding.cs:122-128`). The primary therefore sees the forwarding runtime, never the original peer.
|
||||
- **F16. Consumers of the remote write door.** No C# consumer outside CultLib constructs `CultNetDatabaseServer` (grep of the Aetheria, Epiphany, Ghostlight, huginn and gamecult-ops `*.cs` files finds only vendored CultLib copies). In CultLib, tests reach the put and delete handlers directly in `CultNetJournalTests.cs:738`, `:935-936` and `SoulP4cProbeTests.cs:57`. `CultNetDatabaseServer.ApplyPutAsync` and `ApplyDeleteAsync` (`:73`, `:81`) have no caller in `src` or `tests` besides the handlers. TypeScript puts (the `cultnet-ts` Idunn presence and signed health publishers) target Rust servers.
|
||||
- **F17. The Rust document server admits a put by size only.** In `packages/cultmesh-rs/src/rudp_document_server.rs:682-720`, a `DocumentPutRaw` is refused only when its served response would not fit; no authority is asked. This is F1's gap in the Rust runtime (a follow-up, not stage 1).
|
||||
|
||||
## Cut sequence (stage 1)
|
||||
|
||||
One line per cut not yet mapped. The model page has no empty cell as of 2026-10-28. Remote write authority (`cultmesh-garden-path:cut_spec:cut-remote-write-authority.r1`, mapped 2026-10-28, so no longer a line here) goes first: it waits on nothing, every later server cut stands on a closed write door, and GP1 shows the door open today. The wasm measuring probe is the sibling that may run beside it, since it touches no file this cut does; ruling `browser-sim-runtime` names it the first cut of the browser runtime track, not of stage 1 as a whole.
|
||||
|
||||
- **Wasm runtime measuring probe.** Download size, startup and 60 Hz tick cost of a stub orbs tick under Mono browser-wasm and NetWasm, including `CultCacheBatch` commit cost at tick rate. This is the first cut named by ruling `browser-sim-runtime`, which it can overturn. Its NetWasm and AOT variant waits on `cultlib-gaps:follow_up:cs-aot-safe-registration`.
|
||||
- **Witness services off in server mode.** `CultMeshGameSession` stops constructing the observation server and fact committer when the declaration has no checker (F7). Waits on nothing; can run beside remote write authority.
|
||||
- **Leases as typed state and the lease-derived grant.** `CultMeshAuthorityLease` becomes a CultCache document, the lease catalog a store view, and the Mesh host's `RemoteWriteAuthority` resolves the connection's principal through `CultMeshAuthorityResolver`. Waits on remote write authority and on session and principal.
|
||||
- **Demote `CultNetClientAuthorityScope`.** It becomes derived from the authority declaration (empty in server mode). Waits on game definition.
|
||||
- **Changelog entry for the remote write refusal.** `unity/org.gamecult.cultlib/CHANGELOG.md` is also written by `variants:cut_spec:cut-write-ownership-cs.r1`, so Self adds this cut's bullet at merge, after that cut lands, and the two specs share no file.
|
||||
- **Game definition and server tick runner.** Builder, document and system registration, definition document and hash, fixed tick, tick-boundary commit. Rests on rulings `tick-model`, `tick-state-commit` and `room-durability`; waits on nothing mapped.
|
||||
- **Session and principal.** Join, resume and leave; principal proof; session document. Rests on ruling `player-identity`; waits on game definition (the join checks the definition hash).
|
||||
- **Input and snapshot path.** Input messages with redundancy, a server input buffer, acknowledgements, delta snapshots. Rests on ruling `browser-transport`; waits on session and principal.
|
||||
- **Browser client runtime.** Wasm simulation host, TypeScript shell for transport and rendering, prediction with replay reconciliation. Waits on the measuring probe and `browser-client-store`.
|
||||
- **Orbs reference game and garden-path proof.** The game written with no networking code (`garden-path-needs-no-networking`), and a browser client that writes (`browser-is-first-class`). Waits on all of the above.
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
# CultMesh garden path: target
|
||||
|
||||
Status: draft target for campaign `cultmesh-garden-path`, 2026-10-27. It holds the
|
||||
ends, not the means. The code below is illustrative: it shows what a developer
|
||||
should have to write, and no name in it is decided.
|
||||
|
||||
## The problem
|
||||
|
||||
Operator, 2026-10-27: "the value is in solving the multiplayer scaling problem
|
||||
for indie developers in an ergonomic fashion. Having one authoritative server is
|
||||
fine, up to a point, but there's no real open source playbook on how to ship an
|
||||
MMO, and existing solutions like SpacetimeDB have a huge ergonomics gap. I want
|
||||
CultMesh to feel like magic: as a garden path, you annotate your domain POCOs and
|
||||
simulation code and declare your authority structure ... and it'll all just work
|
||||
without the developer needing to understand networking deeply."
|
||||
|
||||
Prior art: `research/cultmesh-garden-path/prior-art-2026-10-27.md` (frameworks,
|
||||
ergonomics, scale-out) and `research/witness-authoritative-networking/` (consensus
|
||||
and P2P anti-cheat). What the field shows, in short:
|
||||
|
||||
- One API over several authority modes ships today (Photon Fusion: Server, Host,
|
||||
Shared; Unity NGO: client-server and distributed authority), and in both the
|
||||
modes leak into the developer's code (`IsServer` against `HasAuthority`,
|
||||
features missing in one mode).
|
||||
- SpacetimeDB gives one machine per database. Scaling past it, client
|
||||
prediction, and the reducer-shaped rewrite of game code are left to the
|
||||
developer.
|
||||
- Determinism, where it is required at all, is required of the whole simulation
|
||||
(Croquet/Multisynq, Photon Quantum), and that tax is the main ergonomic cost.
|
||||
- Shipped world-scale designs treat a spatial unit as indivisible and move
|
||||
players between units (EVE, Pax Dei, Folia); nobody publishes an open,
|
||||
end-to-end playbook (Astron, the nearest, is discontinued).
|
||||
|
||||
## What a developer writes (illustrative)
|
||||
|
||||
The reference game is a small browser .io game (operator, 2026-10-27: "the .io
|
||||
game, Aetheria later"): players steer orbs, eat pellets, and shoot.
|
||||
|
||||
```csharp
|
||||
[CultDocument("orbs.player", "orbs.player.v1")]
|
||||
public sealed class Player
|
||||
{
|
||||
[CultIndex] public string Id { get; set; } = "";
|
||||
public Fix2 Position { get; set; }
|
||||
public Fix2 Velocity { get; set; }
|
||||
public int Mass { get; set; }
|
||||
}
|
||||
|
||||
[CultDocument("orbs.shot", "orbs.shot.v1")]
|
||||
public sealed class Shot
|
||||
{
|
||||
[CultIndex] public string Id { get; set; } = "";
|
||||
public string Shooter { get; set; } = "";
|
||||
public Fix2 Origin { get; set; }
|
||||
public Fix2 Direction { get; set; }
|
||||
}
|
||||
|
||||
// Runs every tick. Predicted on the owning client, corrected by authority.
|
||||
public sealed class Movement : CultSystem
|
||||
{
|
||||
[Predicted]
|
||||
public void Step(Player player, PlayerInput input, Tick tick) { /* ... */ }
|
||||
}
|
||||
|
||||
// A non-owner may re-run this to settle a dispute, so it must be
|
||||
// deterministic. The analyzer enforces that on this system only.
|
||||
[Adjudicable]
|
||||
public sealed class Hits : CultSystem
|
||||
{
|
||||
public HitResult Resolve(Shot shot, Rewind<Player> target) { /* ... */ }
|
||||
}
|
||||
|
||||
var game = CultMesh.Game("orbs")
|
||||
.Documents<Player, Pellet, Shot>()
|
||||
.Systems<Movement, Eating, Hits>()
|
||||
.Authority(Authority.Server);
|
||||
// .Authority(Authority.TrustedCoop);
|
||||
// .Authority(Authority.Witnessed(w => w.Challenger(Party.Victim).Quorum(3).Arbiter(Party.Server)));
|
||||
// .Authority(Authority.Regions(r => r.Grid(512).Handoff())); // later: past one server
|
||||
```
|
||||
|
||||
Changing the authority line is the whole migration between modes, unless the new
|
||||
mode re-executes a system on a non-owner and that system is not yet
|
||||
deterministic. Then the analyzer names exactly those systems.
|
||||
|
||||
## Invariants
|
||||
|
||||
- `one-model`: server-authoritative, trusted co-op, witnessed and regioned
|
||||
authority are configurations of one authority declaration, answering per
|
||||
document: who writes, who checks, who arbitrates. No mode has its own API for
|
||||
state, systems or queries.
|
||||
- `modes-do-not-leak`: game code does not branch on the authority mode or ask
|
||||
"am I the server"; a system states what it does and the runtime decides where
|
||||
it runs. Where a mode cannot offer a capability, the declaration is refused at
|
||||
startup with a reason, never degraded silently at runtime.
|
||||
- `determinism-is-per-system`: only code a non-owner may re-execute must be
|
||||
deterministic. That requirement is marked on the system and checked by an
|
||||
analyzer at build time, not by documentation or by desync in production.
|
||||
- `state-is-cultcache`: game state is ordinary CultCache documents; the
|
||||
framework adds no second state model and no JSON on the load-bearing path.
|
||||
- `authority-is-typed-state`: who holds authority over what is CultMesh state
|
||||
(leases, shard primaries), inspectable and revocable, never inferred from
|
||||
transport success or peer catalogs.
|
||||
- `garden-path-needs-no-networking`: the reference game is written without the
|
||||
developer touching sockets, serialisation, tick scheduling, prediction or
|
||||
reconciliation code.
|
||||
- `honest-guarantees`: each mode states its fault model (trusted server, trusted
|
||||
peers, honest-majority sampled witnesses with server arbitration) and its
|
||||
documentation and tests claim nothing past it.
|
||||
- `browser-is-first-class`: the reference game's client runs in a browser and can
|
||||
write (submit inputs, own predicted state), not only watch.
|
||||
- `built-for-load`: what the garden path ships that is database-shaped (the
|
||||
tick commit, the batch door, logs, subscriptions) is architected for
|
||||
unreasonable load from the start (operator 2026-10-27,
|
||||
`cultlib-gaps:ruling:database-shaped-built-for-load`): "if what we're shipping
|
||||
is database shaped, people will use it as such".
|
||||
|
||||
## Sequence (each a campaign or stage of its own; one foundation at a time)
|
||||
|
||||
1. Server-authoritative garden path for the .io game: documents, systems,
|
||||
ticked scheduling, client prediction and reconciliation, browser client that
|
||||
writes. This is the proof of ergonomics.
|
||||
2. Trusted co-op: authority moves to the owning peer; same game code.
|
||||
3. Deterministic dispute kernel (CultMath fixed-point hit tests, cross-runtime
|
||||
vectors) and the `[Adjudicable]` analyzer.
|
||||
4. Witnessed authority: signed, hash-chained inputs; victim challenge; sampled
|
||||
network-near witnesses; server arbitration; browser-to-browser channels.
|
||||
Demonstration: a cheating client caught and corrected.
|
||||
5. Regions: spatial authority handoff and interest management past one server.
|
||||
6. Aetheria on the same declaration.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Aetheria until stage 6. Eve and Gjallar (deprecated).
|
||||
- A new CultCache storage engine (the paged single-file store stays postponed,
|
||||
`variants:ruling:page-tree-store-postponed`). The garden path fills gaps in
|
||||
their owners, built for load.
|
||||
- Asynchronous Byzantine total ordering, zero-knowledge proofs, and chains of
|
||||
any kind.
|
||||
- The existing witness aggregator (`CultNetSimulationConsensus`, unsigned and
|
||||
weighted by observed votes) as a base for stage 4: it is the trusted co-op
|
||||
first stage by design and will be replaced, not extended.
|
||||
|
||||
## Open forks (to the Fork Desk, each with prior art first)
|
||||
|
||||
1. Where simulation code runs in the browser: C# compiled to WebAssembly (one
|
||||
implementation everywhere, which also shrinks the determinism problem to
|
||||
fixed-point maths, at the cost of runtime download size), C# on the server
|
||||
with TypeScript systems in the browser (two implementations of each predicted
|
||||
system), or another arrangement.
|
||||
2. Server transport for the browser: WebSocket as today, or WebTransport
|
||||
(QUIC datagrams) on the provider side.
|
||||
3. Tick model: fixed tick with a declared rate, or per-system rates.
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
# TypeScript CultMesh Content Plane: Map
|
||||
|
||||
Campaign `cultmesh-ts-content`. Imagination wrote this on 2026-10-19 at CultLib
|
||||
`8990bd46` (origin/main `a88c629e` plus the target doc), and refreshed it the
|
||||
same day at `2de07065` for C2 and C3, reading C1 from `b8b825e1`. The target
|
||||
is `docs/cultmesh-ts-content-target.md`. The HTTP prior art is
|
||||
`docs/research/2026-10-19-content-addressed-http-answers.md` (cited below as
|
||||
**R**, with its section number). This page holds the body facts, the model
|
||||
page, the rationale and the cut sequence. Specs and questions live in the
|
||||
typed mind, not here.
|
||||
|
||||
## 1. Body facts
|
||||
|
||||
F1-F18 were probed by running code or read from source at `8990bd46`. F19-F27
|
||||
were probed or read at `b8b825e1` (C1's head) and `2de07065`. F28-F33 were
|
||||
probed on 2026-10-19 over the C2+C3 merge tree (`7adbf3cb` merged with C3's
|
||||
head, extracted by `git archive`), on Windows and in a Linux container. Scratch probes
|
||||
ran under the session scratchpad (`imagination/`), not in the repo.
|
||||
|
||||
| # | Fact | Evidence |
|
||||
|---|---|---|
|
||||
| F1 | A C#-packed manifest is a MessagePack **fixarray of 10** (`0x9a`): `[artifactId, kind, version, contentHash, sizeBytes, mimeType, createdAtUtc, chunks[], tags[], metadata{}]`. Each chunk ref is a 4-array `[chunkHash, offset, sizeBytes, recordKey]`. The fixture manifest is 1,111 bytes: 6 chunks (5 x 262,144 + 4,831), content hash `f4ae2a7c…2af1`. The empty body packs to 315 bytes with one zero-size chunk whose hash is `e3b0c442…b855` (the SHA-256 of nothing). | probe: `@msgpack/msgpack` 3.1.3 decode of `contracts/cultmesh/content-vectors.cs-written.json`, kind `manifest` |
|
||||
| F2 | `@msgpack/msgpack` 3.1.3 re-encodes all four C# manifest vectors **byte-identically**, metadata included (`zeta, alpha, mid` keep insertion order). Integers come out minimal (`4831` is `cd12df`, `262144` is `ce00040000`), as MessagePack-CSharp writes them. | same probe, `encode(decode(bytes))` equals the input for all four |
|
||||
| F3 | Three decoder hazards for a TS manifest reader. (a) JS objects put integer-like keys first (`{zeta,"2",alpha,"1"}` iterates `1,2,zeta,alpha`), so metadata must be ordered entries, not an object, or a `{"10":…,"2":…}` manifest re-encodes in another order. (b) Plain `decode` takes the last of duplicate map keys silently (`{"a":"2"}`); C# `Dictionary` and Rust `ordered_map` refuse duplicates. `new Decoder({ mapKeyConverter })` can refuse them: probed. (c) Plain `decode` rounds int64 above 2^53 (`…993` reads as `…992`); `useBigInt64: true` keeps it exact. | probe (node) |
|
||||
| F4 | `@msgpack/msgpack`'s decoder is iterative: a 1,000,000-deep `0x91` input decodes in 160 ms but costs about 158 MB of heap. So nesting does not crash it but amplifies memory about 150 times. `checkPayloadDepth` (the owner of ruling `payload-depth-limit-256`) is not exported from any `@gamecult/cultcache-ts` entry: `document.ts:108` exports only the error and the constant. | probe (node); source `packages/cultcache-ts/src/document.ts:2,108,123` |
|
||||
| F5 | The C# HTTPS connector sends `GET <endpoint>?chunkHash=<hex>&recordKey=<escaped>`, **without** a `sha256:` prefix, because `BuildChunkUri` runs `NormalizeHash` first. Probed URIs: `https://ygg.example/content?chunkHash=5993ac…&recordKey=mesh%3Acdn%3Achunk%3A5993ac…`. An existing endpoint query is kept and joined with `&` (`?tenant=sp&chunkHash=…`). A null record key sends `recordKey=` (empty). Other characters use `Uri.EscapeDataString`, so on the wire `k with space/ü&=?#` becomes `k%20with%20space%2F%C3%BC%26%3D%3F%23`. | probe: `BuildChunkUri` lines copied into a net10.0 console (`CultMeshHttpsContentTransport.cs:114-122`) |
|
||||
| F6 | The connector calls `EnsureSuccessStatusCode` and fails on any status outside 2xx. When `Content-Length` is present and is not the chunk's `SizeBytes`, it throws before reading. With no `Content-Length` it copies the stream without checking the length. Verification belongs to the caller's transfer owner. `CanConnect` admits only `https`. The constructor takes a `Func<HttpMessageHandler>`, so a test can pin a self-signed certificate without modifying the connector. | source `CultMeshHttpsContentTransport.cs:18-29,92-104` |
|
||||
| F7 | `NormalizeHash` (C#) strips a case-insensitive `sha256:` prefix *before* trimming, then `Trim().ToLowerInvariant()`. It never checks for hex or a length of 64. So the record-key grammar accepts any non-blank string, and a provider must add its own hex check before a hash can become a file name. | source `CultMeshCdn.cs:549-560` |
|
||||
| F8 | `StableToken` (the token in a manifest record key) is a C# rule with no Rust mirror: `cultnet-rs/src/content.rs` builds no artifact record key. Its Unicode semantics differ from the naive JS spelling in four probed ways. .NET `ToLowerInvariant` leaves `U+0130` unchanged, while JS gives `i` plus `U+0307`. .NET `Trim` keeps `U+FEFF` (and trims `U+0085`, per `char.IsWhiteSpace`), while JS `trim` drops `U+FEFF` and keeps `U+0085`. JS whole-string lowercasing applies final-sigma (`ΑΣ` becomes `ας`), while per-unit lowercasing gives `ασ`. `char.IsLetterOrDigit` is false on a lone surrogate and on `U+00B2`, which JS `\p{L}\p{Nd}` agrees with only when it is tested per UTF-16 unit. | probe (net10.0 console and node); source `CultMeshCdn.cs:562-580` |
|
||||
| F9 | `CreatedAtUtc` defaults to `DateTimeOffset.UtcNow.ToString("O")`, which looks like `2026-10-19T12:07:25.5235579+00:00`: 7 fractional digits and `+00:00`, never `Z`. | probe (net10.0) |
|
||||
| F10 | `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` are written by `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` and `packages/cultnet-rs/tests/content.rs`, under `CULTNET_WRITE_VECTORS=1`. Each side judges both files. Kinds are `request`, `response` and `manifest`. Rust panics on an unknown kind (`tests/content.rs:302`), and C# requires the Rust labels to equal its own (`CultMeshContentVectorTests.cs:54`). **So a new kind or label in the cs-written file breaks both siblings.** A TS file must be a separate file. | source |
|
||||
| F11 | **Neither content-vector suite runs in CI.** `cultmesh-portability.yml` filters `dotnet test` to `AuthorityProof`, `RealtimeWireProtocol` and a lease churn test. `cultnet-interop.yml` runs `cargo test … rudp` (a name filter) and the Networking `Rudp` filter. The C#/Rust content parity has only ever been run by hand. | grep of `.github/workflows/*.yml` |
|
||||
| F12 | TS→C# vector precedent: `scripts/write-cultmesh-realtime-frame-vectors.mjs` writes `realtime-frame-vectors.ts-written.json` from built `dist`, deliberately from **different inputs** than the C# file. `CultMeshRealtimeWireProtocolTests` judges it, and `cultmesh-portability.yml:145-149` runs it. | source |
|
||||
| F13 | Cross-process precedent for C# against a Node provider exists. `packages/cultnet-ts/test/interop/cultnet-interop.test.ts:536-560` spawns the TS QUIC provider, then runs `dotnet test --filter NativeConnectorReceivesFromExternalProvider` with the endpoint in `CULTMESH_NATIVE_EXTERNAL_ENDPOINT`. The C# test skips without the variable (`tests/GameCult.Mesh.Quic.Native.Tests`, line 21). | source |
|
||||
| F14 | `cultnet-ts` is CommonJS. Its `./wire`, `./contracts` and `./authority` subpaths import only `@msgpack/msgpack`, `ajv` and `cultcache-ts/document`, so they are browser-safe. Its root `index` pulls `node:` modules (`peer`, `rudp`, `transport`, `security` and others). `cultmesh-browser` (ESM) already imports `cultnet-ts/wire`, `/contracts` and `/authority`. `cultmesh-ts` loads `koffi` lazily, inside a function (`realtime-quic-native.ts:139`), so a new subpath of it pays no native load. The root has `esbuild` ^0.28.1 as a devDependency. | source, grep |
|
||||
| F15 | TS packages build with `tsc` and test with `node --test dist-test/test/*.test.js` per workspace. `scripts/run-typescript-workspaces.mjs` runs them in order `cultcache-ts, cultnet-ts, cultmesh-ts, cultmesh-browser`, after `check-typescript-tests-wired.mjs`. No StrykerJS config exists in CultLib (`docs/mutation-testing.md:156-159`). | source |
|
||||
| F16 | **No CultLib TS package except `cultcache-ts` has a publish job.** `publish-packages.yml` triggered on `cultcache-ts-v*`, `cultcache-py-v*`, `cultnet-py-v*` and `cultmesh-py-v*` when this was written; it now triggers on the three `-py-v*` patterns only, because npm publishing is deliberately off until ruled on (a tag run uses the workflow at the tagged commit, so `cultcache-ts-v0.15.0` on `7d292085`, before that change, still ran the old one). Tags `cultnet-ts-v0.1.1` and `cultmesh-ts-v0.1.1` exist, but no workflow consumes them. `npm view` returns 404 anonymously for `cultnet-ts`, `cultmesh-ts`, `cultmesh-browser` and even `@gamecult/cultcache-ts` (`@gamecult/cultcache-ts` may be private; unverified). The doctrine's rule of at most three tags per push stands (SKILL.md:674). | source; `npm view` |
|
||||
| F17 | StreamPixels consumes CultLib as the git submodule `vendor/CultLib` (gitlink `30ee8b97`) with `file:` deps and `pnpm.overrides`. Its CI and Idunn recipes build the vendored workspaces (`npm ci --ignore-scripts`, then build `cultcache-ts`, `cultnet-ts` and `cultmesh-ts`). `apps/web` depends only on `cultnet-ts` today, not on `cultmesh-browser`. | StreamPixels `.gitmodules`, `apps/*/package.json`, CI workflow lines 62-95 |
|
||||
| F18 | The Rust reader sets two guards that the TS reader should match. A caller cap `max_bytes` is checked before any request. And a zero-size chunk is refused unless it is the only one, because a manifest of zero-size chunks would be a request flood. | source `cultnet-rs/src/content.rs:364-385` |
|
||||
| F19 | `BuildChunkUri` on more endpoint shapes, as `AbsoluteUri` (what `HttpClient` sends). `https://h` and `https://h/` both give `https://h/?chunkHash=…&recordKey=k`. `https://h:443/x` drops the default port. `https://H.Example/X/` lowercases the host and keeps the path's case. `https://h/x#frag` keeps the fragment after the query. `https://h/x?a=%20b+c` keeps the existing query verbatim and joins with `&`. **`https://h/x?` (an empty query) gives `https://h/x?&chunkHash=…`**, because `UriBuilder.Query` is `?`, which is not blank. `https://[::1]:8443/c` keeps the bracketed host. `Uri.EscapeDataString("!'()*~-._ +é𐐀")` is `%21%27%28%29%2A~-._%20%2B%C3%A9%F0%90%90%80`: unlike JS `encodeURIComponent`, it escapes `!'()*`. | probe: `dotnet fsi` on .NET 10, the three lines of `BuildChunkUri` (`CultMeshHttpsContentTransport.cs:114-122`) copied verbatim |
|
||||
| F20 | Node 24.15 global `fetch` (undici) against a raw-socket server. A gzip body (`Content-Encoding: gzip`, `Content-Length: 29`) arrives as 1,000 decoded bytes, while `headers.get("content-length")` still says `29`. A body shorter than its `Content-Length` (400 of 1,000) rejects while reading: `TypeError: terminated`, cause code `UND_ERR_RES_CONTENT_LENGTH_MISMATCH`. A body longer than its `Content-Length` (1,000 against 400) is cut silently to 400 by HTTP framing. With no `Content-Length`, all 1,000 bytes arrive. So a reader that trusts the header refuses valid gzipped content, and it learns nothing the received bytes do not tell it (R §6: Fetch decodes content codings, and its own note calls `Content-Length` unreliable). | probe: `imagination/fetchprobe.mjs` |
|
||||
| F21 | The C# TCP content server is the reference for what a provider does per request (`SendChunkAsync`, `CultMeshTcpContentTransport.cs:384-430`). It runs `NormalizeHash` on the requested hash, refuses a non-blank `RecordKey` that is not `CreateRecordKey(hash)` (ordinal compare, message "record key disagrees with its content hash"), resolves the chunk **by hash** (`_resolveChunk(hash)`), and runs `CultMeshCdn.ValidateChunkPayload` before it sends a byte. So **verify-before-serve on every request is the reference behaviour**. C# chunk storage is one record per chunk, keyed by the chunk record key (`CultMeshDatabaseContentProvider`, `CultMeshContentTransfer.cs:41-71`). Per-chunk storage is the reference layout. | source |
|
||||
| F22 | C1's exports at `b8b825e1` (`packages/cultnet-ts/src/content.ts`): `normalizeHash`, `parseSha256Hex`, `sha256Hex`, `chunkRecordKey`, `artifactRecordKey` (async), `validateManifest`, `encodeManifest`, `decodeManifest`, `packContent`, `CULTMESH_CDN_MANIFEST_SCHEMA`, `DEFAULT_CHUNK_SIZE_BYTES` (4,194,304), `EMPTY_SHA256`, and the types `CultMeshCdnChunkRef`, `CultMeshCdnArtifactManifest`, `CultMeshCdnPackOptions` and `CultMeshCdnPackedChunk`. `sha256Hex` throws a plain `Error` ("WebCrypto SHA-256 is unavailable.") when `crypto.subtle` is missing. `validateManifest` takes the whole manifest (it reads `metadata`). `cultnet-ts/authority` already exports `isLoopbackEndpoint` (`cultmesh-authority.ts:377`: `localhost`, `loopback`, `::1`, `::ffff:7f00:1`, `127.x.x.x`), and `cultmesh-browser` already imports it (`index.ts:23`). | `git show b8b825e1:…`; cut report `cut-c1-shapes.h1` landed names |
|
||||
| F23 | The consumer (StreamPixels `dfbe2a6`, `docs/web-character-customization-map.md`) stores one whole-artifact file per content hash at `/srv/streampixels/content/<hex>`, builds manifests at service boot, and hashes every referenced blob at boot (lines 149-159, 175, 317-325). Its artifacts are 10-125 KB (W10), well under the 4 MiB default chunk. It also builds manifests for `catalog.cc` and `character-artifacts.cc`, whose sizes the map does not give. It mounts the handler at `GET /api/content` on its existing server (line 320). Its sketch, `createHttpsContentHandler(store)` (line 293), takes `(chunkHash, recordKey)` and returns bytes or not-found. | StreamPixels `git show dfbe2a6:docs/web-character-customization-map.md` |
|
||||
| F24 | Build shapes for the two packages. `cultmesh-ts` is CommonJS (`module` and `moduleResolution` `Node16`), and its `package.json` exports only `.` (lines 7-13). `cultmesh-browser` is ESM (`"type": "module"`, `moduleResolution` `Bundler`) and is one file, `src/index.ts`, with no relative imports. A new relative import must therefore be spelled `./content.js`, or Node's ESM loader cannot find it at test time. Both packages' `test` scripts run the glob `dist-test/test/*.test.js`, which the wiring check accepts (`scripts/check-typescript-tests-wired.mjs`). CI runs both in `cultmesh-portability.yml`: the browser at lines 119-120, and `cultmesh-ts` at lines 121-129 under the step name "Verify the realtime frame codec with the TypeScript codec". `esbuild` resolves from the root `devDependencies`, as C1's browser-bundle test shows. | source at `b8b825e1` |
|
||||
| F25 | The existing TLS fixture `packages/cultmesh-ts/test/fixtures/quic-test.p12` is a self-signed P-256 certificate with subject `CN=cultmesh-quic-native-tests` and no subjectAltName (fixtures README). Node's TLS hostname check therefore refuses it for `127.0.0.1` even when it is trusted through `NODE_EXTRA_CA_CERTS`. A C# handler that pins by certificate hash does not care about the name. | source: fixtures README |
|
||||
| F26 | `CultMeshCdn.ValidateChunkPayload`, `HashBytes`, `ValidateManifest` and `NormalizeHash` are `internal`, and `GameCult.Mesh` declares no `InternalsVisibleTo`. A test in `tests/GameCult.Mesh.Tests` can therefore verify chunk bytes only with `SHA256.HashData`, or through a public consumer of the connector. The public surface is `CreateRecordKey`, `PackArtifact`, `PublishAsync` and `ReadArtifact(Async)`. | grep of `CultMeshCdn.cs`, `src/GameCult.Mesh` |
|
||||
| F27 | No CultLib workflow runs `cultmesh-ts` interop tests. `cultnet-interop.yml` (windows-latest, main only) runs `cultnet-ts`'s `test/interop`, and its QUIC native case spawns `dotnet test --filter NativeConnectorReceivesFromExternalProvider` through `runDotnetTestFilter` (`cultnet-interop.test.ts:550,2826`). `cultmesh-ts` has a `test/interop/` folder (a QUIC peer script) but no `test:interop` script. | grep of `.github/workflows`, source |
|
||||
| F28 | **The real provider meets the real reader across processes, on Windows and Linux.** C2's handler, store and `toNodeListener` served on `node:https` with a self-signed P-256 certificate (subjectAltName `IP:127.0.0.1, DNS:localhost`, `CA:FALSE`, `serverAuth`, 100-year validity). They served a 4-chunk artifact (3.5 x 64 KiB) and an empty one. C3's `fetchVerifiedContent` ran in a child `node` with `NODE_EXTRA_CA_CERTS` set to that certificate, through the real global `fetch`, and returned both bodies with the right hashes. Without the variable it rejected `network` (cause `DEPTH_ZERO_SELF_SIGNED_CERT`). Behind a test wrapper that flips byte 0 of every non-empty 200 body, it rejected `chunk-hash-mismatch`. Platforms: Windows (Node 24.15) and a `node:24-trixie` container (Node 24.21). | probe: `imagination/probe/{serve,read,drive}.mjs` over the C2+C3 merge tree `8d1482c8` |
|
||||
| F29 | **The C# consumer that verifies is `CultMeshContentTransferService`, and it runs against an HTTPS route without Odin.** The probed path: `CultMeshDiscoveryService` over a test `RouteSource(<https endpoint>)`, then `CultMeshSessionManager` with `LocalDevelopment` trust and the unmodified `CultMeshHttpsContentTransportConnector`, then `CultMeshSessionContentProvider`, then `FetchAsync(manifest)`. This is the shape of `CultMeshContentSessionTests.TcpContentSessionStreamsLargeBodyAndReusesCommittedBody` with the TCP connector swapped for the HTTPS one. It fetched both artifacts from the TS provider, decoding the **TS-encoded** manifest bytes with `CultDocumentMessagePackSerialization.DeserializeUntyped`. Trust was a `SocketsHttpHandler` whose `SslOptions.CertificateChainPolicy` is `CustomRootTrust` with only the fixture certificate in `CustomTrustStore` (`RevocationMode.NoCheck`). That is real chain and name validation, with no callback and no system roots. The default handler failed with `UntrustedRoot`. Against the tampering wrapper, `FetchAsync` threw `AggregateException` over `InvalidDataException` ("CDN artifact chunk hash does not match its manifest."), leaving only `.<hash>.partial`, never `<hash>.body`. The connector itself does not verify (F6). The C# verifier is the transfer service's `BoundedHashingWriteStream` per chunk and its whole-file hash. The results on the Linux container (self-contained linux-x64 publish) were identical. | probe: `imagination/csprobe/Program.cs` and `probe/drive-cs.mjs`; source `CultMeshContentTransfer.cs:115-330`, `CultMeshContentTransports.cs:138-160` |
|
||||
| F30 | The two consumers request the same URLs. C# sent five requests: four chunks, plus the empty chunk `e3b0…`, which the provider answers intrinsically. Each was `/api/content?chunkHash=<hex>&recordKey=mesh%3Acdn%3Achunk%3A<hex>`. The reader sent the same four chunk URLs, and nothing for the empty chunk, which it verifies locally. | same probes, the provider's request log |
|
||||
| F31 | **Build order decides where the cross-process test lives.** `scripts/run-typescript-workspaces.mjs` builds `cultcache-ts, cultnet-ts, cultmesh-ts, cultmesh-browser`, and each package's `test` compiles `test/**/*.ts`, `test/interop` included. A `cultmesh-ts` test that imports `cultmesh-browser` therefore fails to compile on a clean checkout, while a `cultmesh-browser` test that imports `cultmesh-ts/content` follows the order. Named ESM imports of the CommonJS `cultmesh-ts/content` work from an ESM file: all four provider names import as functions. At `7adbf3cb`, `cultmesh-portability.yml` runs the browser step (lines 119-120) **before** the `cultmesh-ts` step (129-130), which is the opposite of the script's order. The wiring check requires `test/interop/*.test.ts` to be run by a `test:interop` script. | probe (node, ESM import); source |
|
||||
| F32 | Node's `execFile("dotnet")` resolves `dotnet.exe` on Windows without an extension. The C3 merge into `7adbf3cb` is clean at C3 head `67ff3f64` (`git merge-tree` gives `666162a3`). It touches the workflow only at comment lines 105-107, and the reader's exports keep their names. The probe's body pattern `(i*31+7)&255` repeats every 256 bytes, so all of its 64 KiB chunks shared one hash. A parity fixture needs non-periodic bytes, or chunk order is invisible. | probe (node); `git merge-tree` |
|
||||
| F33 | `cultmesh-portability.yml` triggers on `push` only for `main` and `codex/**`. A `hands/**` branch runs it through `pull_request` or `workflow_dispatch`, and both triggers exist. The job `reactive-and-resource-lifetime` has .NET 10 and Node 24 on `windows-latest` and `ubuntu-latest`, and it already builds `tests/GameCult.Mesh.Tests` in earlier steps. | source at `7adbf3cb` |
|
||||
|
||||
## 2. Model page
|
||||
|
||||
There is one row per persistent or wire-visible kind. Identity is what names the
|
||||
kind, lifecycle is what happens to it over time, and authority is who decides.
|
||||
The HTTP answers are new design: no runtime has an HTTPS content server (target,
|
||||
"Why now"). They rest on R, which fetched the specs and sources on 2026-10-19
|
||||
and corrected this page's first draft (R §8). In short:
|
||||
|
||||
- **404 for absent** is universal (R §9). Nix also reads 410 and S3's 403 as
|
||||
absent; we send 404 only.
|
||||
- **400 for a malformed id** comes from the IPFS path gateway (generic client
|
||||
error, with a malformed CID as its example) and bazel-remote (a hash that is
|
||||
not 64 lowercase hex). The OCI blob-pull text defines no 400; its
|
||||
`DIGEST_INVALID` is an upload code, so the first draft was wrong to cite it.
|
||||
- **No system specifies a server answer for a stored object that fails its
|
||||
digest** (R §9). Every one puts verification on the client. bazel-remote
|
||||
streams without checking, so a failure after the headers have gone "cannot
|
||||
change the status" (R §4). That is the scar `verify-before-serve` avoids by
|
||||
buffering and hashing before the status line.
|
||||
- **Range** may be ignored (RFC 9110 §14.2). OCI says registries SHOULD
|
||||
support it, which the first draft called "optional". We ignore it.
|
||||
- **`Cache-Control: public, max-age=29030400, immutable`** is the IPFS path
|
||||
gateway's MUST for immutable content (R §1). The trustless gateway itself
|
||||
does not state it.
|
||||
- **The Fetch facts** decide the reader (R §6). Fetch decodes content codings
|
||||
before the page sees the body, so `Content-Length` describes the encoded
|
||||
body, which F20 reproduces in Node. `Content-Length` is CORS-safelisted, and
|
||||
`ETag` is not. `Access-Control-Allow-Origin: *` passes the CORS check for any
|
||||
request whose credentials mode is not `include`.
|
||||
|
||||
| Kind | Identity (what names it) | Lifecycle | Authority (who decides; forbidden writers) |
|
||||
|---|---|---|---|
|
||||
| **Manifest** `gamecult.mesh.cdn_artifact_manifest.v1` | The record key (row below), derived from `artifactId`, `version` and `contentHash`. On the wire it is the 10-array of F1. Its namespace is the schema id. Two manifests for one body under different ids or versions are distinct and legitimate. | Immutable once packed. A new body or version is a new manifest and a new key. Nothing revises one in place, and deletion is not in scope (no GC). `createdAtUtc` is set at pack, and a caller may pin it so that repacking is reproducible. | `packContent` in `cultnet-ts/content` (C1, `b8b825e1`) is the only TS writer. The encoder and decoder in the same module are the only TS byte owners. The reader takes a manifest value and never builds or edits one. **Forbidden:** a second TS manifest type or encoder in `cultmesh-ts`, `cultmesh-browser` or a consumer, and any URL or endpoint field. |
|
||||
| **Chunk ref** `CultMeshCdnChunkRef` | Its `chunkHash` (SHA-256 of the chunk bytes) within one manifest, plus `offset`. Two refs may share a hash: this is legal, and the reader fetches each ref. | Immutable inside its manifest. `recordKey` is derived and may be blank on read. A blank key goes on the wire as `recordKey=` (F5), and the provider accepts it. | The packer writes. `validateManifest` (C1) judges contiguity, non-negative sizes and the size sum. The reader and provider never edit one. |
|
||||
| **Chunk record key** `mesh:cdn:chunk:<hex>` | Fully derived from `normalizeHash(chunkHash)`. It is injective only over valid hex, because C# accepts any non-blank string (F7). | Derived on demand, never stored independently of a manifest. | `chunkRecordKey` in `cultnet-ts/content`. On the HTTP wire the provider **checks** a non-blank `recordKey` against it, as the C# TCP server does (F21), and never derives a file from it. |
|
||||
| **Artifact record key** `mesh:cdn:artifact:<token>:<versionToken\|unversioned>:<hex>` | Derived by `StableToken` (F8). It is not injective: `A/b` and `a b` both give `a-b`, and the content hash at the end disambiguates. | Derived on demand. | `artifactRecordKey` in `cultnet-ts/content` (async; C1 deviation). Truth comes from the C#-written `contracts/cultmesh/cdn-record-key-vectors.json`. Neither C2 nor C3 uses it. |
|
||||
| **Content hash / chunk hash** | Lowercase 64-hex SHA-256. On input, `normalizeHash` accepts `sha256:`, any case and surrounding whitespace, as C# does. The HTTP boundary and the reader also demand 64 hex through `parseSha256Hex`. | Immutable. It *is* the identity. | `sha256Hex` and `parseSha256Hex` (C1) are the only TS hasher and the only hex rule. The provider and reader call them and keep no copy. The negative greps pin this. |
|
||||
| **Stored chunk** (provider disk) | File `<root>/<hex>`, flat, one file per distinct chunk hash. This is the reference layout (F21: C# stores one record per chunk). For a single-chunk artifact the chunk hash equals the content hash, so StreamPixels' whole-artifact file `/srv/streampixels/content/<hex>` (F23) is already a stored chunk. A multi-chunk artifact's publisher writes `packContent(...).chunks`, one file each. | Written by the publisher outside the provider, and never written by the provider. Immutable. Deletion has no owner (GC is out of scope). A corrupt or oversized file stays on disk and is never served. | The publisher writes; **the provider is read-only**. The provider verifies every read against its name before serving (`verify-before-serve`, the C# per-request behaviour of F21). A symlink or a replaced file cannot leak anything, because only bytes that hash to the requested name are ever served. **Forbidden:** any path derived from `recordKey`, the URL path or an unvalidated hash. |
|
||||
| **Provider store index** | **None.** The file name is the index. The directory store maps hex to `<root>/<hex>` on each request, after its own `parseSha256Hex` equality check. No in-memory table exists, so nothing can go stale. | Not applicable: it is computed per request. | `ContentChunkStore` is the port: `readChunk(chunkHash, maxBytes) → Promise<Uint8Array \| undefined>`. `createDirectoryChunkStore(root)` stats before reading and rejects a file over `maxBytes` with `ContentChunkTooLargeError`, so an oversized file is never read into memory. A host may supply another store, for example one indexed from manifests, and the handler verifies whatever any store returns. |
|
||||
| **The empty chunk** `e3b0c442…b855` | The SHA-256 of zero bytes. | Intrinsic: it needs no file. | The provider answers it as 200 with `Content-Length: 0` without consulting the store, so empty artifacts work against any store (bazel-remote does the same, R §4). The reader verifies it locally and sends no request. |
|
||||
| **Wire request** | `GET` or `HEAD <mount>?chunkHash=<hash>&recordKey=<key>`. Extra query parameters are ignored, because an endpoint may carry its own (F5). The mount path belongs to the host, and the handler never reads it. | Stateless. | Built by the C# connector or the TS reader. The TS reader builds the URL **byte-identical** to `BuildChunkUri`, `Uri.EscapeDataString` escaping (F19) and the empty-query `?&` join included, so C# and browser clients share HTTP cache entries. |
|
||||
| **Content coding** | Identity, always. | Not applicable. | Provider: no `Content-Encoding`, and `Cache-Control` carries `no-transform`, which forbids an intermediary to transform the content (RFC 9111 §5.2.2.6). That keeps the C# connector's `Content-Length` check meaningful (F6). The reader does not rely on it: it judges the bytes it received (F20). |
|
||||
| **Answer 200** (GET) | The body is exactly the verified chunk bytes. | Immutable per URL. | Provider. Headers: `Content-Length: <size>`, `Content-Type: application/octet-stream`, `X-Content-Type-Options: nosniff`, `Cache-Control: public, max-age=31536000, immutable, no-transform` and `Access-Control-Allow-Origin: *`. Octet-stream with `nosniff` keeps a public chunk from rendering as HTML on the host's origin. The IPFS `Content-Disposition: attachment` adds nothing over that for a fetch client, so it is not sent. |
|
||||
| **Answer 200** (HEAD) | The same headers as GET, with no body. | Same as GET. | Provider. HEAD verifies exactly as GET does, so it can never advertise a corrupt chunk. RFC 9110 §8.6 requires HEAD's `Content-Length` to equal GET's. |
|
||||
| **No ETag, no 304** | Not applicable. | Not applicable. | Provider. `immutable` with a one-year max-age means browsers do not revalidate. `ETag` is not CORS-exposed (R §6), and the URL is already the content identity. An `ETag` without a 304 path is noise, and a 304 path is a second answer to test. The first draft had both, and both are cut. |
|
||||
| **Answer 400** | `chunkHash` missing, repeated, or not 64 hex after `normalizeHash`. Or `recordKey` repeated, or non-blank and not exactly `mesh:cdn:chunk:<hex>` (the C# server's "record key disagrees" rule, F21). | Stateless. | Provider. Body is a fixed `text/plain; charset=utf-8` reason with no echo of input (bazel-remote echoes the URL; we do not). Headers: `nosniff`, `Cache-Control: no-store`, `Access-Control-Allow-Origin: *`. |
|
||||
| **Answer 404** | A valid hash whose chunk the store does not hold. | Stateless; a later publish can turn it into 200. | Provider. Same error headers as 400. `no-store` keeps an absence from being cached as immutable. |
|
||||
| **Answer 405** | Any method but GET and HEAD, OPTIONS included. | Stateless. | Provider, with `Allow: GET, HEAD`. The reader's GET is a CORS simple request (no custom headers, credentials omitted) and never preflights. A client that adds headers would preflight, and its preflight is refused, which is correct because the provider honours no request header. The first draft's 204 preflight answer is cut. |
|
||||
| **Answer 500** | The store returned bytes whose SHA-256 is not the requested hash; the store holds a file over `maxChunkBytes`; or the store failed. | Persists until the operator replaces the file. | Provider. It sends a fixed reason and none of the chunk's bytes, then calls the host's `onServerFault({ chunkHash, kind, error })` with `kind` ∈ {`hash-mismatch`, `too-large`, `store-error`}, so the host can fail health. A throwing hook does not change the answer. It is 500 and not 404 because the IPFS trustless gateway reserves 500 for "genuine server errors" (R §1), and corruption is one the operator must see, while a 404 would hide it as absence. |
|
||||
| **Range** | Not supported. A `Range` header is ignored, and the answer is 200 with the full chunk (RFC 9110 §14.2). No `Accept-Ranges` header is sent. | Not applicable. | Provider. The chunk is the resumption unit, and resumable TS state is out of scope. |
|
||||
| **Provider memory** | At most `maxChunkBytes` buffered per in-flight request. | Per request. | Provider. Buffering is forced, because the status line cannot be sent before the hash is known (the bazel-remote scar above). `maxChunkBytes` defaults to `DEFAULT_CHUNK_SIZE_BYTES` (4 MiB) and must lie in 1..2147483647. The host bounds concurrency (server `maxConnections` or a reverse proxy), and the README says so. |
|
||||
| **Reader result / error** | A `Uint8Array` of the whole body, or one `CultMeshContentError` with `code` ∈ {`manifest-invalid`, `too-large`, `insecure-endpoint`, `webcrypto-unavailable`, `network`, `http-status`, `length-mismatch`, `chunk-hash-mismatch`, `content-hash-mismatch`}, plus `chunkHash` and `status` where they apply. A caller abort rejects with the signal's own `reason`, unwrapped. Bad options reject with `RangeError`. | Transient, held in memory only. No byte of a failed chunk or body is returned, in whole or in part (`verify-before-use`). | `fetchVerifiedContent` (and `fetchVerifiedChunk`) in `cultmesh-browser`. Concurrency defaults to 4 and is bounded 1..32 (the C# `CultMeshContentTransferOptions` bounds). `maxBytes` defaults to 256 MiB and is checked before any request. The scheme must be `https:`, or `http:` where `isLoopbackEndpoint` (F22) holds. Only status 200 is accepted. **`Content-Length` is not consulted** (F20). The body is read through a stream into the chunk's slot, a body that runs past the chunk's size is cancelled at the first excess byte, and one that ends short is refused. |
|
||||
| **Endpoint** | A URL the caller resolved through CultMesh. | Per call; never persisted in content. | The caller. **Forbidden:** any manifest, chunk-ref or record field carrying it (`no-urls-in-content`). Error messages never echo it, because an endpoint query may carry a tenant or a token. |
|
||||
| **Vector files** | `content-vectors.cs-written.json` (C#), `.rs-written.json` (Rust), `content-vectors.ts-written.json` (TS, manifests only, own inputs) and `cdn-record-key-vectors.json` (C#, record-key traps), all landed by C1. | Each is rewritten only by its writer under `CULTNET_WRITE_VECTORS=1`. CI never sets the variable: the committed files are the contract. | Each writer owns its file. C2 and C3 add no vector file. HTTP behaviour is pinned by tests and, in C4, by the C# transfer service driving the unmodified connector (F29). |
|
||||
|
||||
Two cells rest on reasoning rather than a probe: that the host, not the
|
||||
provider, bounds concurrency, and the cost of hashing on HEAD. Both are
|
||||
defaults a later cut can revise without changing any shape.
|
||||
|
||||
## 3. Rationale
|
||||
|
||||
**Where the CDN shapes live: `cultnet-ts/src/content.ts`, exported as
|
||||
`cultnet-ts/content`.** C1 landed this at `b8b825e1`. Both consumers already
|
||||
depend on `cultnet-ts`, and its subpaths are the repo's established
|
||||
browser-safe entry points (F14). The placement matches Rust, where
|
||||
`cultnet-rs/src/content.rs` owns the same rules.
|
||||
|
||||
**Hashing is WebCrypto in one place.** `crypto.subtle` exists in browsers
|
||||
(secure contexts) and in Node 20+, so one hasher serves the packer, the
|
||||
provider and the reader. WebCrypto has no incremental digest, so every hash
|
||||
needs its bytes in memory. That fits all three users: the reader holds the
|
||||
body (the target keeps artifacts in memory), the provider holds one chunk at a
|
||||
time, and the packer holds the body.
|
||||
|
||||
**Decoding follows the siblings.** C1's report records two places where its
|
||||
decoder is stricter than MessagePack-CSharp: an exact field count, and refusal
|
||||
of nil strings. Ruling `leaf-acceptance-follows-reference` may make that a
|
||||
finding. That belongs to C1's Soul pass, not to C2 or C3, which take manifest
|
||||
values and never decode bytes.
|
||||
|
||||
**No TS CultCache document definition for manifests yet.** This is still
|
||||
follow-up `ts-manifest-document-definition`.
|
||||
|
||||
**Provider: `cultmesh-ts/content`, a request-to-answer core, one store and one
|
||||
Node adapter, in one file.** `createContentChunkHandler(store, options)`
|
||||
returns `(request: { method, url }) => Promise<{ status, headers, body? }>`.
|
||||
The request carries no headers, because the provider honours none: Range is
|
||||
ignored, and conditional requests have no answer now that ETag is cut. The
|
||||
core reads only the query, never the path, which is what makes
|
||||
`chunk-only-route` structural rather than checked. `toNodeListener(handler)`
|
||||
is about 15 lines for `node:http(s)`, and Express, Fastify or Next mount the
|
||||
core the same way. The name says "chunk" because the handler answers chunks
|
||||
only. StreamPixels' sketch name, `createHttpsContentHandler` (F23), implied it
|
||||
does TLS, and it does not: the host terminates TLS. One file, not three,
|
||||
because the store and adapter are about 50 lines between them and have no
|
||||
consumer apart from the core. `cultmesh-ts` is already a Node package, so
|
||||
`node:fs` in the file costs nothing.
|
||||
|
||||
**Store layout: per-chunk flat files.** This is the reference layout (F21),
|
||||
and it needs no index that can go stale. It coincides with StreamPixels'
|
||||
whole-artifact files, because every StreamPixels artifact is a single chunk
|
||||
(F23). The alternative is a store indexed from manifests, mapping a chunk hash
|
||||
to (artifact file, offset, size), so that multi-chunk artifacts could be
|
||||
stored whole. It is not shipped, because it adds a derived index that must be
|
||||
rebuilt when manifests change, and no consumer needs it. A host that wants it
|
||||
writes about 30 lines against the port, and the handler still verifies every
|
||||
byte. **Consumer note for StreamPixels B3:** its `catalog.cc` and
|
||||
`character-artifacts.cc` manifests must stay single-chunk, so their files are
|
||||
the stored chunk. Either they are packed with `chunkSizeBytes` at least the
|
||||
file size and the provider's `maxChunkBytes` is raised to match, or B3
|
||||
publishes the chunk files.
|
||||
|
||||
**Verify on every request, not once at boot.** The invariant says "serves
|
||||
only bytes whose SHA-256 equals the requested chunk hash", and a boot check
|
||||
cannot see a file replaced after boot. C# verifies per request (F21). A
|
||||
"verified at boot" cache would be a cache pretending to be truth. StreamPixels'
|
||||
boot check is its own deploy gate (missing blob means failed health), and it
|
||||
stays useful beside per-request verification. The cost is one SHA-256 of at
|
||||
most 4 MiB per request, and the immutable one-year caching keeps repeats away
|
||||
from the origin.
|
||||
|
||||
**Reader: `fetchVerifiedContent(endpoint, manifest, options)` and
|
||||
`fetchVerifiedChunk(endpoint, chunkHash, sizeBytes, options)` in
|
||||
`cultmesh-browser`, over one internal per-chunk primitive.** The second is the
|
||||
"fetch by hash" a general consumer expects for a single-chunk artifact. It
|
||||
builds no manifest: it validates the hash and size, sends
|
||||
`recordKey=chunkRecordKey(hash)` so its URL equals a C#-packed manifest's, and
|
||||
runs the same primitive. The whole-body check is the chunk check, because for
|
||||
a single chunk the two hashes are the same. Before any request,
|
||||
`fetchVerifiedContent` runs `validateManifest`, `parseSha256Hex` on every
|
||||
hash, the zero-size rule (F18) and the `maxBytes` cap. It then fills a private
|
||||
buffer of `sizeBytes` through a sliding window of at most
|
||||
`maxConcurrentChunkRequests` requests. It uses `credentials: "omit"` and an
|
||||
internal `AbortController` chained to the caller's signal, so the first
|
||||
failure cancels every request still in flight. It hashes each chunk in place
|
||||
and returns the buffer only after the whole-body hash matches.
|
||||
|
||||
**Why the reader ignores `Content-Length`.** The target says the reader
|
||||
"refuses a response whose length differs from the advertised size before
|
||||
reading it all". The header cannot carry that duty. Fetch decodes content
|
||||
codings, and a proxy that gzips in spite of `no-transform` turns a correct
|
||||
1,000-byte chunk into a 29-byte `Content-Length` (F20, R §6). In a browser the
|
||||
reader cannot even see `Content-Encoding` cross-origin, because it is not
|
||||
safelisted, so it cannot tell that case apart. What the invariant protects is
|
||||
bounded reading and no acceptance of a wrong length. The streaming cap
|
||||
delivers both from the bytes actually received: the reader stops at the first
|
||||
byte past the chunk's size, and refuses a short body at its end. The target
|
||||
sentence should be reworded (reported to Self as a contradiction).
|
||||
|
||||
**C3 does not wait for C2.** C3's live test serves from a stub `node:http`
|
||||
loopback server written in the test (about 30 lines). The stub sends what the
|
||||
model page says, plus the misbehaviours: gzip, a short body, an endless body,
|
||||
and wrong bytes. The real provider meets the real reader in C4. This keeps
|
||||
the two cuts parallel, and it lets C3 test misbehaviours the real provider can
|
||||
never produce.
|
||||
|
||||
**C4: the proof lives in `cultmesh-browser/test/interop`, with the C# half
|
||||
in `GameCult.Mesh.Tests`.** The orchestrator is Node, following the QUIC
|
||||
precedent (F13): it starts the provider, runs both consumers and judges what
|
||||
the provider saw. It lives in `cultmesh-browser` and not `cultmesh-ts`,
|
||||
because the workspace build order compiles `cultmesh-ts` first (F31). The
|
||||
browser package can import `cultmesh-ts/content` with types. The reverse
|
||||
would need either an untyped script or a build-order exception. The provider
|
||||
runs in its own process rather than the orchestrator's, so each of the three
|
||||
parties is a separate OS process, and the orchestrator never shares an event
|
||||
loop with the server it judges. The C# half drives the connector through
|
||||
`CultMeshContentTransferService` (F29), because the connector alone does not
|
||||
verify (F6). A parity test that stopped at the connector would show that C#
|
||||
receives bytes, not that C# refuses bad ones. TLS trust is a committed
|
||||
fixture certificate, as the QUIC fixture is (F25). The C# side anchors it
|
||||
through `CustomRootTrust`, which keeps chain and name validation and adds no
|
||||
code path that can accept a different certificate. A hash-pinning
|
||||
`ServerCertificateCustomValidationCallback` would skip name validation and
|
||||
add a callback that has to be read to be trusted. Corruption comes in two
|
||||
kinds. On disk, the real provider refuses it with 500 (C2's
|
||||
verify-before-serve). In flight, a test wrapper flips bytes after the
|
||||
provider has verified them. The in-flight leg is what proves that both
|
||||
consumers verify.
|
||||
|
||||
**"Released"** is ruled: `release-shape-gitlink` (Defaulted by Self).
|
||||
|
||||
## 4. Cut sequence
|
||||
|
||||
C1 (`cut-c1-shapes.r1`, report `h1` under a Soul pass), C2
|
||||
(`cut-c2-provider.r1`) and C3 (`cut-c3-reader.r1`) are specified in the mind.
|
||||
C2 and C3 run in parallel once C1 merges, each in its own worktree from C1's
|
||||
merge. Both are based on `b8b825e1`, and they touch disjoint files except
|
||||
`cultmesh-portability.yml`, where their hunks lie 15 lines apart. What
|
||||
remains:
|
||||
|
||||
4. **C4 `c4-parity`** (`cut-c4-parity.r1`, branch
|
||||
`hands/cultmesh-ts-content-c4-parity` from `7adbf3cb` plus the C3 merge),
|
||||
waiting on C2 and C3. The spec carries the plan. In one paragraph: a
|
||||
node:test orchestrator in `cultmesh-browser/test/interop` packs three
|
||||
artifacts (multi-chunk, single and empty) and spawns the C2 provider as its
|
||||
own `node:https` process with three mounts: honest, tampered and corrupt
|
||||
store. It runs the C3 reader in a child with `NODE_EXTRA_CA_CERTS`, once
|
||||
more without it as a control, and then `dotnet test` over a new
|
||||
`CultMeshHttpsContentInteropTests`. That suite drives the unmodified
|
||||
connector through `CultMeshContentTransferService` and trusts the committed
|
||||
fixture through `CustomRootTrust`. The orchestrator then judges URL parity
|
||||
and the provider's fault log. CI gains the step on both OSes, and the
|
||||
browser and `cultmesh-ts` steps swap to follow the workspace order (F31).
|
||||
5. **C5 `c5-release`**, waiting on C4 and the Soul passes of C2 to C4. Ruling
|
||||
`release-shape-gitlink`: merged to CultLib `main` with version bumps,
|
||||
CHANGELOGs and git tags, and no npm publish. Line-level plan:
|
||||
- Bumps (minor; every change is additive): `cultnet-ts` 0.2.0 to 0.3.0
|
||||
(`./content`), `cultmesh-ts` 0.1.1 to 0.2.0 (`./content`), and `cultmesh-browser` 0.3.0 to 0.4.0 (the reader).
|
||||
`@gamecult/cultcache-ts` gains only the `checkPayloadDepth` re-export:
|
||||
a patch bump, 0.14.0 to 0.14.1 (its version at `b8b825e1`).
|
||||
- Dependency ranges: `cultmesh-ts` and `cultmesh-browser` move
|
||||
`cultnet-ts` to `^0.3.0`. `cultnet-ts`'s `^0.14.0` on
|
||||
`@gamecult/cultcache-ts` already admits 0.14.1 and stays. C4 gives
|
||||
`cultmesh-browser` a devDependency `cultmesh-ts: ^0.1.1`, which does not
|
||||
admit 0.2.0, so it moves to `^0.2.0` in the same commit. Otherwise npm
|
||||
looks for 0.2.0 on the registry and the install fails. `npm install
|
||||
--no-package-lock`, the four workspace tests and the browser's
|
||||
`test:interop` run after the bumps.
|
||||
- A `CHANGELOG.md` per bumped package. None exists in any package at
|
||||
`b8b825e1`, so C5 creates four, each naming the live surface and not the
|
||||
history (Keep a Changelog headings).
|
||||
- A Soul pass over the release diff, then four tags: `cultcache-ts-v…`,
|
||||
`cultnet-ts-v0.3.0`, `cultmesh-ts-v0.2.0` and `cultmesh-browser-v0.4.0`.
|
||||
These go in **two pushes** (at most three tags per push, SKILL.md):
|
||||
`cultcache-ts` first, because its tag triggers `publish-packages.yml`
|
||||
(F16), and the three plane tags second. Whether the `cultcache-ts` tag's
|
||||
npm publish job still runs, and to where, is checked before that push;
|
||||
follow-up `npm-publish-ts-packages` owns the rest.
|
||||
- The consumer's pickup (StreamPixels bumping `vendor/CultLib` to the
|
||||
tagged commit) is StreamPixels' B-cuts, not this campaign.
|
||||
- Size: about 60 lines of version and CHANGELOG text; a short Hands pass.
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
# TypeScript CultMesh Content Plane Target
|
||||
|
||||
Campaign `cultmesh-ts-content`. Self wrote this on 2026-10-19 for the
|
||||
StreamPixels M1 live character preview. That preview is a three.js page that
|
||||
loads glTF bodies and accessories by content hash from Yggdrasil.
|
||||
|
||||
## Why now
|
||||
|
||||
CultMesh moves large immutable artifacts as content-addressed chunks described
|
||||
by a `gamecult.mesh.cdn_artifact_manifest`. C# packs manifests, serves chunks
|
||||
over TCP (`CultMeshContentServer`), fetches them over HTTPS
|
||||
(`CultMeshHttpsContentTransportConnector`), and verifies each chunk and the
|
||||
whole body (`CultMeshContentTransferService`). Rust speaks the chunk messages
|
||||
through `cultnet-rs` `fetch_content` and `answer_content_chunk_request`.
|
||||
TypeScript has none of this: no manifest type, no packer, no provider and no
|
||||
reader. And no runtime has an HTTPS content *server*; the C# HTTPS connector
|
||||
has never had a peer.
|
||||
|
||||
Standing ruling (StreamPixels `state/map.yaml`, "Content plane", operator
|
||||
2026-09-15): HTTPS content transport serves both browser and overlay.
|
||||
Yggdrasil plus SHA-256 verification is the trust boundary for now.
|
||||
Distributed zero-trust distribution is future work. The catalog never stores
|
||||
URLs; clients resolve content through CultMesh.
|
||||
|
||||
## Target state
|
||||
|
||||
- **One TypeScript owner of the CDN document shapes.** It holds the manifest
|
||||
and chunk-ref types, their MessagePack wire form, `normalizeHash`, the
|
||||
record-key grammar (`mesh:cdn:chunk:<hex>` and
|
||||
`mesh:cdn:artifact:<token>:<version>:<hex>`), and the packer. A manifest
|
||||
that TypeScript packs is byte-identical to the one C# packs from the same
|
||||
bytes and options. The owner can be imported in a browser, because the
|
||||
reader needs it there.
|
||||
- **An HTTPS content provider in `cultmesh-ts`.** It answers
|
||||
`GET <endpoint>?chunkHash=<hex>&recordKey=<key>` with exactly the chunk's
|
||||
bytes and a `Content-Length` equal to the chunk's size: the request shape
|
||||
the C# connector sends (`CultMeshHttpsContentTransport.cs` `BuildChunkUri`).
|
||||
Its core is framework-neutral: a request maps to an answer, and a host can
|
||||
mount it on `node:https` or on any HTTP framework. It reads from a content
|
||||
store port. The store shipped with it serves content-addressed files from a
|
||||
directory outside git. The provider never serves a byte it has not
|
||||
verified against its hash, and it never serves anything but a chunk:
|
||||
there is no generic static-file route.
|
||||
- **A verifying reader in `cultmesh-browser`.** Given a resolved endpoint and
|
||||
a manifest, it fetches the manifest's chunks over HTTPS with the C# query
|
||||
shape. It checks each response's length, each chunk's SHA-256 and the whole
|
||||
body's SHA-256 with WebCrypto, and returns the bytes or throws a typed
|
||||
error. No byte that failed its hash reaches the caller, in whole or in part.
|
||||
It runs in browsers and in Node 20+.
|
||||
- **Endpoints come from CultMesh, not from content.** The reader takes the
|
||||
endpoint as an argument that the caller resolved through CultMesh. Manifests
|
||||
carry no URL.
|
||||
|
||||
## Invariants
|
||||
|
||||
- `wire-parity`: manifests, chunk refs and record keys are byte-identical
|
||||
across C#, Rust and TypeScript for the same input. The C# runtime is the
|
||||
reference. TypeScript writes and reads the shared vectors in
|
||||
`contracts/cultmesh/`.
|
||||
- `https-shape-parity`: the C# `CultMeshHttpsContentTransportConnector`
|
||||
fetches content from the TypeScript provider unmodified, in a cross-process
|
||||
test. The TypeScript reader fetches the same chunks from the same provider.
|
||||
- `verify-before-use`: the reader releases no byte that failed its chunk hash
|
||||
or the whole-body hash. It refuses a body longer than the advertised size at
|
||||
its first excess byte, and a shorter one at its end. It does not trust
|
||||
response length headers, because Fetch decodes content codings and a proxy
|
||||
can rewrite them (research 2026-10-19, section 6).
|
||||
- `verify-before-serve`: the provider serves only bytes whose SHA-256 equals
|
||||
the requested chunk hash.
|
||||
- `chunk-only-route`: the provider answers only chunk requests. A path, a
|
||||
filename or a hash outside the store can never select a file.
|
||||
- `no-urls-in-content`: no manifest, chunk ref or record carries an endpoint.
|
||||
- `one-owner-per-shape`: one TypeScript module owns the CDN shapes, and both
|
||||
packages consume it. Neither package keeps a private copy.
|
||||
- `general-purpose`: the plane is judged by what a reasonable consumer
|
||||
expects, not only by StreamPixels' slice (operator, 2026-09-30). Multi-chunk
|
||||
artifacts, empty artifacts, cancellation, bounded concurrency and browser
|
||||
cross-origin use all work.
|
||||
- `released`: the npm packages StreamPixels vendors are published at versions
|
||||
that contain this plane. The release is its own cut with a Soul pass before
|
||||
the tag.
|
||||
|
||||
## Not in this campaign
|
||||
|
||||
- Odin route verification for content. Single-host trust is ruled for now.
|
||||
- Resumable or durable transfer state in TypeScript. The browser holds
|
||||
artifacts in memory.
|
||||
- Distributed or zero-trust distribution, and peer serving.
|
||||
- A TypeScript TCP or RUDP content server, and an HTTPS content server in Rust
|
||||
or C#.
|
||||
- Garbage collection of stored blobs.
|
||||
- StreamPixels' own export, publish and service wiring (its B2 and B3 cuts).
|
||||
StreamPixels is the consumer, not part of this change.
|
||||
|
||||
## Consumer
|
||||
|
||||
StreamPixels `map/web-customize` (dfbe2a6),
|
||||
`docs/web-character-customization-map.md` section B1. Each option's artifact is
|
||||
10-125 KB, and a full avatar is 0.5-0.8 MB. The browser fetches them. The
|
||||
service serves them from `/srv/streampixels/content/` on Yggdrasil, outside
|
||||
git, and hashes every referenced blob at boot.
|
||||
|
|
@ -52,160 +52,6 @@ Other Body facts that shape the map:
|
|||
AetheriaEve `AetheriaRuntimeCatalogStore` (directory-store pages).
|
||||
- **Kotlin reads no store files.** It sees CultNet raw snapshots only.
|
||||
|
||||
### Body at 2026-10-12 (Imagination refresh, `imagination-variants-refresh`)
|
||||
|
||||
Probes run 2026-10-12 from `F:\Projects\CultLib-c2afix` with git only (no builds), after
|
||||
`git fetch origin`. Scratch worktrees under the session scratchpad were removed afterwards.
|
||||
|
||||
- **C2a head.** `hands/variants-c2a` = `origin/hands/variants-c2a` = `de401056`, worktree clean.
|
||||
`main` = `origin/main` = `b3bd8dd5`. Merge base `f789c8d1` (2026-09-30). C2a is 207 commits
|
||||
ahead, main 453. `15ccdbb9`, the base of the five 2026-10-02 specs, is a C2a commit, not a
|
||||
main commit; C2a has never merged main.
|
||||
- **Overlap.** `git diff --name-only` from the merge base: C2a touches 141 files, main 285, 33 in
|
||||
common. In C#, main changed `CultCache.cs` (+18 -4: union guard, internal `Clock` seam),
|
||||
`CultDocumentMessagePackSerialization.cs` (+9 -3: nesting bound 256), and the persistence and
|
||||
schema-compatibility contracts (+7, +21). Everything else in common is under `packages/`:
|
||||
cultmesh-ts `src/index.ts` (main +372 -433, C2a +189 -356), cultcache-rs `lib.rs` (main +276,
|
||||
C2a +761 -171), cultcache-ts store and cache, cultnet-ts replication.
|
||||
- **Merge probe.** `git merge --no-ff --no-commit b3bd8dd5` onto `de401056` in a throwaway
|
||||
worktree: 16 files conflict, 32 hunks. `CultCache.cs` has one: C2a made `MintStoredAt` an
|
||||
`internal static` (the store's `StoredAtOver` calls it), main made it an instance method over a
|
||||
new `Clock` seam for its union golden. The cultmesh-ts conflicts carry a semantic one: main
|
||||
added payload-parse acceptance to the store document (an `accepts` predicate that decodes the
|
||||
payload, and the test "store document handles choose compatible foreign schema payloads"),
|
||||
which C2a's retire-schema-alias rule forbids. Main still carries `preserveIncomingSchemaId`
|
||||
and `resolve_document_and_schema_id_for_raw_record`, which C2a deleted.
|
||||
- **The stash.** `stash@{0}` in `CultLib-c2afix`, "h3 WIP 2026-10-03: commit-time check-only
|
||||
keys, stopped for quota; unverified", based on `de401056`, 2 files, +186 -47
|
||||
(`CultCache.cs`, `DirectoryMessagePackBackingStore.cs`). It is a draft of the operator's
|
||||
at-commit ruling (`unheld-removal-refused-at-commit`): `CultCommitRequest.Checks`, staged
|
||||
checks on each writable store judged under the lock by `RefuseUndeclaredOnDisk`, both stores'
|
||||
unlocked removal reads deleted, and `ExpectUnchanged` comparing foreign records at their read
|
||||
versions (`CultForeignRecord.StoredVersion`). No tests, no contract text. Read with
|
||||
`git stash show -p`; not applied or dropped.
|
||||
- **The High finding is already fixed.** `rawbytes-in-place-edit-lost` (content-version s2) was
|
||||
fixed by h3 (`2a3773a4`, range head `a935d15c`: `RawBytes` deleted, the store owns a file's
|
||||
bytes) and Soul's verdict `cut-c2a-content-version.s3` holds it, with `rawbytes-key-drop-unpinned`
|
||||
retired by deletion and the five s1 pins (`cache-wrote-version-unpinned`,
|
||||
`expect-on-removed-unpinned`, `no-condition-hash-unpinned`, `staleness-own-upsert-unpinned`,
|
||||
`unstored-same-tick-unpinned`) caught at head. `git grep RawBytes de401056 -- src tests`: no
|
||||
match. Those findings were never closed in the mind.
|
||||
|
||||
### Body at 2026-10-15: element ids and the MessagePack walk (Modeling, from the Imagination and Soul passes)
|
||||
|
||||
Probes ran on Yggdrasil (`mcr.microsoft.com/dotnet/sdk:10.0`, MessagePack 3.1.7 in every csproj) as probe
|
||||
tests that were never committed: at `6afa8ed9` for I1 to I4, at `494ad9ab` for r4 and r5, at `6e70626b` for r6.
|
||||
Logs and sources are under `F:\Projects\eureka-handoff-aux-2026-10-15\scratchpad-self-2026-10-14b-eureka\`
|
||||
(`imag\`, `imag-r4\`, `imag-r5\`; Soul's `soul-c2a-id-holes-s5\` is in that session's scratchpad). The typed
|
||||
record of each fact is in the mind: `variants:cut_spec:cut-c2a-element-id-holes.r5` and `.r6` (`first`),
|
||||
verdicts `cut-c2a-element-id-holes.s5` and `.s6`, spec `cut-c2a-ids-header.r1` (`first`). This section keeps
|
||||
what was run and what it showed.
|
||||
|
||||
**What an id is on disk and in memory.**
|
||||
- An id is an ordinary keyed string in the payload. A list element whose `[CultElementId]` member holds
|
||||
`a1b2c3d4e5f6` and one whose plain `Tag` holds the same value encode to identical bytes
|
||||
(`92A1649192AC61316232633364346535663607`; `c2a-ids-header` r1 probe, base `2774a7fe`). No reader can tell
|
||||
from a record's bytes that it holds ids; only the store header says so.
|
||||
- `StubArm`, a union arm that is not an object type, is never minted (`imag\imag1.log`, I1): for a document of
|
||||
`StubArm` and `Unit8`, `Plan` counts 1, `StubArm.Id` stays `''`, `Unit8.Id` is minted, `Inspect` reports 0
|
||||
problems.
|
||||
- Resolver over-mark (I2): a consumer `[assembly: CultCacheFormatterResolver]` claims a keyed arm with a
|
||||
formatter that writes only `N`. The cache mints an id in memory, `Holds` is true and the header is stamped
|
||||
`v3`, but the payload (`92A263319192009104`) carries no id, and a reopen mints a different one (`8f5e929888ca`
|
||||
in memory, `a1ea2b683d98` after reload). The header claims ids the disk lacks.
|
||||
- `[MessagePackFormatter]` on an element type is honoured (I3): the attribute formatter wrote
|
||||
`92A26131919104`, no id, and `Inspect` reported 0 problems at that base. Registration refuses it from r5 on
|
||||
(`declares [MessagePackFormatter]`).
|
||||
- Python store (I4): `py-store/leaf-record.cc` (header `v1`, ids `000000000021/22`) loads in C# with
|
||||
`HoldsIds = true`, `IdsInMemoryOnly = false`. A C# write that adds an unrelated record copies it forward,
|
||||
the header stays `v1`, and the ids survive reload. cultcache-py, -ts and -rs have no declaration of which
|
||||
member is an id, so they keep a `v3` they found and never decide one.
|
||||
- Header narrowing (`c2a-ids-header` r1): per-record `StoredHoldsIds`, set from the type that decoded a
|
||||
record, is falsified by the identical-bytes probe above. The durable header owns "these stored bytes hold
|
||||
ids" at store granularity. A write that copies any record forward keeps a marked header; only a write that
|
||||
encodes every record decides by content. `StoredHoldsIds` and `KnownIdFree` are deleted.
|
||||
- Object list versus value list (r4, `imag-r4\probe1.log`, `probe2.log`): ids sit on object-list elements only
|
||||
(ruling `q7-object-list-elements-only`). `Dictionary`, `ConcurrentDictionary`, `SortedDictionary`,
|
||||
`IReadOnlyDictionary`, and `ILookup` of keyed arms round-trip with the id at
|
||||
the element's own slot (e.g. `ConcDict` `81A16B92AC30313233343536373839616200`: key, then `[id, 0]`). A list
|
||||
or map of scalars has no element to carry one and never enters the id path.
|
||||
|
||||
**How MessagePack decides what it writes** (r5 `imag-r5\probe1.log`, `probe3.log`; Soul s5 `job2.log`).
|
||||
- Member discovery, `MessagePack.Internal.ObjectSerializationInfo.CreateOrNull(Type, bool forceStringKey,
|
||||
bool contractless, bool allowPrivate)`, is internal, and so is `StandardResolver.Resolvers`. The public
|
||||
surface allows one test: `StandardResolver`'s formatter for `T` is reference-equal to the formatter one
|
||||
public singleton hands out for `T`, each caching one instance per type. `DynamicObjectResolver.Instance`
|
||||
matches keyed classes and structs, `DynamicUnionResolver.Instance` `[Union]` types,
|
||||
`DynamicGenericResolver.Instance` List, arrays, maps, KeyValuePair, tuples, Lazy, Memory, ArraySegment and
|
||||
Nullable, and `ImmutableCollectionResolver.Instance` (namespace `MessagePack.ImmutableCollection`)
|
||||
`ImmutableArray` and `ImmutableList`. Asking `DynamicObjectResolver` for a collection type throws
|
||||
`TypeInitializationException`, so a family probe swallows exceptions.
|
||||
- Kind facts (`probe3`): a `[MessagePackObject]` class implementing only `IEnumerable<T>` or
|
||||
`IReadOnlyList<T>` (`KBag`, `KNode`, `KRo`) is written by key (object resolver). One that is also
|
||||
`ICollection<T>`, a `List<T>` subclass or a `Dictionary` subclass (`KC2`, `KColl`, `KList2<T>`, `KDict`) is
|
||||
written as a collection (generic resolver) and its `[Key]` members are dropped.
|
||||
- Member facts (`probe3`; `ap` is `AllowPrivate`, `R` written, `W` read back). A member is written when it
|
||||
has an integer `[Key]`, no `[IgnoreMember]`, and is a public field or a property with a public getter. It is
|
||||
read back when it is a public non-readonly field, a property with a public setter (`init` counts), or a
|
||||
`[SerializationConstructor]` parameter. At `ap=False`: `PG` (private getter) `R=False W=True`, `PS`
|
||||
(private setter) `R=True W=False`, `GetOnly` `R=True W=False`, `InitOnly` both. `IdPriv`
|
||||
(`Id { get; private set; }`) is written and not read back (wire `91AC62...`, back `<null>`), and `IdCtor` is
|
||||
`W=False`. At `ap=True` every one of these, `priv` included, is `R=True W=True`, except `GetOnly`
|
||||
(`W=False`).
|
||||
- AllowPrivate attribute form (Soul s5 `SoulS5Probe.C`, `ApTrueArm`): on a type declared
|
||||
`[MessagePackObject(AllowPrivate = true)]`, `StandardResolver` writes private `[Key]` members and a
|
||||
private-setter id round-trips. The oracle test calls `CreateOrNull(type, false, false, false)` and reports
|
||||
none of them, so the walk's member set misses that form.
|
||||
- Collection written as object (r6 `probe2.log`): a `[DataContract]` class implementing only `IEnumerable<T>`
|
||||
over an id-carrying arm (`DcSeq`) is a collection in the walk, yet `StandardResolver`'s formatter is
|
||||
`DynamicObjectResolver`'s (wire `91 91 91 92 AC..`, written as an object). A `[DataContract]` `List<T>`
|
||||
subclass (`DcColl`) is written by the generic resolver, and the object resolver hands out a different
|
||||
instance for it. For `DcBad` (two members on one `Order`) `StandardResolver` uses the generic resolver, and
|
||||
Soul s5 measured both `DcColl` and `DcBad` passing the door.
|
||||
- Geometry (r6 `probe2.log`, `probe3.log`): Geometry has a direct MessagePack 3.1.7 `PackageReference`, so the
|
||||
source generator runs, and its assembly carries `[GeneratedAssemblyMessagePackResolverAttribute(
|
||||
typeof(GeneratedMessagePackResolver), 3, 1)]`. For `CultGeometryDomainNode`, `DomainDocument` and
|
||||
`FeatureClaim`, `StandardResolver` returns `GeneratedMessagePackResolver`'s formatter and every public
|
||||
singleton returns a different instance or null. For `CultGeometryDomainNode[]` and `FeatureClaim[]` it is
|
||||
the generated resolver's `ArrayFormatter`, not `DynamicGenericResolver`'s. No Geometry id-path type matches
|
||||
a family singleton. Caching.Tests has no generator (analyzers do not flow through a `ProjectReference`), so
|
||||
its keyed types match `DynamicObjectResolver`.
|
||||
- Generated assemblies (Soul s5, fresh generator-enabled assembly `SoulGen`, `job2.log` GEN rows): a keyed
|
||||
`List` or `Dictionary` subclass (`GKColl`, `GKDict`) is written by key, wire `["c",[7]]`, with its elements
|
||||
dropped by MessagePack; the walk reads it by key too (Plan 0). A generated-assembly `[DataContract]`
|
||||
enumerable (`GDcSeq`) is still written by `DynamicObjectResolver`, because the generator emits no
|
||||
DataContract formatters.
|
||||
- Null standard (Soul s6 finding `null-standard-untyped`): for a non-generic interface or abstract class that
|
||||
enumerates arms, `StandardResolver` returns null instead of throwing, and the door neither refuses nor
|
||||
types it.
|
||||
- Wire pin (Soul s5, s6 `SoulS5Pin`): a deterministic document with 12 minted ids, encoded by the codec and
|
||||
decoded independently with `MessagePackSerializer.ConvertToJson`, carries every id. The ids are
|
||||
byte-identical at `494ad9ab` and `57fdf70c`, `0f8964b58a08` and `7eb4fb13d530` among them.
|
||||
- Not measured: Unity/IL2CPP, where the package bundles its own `MessagePack.dll` (version not read) and the
|
||||
dynamic resolvers cannot emit, and MessagePack versions other than 3.1.7.
|
||||
|
||||
**Whole-list holders at `57fdf70c`** (Imagination r2 for `c2a-id-closed-world`, 2026-10-16; probe tests in a
|
||||
scratch commit `dc629459` that was never pushed, Yggdrasil `sdk:10.0`; logs `job1.log` and `job2.log` in the
|
||||
Imagination scratchpad `imagination-c2a-closed-world-r2`).
|
||||
- Shapes the walk does not own already behave as whole values. A keyed document holding
|
||||
`ImmutableArray<PElem>`, `(List<PElem>, int)`, `KeyValuePair<string, List<PElem>>`, a non-enumerable
|
||||
`[DataContract]` holder, or a string-keyed holder (`[Key("items")]`) gives `Inspect` problems 0, an empty
|
||||
IdPath, `CanHoldIds` false, `Plan` 0 (random and deterministic), and `Holds` false with ids set.
|
||||
- The marker and the copy check do not count them. Committed with ids `aaaaaaaaaaaa`/`bbbbbbbbbbbb`, the store
|
||||
header stays `cultcache.store.v1`. A fresh cache that writes another record over that file (the copy-forward
|
||||
check) keeps `v1`. Reload returns the ids exactly as written, and unset ids stay null after write and reload.
|
||||
- Variants replace them whole, and that is the C2a resolver's behaviour for every list. A variant `Set` of the
|
||||
`ImmutableArray` slot resolved to `[9:eeeeeeeeeeee]` with the base untouched, header `v2`, `HoldsIds` false.
|
||||
A path step `[1:aaaaaaaaaaaa]` is refused: "this runtime resolves top-level Set overrides only". No C2a gap.
|
||||
The gap belongs to C2b, in the cut sequence below.
|
||||
- AllowPrivate is read from the most derived `[MessagePackObject]`. `ApDerived` (AllowPrivate over a base
|
||||
without it) is written `[[],[],4,[]]`: base-chain privates and a private-getter property, with
|
||||
`ObjectSerializationInfo` at `allowPrivate: true` = `[baseStash, own, N, Pg]`. `NoApDerived` (plain over an
|
||||
AllowPrivate base) is written `[nil, 5]`. The walk read only `[N]` for both.
|
||||
- A `[DataContract]` `List<T>` subclass is written by `NonGenericListFormatter`, and its record does not read
|
||||
back ("System.Object[] is not of type PElem"), whatever the walk does. That is a MessagePack limit, not an
|
||||
id fact.
|
||||
|
||||
## 2. Model page (step 0b)
|
||||
|
||||
| Kind | Named by | Over time | Decided by |
|
||||
|
|
@ -214,16 +60,16 @@ Imagination scratchpad `imagination-c2a-closed-world-r2`).
|
|||
| Base reference | the base's key, in the record's variant slot | changed only by an explicit rebase; a base cannot be deleted while it has variants, unless the same batch rebases or removes them | author; the cache refuses a cycle, a missing base, a different concrete type, a global type |
|
||||
| Override entry | `(op, path, id, value)`; op is Set, Insert or Remove | added by editing; removed by clearing; survives base edits while its path resolves | author; the cache refuses entries a base edit strands (Q2); an entry whose member left the type soft-drifts with a warning (Q4 c) |
|
||||
| Member path | a sequence of `(slot, elementId)` steps; `slot` is the MessagePack `[Key]`; `elementId` is `""` unless the step enters an object-list element | a type change can invalidate it | the type's owner (slot authority); ids by the element's creator |
|
||||
| Element id | a `[CultElementId]` string member on the element type of an object list in a shape the walk owns (arrays, `IEnumerable<T>`, `IDictionary`/`IReadOnlyDictionary` values, `[MessagePackObject]`/`[Union]` types with integer keys), one MessagePack writes and reads back (integer `[Key]`, no `[IgnoreMember]`; public, or any visibility under the most derived `AllowPrivate`); `""` means unset. **Exception (q3 amended 2026-10-16):** a shape the walk does not own is one whole value: `ImmutableArray`, `ArraySegment`, `Memory`, `KeyValuePair`, tuples, `Lazy`, unkeyed or `[DataContract]` classes, string-keyed types, a type or member with `[MessagePackFormatter]`, and a non-public member without `AllowPrivate`. Its elements carry no ids: a `[CultElementId]` inside is a plain string | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element. Inside a whole value: never minted, never checked, never counted for `v3` | cache mints it; it is never retargeted. `CultElementIds.Classify` alone decides owned versus whole |
|
||||
| Element id | a `[CultElementId]` string member on an object-list element type; `""` means unset | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element | cache mints it; it is never retargeted |
|
||||
| Resolved view | `CultStoredDocument.Document` for a variant key | recomputed inside the hold that admits the variant, or any base on its chain | the cache only; never persisted, never written back (Q1) |
|
||||
| Index and name entries | as today, from the resolved view | a dependent's entries are dropped and re-added in the same `Apply` as its base change | the cache; a variant must override the `[CultName]` member (rule R6) |
|
||||
| Watches | as today; each re-resolved dependent emits `Updated` with its own `Sequence` | published in the same hold as the base change | the cache |
|
||||
| Compare-exchange | `(schemaId, storedAt)` of the stored delta (ruling 2) | a base change does not stamp its variants | the cache; the resolved view is not an identity |
|
||||
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5); a marked `v3` (`v5` directory) stays while a write copies any record forward, and only a write that encodes every record drops it by content | the writing store, from content; the durable header alone says the bytes hold element ids |
|
||||
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5) | the writing store, from content |
|
||||
| Single-file store file | the path; one MessagePack array `[header, catalog, records]` | replaced whole and atomically; refused when unreadable; see "Store authorities" | each runtime's one reader decides what is a store; the writer derives the header from it |
|
||||
| Store lock and staging files | `<file>.lock`; a unique temp beside the store | the lock is taken by C# and Rust writers (CultMesh included, through `ReplaceDurable`); staging is removed on a failed write | the store, never CultMesh or a consumer |
|
||||
| Variant slot on the wire (CultNet) | **empty cell**: no CultNet message carries a delta today | C5 | ruling 4 decides deltas; the message shape is C5's design |
|
||||
| Dictionary entries, scalar lists, arrays, whole-list holders | **empty cell**: no stable identity exists | whole-member Set only (Q7; whole-list holders by q3 amended) | Q7, q3 amended |
|
||||
| Dictionary entries, scalar lists, arrays | **empty cell**: no stable identity exists | whole-member Set only (Q7) | Q7 |
|
||||
| Template (non-item) bases | **empty cell**: parked by ruling 6 | none | not in these cuts |
|
||||
|
||||
## 3. Where the Body contradicts the target
|
||||
|
|
@ -335,43 +181,6 @@ Dictionary entries, scalar lists and arrays are replaced whole.
|
|||
- **Recommend yes.** Aetheria's cases (`Hardpoints` whole; `Behaviors[id]...Max`) fit. Keyed
|
||||
dictionary addressing can be added later without a format change. Affects C2.
|
||||
|
||||
## Cut sequence (2026-10-15)
|
||||
|
||||
Landed since 2026-10-12: `c2a-declared-apply` (r3, Soul s5) and `main-into-c2a` (r2, Soul s2); the
|
||||
reconcile step is done, `hands/variants-c2a` is at the reconciled head `6afa8ed9`. Element ids part 1,
|
||||
`c2a-element-id-holes` (r6), is held by Soul s5/s6 on `hands/variants-c2a-element-id-holes`. Mapped
|
||||
now: part 2, `c2a-id-closed-world` r2 (2026-10-16, under the amended q3), not blocked. Its open fork
|
||||
`variants:question:c2a-whole-list-opt-in` holds under every option. Later cuts, one line each, mapped
|
||||
only when Self is about to dispatch them:
|
||||
|
||||
1. **reconcile**: done at `6afa8ed9`.
|
||||
2. **parity-ts**: TypeScript write set, one record per key, undeclared records carried and refused,
|
||||
unheld removals judged at commit. Waits on element ids parts 1 and 2 merged into C2a.
|
||||
3. **parity-rs**: Rust `push_all` and `put_prepared_batch` apply a write set, ownership by
|
||||
declaration, the same commit-time checks. Waits as parity-ts; can run beside it.
|
||||
4. **parity-py**: Python write-set apply. Waits as parity-ts, and on `cultlib-gaps` cut
|
||||
`py-msgpack-default` landing on main (then a main merge into C2a first).
|
||||
5. **write-set-vectors**: one cross-runtime manifest, read and acted on by all four runtimes;
|
||||
carries follow-ups `c0-bad-canonical-in-manifest`, `legacy-vectors-all-runtimes`,
|
||||
`directory-at-path-ts-py-pinned`. Waits on the three parity cuts.
|
||||
6. **land-c2a**: final main merge, full verification, Soul, then `git merge --no-ff` onto main
|
||||
with an empty diff against the verified head; no tag. Waits on write-set-vectors, Soul holding,
|
||||
and `asura:cut_spec:cut-semver-measured` landed. Asura Cut 3 bases on its merge commit.
|
||||
|
||||
Element-id part 2: `c2a-id-closed-world` r2 decides deferred findings `s5.member-set-misses-allowprivate`, `s5.enumerating-rule-unpinned`, `s6.null-standard-untyped`, `s6.two-recognisers-one-rule`, `s4.unrecognised-holders-fail-open` and follow-up `c2a-datacontract-holder-unwalked` (see its `first`).
|
||||
|
||||
- **marker-for-whole-lists** (only if `c2a-whole-list-opt-in` is ruled `marker-when-ids-declared` or `marker-for-any-keyed-object`): a CarriesElements predicate, a refusal unless the member is marked, and a public attribute. It is additive after part 2.
|
||||
- **C2b constraint** (owed to `variants:follow_up:remaining-variant-cuts`): nested path resolution must refuse a step `[slot:elementId]` that enters a whole value, naming the holder. A whole value is any shape `CultElementIds.Classify` calls Leaf, so C2b asks Classify and keeps no recogniser of its own. Today's C2a resolver refuses every non-top-level step, so no store can hold such an override yet.
|
||||
|
||||
Rationale. The 2026-10-02 order (parity, vectors, then merge main) was right when main was close.
|
||||
Main has since moved 453 commits and rebuilt exactly what the parity cuts change: cultcache-ts
|
||||
payload ownership, cultmesh-ts, cultcache-rs. Parity mapped before the merge would be mapped
|
||||
twice. Merging first means every later cut is mapped once, on the code it changes. The C# fix
|
||||
batch goes first because the operator's at-commit ruling is the contract the parity runtimes
|
||||
copy; and it touches C# files main barely changed, so it can run beside the merge with one
|
||||
small reconcile (the `MintStoredAt` region). The High finding is not on this path: h3 fixed it
|
||||
and Soul s3 held it (body facts above).
|
||||
|
||||
## 5. Cuts, in order
|
||||
|
||||
All cuts are in `F:\Projects\CultLib`. Rebase `codex/document-variants` (docs only) onto
|
||||
|
|
@ -1605,7 +1414,7 @@ The one door per runtime, and who walks through it:
|
|||
- **Inputs.** The bytes at the path. **Outputs.** The decoded store with header and catalog; or a typed refusal (`CultStoreUnreadableException` in C#; `CultCacheStoreUnreadable{path, kind}` in Rust, kind `Undecodable` or `UnsupportedFormat`; `StoreUnreadableError` in TS and Python). A refusal names a header only as `cultcache.store.v<digits>`.
|
||||
- **Derived state.** The write header is never decided by the writer: it is the reader's verdict (header read, whole-store flag, whether ids or variants are held). A rewrite of a file that cannot be read cannot happen, because the reader throws first.
|
||||
- **Forbidden writers.** A second header classifier. Deleted: Rust `leading_header`, `read_array_header`, `read_string`; cultnet-ts's own decode of the store header; the TS async `readStore`; Rust `StoreUnreadableError` (negative grep `StoreUnreadableError` in `packages/cultcache-rs`: no match); Python `_decode_v1_snapshot`.
|
||||
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore needs a cultcache-ts release that exports it, which is the release that ships with this change; the manifests move to that version then (c0-ships-with-c2a), not before.
|
||||
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore declares cultcache-ts `^0.15.0`, the first release that exports it.
|
||||
- **Cut line.** `merge-r1` h3 (Rust) and h4 (TS, cultnet-ts); spec `cut-merge-r1.r3` authority map.
|
||||
- **Pinned by.** `tests/vectors/document-variants-c2a/readability/manifest.txt`, walked by `StoreReadabilityTests.OpenReadsExactlyTheFilesTheVectorsSayItReads` (C#), `CultMeshSingleFileReadabilityTests.AWriteReplacesAFileExactlyWhenItReads` (Mesh), `a_file_is_replaced_exactly_when_it_reads` (RS:5132), "SingleFileMessagePackBackingStore replaces a file exactly when it reads" (TS test :2321) and `test_single_file_replaces_a_file_exactly_when_it_reads` (Python). AUTH cases: `packages/cultnet-ts/test/idunn-runtime-authority.test.ts`. Declared divergences in the manifest: `legacy-envelopes.bin` (C# refuses; the others read and replace it) and `variant-slot-v3.bin` (C# reads; the others refuse).
|
||||
- **Known gaps.** `variants:follow_up:c0-bad-canonical-in-manifest` (`current-catalog-bad-canonical.msgpack` is not in the manifest; TS reads it, Rust refuses). `variants:follow_up:legacy-vectors-all-runtimes` (the three `legacy-catalog-*` c0 vectors have a verdict only in C# tests; all four runtimes refuse them, probed). `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it).
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
# Element-id sweep list (C2a)
|
||||
|
||||
Data for the per-consumer sweep in docs/document-variants-cut.md, C2. Every object type below sits in a list (`List<T>`, `T[]`, `IEnumerable<T>`, and the like) reachable from a registered document type, and needs one string member with `[Key(n)]` and `[CultElementId]` before that consumer can take a CultLib that requires ids. A union base takes the member on the base; its subclasses inherit it, so put the slot above the subclasses' highest key.
|
||||
|
||||
Method: a static scan of `F:\Projects\<consumer>` C# files for types marked `[CultDocument(`, walked through keyed members, base classes, dictionary values and `[Union]` subtypes. Read-only; no consumer was edited. Worktree copies (`Aetheria-catalog`, `CultLib-*`, and so on) are excluded. Struct elements are values and need no id. Non-C# consumers carry the id as an ordinary member and are not listed. The scan is text-based: a document registered without a literal `[CultDocument(` attribute, or a type whose members lack an integer `[Key]`, is missed, so the sweep pass should confirm each repo by registering its types once.
|
||||
|
||||
Lines: `Type file:line (via Owner.Member)`, paths relative to the consumer root.
|
||||
|
||||
|
||||
## Aetheria (13 element types)
|
||||
|
||||
- BehaviorData Assets/Scripts/ServerShared/Behaviors/Behaviors.cs:190 (via ConsumableItemData.Behaviors)
|
||||
- EntityPack Assets/Scripts/ServerShared/EntitySerializer.cs:171 (via ZonePack.Entities)
|
||||
- ProductRole Assets/Scripts/ServerShared/FactionProduct.cs:38 (via FactionProductData.Roles)
|
||||
- InputLayoutRow Assets/Scripts/ServerShared/InputLayout.cs:32 (via InputLayout.Rows)
|
||||
- InputLayoutColumn Assets/Scripts/ServerShared/InputLayout.cs:52 (via InputLayoutKeyRow.Columns)
|
||||
- ItemRole Assets/Scripts/ServerShared/ItemData.cs:325 (via CraftedItemData.Roles)
|
||||
- AudioStat Assets/Scripts/ServerShared/ItemData.cs:441 (via EquippableItemData.AudioStats)
|
||||
- HardpointData Assets/Scripts/ServerShared/ItemData.cs:553 (via HullData.Hardpoints)
|
||||
- StatTerm Assets/Scripts/ServerShared/ItemData.cs:630 (via PerformanceStat.Terms)
|
||||
- LoadoutSlot Assets/Scripts/ServerShared/Loadout.cs:23 (via Loadout.Slots)
|
||||
- RoleFill Assets/Scripts/ServerShared/Provenance.cs:84 (via Lot.Roles)
|
||||
- SavedActionBarBinding Assets/Scripts/ServerShared/SavedGame.cs:168 (via SavedGame.ActionBarBindings)
|
||||
- Asteroid Assets/Scripts/ServerShared/ZoneData.cs:133 (via AsteroidBeltData.Asteroids)
|
||||
|
||||
## AetheriaEve (115 element types)
|
||||
|
||||
- AetheriaCorporationAllegiance Aetheria.State/Documents/AetheriaCorporation.cs:67 (via AetheriaCorporation.Allegiances)
|
||||
- AetheriaShapeCell Aetheria.State/Documents/AetheriaItemDefinition.cs:178 (via AetheriaItemDefinition.ShapeCells)
|
||||
- AetheriaItemHardpoint Aetheria.State/Documents/AetheriaItemDefinition.cs:188 (via AetheriaItemDefinition.Hardpoints)
|
||||
- AetheriaCurveKey Aetheria.State/Documents/AetheriaItemDefinition.cs:222 (via AetheriaItemDefinition.ThermalPerformanceCurveKeys)
|
||||
- AetheriaItemAudioStat Aetheria.State/Documents/AetheriaItemDefinition.cs:238 (via AetheriaItemDefinition.AudioStats)
|
||||
- AetheriaBehaviorPayload Aetheria.State/Documents/AetheriaItemDefinition.cs:267 (via AetheriaItemDefinition.BehaviorPayloads)
|
||||
- AetheriaBehaviorField Aetheria.State/Documents/AetheriaItemDefinition.cs:286 (via AetheriaBehaviorPayload.Fields)
|
||||
- AetheriaBehaviorMapEntry Aetheria.State/Documents/AetheriaItemDefinition.cs:296 (via AetheriaBehaviorValue.MapEntries)
|
||||
- AetheriaBehaviorValue Aetheria.State/Documents/AetheriaItemDefinition.cs:306 (via AetheriaBehaviorValue.Children)
|
||||
- AetheriaLegacyCatalogFile Aetheria.State/Documents/AetheriaLegacyCatalogQuarantine.cs:41 (via AetheriaLegacyCatalogQuarantine.NameFiles)
|
||||
- AetheriaMigrationCount Aetheria.State/Documents/AetheriaMigrationLedger.cs:35 (via AetheriaMigrationLedger.Counts)
|
||||
- AetheriaLegacyCatalogEntrySummary Aetheria.State/Documents/AetheriaMigrationLedger.cs:45 (via AetheriaMigrationLedger.LegacyCatalogEntries)
|
||||
- AetheriaStoryFileHash Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:42 (via AetheriaPlayerSettings.StoryFileHashes)
|
||||
- AetheriaInputBindingOverride Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:85 (via AetheriaPlayerInputSettings.BindingOverrides)
|
||||
- AetheriaEntityLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:119 (via AetheriaEntityLoadout.Children)
|
||||
- AetheriaLoadoutItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:186 (via AetheriaEntityLoadout.Equipment)
|
||||
- AetheriaCargoBayLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:199 (via AetheriaEntityLoadout.CargoContents)
|
||||
- AetheriaFactionZoneState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:268 (via AetheriaRunState.HomeZones)
|
||||
- AetheriaAgentTaskState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:275 (via AetheriaRunState.AgentTasks)
|
||||
- AetheriaDroppedPickupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:346 (via AetheriaZoneState.DroppedPickups)
|
||||
- AetheriaOrbitSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:362 (via AetheriaZoneState.Orbits)
|
||||
- AetheriaBodySnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:384 (via AetheriaZoneState.Bodies)
|
||||
- AetheriaBodyResource Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:449 (via AetheriaBodySnapshot.Resources)
|
||||
- AetheriaAsteroidSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:459 (via AetheriaBodySnapshot.Asteroids)
|
||||
- AetheriaAsteroidMiningAccumulatorSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:484 (via AetheriaAsteroidSnapshot.MiningAccumulators)
|
||||
- AetheriaColor Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:545 (via AetheriaGasGiantVisualState.Colors)
|
||||
- AetheriaFogFieldEmitterSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:691 (via AetheriaEntitySnapshot.FogFieldEmitters)
|
||||
- AetheriaLoadoutGenerationSelection Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:723 (via AetheriaLoadoutGenerationReceipt.Selections)
|
||||
- AetheriaEntityContactSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:734 (via AetheriaEntitySnapshot.Contacts)
|
||||
- AetheriaEntityItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:751 (via AetheriaEntitySnapshot.Equipment)
|
||||
- AetheriaWeaponGroupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:782 (via AetheriaEntitySnapshot.WeaponGroups)
|
||||
- AetheriaEntityStatGrid Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:789 (via AetheriaEntitySnapshot.StatGrids)
|
||||
- AetheriaActiveConsumableSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:805 (via AetheriaEntitySnapshot.ActiveConsumables)
|
||||
- AetheriaConsumableBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:828 (via AetheriaActiveConsumableSnapshot.BehaviorStates)
|
||||
- AetheriaBehaviorProgressSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:844 (via AetheriaEntitySnapshot.BehaviorProgress)
|
||||
- AetheriaWeaponStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:863 (via AetheriaEntitySnapshot.WeaponStates)
|
||||
- AetheriaBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:924 (via AetheriaEntitySnapshot.BehaviorStates)
|
||||
- AetheriaFactionRelationshipState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:1093 (via AetheriaRunState.FactionRelationships)
|
||||
- AetheriaItemRarityTier Aetheria.State/Documents/AetheriaTradeValuePolicy.cs:29 (via AetheriaTradeValuePolicy.Tiers)
|
||||
- AetheriaRuntimeAgentTaskCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs:35 (via AetheriaRuntimeRunCheckpointCommit.AgentTasks)
|
||||
- AetheriaRuntimeAssetManifestEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssetDocuments.cs:102 (via AetheriaRuntimeAssetManifestDocument.Assets)
|
||||
- AetheriaRuntimeCurveKey Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:446 (via AetheriaRuntimeShotReceiptCommit.GuidanceCurve)
|
||||
- AetheriaRuntimeCorporation Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:620 (via AetheriaRuntimeCatalogSnapshot.Corporations)
|
||||
- AetheriaRuntimeNameFile Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:669 (via AetheriaRuntimeCatalogSnapshot.NameFiles)
|
||||
- AetheriaRuntimeLoadoutTemplateSnapshot Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:796 (via AetheriaRuntimeLoadoutTemplatesDocument.Templates)
|
||||
- AetheriaRuntimeDaemonCommandBoundaryEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs:462 (via AetheriaRuntimeDaemonCommandBoundaryDocument.Commands)
|
||||
- AetheriaRuntimeDaemonSoaIdentityDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:143 (via AetheriaRuntimeDaemonSoaViewDocument.Identities)
|
||||
- AetheriaRuntimeDaemonRenderGroupDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:156 (via AetheriaRuntimeDaemonSoaViewDocument.RenderGroups)
|
||||
- AetheriaRuntimeDaemonSoaBufferDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:222 (via AetheriaRuntimeDaemonSoaViewDocument.Buffers)
|
||||
- AetheriaRuntimeDaemonSoaColumnDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:253 (via AetheriaRuntimeDaemonSoaViewDocument.Columns)
|
||||
- AetheriaRuntimeDaemonSoaDirtyRangeDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:284 (via AetheriaRuntimeDaemonSoaViewDocument.DirtyRanges)
|
||||
- AetheriaRuntimeEveSurfaceAdvertisement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:11 (via AetheriaRuntimeDaemonProviderAdvertisementDocument.EveSurfaces)
|
||||
- AetheriaRuntimeEvePluginRequirement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:58 (via AetheriaRuntimeEveSurfaceAdvertisement.RequiresPlugins)
|
||||
- AetheriaRuntimeZoneTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:410 (via AetheriaRuntimeZoneContactsDocument.Targets)
|
||||
- AetheriaRuntimeZoneContactRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:441 (via AetheriaRuntimeZoneContactsDocument.Contacts)
|
||||
- AetheriaRuntimeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:551 (via AetheriaRuntimeStationRefitEntityOption.CargoItems)
|
||||
- AetheriaRuntimeStationRefitEntityOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:585 (via AetheriaRuntimeStationRefitDocument.AvailableEntities)
|
||||
- AetheriaRuntimeStationDockingBayRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:620 (via AetheriaRuntimeStationRefitDocument.DockingBays)
|
||||
- AetheriaRuntimeStationLoadoutRestoreOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:655 (via AetheriaRuntimeStationRefitDocument.LoadoutRestoreOptions)
|
||||
- AetheriaRuntimeStationCargoTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:674 (via AetheriaRuntimeStationRefitDocument.CargoTargets)
|
||||
- AetheriaRuntimeSectorMapZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:765 (via AetheriaRuntimeSectorMapDocument.Zones)
|
||||
- AetheriaRuntimeSectorMapLink Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:802 (via AetheriaRuntimeSectorMapDocument.Links)
|
||||
- AetheriaRuntimeZoneRenderAdjacentZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1197 (via AetheriaRuntimeZoneRenderDocument.AdjacentZones)
|
||||
- AetheriaRuntimeZoneRenderWormholeExit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1210 (via AetheriaRuntimeZoneRenderDocument.WormholeExits)
|
||||
- AetheriaRuntimeZoneRenderBodyPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1229 (via AetheriaRuntimeZoneRenderDocument.BodyPoses)
|
||||
- AetheriaRuntimeZoneRenderAsteroidBeltPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1260 (via AetheriaRuntimeZoneRenderDocument.AsteroidBeltPoses)
|
||||
- AetheriaRuntimeZoneRenderAsteroidInstancePose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1286 (via AetheriaRuntimeZoneRenderAsteroidBeltPose.InstancePoses)
|
||||
- AetheriaRuntimeViewportObject Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1382 (via AetheriaRuntimeGameViewportDocument.Objects)
|
||||
- AetheriaRuntimeInventoryItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1458 (via AetheriaRuntimeViewportObject.Inventory)
|
||||
- AetheriaRuntimeGravityInfluence Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1493 (via AetheriaRuntimeGameViewportDocument.GravityInfluences)
|
||||
- AetheriaRuntimeBodyView Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1530 (via AetheriaRuntimeGameViewportDocument.Bodies)
|
||||
- AetheriaHangarShip Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:119 (via AetheriaHangarState.Ships)
|
||||
- AetheriaHangarItemStack Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:129 (via AetheriaHangarState.Inventory)
|
||||
- AetheriaHangarCurrency Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:136 (via AetheriaHangarState.Currencies)
|
||||
- AetheriaDeploymentReceipt Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:155 (via AetheriaHangarState.Deployments)
|
||||
- AetheriaProgressionVerseOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeProgressionSourceDocuments.cs:45 (via AetheriaProgressionSourceDocument.AvailableVerses)
|
||||
- AetheriaRuntimeStoryFileHashDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:87 (via AetheriaRuntimePlayerSettingsDocument.StoryFileHashes)
|
||||
- AetheriaRuntimeInputBindingOverrideDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:108 (via AetheriaRuntimePlayerSettingsDocument.BindingOverrides)
|
||||
- AetheriaRuntimeEntityLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:78 (via AetheriaRuntimeEntityLoadoutCommit.Children)
|
||||
- AetheriaRuntimeLoadoutItemSlotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:143 (via AetheriaRuntimeEntitySnapshotCommit.Equipment)
|
||||
- AetheriaRuntimeCargoBayLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:159 (via AetheriaRuntimeEntitySnapshotCommit.CargoContents)
|
||||
- AetheriaRuntimeGameEventCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:490 (via AetheriaRuntimeRunCheckpointCommit.GameEvents)
|
||||
- AetheriaRuntimeFactionZoneCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:509 (via AetheriaRuntimeRunCheckpointCommit.HomeZones)
|
||||
- AetheriaRuntimeShotReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:539 (via AetheriaRuntimeRunCheckpointCommit.ShotReceipts)
|
||||
- AetheriaRuntimePickupContactReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:591 (via AetheriaRuntimeRunCheckpointCommit.PickupContactReceipts)
|
||||
- AetheriaRuntimeDamageCellCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:603 (via AetheriaRuntimeShotReceiptCommit.DamageCells)
|
||||
- AetheriaRuntimeCorporationSurveyCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:614 (via AetheriaRuntimeRunCheckpointCommit.CorporationSurveys)
|
||||
- AetheriaRuntimeFactionRelationshipCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:623 (via AetheriaRuntimeRunCheckpointCommit.FactionRelationships)
|
||||
- AetheriaRuntimeZoneSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:636 (via AetheriaRuntimeRunCheckpointCommit.Zones)
|
||||
- AetheriaRuntimePhysicalPayloadCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:699 (via AetheriaRuntimeZoneSnapshotCommit.PhysicalPayloads)
|
||||
- AetheriaRuntimeDroppedPickupCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:771 (via AetheriaRuntimeZoneSnapshotCommit.DroppedPickups)
|
||||
- AetheriaRuntimeOrbitSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:805 (via AetheriaRuntimeZoneSnapshotCommit.Orbits)
|
||||
- AetheriaRuntimeBodySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:830 (via AetheriaRuntimeZoneSnapshotCommit.Bodies)
|
||||
- AetheriaRuntimeBodyResourceCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:901 (via AetheriaRuntimeBodySnapshotCommit.Resources)
|
||||
- AetheriaRuntimeAsteroidCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:911 (via AetheriaRuntimeBodySnapshotCommit.Asteroids)
|
||||
- AetheriaRuntimeAsteroidMiningAccumulatorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:936 (via AetheriaRuntimeAsteroidCommit.MiningAccumulators)
|
||||
- AetheriaRuntimeColorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1008 (via AetheriaRuntimeGasGiantVisualCommit.Colors)
|
||||
- AetheriaRuntimeEntitySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1024 (via AetheriaRuntimeZoneSnapshotCommit.Entities)
|
||||
- AetheriaRuntimeFogFieldEmitterCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1198 (via AetheriaRuntimeEntitySnapshotCommit.FogFieldEmitters)
|
||||
- AetheriaRuntimeEquipmentStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1240 (via AetheriaRuntimeEntitySnapshotCommit.EquipmentStates)
|
||||
- AetheriaRuntimeLoadoutGenerationSelectionCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1264 (via AetheriaRuntimeLoadoutGenerationReceiptCommit.Selections)
|
||||
- AetheriaRuntimeEntityContactCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1275 (via AetheriaRuntimeEntitySnapshotCommit.Contacts)
|
||||
- AetheriaRuntimeEntityStatGridCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1291 (via AetheriaRuntimeEntitySnapshotCommit.StatGrids)
|
||||
- AetheriaRuntimeActiveConsumableCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1307 (via AetheriaRuntimeEntitySnapshotCommit.ActiveConsumables)
|
||||
- AetheriaRuntimeConsumableBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1330 (via AetheriaRuntimeActiveConsumableCommit.BehaviorStates)
|
||||
- AetheriaRuntimeBehaviorProgressCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1355 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorProgress)
|
||||
- AetheriaRuntimeWeaponStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1374 (via AetheriaRuntimeEntitySnapshotCommit.WeaponStates)
|
||||
- AetheriaRuntimeBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1444 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorStates)
|
||||
- AetheriaRuntimeStarbridgeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:71 (via AetheriaRuntimeStarbridgeScenarioDocument.StationStock)
|
||||
- AetheriaRuntimeStarbridgeWaveDefinition Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:83 (via AetheriaRuntimeStarbridgeScenarioDocument.Waves)
|
||||
- AetheriaRuntimeStarbridgeWaveForecast Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:94 (via AetheriaRuntimeStarbridgeSessionSummaryDocument.WaveForecast)
|
||||
- AetheriaRuntimeStarbridgeRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:104 (via AetheriaRuntimeStarbridgeScenarioDocument.RuntimeRoles)
|
||||
- AetheriaRuntimeAuthorityRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:194 (via AetheriaRuntimeVerseAuthorityPolicyDocument.RuntimeRoles)
|
||||
- AetheriaRuntimeAuthorityRule Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:201 (via AetheriaRuntimeVerseAuthorityPolicyDocument.Rules)
|
||||
- AetheriaRuntimeArenaSeat Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:255 (via AetheriaRuntimeArenaRosterDocument.Seats)
|
||||
|
||||
## AquaSynth (0 element types)
|
||||
|
||||
None.
|
||||
|
||||
|
||||
## Aquarium (1 element types)
|
||||
|
||||
- AquariumFaceVoiceEndpointState src/Aquarium.Epiphany/State/AquariumLiveState.cs:61 (via AquariumLiveState.FaceVoiceEndpoints)
|
||||
|
||||
## Brokkr (4 element types)
|
||||
|
||||
- BrokkrGameObjectSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:42 (via BrokkrHostSnapshot.sceneObjects)
|
||||
- BrokkrComponentSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:62 (via BrokkrGameObjectSnapshot.components)
|
||||
- BrokkrSerializedPropertySnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:73 (via BrokkrComponentSnapshot.properties)
|
||||
- BrokkrAssetSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:84 (via BrokkrHostSnapshot.assets)
|
||||
|
||||
## Delvehold (0 element types)
|
||||
|
||||
None.
|
||||
|
||||
|
||||
## Eve (17 element types)
|
||||
|
||||
- EveAssetCatalogEntry packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
|
||||
- EveAssetVariant packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
|
||||
- EveEntitySoaBuffer packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
|
||||
- EveEntitySoaColumn packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
|
||||
- EveEntitySoaDirtyRange packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
|
||||
- EveEntityIdentity packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
|
||||
- EveEntityRenderGroup packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
|
||||
- EveInputActionDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
|
||||
- EveInputProfileDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
|
||||
- EveInputBindingDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
|
||||
- EveProviderWitness packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
|
||||
- EveAdvertisedSurface packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
|
||||
- EveAdvertisedCommand packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
|
||||
- EveSurfaceComponent packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
|
||||
- EveEmbeddedDocumentSlot packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
|
||||
- EveStyleToken packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
|
||||
- EveCommandTemplate packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
|
||||
|
||||
## EvePlugins (1 element types)
|
||||
|
||||
- EveFieldsSplatLayer plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields/Runtime/EveFieldsDocuments.cs:38 (via EveFieldsSplatsDocument.Layers)
|
||||
|
||||
## Ghostlight (17 element types)
|
||||
|
||||
These types are a vendored copy of Eve (`vendor/eve`); they change when Eve does and the pin bumps.
|
||||
|
||||
- EveAssetCatalogEntry vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
|
||||
- EveAssetVariant vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
|
||||
- EveEntitySoaBuffer vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
|
||||
- EveEntitySoaColumn vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
|
||||
- EveEntitySoaDirtyRange vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
|
||||
- EveEntityIdentity vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
|
||||
- EveEntityRenderGroup vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
|
||||
- EveInputActionDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
|
||||
- EveInputProfileDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
|
||||
- EveInputBindingDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
|
||||
- EveProviderWitness vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
|
||||
- EveAdvertisedSurface vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
|
||||
- EveAdvertisedCommand vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
|
||||
- EveSurfaceComponent vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
|
||||
- EveEmbeddedDocumentSlot vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
|
||||
- EveStyleToken vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
|
||||
- EveCommandTemplate vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
|
||||
|
||||
## Gjallar (3 element types)
|
||||
|
||||
- GjallarAdvertisedSchema src/Gjallar/VerseState.cs:462 (via GjallarProviderAdvertisementRecord.Schemas)
|
||||
- GjallarAdvertisedWitness src/Gjallar/VerseState.cs:470 (via GjallarProviderAdvertisementRecord.Witnesses)
|
||||
- GjallarAdvertisedSurface src/Gjallar/VerseState.cs:480 (via GjallarProviderAdvertisementRecord.Surfaces)
|
||||
|
||||
## Heimdall (12 element types)
|
||||
|
||||
These types are a vendored copy of CultLib (`vendor/CultLib`); C2a already adds the ids in CultLib, so they resolve when the pin bumps. The witness bundle types are the fork reported with C2a.
|
||||
|
||||
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
|
||||
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
|
||||
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
|
||||
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
|
||||
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
|
||||
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
|
||||
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
|
||||
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
|
||||
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
|
||||
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
|
||||
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
|
||||
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
|
||||
|
||||
## Mimir (0 element types)
|
||||
|
||||
None.
|
||||
|
||||
|
||||
## StreamPixels (20 element types)
|
||||
|
||||
`CultGeometryDomainNode` is the vendored CultLib copy; C2a already adds its id.
|
||||
|
||||
- ActivationCondition apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ActivationCondition.cs:18 (via CharacterConditions.Conditions)
|
||||
- BattleEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/BattleEffect.cs:8 (via BattleAction.Effects)
|
||||
- GearStatEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/GearStatEffect.cs:7 (via GearData.StatEffects)
|
||||
- OverlaySceneIntent apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/OverlayFrameDocuments.cs:33 (via OverlaySceneIntentWindow.Intents)
|
||||
- PlayedIntentRecord apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/PlayedIntentLedger.cs:25 (via OverlayPlayedIntents.Entries)
|
||||
- StatModifier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatModifier.cs:7 (via GearStatEffect.Modifiers)
|
||||
- StatusEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatusEffect.cs:10 (via StatusData.Effects)
|
||||
- WeaponDamageTypeMultiplier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/WeaponDamageTypeMultiplier.cs:7 (via WeaponData.DamageMultipliers)
|
||||
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
|
||||
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
|
||||
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
|
||||
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
|
||||
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
|
||||
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
|
||||
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
|
||||
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
|
||||
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
|
||||
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
|
||||
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
|
||||
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
|
||||
|
||||
## Ymir (1 element types)
|
||||
|
||||
- YmirTransportLoweringDocument src/Ymir.Core/YmirServicePublicationDocument.cs:63 (via YmirProviderAdvertisementDocument.CommandLowerings)
|
||||
|
||||
## CultLib itself
|
||||
|
||||
C2a adds ids to: `CultGeometryDomainNode`, `CultGeometryFeatureClaim`, `CultGeometryContributionRow` (`src/GameCult.Geometry/CultGeometryDocuments.cs`); `CultMeshBodyDescriptor` (`src/GameCult.Mesh/CultMeshBodies.cs:35`); `CultMeshDiscoveryCandidateDocument` (`CultMeshDiscoveryService.cs:418`); `CultMeshEntityPrefabAssetRef`, `CultMeshEntityPrefabComponent`, `CultMeshEntityPrefabNode` (`CultMeshEntityPrefabs.cs`); `CultWitnessContractPin`, `CultWitnessArtifactEntry`, `CultWitnessTimingEntry` (`src/GameCult.Networking/CultWitnessArtifactBundle.cs`).
|
||||
|
||||
`CultMeshCdnChunkRef` (`src/GameCult.Mesh/CultMeshCdn.cs:73`) has a derived id, `[CultElementId(nameof(Offset))]`: the decimal text of its offset, never random, so packing the same body encodes the same manifest bytes. `contracts/cultmesh/content-vectors.*.json` and the Rust runtime carry it. A consumer with content-addressed elements declares the same way.
|
||||
|
|
@ -25,5 +25,5 @@ closure through UPM rather than NuGet restore.
|
|||
Install the Unity package from the immutable release tag:
|
||||
|
||||
```text
|
||||
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v2.0.0
|
||||
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.57
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
# Content-addressed HTTP answers: what the specs and sources say
|
||||
|
||||
Fork served: **cultmesh-ts-content C2 provider HTTP answers** (session label
|
||||
self-2026-10-19-cm-content). Eyes pass, 2026-10-19. Facts with evidence pointers
|
||||
only; no recommendation. Line numbers refer to the raw files fetched on
|
||||
2026-10-19 (master/main at that date), so they drift.
|
||||
|
||||
Source shorthands:
|
||||
|
||||
- **TG** = IPFS Trustless Gateway spec, https://specs.ipfs.tech/http-gateways/trustless-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/trustless-gateway.md)
|
||||
- **PG** = IPFS Path Gateway spec, https://specs.ipfs.tech/http-gateways/path-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/path-gateway.md)
|
||||
- **OCI** = OCI Distribution Spec, https://raw.githubusercontent.com/opencontainers/distribution-spec/main/spec.md
|
||||
- **Fetch** = WHATWG Fetch Standard, https://fetch.spec.whatwg.org/
|
||||
- **RFC 9110/9111/9112**, https://www.rfc-editor.org/rfc/rfc9110.txt (and 9111, 9112, 8246)
|
||||
|
||||
Two things the draft model page said "from memory" are checked at the end
|
||||
(section 8).
|
||||
|
||||
## 1. IPFS Trustless Gateway (`application/vnd.ipld.raw`)
|
||||
|
||||
| Case | What the spec says | Pointer |
|
||||
|---|---|---|
|
||||
| Found | Body is "An opaque bytes matching the requested block CID"; "The Body hash MUST match the Multihash from the requested CID." | TG "Block Responses (application/vnd.ipld.raw)" (src l.341-346) |
|
||||
| Unknown/absent hash | `404 Not Found` MUST be returned when the root block is not in the gateway's storage; applies to HEAD for any CID and GET of raw blocks. Body not specified. For non-recursive gateways it "definitively signals" the content is not in the dataset. | TG "Response Status Codes / 404 Not Found" (src l.274-283); "Recursion" (l.606-612) |
|
||||
| Malformed request | TG defines no status for a malformed CID. It says servers SHOULD "validate CID format and encoding before processing requests". PG says `400 Bad Request` is a "generic client error", example "malformed CID". TG gives 400 for missing `Accept` in strict trustless mode (l.108) and for an out-of-range `entity-bytes` (l.210). | TG "Security Considerations / HTTP Servers" (src l.616-628); PG "Response Status Codes" |
|
||||
| Stored object failing its digest | **Not specified for servers.** The only requirement is client-side: clients MUST verify block digest against the CID before processing or storing. TG `500` is "only for genuine server errors, not for content unavailability" (storage failure, resource exhaustion, unexpected internal errors); corruption is not named. | TG "HTTP Clients / Block Validation" (src l.629-631); "500 Internal Server Error" (l.294-296) |
|
||||
| Range | TG defines no Range support for raw blocks. PG says Range is "for requesting specific byte range of UnixFS files and raw blocks"; gateways SHOULD support single-range, multi-range optional; success is `206` with `Content-Range`; `Accept-Ranges` is optional, `none` is the example for non-deterministic CAR order. | PG "Range (request header)" (src l.184-193), "Content-Range" (l.626), "Accept-Ranges" (l.632-640) |
|
||||
| HEAD | `HEAD /ipfs/{cid}` is GET without payload; missing root block is 404 for HEAD too. PG: gateways SHOULD do minimal work to produce headers such as `Content-Length`. For the probe path, all HEAD headers are optional. | TG "HEAD /ipfs/{cid}" (src l.68-72), "404" (l.274-283), "Dedicated Probe Paths" (l.525-548); PG HEAD text |
|
||||
| CORS | **TG defines none.** PG only says gateways serving cross-origin traffic SHOULD list `Ipfs-Uri` (and `X-Ipfs-Path`) in `Access-Control-Expose-Headers`. Kubo's config doc says its default `Gateway.HTTPHeaders` is `{}` "+ implicit CORS headers from `boxo/gateway#AddAccessControlHeaders` and ipfs/specs#423"; the values themselves are not in that file. | PG header sections; https://raw.githubusercontent.com/ipfs/kubo/master/docs/config.md l.1484-1488 |
|
||||
| Cache-Control | Response value is in PG, not TG: `Cache-Control: public, max-age=29030400, immutable` "MUST be returned for every immutable resource under `/ipfs/` namespace". (Request-side `Cache-Control: only-if-cached` returns `412` when the root block is not immediately available.) | PG "Cache-Control (response header)" (src l.488-500); TG "Cache-Control: only-if-cached" (l.120-126) |
|
||||
| ETag | TG: "MUST be returned and follow the recommendations in" PG. PG default is the requested CID in quotes: `Etag: "bafy…foo"`; format-specific variants add a suffix (`"bafy…foo.raw"`); non-deterministic streams use `W/`. PG: gateway MUST compare `If-None-Match` against its `Etag` (answer 304). TG: clients SHOULD NOT assume ETags are portable across gateways. | TG "Etag (response header)" (src l.324-338); PG l.130-136, l.453-486 |
|
||||
| Content-Type | "MUST be returned"; for raw blocks the type is `application/vnd.ipld.raw` (IANA registered). No charset parameter is specified. | TG "Content-Type (response header)" (src l.300-308); IANA entry linked at l.343 |
|
||||
| Other headers | `Content-Disposition` MUST be `attachment` with `filename="<cid>.bin"` for raw blocks, "to ensure requested bytes are not rendered by a web browser". PG: `X-Content-Type-Options: nosniff` should be returned with `application/vnd.ipld.raw` and `.car` (from the PG fetch summary; line not re-grepped). | TG "Content-Disposition" (src l.310-316); PG "X-Content-Type-Options" |
|
||||
| Content-Encoding / Length | PG: `Content-Length` "Represents the length of returned HTTP payload", and warns "the value may differ from the real size of requested data if compression or chunked `Transfer-Encoding` are used", citing ipfs/specs#461 (an open TODO in the spec). | PG "Content-Length (response header)" (src l.613-624) |
|
||||
| Size cap | Clients SHOULD cap block size; the spec names 2 MiB as the p2p block limit (see Block Limits). | TG "Block Limits" (src l.558-573) and "HTTP Clients" (l.629-640) |
|
||||
|
||||
## 2. OCI Distribution Spec, blob `GET`/`HEAD /v2/<name>/blobs/<digest>`
|
||||
|
||||
| Case | What the spec says | Pointer |
|
||||
|---|---|---|
|
||||
| Found (GET) | "MUST provide the expected blob, with a response code that MUST be `200 OK`." Response MUST contain `Docker-Content-Digest`; "If present, the value of this header MUST be a digest matching that of the response body." Clients MAY ignore it, but if used MUST verify it; "Clients SHOULD verify that the response body matches the requested digest." | OCI "Pulling blobs" (src l.180-195) |
|
||||
| Unknown | `404 Not Found` MUST be returned (GET and HEAD). The endpoint table lists only `200` success and `404` failure. Error code names `BLOB_UNKNOWN` (code-1) exist in the error table; this section does not require a JSON body. | OCI l.194, l.209-216; Endpoints table l.846 (end-2); Error Codes table l.891 |
|
||||
| Malformed digest | **Not specified for blob pull.** The table lists only 404. (Other endpoints: PUT blob 400 on digest problems; DELETE blob 404/400/405.) `DIGEST_INVALID` is defined as "provided digest did not match uploaded content" (upload semantics). | OCI table l.846-858; code-4 l.894 |
|
||||
| Stored object failing its digest | **Not specified for servers.** Client-side verification only (above). | OCI l.188-193 |
|
||||
| Range | "A registry SHOULD support the `Range` request header in accordance with RFC 9110 (section 14)." No `206`/`416`/`Accept-Ranges` text for blob pull (416 appears only in the upload flow, l.392, 421). | OCI l.196 |
|
||||
| HEAD | HEAD on an existing blob MUST return `200`; MUST contain `Docker-Content-Digest` and `Content-Length` ("the size in bytes"); not found is `404`. Note: older registries may omit the digest header; clients "can reasonably assume the digest algorithm used is sha256". | OCI "Checking if content exists" (src l.198-216) |
|
||||
| CORS | **Not in spec.** | whole OCI spec (no `Access-Control` occurrence found in the `grep` of the file) |
|
||||
| Cache-Control/ETag | **Not in spec.** | OCI spec |
|
||||
| Content-Type | **Not specified for blob GET**; `application/octet-stream` appears only for upload requests. | OCI upload sections |
|
||||
| Redirects | Registries may answer blob GET with a redirect; clients must not forward `Authorization` across hosts. | OCI pull text (from the WebFetch of the same file; not re-grepped) |
|
||||
|
||||
Known failures near this area (NVD records, fetched 2026-10-19):
|
||||
|
||||
- CVE-2024-3727 (containers/image, Red Hat): "integrity-check flaw" allowing unexpected authenticated registry access, resource exhaustion, local path traversal. It affects the client library, not a registry server. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-3727
|
||||
- CVE-2026-85747 (distribution/distribution, fixed 3.1.2): the client's manifest `Get()` does not hash the body against the requested digest, so a malicious registry can substitute content. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-85747 (status "Awaiting Analysis")
|
||||
- CVE-2026-93317 (Docker build): a registry or OCI-layout blob source could supply content not matching the claimed digest and have it cached under that digest. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-93317 (status "Awaiting Analysis")
|
||||
- Searched for a path traversal CVE in a registry server's digest-to-path mapping: none surfaced.
|
||||
|
||||
## 3. Nix binary cache (HTTP)
|
||||
|
||||
The manual's Binary Cache page defines layout only (a `nix-cache-info` file, a
|
||||
`.narinfo` per object with "a (usually relative) URL to the corresponding
|
||||
compressed NAR", the NAR). It states no status codes, headers, CORS, Range or
|
||||
Cache-Control. https://nix.dev/manual/nix/2.35/protocols/binary-cache/ (page head).
|
||||
The behavior below is from the Nix client/writer source.
|
||||
|
||||
| Case | What the source shows | Pointer |
|
||||
|---|---|---|
|
||||
| Found | Writer stores NARs at `nar/<FileHash nix32>.nar<ext>`, i.e. named by the hash of the compressed file; narinfo records `FileHash`, `FileSize`, plus `NarHash`. | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/binary-cache-store.cc l.156-181 |
|
||||
| Absent | Client maps HTTP `404` and `410` to `NotFound` ("The file is definitely not there"); `403` to `Forbidden`; `408`/`429` to `Transient`. `fileExists` issues a **HEAD** and treats `NotFound` **and `Forbidden`** as absent, "S3 buckets return 403 if a file doesn't exist and the bucket is unlistable". | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/filetransfer.cc l.826-838; .../http-binary-cache-store.cc l.184-202 |
|
||||
| Malformed request | Not specified. | n/a |
|
||||
| Failing digest | Client side only; the writer's `FileHash`/`NarHash` are checked by the consumer. No server answer defined. | binary-cache-store.cc l.175-181, l.313 |
|
||||
| Range | Client records `acceptRanges` for retries (client-side use of Range). No server rule. | filetransfer.cc l.219 |
|
||||
| HEAD | Used for existence probing (above). | http-binary-cache-store.cc l.190 |
|
||||
| CORS / Cache-Control / ETag | Not in spec or the files read. | n/a |
|
||||
| Content-Type | Writer uploads `text/x-nix-narinfo` (narinfo), `text/x-nix-cache-info`, and `application/x-nix-nar` (NAR). | binary-cache-store.cc l.57, l.135, l.270 |
|
||||
| Content-Encoding | Writer can upload with a `Content-Encoding` header naming the compression (http-binary-cache-store.cc l.232). Client sets `CURLOPT_ACCEPT_ENCODING ""` (accept any supported) and tracks a `hasContentEncoding` flag. The NAR itself is stored already compressed (named by compressed-file hash), so transport encoding and stored compression are separate. | filetransfer.cc l.223-224, l.616 |
|
||||
|
||||
## 4. Bazel remote cache, HTTP
|
||||
|
||||
| Case | What the source/spec says | Pointer |
|
||||
|---|---|---|
|
||||
| Protocol | Bazel docs: "Binary data (BLOB) is uploaded via PUT requests and downloaded via GET requests"; action results under `/ac/`, files under `/cas/`; any "HTTP/1.1 server that supports PUT and GET" can serve. The doc page states **no** status codes, no digest verification on PUT, no Content-Type, no compression, no Range. HEAD is not mentioned there. | https://bazel.build/remote/caching "HTTP caching protocol" |
|
||||
| bazel-remote README | CAS key is "the lowercase SHA256 hash of the entry"; `/cas/<key>` or `/<instance>/cas/<key>` (instance "optional but ignored"); "HEAD requests can be used to confirm whether a key exists"; the empty blob is "always available, even if the cache is empty" (HEAD example: `200` with `Content-Length: 0`); GET with `Accept-Encoding: zstd` "may" return zstd data; PUT of zstd requires `Content-Encoding: zstd` and `X-Digest-SizeBytes`. | https://raw.githubusercontent.com/buchgr/bazel-remote/master/README.md (HTTP section) |
|
||||
| Found | `200`, `Content-Type: application/octet-stream`, `Content-Length` set to the stored size (uncompressed path). | https://raw.githubusercontent.com/buchgr/bazel-remote/master/server/http.go l.275-282 |
|
||||
| Absent | `404`, body "Not found" via `http.Error`. | http.go l.268-271 |
|
||||
| Malformed | Path must match `^/?(.*/)?(ac/\|cas/)([a-f0-9]{64})$` (lowercase 64-hex); otherwise `400` with the escaped URL echoed in the message (`html.EscapeString`). | http.go l.33, l.101-112, l.216-223 |
|
||||
| Failing digest (read) | No digest check on the GET path in this handler (reader returned by `cache.Get`, `io.Copy` to the client). A failure after headers are sent is logged and cannot change the status ("we've already started writing data"). | http.go l.284-297 |
|
||||
| Digest check (write) | PUT checks size match (`400` "sizes don't match") and `Content-Encoding` is `identity`/`zstd`/absent (else `400`); the remaining digest verification is in code not read here. | http.go l.306-345, l.372-388 |
|
||||
| Compression | When zstd is chosen the handler sets `Content-Encoding: zstd` and **omits `Content-Length`** ("TODO: calculate Content-Length for compressed blobs too"). | http.go l.246-250, l.275-281 |
|
||||
| HEAD | `200` with `Content-Length` of the size, or `404`. | http.go l.468-491 |
|
||||
| Range / CORS / Cache-Control / ETag | Not present in the handler excerpts read. | n/a |
|
||||
|
||||
## 5. Git over HTTP
|
||||
|
||||
Smart HTTP does not address single objects. Dumb HTTP does: URL
|
||||
`$GIT_URL/objects/XX/YYYY…` for a loose object.
|
||||
https://git-scm.com/docs/http-protocol (introduction example).
|
||||
|
||||
| Case | What the spec/source says | Pointer |
|
||||
|---|---|---|
|
||||
| Absent | Server "MUST NOT respond with `200 OK`"; "SHOULD respond with `404 Not Found`, `410 Gone`, or any other suitable HTTP status code which does not imply the resource exists". Access denied MUST be `403`. | git http-protocol "General Request Processing" |
|
||||
| Caching | Clients MAY reuse a cached entity per `Cache-Control`/`Expires` (follow RFC 2616); servers MAY return `304`; clients treat `304` as `200`. Smart services must prevent caching. The page states no Cache-Control value for object files. | same section |
|
||||
| Content-Type | Specified only for `info/refs` (`text/plain; charset=utf-8` SHOULD, clients MUST NOT validate); nothing for object files. | same page |
|
||||
| Failing digest | Client side: the dumb walker compares the received object's computed id with the requested id and errors `File %s has bad hash`. | https://raw.githubusercontent.com/git/git/master/http-walker.c l.540-541 |
|
||||
| Missing target | The client treats a missing-target result as `HTTP_MISSING_TARGET` (http.c l.1944). | https://raw.githubusercontent.com/git/git/master/http.c |
|
||||
| Content-Encoding | Client sets `CURLOPT_ENCODING ""` at http.c l.2284 (accept any encoding curl supports, curl decodes transparently) and sends `Pragma: no-cache` (l.1452). The http-protocol page says nothing about gzip for objects. | http.c |
|
||||
| Range, CORS, ETag | Not in the page. | n/a |
|
||||
|
||||
## 6. Browser fetch facts (verifying reader)
|
||||
|
||||
| Fact | Evidence |
|
||||
|---|---|
|
||||
| CORS-safelisted response-header names are exactly `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified`, `Pragma`. So `Content-Length` is readable cross-origin from a CORS response without `Access-Control-Expose-Headers`. `ETag` is **not** on the list; it needs `Access-Control-Expose-Headers` to be visible. | Fetch 2.2.2 "Headers", CORS-safelisted response-header name (https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) |
|
||||
| The CORS-exposed header-name list comes from `Access-Control-Expose-Headers`; a CORS filtered response exposes safelisted names plus that list. | Fetch 2.2.6 "Responses" (https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list); 3.3.3 |
|
||||
| CORS check: failure if `Access-Control-Allow-Origin` is absent; "If request's credentials mode is not "include" and origin is `*`, then return success." With credentials mode `include`, `*` is not enough (needs exact origin plus `Access-Control-Allow-Credentials: true`). | Fetch 4.10 "CORS check"; 3.3.5 |
|
||||
| A CORS-preflight happens for non-safelisted methods/headers (`GET`/`HEAD` with no custom headers are simple requests). | Fetch 3.3 (general); not re-quoted here |
|
||||
| Fetch decodes content codings before exposing the body. In HTTP-network fetch: it records encoded size from transmitted bytes, sets `bytes` to "the result of handling content codings", then increases decoded size by the decoded length. Unsupported codings: bytes are returned as-is; decoding error: failure. | Fetch 4.7 "HTTP-network fetch"; 2.2.4 "handle content codings" (https://fetch.spec.whatwg.org/#handle-content-codings) |
|
||||
| The spec itself notes: "This makes the `Content-Length` header unreliable to the extent that it was reliable to begin with." (the header describes the encoded body; the body the page reads is decoded.) | Fetch 4.7 (fetch_t.txt l.9670) |
|
||||
| When a request has a `Range` header, the browser appends `Accept-Encoding: identity`, "This avoids a failure when handling content codings with a part of an encoded response." | Fetch 4.6 "HTTP-network-or-cache fetch" (l.8760-8767) |
|
||||
| HTTP framing: with a valid `Content-Length`, a response is incomplete if fewer octets arrive than `Content-Length`; a client MUST record it as incomplete. `Content-Length` "refers specifically to the amount of data enclosed" (encoded form on the wire). | RFC 9112 section 8 "Handling Incomplete Messages"; RFC 9110 section 8.6 |
|
||||
| `Cache-Control: no-transform`: an intermediary "MUST NOT transform the content". | RFC 9111 section 5.2.2.6 |
|
||||
| `immutable` extension: identifies responses that will not be updated during freshness lifetime so clients need not revalidate. | RFC 8246 section 2 |
|
||||
|
||||
## 7. HTTP core facts that apply to all five systems
|
||||
|
||||
| Fact | Evidence |
|
||||
|---|---|
|
||||
| Range: "A server MAY ignore the Range header field"; a server MUST ignore Range for methods without defined range handling (GET is the only one). | RFC 9110 section 14.2 "Range" (l.6476-6484) |
|
||||
| HEAD: "server SHOULD send the same header fields" as for GET; a `Content-Length` on HEAD MUST equal the GET length if sent. | RFC 9110 section 9.3.2 (l.3993), section 8.6 (l.3225) |
|
||||
| `304`: a `Content-Length` in 304 MUST equal what a 200 would carry, if present. | RFC 9110 section 8.6 |
|
||||
| `Content-Length` MUST NOT be sent on `204`. | RFC 9110 section 8.6 |
|
||||
|
||||
## 8. Cross-check against the draft model page's from-memory claims
|
||||
|
||||
| Draft claim (map l.44-49) | Finding |
|
||||
|---|---|
|
||||
| IPFS trustless gateway answers raw blocks with `Cache-Control: public, max-age=29030400, immutable` | In **PG**, not TG: the `/ipfs/` immutable value is a MUST there; TG itself does not state the response value. |
|
||||
| IPFS uses the CID as `ETag` | PG: default `Etag: "<cid>"` quoted; variants add suffixes (raw: `"<cid>.raw"`). |
|
||||
| IPFS returns 400 for an unparseable CID | PG lists `400` as "generic client error" with malformed-CID as the example. TG itself lists no malformed-CID rule. |
|
||||
| OCI `GET`/`HEAD` returns 200 with the digest header | Confirmed (`Docker-Content-Digest` MUST on GET and HEAD; `Content-Length` MUST on HEAD). |
|
||||
| OCI 404 `BLOB_UNKNOWN` | 404 confirmed; `BLOB_UNKNOWN` exists as a code name; the blob-pull text does not mandate a JSON body. |
|
||||
| OCI 400 `DIGEST_INVALID` on a bad digest | **Not in the blob-pull text.** Endpoint table lists only 404 for blob GET/HEAD; `DIGEST_INVALID` means "digest did not match uploaded content". |
|
||||
| OCI Range (206/416) optional | Spec says "SHOULD support" Range per RFC 9110; no 206/416/Accept-Ranges text for pull. |
|
||||
| Nix `<hash>.narinfo` 200 or 404, probed with HEAD | Client source: HEAD for existence; 404 and 410 both map to NotFound; 403 also treated as absent in `fileExists`. |
|
||||
| Map's own claims (open `*` CORS, 500 on corrupt, Range ignored, Cache-Control immutable) | `*` with non-`include` credentials passes the CORS check (Fetch 4.10). No system surveyed specifies a status for a corrupt stored object. Ignoring Range is permitted (RFC 9110 14.2). |
|
||||
|
||||
## 9. Where the systems agree and differ (matrix of what was found)
|
||||
|
||||
| Item | IPFS TG/PG | OCI | Nix | Bazel (bazel-remote) | Git dumb |
|
||||
|---|---|---|---|---|---|
|
||||
| Absent | 404 (MUST, TG) | 404 (MUST) | 404/410 -> NotFound; 403 also "absent" in HEAD probe | 404 "Not found" | 404/410/other non-200 |
|
||||
| Malformed id | PG 400 generic; TG silent | silent for blob pull | silent | 400 with message | silent |
|
||||
| Corrupt stored object (server) | silent; client MUST verify | silent; client SHOULD verify | silent; client verifies hashes | no read-path check in handler | silent; client errors "bad hash" |
|
||||
| Range | PG: 206 supported (raw blocks); TG silent | SHOULD support | client-side retry only | not in handler | not in spec |
|
||||
| HEAD | same as GET minus body; 404 | 200 + digest + Content-Length MUST | used to probe | 200 + Content-Length or 404 | not in spec |
|
||||
| CORS | none in TG/PG (Kubo adds implicit headers) | none | none | none | none |
|
||||
| Cache-Control on content | PG: `public, max-age=29030400, immutable` MUST | none | none | none | defers to RFC 2616 |
|
||||
| ETag | PG: quoted CID | none | none | none | 304 allowed |
|
||||
| Content-Type | `application/vnd.ipld.raw` | unspecified (pull) | `application/x-nix-nar` etc. | `application/octet-stream` | unspecified (objects) |
|
||||
| Content-Length vs encoding | PG warns they may differ | n/a | transport `Content-Encoding` separate from stored compression | zstd path omits Content-Length | client auto-decodes |
|
||||
| nosniff / attachment | TG: Content-Disposition attachment MUST; PG: nosniff | none | none | none | none |
|
||||
|
||||
## 10. Limits of this pass
|
||||
|
||||
- Fetch spec 4.7 was read from the HTML text; the exact algorithm line that fails the fetch on a body shorter than Content-Length was not found in the Fetch text (framing is RFC 9112 section 8, which says the client records the message incomplete).
|
||||
- bazel-remote digest verification on PUT was not read to the end; the read-path (GET) has no digest check in the handler.
|
||||
- Kubo's actual default CORS header values were not retrieved (only the pointer to boxo and ipfs/specs#423).
|
||||
- git CVEs were not searched; Nix and Bazel CVEs were not searched.
|
||||
- OCI "CORS: not in spec" is from a search of the spec file, not from a registry implementation (distribution/distribution's own headers were not read).
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Consumer pinning at a breaking land: prior art and internal state
|
||||
|
||||
Serves fork `variants:question:unpinned-consumers-at-land` (campaign `variants`, session `self-2026-10-22-eureka-c2a`).
|
||||
Eyes pass, 2026-10-22. Facts and evidence pointers only; no recommendation.
|
||||
|
||||
Question as admitted: Eve.Surface and Brokkr build against an unpinned sibling `..\CultLib` source checkout. C2a (refuse element types with no id) lands on main. Options: pin-then-land, sweep-then-land, land-and-break.
|
||||
|
||||
## Sources
|
||||
|
||||
| # | Source | URL | Status |
|
||||
|---|---|---|---|
|
||||
| S1 | Abseil, About / philosophy | https://abseil.io/about/philosophy | fetched |
|
||||
| S2 | Abseil, compatibility guidelines | https://abseil.io/about/compatibility | fetched |
|
||||
| S3 | Software Engineering at Google, ch. 21 Dependency Management | https://abseil.io/resources/swe-book/html/ch21.html | fetched |
|
||||
| S4 | Software Engineering at Google, ch. 16 Version Control and Branch Management | https://abseil.io/resources/swe-book/html/ch16.html | fetched (does not cover live at head) |
|
||||
| S5 | Russ Cox, Minimal Version Selection | https://research.swtch.com/vgo-mvs | fetched |
|
||||
| S6 | Cargo book, Cargo.toml vs Cargo.lock | https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | fetched |
|
||||
| S7 | npm docs, package-lock.json | https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json | fetched |
|
||||
| S8 | Chromium src, dependencies doc (DEPS, gclient, roll-dep) | https://chromium.googlesource.com/chromium/src/+/main/docs/dependencies.md | fetched |
|
||||
| S9 | Pro Git, Submodules | https://git-scm.com/book/en/v2/Git-Tools-Submodules | fetched |
|
||||
| S10 | chromium infra dependencies.md | https://chromium.googlesource.com/infra/infra/+/main/docs/dependencies.md | 404, not retrieved |
|
||||
| L1 | `F:\Projects\Aetheria\Directory.Build.targets`, `Directory.Build.props`, git history | local files | read |
|
||||
| L2 | `F:\Projects\Aetheria\docs\cultcache-migration-cut.md` (Q6 table, lines ~565-580; pin guard notes ~1900-1925) | local file | read |
|
||||
| L3 | `F:\Projects\Eve` and `F:\Projects\Brokkr` csprojs and git file lists | local files | read |
|
||||
| L4 | voidbot `search_history` (two queries), `search_sources` (one query) | MCP | queried |
|
||||
|
||||
Anything not in a "fetched" row above is not claimed from the web.
|
||||
|
||||
## 1. Established practice: who pays for a breaking upstream change, and when
|
||||
|
||||
- **Abseil / live at head (S1, S2).** Recommends users track the latest code. Says it will provide a tool to perform an API refactor for well-behaved code (S1). Requires of downstream: show how you call it, stay within the compatibility guidelines; otherwise fork or use periodic supported branches (about every 6 months, patched 2 years) (S1). Says every change is potentially breaking for someone; breaks are announced in commit messages and LTS notes, and users are asked to report undetected breaks on GitHub (S2). Users should be able to temporarily pin to an earlier commit if a newer change is a problem (S2). S1/S2 do not mention Rosie, the Beyonce rule, or caller-fixing; the fetched pages describe tooling and a feedback channel only.
|
||||
- **Live at Head as described in SWE at Google ch. 21 (S3).** Providers are expected to test changes against downstream dependents before committing; breaking changes wait until dependents are updated or an automated migration tool exists. Consumers must keep meaningful tests. Stated preconditions: unit tests across dependencies run in CI, a visible reverse-dependency graph, substantial compute, no pinning of dependencies, provider acceptance of the downstream-testing duty. Called "costly but effective" inside Google and "largely unproven at scale." Notes that under SemVer a major bump costs maintainers little and many indirect users a lot. Ch. 16 (S4) does not address live at head; it states large-scale changes across trunk are costly and tooling-supported.
|
||||
- **Go MVS (S5).** Selects the oldest version satisfying all declared minimums. Publishing a new version changes nobody's build; a consumer gets it only when a requirement names it, directly or through another module. Upgrades are explicit edits to `go.mod` by the top-level module. Compatibility rule: newer versions inside one major version are expected to work where older did; a major version is a different module identity. Cost of a breaking change falls on whoever opts into the new version, at the time they opt in.
|
||||
- **Cargo (S6).** Git dependency without `rev` takes the default-branch tip at first build and fixes that commit in `Cargo.lock`; changes arrive only on `cargo update`. A `rev` in `Cargo.toml` pins explicitly; the Cargo book calls hand-managed SHAs "tedious and error prone" and prefers the lockfile. Consumer pays at `cargo update` time.
|
||||
- **npm (S7).** `package-lock.json` records the exact tree; installs stay on it until the lockfile changes and is committed. It is not published with a package. The page gives no timeline for when updates reach a consumer.
|
||||
- **Chromium DEPS (S8).** `DEPS` stores dependency URLs and commit hashes (often in `vars`). Developers roll with `gclient setdep`, `roll-dep`, or manual `git update-index`; some projects (Skia) are autorolled. The page says manual rolling after an upstream change is good practice to confirm the roll compiles and tests pass. It does not describe revert procedure. Cost is paid by the roller at roll time.
|
||||
- **Git submodules (S9).** The superproject records one commit SHA (gitlink, mode 160000). Upstream commits never move the pin; it moves when someone updates the checkout and commits the new SHA in the superproject. Collaborators receive the pin via pull and must run `git submodule update`.
|
||||
|
||||
## 2. Internal precedent
|
||||
|
||||
- **Aetheria pin (L1).** `Directory.Build.props` holds `CultLibRevision` (currently `45c2f4006f5f7dab357426cabd3197e8d0a17f66`) and `CultMathRevision` (`9c1b06cd703bfa44352050e819b0d0659cf5978a`). `Directory.Build.targets` target `VerifyCultLibRevision` (BeforeTargets `ResolveProjectReferences`) errors if the sibling checkout's `git rev-parse HEAD` differs from the pin, or if `git status --porcelain` is non-empty; escape hatch `SkipCultLibRevisionCheck=true`. File is 39 lines; the pin is one pair of properties per pinned package.
|
||||
- **Why added (L1 git).** Commit `4f4beb0f` (2026-09-14, "Pin CultLib for headless builds"): "Directory.Build.targets carries Delvehold's VerifyCultLibRevision guard, checking for CultMath.csproj: HEAD must equal the pin and the worktree must be clean." Commit `e91ba369` (2026-09-24, "Pin CultLib per package, as Unity's manifest does"): headless build must compile the revisions Unity consumes; a single pin would have compiled six commits of newer CultCache under headless tests while Unity stays on its tag. The Aetheria cut doc (L2, ~line 1905) calls the guard "the only working pin for a sibling checkout." Aetheria's `Packages/manifest.json` pins Unity packages by git tag.
|
||||
- **Pin guard already in Delvehold (L4/search_sources hit, `Delvehold:Directory.Build.targets`)** and a variant in AetheriaEve (archived, `Aetheria.State.Dependencies.targets`, also checks a Unity package tree hash). Delvehold's guard checks `GameCult.Mesh.csproj` exists, HEAD equals `CultLibRevision`, tree clean. Date of Delvehold's first guard: not determined.
|
||||
- **Consumer table (L2, Q6 in Aetheria `docs/cultcache-migration-cut.md`).** Aquarium, Ymir, AquaSynth, Mimir, Gjallar: ".NET sibling, unpinned", compile against main at once. Brokkr, Eve, EveUnity, EvePlugins: "Unity package / sibling", pin "various". Delvehold: sibling with revision guard at `334e60f`, 52 behind, "Not until it re-pins." Heimdall: submodule `b6b1d9c`. Rust consumers (Idunn, Epiphany, Odin, Ghostlight, Muninn, Ratatoskr, CodexConnector): Cargo by commit.
|
||||
- **Operator discussion (L4).** voidbot `search_history` ("pin CultLib revision vs track main for consumers"; "Brokkr Eve sibling CultLib checkout build pin") returned no relevant operator statement on pinning versus tracking; hits were unrelated Discord chatter. Nearest operator words found in the Discord index: Metacrat, 2026-05-09, "Oh, anyone using CultLib will still be paying the blood price, you can count on that" (message 1502671320406687894; context not fetched, so its subject is not confirmed). No operator quote on pinning versus tracking was found in memory files grepped (`C:\Users\Meta\.claude\projects\F--Projects\memory`, pattern pin/track main/unpinned/sibling) beyond a retired Idunn pin line and Odin/Muninn pin-branch coordination notes.
|
||||
- Aetheria cut doc operator mentions near pins are about content (Schematic-shape drawer, 2026-09-14; Studio grouping, 2026-09-17), not pinning policy.
|
||||
|
||||
## 3. Current state of the two consumers (L3)
|
||||
|
||||
- **Eve.** `packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj:16` `CultLibRoot` defaults to `$(MSBuildThisFileDirectory)..\..\..\CultLib`; lines 20-21 ProjectReference `$(CultLibRoot)\src\GameCult.Caching\...` and `...\GameCult.Mesh\...`. `tests/GameCult.Eve.Surface.Serialization.Tests/*.csproj:19` references `$(CultLibRoot)\src\GameCult.Mesh\...` (and Eve.Surface by relative path). `git ls-files` shows only `packages/org.gamecult.eve.surface/Directory.Build.props` (sets only BaseOutputPath/BaseIntermediateOutputPath/MSBuildProjectExtensionsPath, no revision). No `Directory.Build.targets`, no `global.json`, no `.gitmodules`, no revision property or check. Eve also holds Cargo.toml/Cargo.lock (Rust side, not examined).
|
||||
- **Brokkr.** `brokkr-command/Brokkr.Command.csproj:7`, `brokkr-contracts.Tests/Brokkr.Contracts.Tests.csproj:7`, `brokkr-contracts/Brokkr.Contracts.csproj:8`: `CultLibRoot` defaults to `..\..\CultLib`. ProjectReferences: Caching, Caching.MessagePack, Mesh, Networking (by project). No `Directory.Build.*`, no `.gitmodules` in `git ls-files`; no revision property or check.
|
||||
- Neither repo contains `CultLibRevision`, a `VerifyCultLibRevision` target, or any HEAD check. Both build whatever `F:\Projects\CultLib` currently holds (including dirty or other-branch states).
|
||||
- Fork text claims (not re-verified here): Eve.Surface has 0 `[CultElementId]`; Brokkr `[CultDocument]` files show no object lists by grep.
|
||||
|
||||
## Gaps
|
||||
|
||||
- S10 (Chromium infra) 404; Chromium autoroller behavior beyond S8 not fetched.
|
||||
- Rosie / Beyonce rule not found in fetched pages; not cited from memory.
|
||||
- No operator statement on pin-versus-track found in voidbot or the memory grep.
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
Serves the C2a fork on finding variants:finding:cut-land-c2a-gate.s1.reflist-record-read-only-at-head (campaign variants), 2026-10-23.
|
||||
|
||||
# Prior art: read with an old writer schema, then write back
|
||||
|
||||
Facts only. Pages were fetched 2026-10-23 through a summarising fetcher; "not stated" means the fetched page did not say it, not that no source does. Items marked (memory) are my background knowledge, not verified from a fetched page.
|
||||
|
||||
## 1. Apache Avro
|
||||
- Source: https://avro.apache.org/docs/current/specification/ (Schema Resolution; summarised via apache/avro doc source), Java `DataFileWriter` javadoc (1.11.1).
|
||||
- Model: the writer schema travels with the data. The reader resolves it against its own reader schema. Resolution is a read-time projection; the spec defines no write-back.
|
||||
- Data loss on projection: "the writer's value for that field is ignored" when the reader schema lacks the field. A reader-only field takes its default, or the read errors if it has none.
|
||||
- Write-back: the spec states no rule. Object container files store one schema in the header ("Each datum in a file is of the same schema"). `appendTo` is documented only as "Open a writer appending to an existing file"; it does not say which schema the append uses. (memory: Java `appendTo` reuses the file's header schema, so an append never restamps.)
|
||||
- Stated reason for the read-time design: readers and writers evolve separately (fields added or removed in a different software version).
|
||||
|
||||
## 2. Protocol Buffers (proto3)
|
||||
- Source: https://protobuf.dev/programming-guides/proto3/ (Unknown Fields).
|
||||
- On write: unknown fields are "included during parsing and in the serialized output" (matches proto2). Round-trip keeps fields the current type does not know.
|
||||
- Loss cases the page lists: serializing to JSON, and copying a message field by field into a new message (it recommends `CopyFrom`/`MergeFrom`).
|
||||
- No schema id or version is written. The wire format carries only field numbers, so "restamp" has no meaning here.
|
||||
- Not stated: behaviour in versions before 3.5 (memory: proto3 dropped unknown fields before 3.5, then reversed). The Java reference page says nothing on preservation.
|
||||
- Stated reason: preserve data across a mixed-version fleet.
|
||||
|
||||
## 3. Confluent Schema Registry
|
||||
- Source: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
|
||||
- BACKWARD (default): consumers on the new schema read data written with the new or the previous version. FORWARD: data written with the new schema is readable by consumers on the old one. FULL: both.
|
||||
- Write: "Each schema version gets a unique ID and an incremented version number." A producer writes under the schema it holds (its own current version). The id is stamped per message at serialisation, from the producer's schema.
|
||||
- Upgrade order is how the rules prevent loss. BACKWARD means upgrade consumers before producers. FORWARD means upgrade producers first, and keep old-schema data away from consumers until they are upgraded.
|
||||
- Not stated on the page: what a consume-modify-produce app writes. (memory: it re-serialises under its own latest schema, so fields it does not model are lost unless a compatibility rule kept them.)
|
||||
- The registry gates registration. It does not gate individual rewrites.
|
||||
|
||||
## 4. Document DBs and ORMs with stored versions
|
||||
### MongoDB schema-versioning pattern
|
||||
- Sources: https://www.mongodb.com/blog/post/building-with-patterns-the-schema-versioning-pattern ; https://www.mongodb.com/docs/manual/data-modeling/design-patterns/data-versioning/schema-versioning/
|
||||
- Documents carry `schema_version` (absent means 1). The app keeps a handler per version and writes new documents at the current version.
|
||||
- Migration strategy is the app's choice: migrate all, migrate "when accessed" (lazy), or never.
|
||||
- Not stated: whether an update of an old document restamps it. The manual's update example branches on `schemaVersion` and writes into each version's own shape. It notes its `else: null` branch adds stray null fields to the other shape.
|
||||
- Stated reasons: no downtime, failed migrations easier to handle, timing flexibility. Cost: handler code per live version.
|
||||
|
||||
### Realm (Atlas Device SDKs, now deprecated)
|
||||
- Source: https://www.mongodb.com/docs/atlas/device-sdks/sdk/swift/model-data/change-an-object-model.md
|
||||
- Per-file schema version. A changed schema needs an incremented `schemaVersion`, plus a `migrationBlock` for non-trivial changes. The file is migrated whole at open, before any read or write, and Realm "will throw an error" if the version is lower or the same but the schema differs.
|
||||
- No lazy per-object restamp. Writes happen only on a migrated file.
|
||||
- Data loss: the manual says "Deleting a property will not impact existing objects", i.e. removal is automatic and needs no developer step. Required new properties and renames need a manual block. (memory: removed-property values are dropped from the file.)
|
||||
|
||||
### Core Data
|
||||
- Source: web search results only (Apple forum threads 745114 and 785427, WWDC22-10120, "staged migrations" doc). The Apple docs URL I tried returned 404.
|
||||
- A store whose model hash differs from the current model fails to open with an incompatible-version error. Opening migrates the whole store when `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` are set. Lightweight migration covers only obvious patterns (add entity, remove relationship, required to optional, rename). Anything else needs a staged or custom migration.
|
||||
- Migration is store-wide at open. It does not happen per record on write.
|
||||
|
||||
### EF Core
|
||||
- Not fetched. (memory: no per-row schema stamp. Migrations are explicit, ordered DDL, tracked in a `__EFMigrationsHistory` table.)
|
||||
|
||||
## 5. Old-schema data held read-only until explicit migration
|
||||
### Elasticsearch archive indices
|
||||
- Sources: https://www.elastic.co/docs/deploy-manage/upgrade/deployment-or-cluster/reading-indices-from-older-elasticsearch-versions ; https://www.elastic.co/guide/en/elasticsearch/reference/current/archive-indices.html ; 8.0 migration notes (7.0+ indices readable, 6.x or earlier must be reindexed).
|
||||
- Indices from an older major are readable through search and field-caps. "They do not support the Get API or any write APIs."
|
||||
- The route to a writable copy is reindex into a new index that is "fully compatible with the current Elasticsearch version". `_source` is readable, so no data is lost on reindex.
|
||||
- Stated reason: the page does not say why they are read-only. It describes archive access as "slower read-only access", with limited query capability. Some legacy mappings cannot be imported (the original is kept in `_meta`), and that is the one place the page shows a lossy translation.
|
||||
- Third-party (Search Guard blog, unverified against Elastic): in 9.x a plain `index.blocks.write` does not satisfy the boot check; `index.verified_read_only` is needed.
|
||||
- This is the closest match to option (b) in the fetched set: an old format, readable but not writable, left until the owner reindexes it.
|
||||
|
||||
## Where restamp-on-write loses data (from the sources above)
|
||||
- Avro: a field present in the writer schema and absent from the reader schema is dropped at read, "ignored". Any rewrite from the resolved value would omit it.
|
||||
- Protobuf: unknown fields survive a binary round trip. They are lost via JSON, or when copied field by field.
|
||||
- Realm: removed properties are deleted by the automatic migration.
|
||||
- MongoDB: a handler that writes only the current shape can lose or relocate old-shape fields (the manual's example adds nulls).
|
||||
- Confluent: loss is prevented by compatibility rules at registration time, not at each write.
|
||||
|
||||
## Pattern across the set
|
||||
- No fetched system restamps a single record on write when the record's writer schema differs from the application's type, on an app-visible, per-record basis.
|
||||
- Avro, protobuf and Confluent carry the schema per file or message.
|
||||
- Realm, Core Data and Elasticsearch move or block the whole store or index at open.
|
||||
- MongoDB leaves it to the app.
|
||||
- Whole-store migration at open is the usual explicit step (Realm, Core Data). Elasticsearch is the one case of read-only-until-reindex.
|
||||
- Not found: a system that declares per-schema-id compatibility, as in option (b). Avro schema resolution and Confluent compatibility rules are the nearest analogues. Both are read-side or registration-side declarations.
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
Serves question variants:question:store-hold-user-code (campaign variants), 2026-10-23.
|
||||
|
||||
# Prior-art check: user code under a lock
|
||||
|
||||
Facts and pointers only. Each source was fetched or searched this session; what was not reachable is marked.
|
||||
|
||||
## 1. Goetz et al., Java Concurrency in Practice (Addison-Wesley, 2006), ch. 10 "Avoiding Liveness Hazards"
|
||||
Status: EXISTS, SECTION NUMBER CORRECTED.
|
||||
- "Open Calls" is **10.1.4**, not 10.1.3. 10.1.3 is "Deadlocks Between Cooperating Objects"; 10.1.2 is "Dynamic Lock Order Deadlocks"; 10.1.5 is "Resource Deadlocks". Source: text extracted from a PDF copy of the book (its TOC lists Listing 10.6 "Using Open Calls to Avoiding Deadlock Between Cooperating Objects", p. 133). The copy is an unofficial one at library.megu.edu.ua; a publisher link was not checked.
|
||||
- (a) 10.1.3 gives the warning sign: an "alien method" (defined p. 40 as a method whose behavior the calling class does not fully specify) called while holding a lock. Paraphrase: such a call may take other locks (deadlock risk) or block a long time, stalling threads that need the lock you hold. 10.1.4 defines an open call as a call made with no locks held (credited to Lea, CPJ 2.4.1.3) and says programs restricted to open calls are far easier to analyze for deadlock-freedom than ones that allow alien calls under lock.
|
||||
- Cost it names: moving a call out of a synchronized block makes an atomic operation non-atomic (its getImage example stops being a snapshot). Where atomicity is needed it suggests structuring so only one thread can run the code path after the open call (service shutdown example).
|
||||
- (b), (c): not addressed in this section.
|
||||
- Pointer: JCIP 10.1.3 and 10.1.4, pp. ~132-134 (chapter 10 starts p. 128).
|
||||
|
||||
## 2. Sutter, "Effective Concurrency: Avoid Calling Unknown Code While Inside a Critical Section"
|
||||
Status: VERIFIED, with date caveat.
|
||||
- Dr. Dobb's column in the "Effective Concurrency" series. Sutter's blog teaser is dated 2007-11-06: https://herbsutter.com/2007/11/06/effective-concurrency-avoid-calling-unknown-code-while-inside-a-critical-section/ . Original link ddj.com/architect/202802983; Dr. Dobb's mirror https://drdobbs.com/cpp/avoid-calling-unknown-code-while-inside/202802983 (4 pages). The Dr. Dobb's issue date was not shown in anything fetched; the blog date is the only date confirmed. www.drdobbs.com did not resolve from this machine, so the column body comes from search-result summaries and the blog teaser, not a full first-hand read.
|
||||
- (a) Core claim: locks do not compose; two separately correct modules can deadlock when combined. Trigger: caller inside a critical section, callee enters another critical section or blocks, another thread can take the same two in the opposite order. "Unknown code" listed as: library functions, plug-ins, callbacks/function pointers, virtual methods, generic types.
|
||||
- Mitigations listed: move the call outside the critical section; pass copies instead of shared data; narrow the critical section (finer locks, reader-writer); require callees not to block (documentation/discipline only).
|
||||
- (b), (c): not addressed.
|
||||
|
||||
## 3. Linux lockdep
|
||||
Status: VERIFIED, with a nuance.
|
||||
- Source: kernel docs "Runtime locking correctness validator", https://docs.kernel.org/locking/lockdep-design.html .
|
||||
- (b) Lockdep tracks lock classes, not instances, and records dependencies between classes as sequences are observed. It reports a circular order as a potential deadlock even if the deadlock never happened. Its "Proof of 100% correctness" section says that once each simple single-task locking sequence has occurred at least once, no timing of them can deadlock. Each new lock chain is validated once and hashed; repeats skip validation.
|
||||
- Nuance: it flags the dependency edge that closes a cycle, so both orders must have been exercised at some point (possibly by different, non-concurrent runs). A lone nesting is flagged only if it breaks a single-lock rule (IRQ-safe vs unsafe, same-class recursion). "Refuses on first sight" fits the closing edge or a single-lock rule violation only. It warns; it does not block the acquisition.
|
||||
|
||||
## 4. ConcurrentHashMap re-check after locking
|
||||
Status: VERIFIED.
|
||||
- Source: OpenJDK java/util/concurrent/ConcurrentHashMap.java, `putVal` and `computeIfAbsent`: head node `f` is read unlocked, `synchronized (f)` is taken, then `tabAt(tab, i) == f` is re-checked; on failure nothing is done and the loop retries. The overview comment gives the reason: the locked node can be replaced, so first-in-bin must be confirmed after locking. (Only the first 100k of 269k chars were read; both methods were reported found there.) Empty-bin `computeIfAbsent` uses a ReservationNode plus CAS instead.
|
||||
- (c) This is the pattern: choose the lock from an unlocked read, validate the read after acquiring.
|
||||
|
||||
## 5. LMDB
|
||||
Status: VERIFIED (paraphrase). Not about alien code or lock order.
|
||||
- Source: lmdb.h, `mdb_txn_begin` and `mdb_env_open` (https://github.com/LMDB/lmdb, libraries/liblmdb/lmdb.h). A thread may hold one transaction at a time (plus children); a transaction and its cursors belong to one thread; one write transaction at a time, so writers cannot deadlock each other; MDB_NOTLS ties reader slots to the txn object instead of the thread, and the application must then serialize writers itself because write locking is unaware of user threads.
|
||||
- Relevance: "one hold per thread" as a deliberate rule, parallel to CultCache's one-cache-gate-per-thread.
|
||||
|
||||
## 6. SQLite btmutex.c
|
||||
Status: VERIFIED (summary of trunk source).
|
||||
- Source: https://github.com/sqlite/sqlite/blob/master/src/btmutex.c (read via raw.githubusercontent.com; sqlite.org served a bot check).
|
||||
- (b) Prevention by ordering, not detection: sharable B-trees on a connection sit in a list sorted by BtShared address and are always locked ascending (`sqlite3BtreeEnterAll`). `sqlite3BtreeEnter` try-locks first; on failure it releases later-ordered locks, waits, then reacquires in order. `wantToLock` is a count that makes a non-recursive mutex behave recursively.
|
||||
- No alien-code or callback discussion in these comments.
|
||||
|
||||
## Counter-positions and how they stay safe
|
||||
- Callbacks under lock, documented: Java `ConcurrentHashMap.compute`/`computeIfAbsent`. Javadoc (Java 21): the function should be short and simple because other threads' updates may block meanwhile, and must not modify the map. The javadoc does not say "internal lock"; that it runs under the bin lock is inferred from the source in item 4. Safety rests on documented restrictions, not enforcement.
|
||||
- Opposite choice: .NET `ConcurrentDictionary.GetOrAdd(key, valueFactory)`. Writes use fine-grained locks but valueFactory runs outside them, "to avoid the problems that can arise from executing unknown code under a lock" (doc remarks). Price: not atomic; the factory may run more than once; a recheck decides the winner (another thread's insert wins). https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd . Same shape as option `encode-first` (user code outside the lock, then recheck and commit) and as item 4.
|
||||
- JCIP and Sutter both name the cost of leaving the lock: lost atomicity. Sutter also offers "pass copies" and "callee must not block" as alternatives to leaving.
|
||||
- Not researched (no source fetched): Linux notifier chains, Java `synchronized` event listeners, other systems that call back under lock by design. No verified citation for them.
|
||||
|
||||
## Unverified / not done
|
||||
- JCIP: section numbers and text come from one PDF copy, not the publisher edition.
|
||||
- Sutter: full column body not read first-hand.
|
||||
- No Discord or voidbot content read.
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# Prior art: a process-global default registry deciding write ownership
|
||||
|
||||
Serves `variants:question:registryless-write-reads-shared` (campaign `variants`). Eyes pass, session `self-2026-10-27b-eureka`, 2026-10-27. Facts only; items marked "summary" rest on search summaries not opened.
|
||||
|
||||
# Prior art: process-global type registries and write/overwrite ownership
|
||||
|
||||
Fork: `variants:question:registryless-write-reads-shared` (viewed in full via eureka-state, status InForce, options `shared-decides` / `written-type-only`, recommended `shared-decides` at ~65%).
|
||||
Facts only. No conclusion. Date 2026-10-27.
|
||||
|
||||
Method note: web pages were read through WebFetch (a small model summarises the page) and WebSearch summaries. Quotes below are what those tools returned; where only a summary was available it is marked "summary". Nothing was run.
|
||||
|
||||
## A. Local mechanics (CultLib, read-only, `C:\cl-wt\c2a-land\src\GameCult.Caching\CultCache.cs`)
|
||||
|
||||
- `CultDocumentRegistry.Shared` is `Lazy<CultDocumentRegistry>` (line 639-645). Its public constructor calls `Refresh()` (647-650), which scans `ReflectionExtensions.GetAttributedDocumentTypes()` (688; defined in `ReflectionExtensions.cs:10`) and rebuilds the indexes under `_registrationGate`. So Shared's initial content is whatever `[CultDocument]` types are in loaded assemblies at first touch of `Shared`.
|
||||
- `GetRequired(Type)` (697-711) registers an unregistered `[CultDocument]` type on demand. So Shared also grows whenever any caller asks for a type. `Refresh()` is public and rebuilds the index from currently loaded assemblies.
|
||||
- `ForTypes(IEnumerable<Type>)` (658-669) builds a registry with `discoverLoadedDocuments:false`, containing only the given types (plus those `GetRequired` pulls in).
|
||||
- `CultCache(registry = null, ...)` sets `_registry = registry ?? CultDocumentRegistry.Shared` (2202-2205).
|
||||
- `WriteDocument<TDocument>(key, doc, storedAt, registry = null)` (4515-4545): `resolvedRegistry = registry ?? Shared`; `catalogEntry = resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`; then `Cacheless(resolvedRegistry, ...)` lends that registry to the store for the call. XML doc (4510) says Shared "can only refuse more".
|
||||
- `WriteRecord(..., registry = null)` (4491-4497): XML doc (4489) says that with registry omitted "it decides nothing for copied records and the one entry is the only claimant".
|
||||
- `OwnsOnDisk(upserts)` (4741-4759): when the store is cacheless and upserts are given, ownership of an on-disk record is decided by `CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), ..., claimant => writes.Any(entry => entry.SchemaId == claimant.SchemaId))`. The comment (4731-4735, 4738-4740) says written entries shadow the lent registry's type for their ids, the registry adds other claimants, and the record is owned only when the claimant is a written entry, "a registry can only refuse more". `CatalogEntryLookup` ctor at 604 takes `(entries, CultDocumentRegistry? others)`.
|
||||
- Net: with `registry == null` the lent registry's claimant set is Shared's current contents, which depend on assemblies loaded and on every prior `GetRequired` call in the process.
|
||||
|
||||
## B. Q1. Process-global, lazily filled registries in established libraries
|
||||
|
||||
### MongoDB C# driver, `BsonClassMap`
|
||||
- Global: yes. `BsonClassMap.RegisterClassMap` is process-wide static; unregistered types are auto-mapped on first use. Docs: "You must register a class map _before_ it's needed in your code." and "We recommend registering class maps before initializing a connection with MongoDB." https://www.mongodb.com/docs/drivers/csharp/current/serialization/class-mapping/ (WebFetch; the page itself says nothing on freezing or late-registration errors).
|
||||
- Behaviour depends on what has been touched: EF Core provider issue EF-88 reports "Unknown discriminator value 'List`1'" when the first operation after startup is a read; reporter links it to a class map that is created only on write. Status Closed/Fixed in 8.0.0; page has no maintainer comment. https://jira.mongodb.org/browse/EF-88 (WebFetch). Reporter says explicit `RegisterClassMap` at startup did not fix it for them.
|
||||
- A MongoDB community answer (via search summary, not opened): discriminator mappings persist once registered, so a read after an insert works; fix offered is `[BsonKnownTypes]`. https://mongodb.com/community/forums/t/c-driver-why-is-bsonknowntypes-attribute-needed-for-polymorphism/2183 (summary only).
|
||||
- Register-once contract: CSHARP-4424 states the three `RegisterClassMap` methods are "meant to only be called once for a given type"; requested because several libraries share data models and do not control init order. Closed Done, fix version 2.19.0; the page does not say which API shipped. https://jira.mongodb.org/browse/CSHARP-4424 (WebFetch). Community workaround named in search summary: `BsonClassMap.IsClassMapRegistered`.
|
||||
- CSHARP-1879 (search summary): `RegisterClassMap` accepted an unfrozen map and failed later on use; fixed 2.4.2. https://jira.mongodb.com/browse/CSHARP-1879 (not opened).
|
||||
- CSHARP-5597 (search summary): `BsonSerializer.RegisterDiscriminator` has no effect; discriminator dictionary populated at class-map registration. https://jira.mongodb.org/browse/CSHARP-5597 (not opened).
|
||||
- Polymorphic docs: "You must specify hierarchical discriminators in root-to-leaf order." and the relationship of all classes in a hierarchy must be documented before deserialization. https://www.mongodb.com/docs/drivers/csharp/current/serialization/polymorphic-objects/ (WebFetch).
|
||||
- Particular docs report an order-dependent GuidSerializer failure ("incorrect order of class mappings"). https://docs.particular.net/persistence/upgrades/mongodb-4to5 (search summary, not opened).
|
||||
- Did they move away: not found. Class maps remain global static; mitigation is documentation plus the IfNotRegistered API.
|
||||
|
||||
### protobuf-net `RuntimeTypeModel.Default`
|
||||
- Global: yes. README: "The Serializer.* methods are basically just shortcuts to RuntimeTypeModel.Default.*". https://cdn.jsdelivr.net/gh/protobuf-net/protobuf-net@main/README.md (WebFetch).
|
||||
- Lazy fill, freezing, order-dependent bugs: NOT verified. README is silent; a direct docs fetch (docs.protobuf-net.dev/runtime) was redirected and not followed. No issue found.
|
||||
|
||||
### MessagePack-CSharp
|
||||
- Global mutable default exists: `MessagePackSerializer.DefaultOptions`. README (WebFetch): labelled "Global state; Not recommended when writing library code." Safe if you control all MessagePack code; libraries should pass options explicitly because others "may change this static property." Libraries "must not use StaticCompositeResolver - as it is global state". https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/master/README.md
|
||||
- Resolvers are an ordered list (`CompositeResolver`): order matters by construction.
|
||||
- Whether it moved away from the global: not found.
|
||||
|
||||
### System.Text.Json
|
||||
- `JsonSerializerOptions.Default`: "Gets a read-only, singleton instance"; remarks give a performance reason: shared instance avoids "redundant recomputation of converters". https://learn.microsoft.com/dotnet/api/system.text.json.jsonserializeroptions.default (WebFetch). The page states no rationale about cross-component interference; any such reading is inference. Design-discussion issue in dotnet/runtime: searched, not found. Whether Default was ever mutable: not verified.
|
||||
- Type-info resolvers: not researched beyond the above.
|
||||
|
||||
### Json.NET
|
||||
- `TypeNameHandling` is a security concern, not an order concern: CA2326 says "An attacker could modify the serialized data to include unexpected types"; mitigation is a custom `ISerializationBinder` restricting types. https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2326 (WebFetch).
|
||||
- `JsonConvert.DefaultSettings` global behaviour/guidance: the Newtonsoft page fetched says nothing about global effects. Not verified.
|
||||
|
||||
### Orleans
|
||||
- Serializer discovery is build-time source generation (`[GenerateSerializer]`, `[Id]`), not a runtime scan; the docs' older code-generation section lists scan of assemblies referencing the core library plus `KnownType`/`KnownAssembly` attributes. Aliases: "Type aliases are globally scoped, and you cannot have two aliases with the same value in an application." https://learn.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/serialization (saved locally: `C:\Users\Meta\.claude\projects\F--Projects-CultLib\0840c7c7-0e31-410e-9012-afb79c3c0742\tool-results\toolu_0148m7no1AbrJzWmNLicn6N9.txt`, lines 134, 839-841).
|
||||
- Type manifest / codec registry runtime behaviour, issues: not researched.
|
||||
|
||||
### EF Core
|
||||
- Model is built per `DbContext` type and cached (IMemoryCache; "the built model has a cache size of 100"); initialised at first operation: "just creating a DbContext instance does not cause the EF model to be initialized." https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics
|
||||
- Assembly scan: `ApplyConfigurationsFromAssembly` — "The order in which the configurations will be applied is undefined, therefore this method should only be used when the order doesn't matter." https://learn.microsoft.com/en-us/ef/core/modeling/ ; and "`EntityTypeConfigurationAttribute` types will not be automatically discovered in an assembly."
|
||||
- So EF's registry is per context type, not process-global; assembly scan is opt-in and documented as order-undefined.
|
||||
|
||||
### Marten
|
||||
- Storage is a table and upsert function per document type, alias `mt_doc_<alias>`; alias collisions are resolved with `DocumentAlias`. https://martendb.io/schema/storage (WebFetch). Hierarchies share one table via `AddSubClass<T>()`; subclass docs are "stored as the parent type" in the identity map. https://www.martendb.io/documents/hierarchies (WebFetch). Schema is configured in `StoreOptions` per store, not a process-global.
|
||||
|
||||
### Java ServiceLoader
|
||||
- "Service providers are loaded and instantiated lazily, that is, on demand." Providers on the class path load in the order `getResources` finds config files; "The ordering of modules in a module layer is not defined." "Instances of this class are not safe for use by multiple concurrent threads." Caches cleared by `reload`. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ServiceLoader.html (WebFetch). Visibility is class-loader dependent ("must be visible to the class loader").
|
||||
|
||||
## C. Q2. Write/overwrite decisions against an existing record of another type
|
||||
|
||||
- RavenDB: REST put docs, request body section: updates must "include its collection name in the metadata" and "A document's collection cannot be modified." Server error code for mismatch: the first fetch summary said HTTP 500; not confirmed on a second fetch. https://docs.ravendb.net/7.1/client-api/rest-api/document-commands/put-documents (WebFetch twice). Ayende, RavenDB 4.0 storage: document key namespace is global, same key cannot exist in two collections (summary). https://ayende.com/blog/173954/the-design-of-ravendb-4-0-physically-segregating-collections The check is against the collection named in the write (derived from the writer's type by the client), not a registry; the "client maps entity type to collection" part is inference from the search summary, not a quoted source. `PutDocumentCommand` page: a missing collection means `@empty`; concurrency via `changeVector`, null "to skip the check". https://docs.ravendb.net/client-api/commands/documents/put
|
||||
- Marten: per-type tables, so the same id under two types are two rows in two tables; no cross-type check. Same id across subclasses of one hierarchy table: not covered by docs fetched; unverified. Marten docs: upsert is used so users "don't have to check whether a document is new or existing" (search summary of https://www.martendb.io/documents/storing, not opened).
|
||||
- MongoDB: the server has no type; `_id` unique per collection; the C# driver writes a `_t` discriminator from the written object's class map. Whether the driver checks an existing document's `_t` on replace: not researched; nothing found stating it does.
|
||||
- Couchbase: key unique within a collection; upsert replaces; no server-side type field enforcement; guidance is one type per collection. https://docs.couchbase.com/server/7.0/learn/data/scopes-and-collections.html (search summary, not opened).
|
||||
- Orleans: grain identity is type plus key ("type/key"), so same key under two grain classes is two identities, not a conflict. https://learn.microsoft.com/en-ca/%20dotnet/orleans/grains/grain-identity (search summary, not opened). How storage providers key state: not found.
|
||||
- Result of this pass: no store found in which a process-global type registry decides whether a write may replace an existing record of another type. Found: collection/type fixed by metadata of the writer (RavenDB), type as part of the key (Orleans, Marten tables), or no check (Couchbase upsert). This is a negative search result, not proof of absence. Not searched: Cosmos DB, DynamoDB, EF Core TPH update of an existing row with another discriminator, Realm, LiteDB, Akka.Persistence.
|
||||
|
||||
## D. Q3. Guidance on ambient/global registries and determinism
|
||||
|
||||
- Mark Seemann (ploeh, 2019): after discussion with co-author van Deursen, "Ambient Context really is an anti-pattern." Reasons given: he has never used it and superior alternatives exist; he says the claim is unproven. https://blog.ploeh.dk/2019/01/21/some-thoughts-on-anti-patterns/ (WebFetch). Examples he cites as found in the BCL (search summary, not verified on page): `Thread.CurrentPrincipal`, `CultureInfo.CurrentCulture`, `HttpContext.Current`. DI book 2nd ed. chapter 5 lists it among DI anti-patterns: https://www.oreilly.com/library/view/dependency-injection-principles/9781617294730/OEBPS/Text/c05.xhtml (not opened).
|
||||
- MessagePack README (above) is itself a library-author statement on global mutable defaults. MongoDB CSHARP-4424 (above) states the shared-library init-order problem. EF Core docs (above) state assembly-scan order is undefined.
|
||||
- No .NET guidance specifically about AppDomain-wide/assembly-scan registries and write decisions found.
|
||||
|
||||
## E. Not verified / gaps
|
||||
- protobuf-net lazy registration, freezing and bug history.
|
||||
- Why System.Text.Json made Default immutable; no maintainer statement found.
|
||||
- Json.NET DefaultSettings guidance.
|
||||
- RavenDB exact error text and HTTP status; whether the stored document is unchanged after refusal.
|
||||
- Marten same-id-across-subclasses behaviour.
|
||||
- MongoDB driver behaviour on replacing a document whose `_t` differs.
|
||||
- Several items above rest on WebSearch summaries I did not open (marked "summary").
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# Prior art: structural versus name-inclusive schema ids
|
||||
|
||||
Serves the store-identity question raised 2026-10-27 in campaign `variants` (operator: member and declared names vary with runtime naming conventions). Eyes pass, session `self-2026-10-27b-eureka`. Facts only; items marked "summary" rest on search summaries not opened.
|
||||
|
||||
# Prior art: structural vs name-inclusive schema identity (campaign variants)
|
||||
|
||||
Eyes pass, 2026-10-27. Facts and evidence pointers only. "(fetched)" = page opened via WebFetch (the answer is a small-model extraction of the page, not raw text). "(summary)" = WebSearch result summary, page not opened.
|
||||
|
||||
## 0. Local facts (read-only)
|
||||
|
||||
- `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs:717-718` schemaId = Sha256(BuildSemanticFingerprint(...)). `:735-761` fingerprint = `schemaName|schemaVersion` then per member ordered by Slot: `slot:MemberName:TypeName:ref|value:TargetSchemaName:one|many`. `contentHash` (`:716`) is a separate Sha256 of canonical JSON (`:763+`).
|
||||
- `TypeName` = `CultSchemaTypeNames.FromType`: CLR `Type.FullName` (namespace-qualified), generics as `Name<args>`, arrays `T[]` (`src\GameCult.Caching\CultSchemaTypeNames.cs:9-31`). So the fingerprint contains .NET type names as well as member names.
|
||||
- Contract `Contracts\cultcache-persistence-format.md:257-284` lists what canonicalization must preserve: slots, types, required/default, nullability, collection semantics, enum constraints, encoding shape. Member names and declared schema name are not in that list. `:171-179` says the id is not derived from C#/Rust/TS type names and the same shape should give the same id "regardless of origin language". `:180-190` defines schemaId = "same semantic persistence shape?" vs contentHash = "exact same canonical schema document?". Example catalog entry (`:140-170`) carries optional `supersedesSchemaIds`, `migrationFamily`; its example canonicalSchema fields do carry `"name": "Name"` (PascalCase). Soft-migration reader flow `:286-345` (exact_match / soft_migrate / reject).
|
||||
- Runtimes do not derive ids; they emulate C# names:
|
||||
- Python: `packages\cultcache-py\src\cultcache_py\documents.py:326` `member_name=_pascal_case(field.name)`; `:516-517` splits on `_` and capitalises. `schema_id` defaults to `self.type` (`:218`); `compatible_schema_ids` field (`:188,242`).
|
||||
- TypeScript: `packages\cultcache-ts\src\codec.ts:226-232` `pascalCase` (splits on non-alphanumerics), `:267` `memberName: pascalCase(field.name)`; throws if a codec has no C# type name (`:~262`).
|
||||
- Rust: `packages\cultcache-rs\src\lib.rs:295,308` `serde(rename_all = "camelCase")` on envelope structs; `schema_id: Some(T::TYPE.to_string())` at `:2856`. I did not trace how Rust document types obtain a sha256 id.
|
||||
- Python test pins a literal id from shared vectors: `packages\cultcache-py\tests\test_cultcache.py:808` (`tests\vectors\document-variants-c0`).
|
||||
- C# also has `CompatibleSchemaIds` (`CultCache.cs:28,313,570`): a record whose persisted id is in the list is accepted (an alias-list mechanism; I saw only the grep lines, not the logic).
|
||||
- Ruling `variants:ruling:wire-id-is-version` (eureka-state, InForce, ruled 2026-09-30, operator quote "Agreed on schema identity"): wire schema id = declared version string `<name>.vN`; every runtime stamps/resolves it exactly; a wire binding without `.v<digits>` is refused; "Store ids are unchanged." Cited source: document-variants-cut.md:552-555 (not opened).
|
||||
|
||||
## 1. How established formats define identity/compatibility, and whether names participate
|
||||
|
||||
Protocol Buffers (fetched https://protobuf.dev/programming-guides/proto3/ and /json/)
|
||||
- Field number identifies the field in the binary wire; changing it = delete + add. Field names appear only in TextProto/JSON encodings; reserve deleted numbers and, for JSON/text, names. The proto3 page has no explicit safe/unsafe entry for renaming; names matter only for JSON/text.
|
||||
- JSON mapping: names become lowerCamelCase unless `json_name` is set; "Parsers accept both the lowerCamelCase name" and the original proto name (/programming-guides/json/).
|
||||
- Message type identity in `Any` is by name: default type URL `type.googleapis.com/<package>.<message>`, "a globally unique identifier" (proto3 page, #any). Type identity is nominal; compatibility is by field numbers.
|
||||
|
||||
Apache Avro (fetched https://avro.apache.org/docs/1.11.1/specification/)
|
||||
- Parsing Canonical Form (PCF): STRIP keeps only attributes relevant to parsing (type, name, fields, symbols, items, values, size); doc and aliases dropped; FULLNAMES replaces short names with fullnames and drops namespace attributes. Textually equal PCF means "the same" for any reader. Names ARE in the canonical form (record and field names), as is field declaration order. Defaults are absent from the kept list (extractor's inference; spec text not confirmed).
|
||||
- Fingerprints: CRC-64-AVRO in single-object encoding (marker C3 01 + 8-byte LE fingerprint); spec recommends at least a 64-bit Rabin, with SHA-256 / MD5 options for larger sets.
|
||||
- Name rules: `[A-Za-z_][A-Za-z0-9_]*`, dot-separated namespaces, equality on case-sensitive fullname.
|
||||
- Resolution: records match on same unqualified name; "Named types and fields may have aliases"; implementations "may optionally" use aliases to map writer to reader. So in Avro names participate in identity (fingerprint) and resolution; aliases are the rename mechanism and are excluded from the fingerprint.
|
||||
|
||||
Cap'n Proto (fetched https://capnproto.org/language.html)
|
||||
- Files and types carry 64-bit IDs; `capnp id` generates them randomly. Default ID for a declaration without one = first 8 bytes of MD5 of (parent scope ID, declaration name). "You cannot change the name of a type that doesn't have an explicit ID"; symbolic names can change if ID/ordinals stay. Stated rationale for IDs: short, unambiguous, full names waste wire space. The page does not say why structural hashes were rejected; I found no Varda statement (search pointed at a mailing-list thread "Schema Refactoring and Unique IDs", Aug 2023, unopened).
|
||||
- Encoding page (https://capnproto.org/encoding.html, fetched): adding fields is backward compatible because field positions depend only on lower-numbered fields; nothing on type identity.
|
||||
|
||||
FlatBuffers (fetched https://flatbuffers.dev/evolution/ and /schema/)
|
||||
- Field positions by declaration order or explicit `id: n` (contiguous from 0, on all fields if used). Renaming "generally OK" because names are not in the binary. `file_identifier` is exactly 4 chars at bytes 4-7; docs frame it as a file-level check and recommend unions for distinguishing messages.
|
||||
|
||||
Thrift (fetched https://raw.githubusercontent.com/apache/thrift/master/doc/specs/thrift-binary-protocol.md)
|
||||
- Binary protocol: field header = type + field id; "the field name is not encoded". (IDL page gave nothing on omitted ids.)
|
||||
|
||||
MessagePack-CSharp (fetched https://github.com/MessagePack-CSharp/MessagePack-CSharp, first 100k of 105k chars)
|
||||
- `[Key(int)]` on all members => array encoding; string keys => map. Keys should be sequential from 0 (gaps cost nulls); do not reuse indexes of removed members; keep obsolete members until clients update. Union keys are ints, unique, serialized as [key, object]. No type identity in the payload.
|
||||
|
||||
Microsoft Bond (fetched https://microsoft.github.io/bond/manual/bond_cpp.html)
|
||||
- Changing field ordinals breaks wire compat; no explicit versioning; Simple JSON matches by field name so a rename breaks it. No schema hash/GUID documented there. Bebop: wiki page empty (points to docs.bebop.sh, not opened).
|
||||
|
||||
Unison (fetched https://www.unison-lang.org/docs/the-big-idea/ and /docs/language-reference/unique-types; hash size from search summary)
|
||||
- Definitions identified by hash of syntax tree; names are "separately stored metadata"; hash excludes names (512-bit SHA3 per search summary, unopened).
|
||||
- Structural types: "Unison does not uniquely identify a type by its name"; same-shape types are the same type (Suit/Direction, four nullary constructors, interchangeable). `unique` types: compiler generates a UUID that feeds the hash, so identity becomes nominal. Which is the default keyword was not confirmed.
|
||||
|
||||
IPLD (fetched https://ipld.io/docs/schemas/)
|
||||
- Matching is structural: "the names of types aren't expected to be found in the serialized data"; match is by structural outline. The page does not say schemas are themselves content-addressed. Versioning is left to developers; "Migrations" subpage not opened.
|
||||
|
||||
## 2. Failure modes of purely structural identity
|
||||
|
||||
- Unison documents it directly: structurally identical enums are one type unless declared `unique` (unison-lang.org/docs/language-reference/unique-types, fetched). Its mitigation is a per-declaration UUID mixed into the hash (identity re-nominalised).
|
||||
- Language literature (summary, pages not opened): structural typing is prone to accidental conformance/type equivalence; nominal typing prevents it at the cost of rigidity (Wikipedia "Nominal type system"; arXiv 1007.2123 lecture notes; Erlang nominals doc https://www.erlang.org/doc/system/nominals.html says nominal types exist to prevent misuse of same-structure types). TypeScript "brand" = phantom discriminator field. No serialization-specific source found.
|
||||
- Formats that avoid the collision by carrying a name or tag outside the shape: protobuf Any type URL (full name), Avro fullname in PCF, Cap'n Proto explicit IDs, Unison `unique`, MessagePack-CSharp union key ints, FlatBuffers file_identifier/unions.
|
||||
- Not found: a documented incident of two same-shape serialized types colliding under a structural id (one search, nothing).
|
||||
|
||||
## 3. Failure modes of name-inclusive identity across languages
|
||||
|
||||
- Protobuf: default camelCase conversion is "always valid but not necessarily unique"; proto3 compiler rejects JSON-name conflicts; serialized duplicate keys parse differently by runtime (Edition Zero design doc, summary: https://flutter.googlesource.com/third_party/protobuf/+/HEAD/docs/design/editions/edition-zero-json-handling.md). Snake_case vs camelCase JSON mismatches in practice: https://github.com/fullstorydev/grpcurl/issues/105 (summary); https://github.com/open-telemetry/opentelemetry-proto/issues/476 (summary: spec contradicted itself on camelCase vs original names). Protobuf's mitigation: parsers accept both forms.
|
||||
- Serde (fetched https://serde.rs/container-attrs.html): `rename_all` accepts lowercase, UPPERCASE, PascalCase, camelCase, snake_case; per-field `rename`.
|
||||
- Avro restricts names to `[A-Za-z_][A-Za-z0-9_]*` with case-sensitive fullname equality, so snake_case and PascalCase spellings are distinct names; renames go through aliases, which PCF drops.
|
||||
- Cap'n Proto: auto-derived IDs depend on the declaration name, so a rename without an explicit ID changes the ID.
|
||||
- Bond Simple JSON: rename breaks compatibility (name matching).
|
||||
- Local: Python/TS already convert to PascalCase to reproduce C#'s fingerprint (section 0); the fingerprint's TypeName is the CLR full name (`CultSchemaTypeNames.cs:30`) and TS throws when a codec lacks a C# type name.
|
||||
- Registry normalisation (summary): Confluent Schema Registry `normalize.schemas=true` maps semantically-equal schemas to one id (https://www.confluent.io/blog/best-practices-for-confluent-schema-registry/); sources disagree on whether JSON Schema property order is normalised.
|
||||
|
||||
## 4. Migration precedents for changing identity derivation
|
||||
|
||||
- Git SHA-1 -> SHA-256 (fetched https://git-scm.com/docs/hash-function-transition): per-repo format marker (`extensions.objectFormat`, `compatObjectFormat`, `repositoryFormatVersion=1` makes old git refuse); bidirectional SHA-256<->SHA-1 mapping (pack idx v3, `loose-object-idx`); four transition modes (dark launch, early, late, post); conversion at the network boundary. It is a design doc, not a statement of what has shipped.
|
||||
- IPFS CIDv0 -> CIDv1 (fetched https://docs.ipfs.tech/concepts/content-addressing/): v0 (46 chars, "Qm") and v1 (self-describing version/codec/multihash) coexist; `ipfs cid format -v 1` converts; v1->v0 only for dag-pb.
|
||||
- Docker manifest schema 1 -> 2 (summary, mirror pages unopened): digests differ per manifest format; pull-by-digest gets no translation, pull-by-tag may be translated; repush converts. https://pulp.plan.io/issues/2099.
|
||||
- Warpcast API hash v1 -> v2 (summary, https://warpcast.notion.site/Warpcast-API-Hash-Migration-Public-ccdd6de8ac604783b9596cfd623ddbac): two-week dual-hash period, then only v2.
|
||||
- Cap'n Proto explicit-ID escape hatch for renamed/moved declarations; Avro aliases as the rename bridge in resolution; MessagePack-CSharp keep-obsolete-members (all fetched, above).
|
||||
- Local: contract `supersedesSchemaIds` / `migrationFamily` (`cultcache-persistence-format.md:~140-170`) and C#/Py `CompatibleSchemaIds` exist as alias lists.
|
||||
- Not found: Unison changing its hash derivation (one search, no docs); any serialization store that switched from name-inclusive to structural ids.
|
||||
|
||||
## Could not verify / not done
|
||||
- Cap'n Proto designer's rationale against structural hashing (not in docs; mailing list unopened).
|
||||
- Avro: whether PCF drops defaults (spec text) and any JIRA on it; spec page was summarised by the fetch model.
|
||||
- Bebop, Thrift IDL id-omission rule, Unison default struct/unique keyword, IPLD Migrations page, Edition Zero doc (summary only), Confluent blog (summary only), nominal/structural papers (titles only).
|
||||
- Rust runtime's sha256 derivation path; `document-variants-cut.md:552-555` not opened; `CompatibleSchemaIds` logic not read.
|
||||
- eureka-state worked (whoami ok); only the one ruling was queried.
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Prior art: presence provider as a library, launcher as a consumer
|
||||
|
||||
Served: `cultlib-gaps:cut_spec:cut-cultnet-rs-presence-provider-a.r1` and the
|
||||
Defaulted rulings `presence-provider-network-free-core`,
|
||||
`presence-credentials-both-idunn-drivers` and `presence-provider-consumers-adopt`,
|
||||
applying the operator's `gamecult-chat:ruling:presence-one-owner-library-launcher-consumer`.
|
||||
Imagination pass, 2026-10-29, from recall of the cited manuals (not re-fetched).
|
||||
|
||||
- libsystemd `sd_notify(3)`: a service links a small library and reports its
|
||||
own state (READY, STATUS, WATCHDOG); programs that cannot link it use
|
||||
`systemd-notify(1)` or a wrapper. Same split as cultnet-rs provider plus
|
||||
idunn-launch as the sidecar consumer.
|
||||
- `sd_listen_fds_with_names(3)`: checks LISTEN_PID against the process, sets
|
||||
close-on-exec on passed descriptors, finds them by name, and can unset the
|
||||
LISTEN_* environment. This set is the descriptor rule the provider adopts.
|
||||
- systemd watchdog (`WATCHDOG=1` within `WatchdogSec`): the model for the
|
||||
warming heartbeat.
|
||||
- Consul: TTL checks updated by the service through the client API, versus
|
||||
script/HTTP checks the agent runs for services that cannot. In-process
|
||||
versus sidecar again.
|
||||
- Kubernetes: kubelet-run probes observe from outside; readiness gates are
|
||||
reported by the app. Idunn's route challenge is the outside observer, the
|
||||
signed presence the app's own report.
|
||||
|
||||
Existing copies compared: CodexConnector `src/idunn_health.rs` (two
|
||||
descriptors, strict PID check), Idunn launcher (two descriptors, no PID check),
|
||||
cultnet-ts `idunn-runtime-authority.ts` (descriptors by name, systemd private
|
||||
PID case). The provider takes the strictest union.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Union and record decode tolerance: prior art and CultCache's stance
|
||||
|
||||
Fork: thing2:question:contract-decode-tolerance. Follow-up: thing2:follow_up:cultcache-union-parity.
|
||||
Eyes pass, 2026-10-10, session self-2026-10-10c-eureka. Facts only; no recommendation.
|
||||
Tags: [fetched] read this session; [local] read in the repo this session; [background] prior knowledge, not refetched; [not found] the fetched page did not say.
|
||||
|
||||
## 1. Established systems
|
||||
|
||||
### Protocol Buffers
|
||||
- Unknown fields: proto3 "preserve[s] unknown fields and include[s] them during parsing and in the serialized output", matching proto2. Lost on JSON serialization and on field-by-field copy into a new message; CopyFrom/MergeFrom avoid the loss. (protobuf.dev/programming-guides/proto3/, "Unknown Fields") [fetched]. The version proto3 started preserving (3.5) [background].
|
||||
- Unknown oneof case: no distinct state. The page warns a NOT_SET result is ambiguous: the oneof may be unset, or set to a field from a newer schema [fetched]. A oneof member the reader lacks is an ordinary unknown field, so it sits in the unknown-field set and is re-emitted [background].
|
||||
- Unknown enum values: "unrecognized enum values will be preserved in the message" (raw int in open-enum languages, accessor in Java) and re-serialized [fetched].
|
||||
|
||||
### Cap'n Proto
|
||||
- Union: a separate tag says which member is set. Adding a member to a union is safe because "existing programs should already know to check the union's tag"; they "may or may not behave reasonably when the tag has a value they don't recognize". Moving an existing field into a new union can show an old reader "a garbage value or throw an exception" (capnproto.org/language.html, "Unions", "Evolving Your Protocol") [fetched].
|
||||
- which() on an unknown discriminant: [not found] in the fetched language and encoding pages. C++ generated which() returns the tag as an enum and the reader is expected to switch with a default [background].
|
||||
- Preservation: [not found] on the fetched pages; structs "can be traversed (e.g., copied) without knowing their type" (encoding.html, Structs) [fetched]. Copy preservation of unknown data is [background].
|
||||
|
||||
### Avro (schema resolution; reader schema and writer schema both required)
|
||||
- Both unions: reader takes its first branch matching the writer's selected branch; "if none match, an error is signalled". Writer union, reader non-union: the selected writer branch must match, else error (Avro spec, Schema Resolution) [fetched; also quoted from the 1.11.1 spec in docs/research/read-only-record-signal-prior-art.md:176-178].
|
||||
- Record, writer has an extra field: "the writer's value for that field is ignored". Reader field missing from the writer: reader's default if it has one; no default is an error [fetched].
|
||||
- Enum, writer symbol absent in reader: reader's declared default if any, else error [fetched].
|
||||
- Preservation: none. Extra fields are dropped by resolution [fetched].
|
||||
|
||||
### FlatBuffers
|
||||
- Union evolution: appending a member is safe ("CodeV1 will not recognize the another_a"); inserting in the middle without an explicit discriminant misreads members; inserting with an explicit discriminant is safe (flatbuffers.dev/evolution/) [fetched].
|
||||
- What an old reader sees for an unknown union type, and re-serialization preservation: [not found] on that page. Generated readers expose a type byte plus a union table pointer; an unknown type value falls outside the generated enum [background].
|
||||
|
||||
### Thrift
|
||||
- Unknown fields and unknown union field ids: [not found] in thrift-rpc.md. Protocol readers skip unrecognized field ids and drop the data; an unknown union id leaves the union empty, and a union with no field set can fail validation [background].
|
||||
|
||||
### MessagePack-CSharp [Union]
|
||||
- README Union section says only "Unique union keys are required"; it states no behaviour for an unknown key and no option for it [fetched, first 100k chars of the README].
|
||||
- Missing keys: members initialize to default (README, Object Serialization) [fetched]. Extra array elements for [Key(int)] and unknown string keys: README silent; it shows a manual formatter that loops and skips extras [fetched].
|
||||
- Generated and dynamic union formatters skip the payload and yield null when the key is absent from the union's key map [background]. The estate's own measurement agrees: unknown arm key decodes to null, no error (question document, "Measured in the C# reference... MessagePack-CSharp 3.1.7") [local, via eureka-state view]. The vendored copy at F:\Projects\CultPong\Assets\Scripts\MessagePack\Resolvers\DynamicUnionResolver.cs does a key-map TryGetValue at :347 (type-map TryGetValue at :206); the miss branch was not read.
|
||||
- No configurable unknown-union behaviour found in the README; whether a newer version adds one [not found].
|
||||
|
||||
### serde
|
||||
- enum-representations page says nothing on unknown variant names, #[serde(other)] or deny_unknown_fields [fetched]. serde_derive de.rs was fetched but the identifier/enum_ modules holding unknown_variant were not in the excerpt [not found].
|
||||
- [background] default: an unknown variant is an error ("unknown variant `X`, expected one of ..."); #[serde(other)] on a unit variant makes the identifier decoder return it for any unknown name (units only, payload discarded); unknown struct fields are ignored unless #[serde(deny_unknown_fields)]; a missing field is an error unless #[serde(default)] or Option.
|
||||
|
||||
### Bincode / postcard
|
||||
- [background] Both are non-self-describing: an enum is a variant index (bincode u32, postcard varint) followed by fields in order; an out-of-range index is a decode error; there are no field tags, so an extra or missing slot is detectable only by running out of or leaving bytes. Evolution is by schema versioning outside the format. Not fetched.
|
||||
|
||||
## 2. Preservation across an older reader, and the failure each answers
|
||||
| System | Unknown data survives re-encode? | Failure answered |
|
||||
|---|---|---|
|
||||
| Protobuf | yes: unknown fields (hence unknown oneof cases) and unknown enum ints, in binary APIs only [fetched] | old intermediary re-serializing a newer message (purpose: background) |
|
||||
| Cap'n Proto | page silent; reader sees a tag it may not know and must handle it [fetched] | in-place access; append-only evolution |
|
||||
| Avro | no: extra field dropped, or error on no branch / no default [fetched] | writer schema travels with data, so resolution is exact and loud |
|
||||
| FlatBuffers | page silent [fetched] | append-only evolution, explicit discriminants |
|
||||
| Thrift | no (skipped) [background] | forward tolerance for old readers |
|
||||
| MessagePack-CSharp | no: arm becomes null (measured), extra slot skipped | not documented |
|
||||
| serde | no; error by default [background] | type safety; leniency is opt-in |
|
||||
| Bincode/postcard | n/a [background] | compactness; no evolution in the format |
|
||||
| Unity SerializeReference, DataContract ExtensionData | yes: bytes retained and recoverable (docs/research/read-only-record-signal-prior-art.md:182-205) [local] | data loss from missing types |
|
||||
|
||||
## 3. CultCache's own stance (F:\Projects\CultLib)
|
||||
- docs/persisted-record-encoding.md:1-19: persisted records are verified by re-encoding what was decoded and comparing bytes; a record that reads but re-encodes differently is refused, on live data at startup or next read. Three Idunn guards cited at :10-14. [local] A tolerated extra trailing slot or a null-collapsed arm would not re-encode to the stored bytes; whether those guards would run on such records was not tested.
|
||||
- No evolution/compatibility doc: `ls docs | grep persist|evol|compat|schema|version` returns only persisted-record-encoding.md. docs/document-variants-cut.md:327-333 covers union-slot reuse at registration ("late union-slot reuse passes registration"; "interface unions are never minted") [local, grep lines only].
|
||||
- Reference union handling: src/GameCult.Caching/CultInspectorModel.cs:768-774 classifies an abstract/interface type by its [UnionAttribute]s ordered by key and refuses a type with none ("declares no [Union] subtypes", :772); also :120. This is the inspector's shape classifier; decode is MessagePack-CSharp's. No other use of UnionAttribute in src/GameCult.Caching. [local]
|
||||
- Trailing-slot tolerance: packages/cultcache-ts/test/cult-cache.test.ts:1176-1195, "CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots": a legacy-payload store with fewer slots reads with `tags: []`; a mismatched-type payload rejects with /Expected string/u. Missing trailing slots are tested; extra trailing slots are not. [local]
|
||||
- TS decode: packages/cultcache-ts/src/cult-cache.ts:679-692 wraps a formatter as `schema.parse(decode(payload))`, so TS strictness is whatever the zod schema says; no TS union codec (follow-up text). [local]
|
||||
- Rust: packages/cultcache-rs/src/*.rs has no unknown-arm or extra-slot handling found by grep; "unknown" hits are store-header tests (:4889-4890 refuses an unknown header by name; :5009-5019 unknown-header.msgpack, variant-v2.msgpack). Rust enum arms persist as serde external tags (follow-up text). [local]
|
||||
- Consumers persisting unions: Aetheria (F:\Projects\Aetheria) has `[Union]` abstract classes in Assets/Scripts/ServerShared: Provenance.cs:96-98 (keys 0-2), ItemInstance.cs:18-21 (keys 0-3) and :29-31, Environment.cs:73. The assembly registers CultCache documents (AssemblyInfo.cs:3-4; Galaxy.cs, ItemData.cs, AetheriaStores.cs reference CultCache); ItemInstance is held in entity item lists (Entity.cs:2003). An older build reading a store with a newer arm key would take the C# null-arm path, if these types sit inside persisted CultCache documents; that persistence path was not traced. Aetheria-legacy has more unions (Behaviors.cs:29-38, Item.cs:43-63, Contract.cs:10-11); commented-out keys 8, 9 at Behaviors.cs:37-38 show arms retired in place. [local]
|
||||
- Ghostlight and Huginn: not searched for [Union] (the grep matched only Aetheria). Follow-up text: "No live record crosses runtimes as a union today" [local, view]. Same-runtime older-build-reads-newer-store: unverified.
|
||||
- The question document records the C# measurement: extra trailing slot skipped and equal; missing trailing slot leaves member default; unknown arm null, for a Node in a list and for an Outcome; Rust and TS union decoders with explicit keys refuse by construction; Rust extra-slot behaviour unmeasured [local, view].
|
||||
|
||||
## 4. Operator's stated value, and how each option relates (inferred)
|
||||
Verbatim, operator-rapport.md:50: "nobody should have to carry bytes they don't understand ... CultCache's whole promise is that a cache's contents are self-identifying via the upfront catalog." Context: pushing back on storing retired slots as metadata (:49). [local]
|
||||
- Preserve-unknown (protobuf, Unity): the older reader carries bytes the catalog does not describe. Opposite to the stated value.
|
||||
- Drop silently (Avro extra fields, MessagePack slot skipping, Thrift skip): nobody carries the bytes, but the loss is silent and a re-encode no longer equals the stored bytes (persisted-record-encoding.md:4-6).
|
||||
- Null the arm (current C# union behaviour): the unknown arm becomes a value the catalog does not describe either; the record decodes as if valid.
|
||||
- Refuse (Avro no-branch, serde default, Rust/TS explicit-key decoders, store-header refusal): the reader declares it cannot understand the bytes; consistent with self-identifying content because undescribed content is not accepted.
|
||||
- Tolerate trailing slots but refuse unknown arms (fork option 1): the catalog-described slot list governs required members; slots beyond it are skipped, which is the drop-silently pattern for slots and the refuse pattern for arms.
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
# Cycles CPU FMA contraction: facts for the kernel-pin fork
|
||||
|
||||
Serves finding `cultmath-tapes:finding:cut-cycles-converters.s1.kernel-pin-unraised-fork` (kind Finding, InForce, Medium, admitted 2026-10-13, soul-cycles-converters-s1).
|
||||
Gathered by eyes-cycles-fma, session self-2026-10-14-eureka, 2026-10-14. Facts only; no recommendation.
|
||||
|
||||
Strength marks: [src] read from source text; [doc] vendor or standards documentation; [web] issue, forum or search-result summary; [abs] absence of evidence (searched, not found).
|
||||
"Raw" Blender paths below are fetched as `https://projects.blender.org/api/v1/repos/blender/blender/raw/<path>?ref=<tag or branch>`. Local copies of the main-branch files are in `...\scratchpad\eyes-cycles-fma\dl\`.
|
||||
|
||||
## 0. The finding's own record (what is being tested)
|
||||
|
||||
- [src] The finding quotes the oracle probes: with the AVX2 kernel, 17 function rows differ from committed fixtures (floored_modulo 151/1024 cases ... color 55/1024), worst `mix_overlay` 493 ulp, `cycles_floored_modulo` 958152704 ulp (sign flip). Yggdrasil's /proc/cpuinfo shows avx2 and fma.
|
||||
- [src] Oracle code: `kernel_check()` in `tools/cycles-oracle/oracle.py` bakes FLOORED_MODULO of 10000 by 0.1 and compares to numpy unfused `a - floor(a/b)*b`; exits 4 if the kernel fuses. Read with `git show e48f9488:packages/cultmath/tools/cycles-oracle/oracle.py` (lines ~528-551), README lines 20-30, `CyclesOracleTests.cs` lines 5-18 (those paths are not in the working tree at main; they exist at commit e48f9488). The oracle Blender is 5.2.2.
|
||||
|
||||
## 1. Cycles CPU kernels and flags
|
||||
|
||||
1. [src] The CMake file defines two x86 CPU kernel variants only: an SSE4.2 baseline and an AVX2 kernel (`WITH_KERNEL_AVX2`). No AVX-512 flag or kernel appears; `kernel.cpp` lists `cpu` and `cpu_avx2`. `intern/cycles/CMakeLists.txt` lines 76-130, 137-143 (main and v5.2.2); `intern/cycles/device/cpu/kernel.cpp` line 11 (v5.2.2): `KERNEL_NAME_EVAL(cpu, name), KERNEL_NAME_EVAL(cpu_avx2, name)`.
|
||||
2. [src] GCC/Clang flags (CMakeLists.txt lines 97-100): SSE4.2 = `-msse -msse2 -msse3 -mssse3 -msse4.1 -msse4.2`; AVX2 = SSE4.2 flags + `-mavx -mavx2 -mfma -mlzcnt -mbmi -mbmi2 -mf16c`. The `-mfma` flag is what makes FMA instructions available only in the AVX2 kernel. MSVC: AVX2 kernel `/arch:AVX2`, SSE4.2 kernel empty on x64 (lines 82-89). Intel compilers use `/QxCORE-AVX2` or `-xcore-avx2`.
|
||||
3. [src] Contraction flags, GCC/Clang block (CMakeLists.txt lines 160-180, main = v5.2.2): `-fno-trapping-math`, `-fno-math-errno`, `-fno-signed-zeros`, `-ffp-contract=on`, `-freciprocal-math`; `-fassociative-math` is commented out ("problems with bsdf_D for GGX on Linux (#130389)"); `-ffinite-math-only` deliberately not set. These are appended to the global `CMAKE_CXX_FLAGS`, so they apply to both kernels; only `-mfma` differs. GCC additionally gets `-fno-signaling-nans`, `-fno-rounding-math`, `-mfpmath=sse`.
|
||||
4. [src] MSVC (non-clang) block (line 149): `/fp:fast`, with the comment "Unlike GCC/clang we still use fast math, because there is no fine grained control and the speedup we get here is too big to ignore." For clang-cl the GCC/Clang flag list is passed with a `/clang:` prefix (line 198).
|
||||
5. [src] History of the contraction flag (Forgejo API commit messages):
|
||||
- `f96f3c33af`, 2024-09-30, "Cycles: Add some more math optimizations": added `-ffp-contract=fast`, `-freciprocal-math`, `-fassociative-math`. Message: "These are used on Windows and HIP anyways, so our code can't expect exact IEEE semantics in any case." Measured "~1.5% speedup in my very quick test".
|
||||
- `0fc58f0aba`, 2024-11-25, "Disable -fassociative-math on Linux to avoid GGX issues".
|
||||
- `7b19e74cad`, 2025-12-14 (PR 150318): changed `fast` to `on`: "Disable FMA auto-vectorization across different statements in Cycles, because GCC 15.2.1 manages to ruin the viewport's scaling and offset" (GCC bug 106902). It also removed a `remove_cc_flag("-ffp-contract=off")` block ("Not needed to make cycles automated tests pass with -march=native. However Blender itself needs this flag").
|
||||
- Before 2024-09-30 the CMake file carried no Cycles-specific contraction flag; the global Blender flag was `-ffp-contract=off` (stripped for Cycles by that `remove_cc_flag` block). [src, inferred from the removed lines; the earlier state was not read at an older tag.]
|
||||
6. [doc] GCC definition: `-ffp-contract=on` "enables contraction within one expression, but not across different statements"; `fast` forms FMA "if the target has native support"; default `off` for ISO C, `fast` otherwise. Source: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html. (Which GCC release implemented `on` for C++ was not checked.)
|
||||
7. [src] Cycles' floored modulo is one C++ expression: `return (b != 0.0f) ? a - floorf(a / b) * b : 0.0f;` in `intern/cycles/util/math_base.h` (v5.2.2 line 613-616; main line 646-649). `NODE_MATH_FLOORED_MODULO` calls it from `kernel/svm/math_util.h` line 164. `safe_modulo` (truncated) uses `fmodf`, not an expression. `-freciprocal-math` also permits `a/b` to become `a * (1/b)`.
|
||||
8. [src] Env switch: `intern/cycles/util/debug.cpp` (v5.2.2) line 29 `CHECK_CPU_FLAGS(avx2, "CYCLES_CPU_NO_AVX2")`, line 23 `flag = (getenv(env) == nullptr)`; presence of the variable disables the AVX2 kernel. [web] added 2015-02 by Sergey Sharybin, commit 4b04a4e, described as developer-only: https://lists.blender.org/pipermail/bf-blender-cvs/2015-February/073396.html (via search summary).
|
||||
9. [src] The AVX2 kernel is selected at runtime by CPU capability: `system.cpp` `system_cpu_support_avx2()` requires avx2 and f16c ("F16C is considered part of AVX2 for our purpose"), so a default Blender on any AVX2 CPU uses it.
|
||||
10. [src] Cycles' own render tests are image tests with a default `fail_threshold = 0.016`, `fail_percent = 1` (`tests/python/modules/render_report.py` lines 287-288), overridden per test (e.g. 0.032, 0.06, 0.048; `tests/python/cycles_render_tests.py` ~339-357). They compare images, not per-node bit patterns. A grep for AVX/NO_AVX in these two files found nothing, so the tests do not appear to pin a kernel. [src for what was found; absence is [abs]]
|
||||
|
||||
## 1b. GPU backends
|
||||
|
||||
11. [src] CUDA: `intern/cycles/device/cuda/device_impl.cpp` line ~225 passes `--use_fast_math` to nvcc (nvcc fuses FMA by default; `--fmad` default is documented NVIDIA behaviour, not fetched here). `kernel/device/cuda/compat.h` line 89 has the comment "Use fast math functions".
|
||||
12. [src] HIP: `kernel/device/hip/compat.h` line 91 "Use fast math functions" (the HIP compile flags were not located; the commit message in item 5 states HIP already runs without exact IEEE semantics).
|
||||
13. [src] Metal: `device/metal/device_impl.mm` line 570 `options.fastMathEnabled = YES;`.
|
||||
14. [src] oneAPI: `kernel/device/oneapi/CMakeLists.txt` line 71 `-ffast-math`, line 83 `-cl-fast-relaxed-math` for spir64.
|
||||
15. [src] OptiX: not separately examined (OptiX kernels are CUDA-compiled).
|
||||
16. [src] The fp-contract/fast-math choices are thus explicit in all of: CPU (`on`, both kernels), MSVC CPU (`/fp:fast`), CUDA, HIP, Metal, oneAPI.
|
||||
|
||||
## 1c. Do Blender developers treat CPU/GPU numeric differences as bugs
|
||||
|
||||
17. [web] Issue #101114 (Power math node precision): a developer said they do not handle such precision issues as bugs since implementation is by the GPU driver and manufacturer. https://developer.blender.org/T101114 (search-result summary; page not read).
|
||||
18. [web] Issue #68687 (Eevee modulo inconsistent): a developer said the imprecision "is likely to remain as it is an implementation behavior we cannot ensure"; reporters noted Cycles gave the expected result. https://developer.blender.org/T68687 (summary).
|
||||
19. [web] Issue #53348 (Cycles CPU vs GPU in World nodes): a developer attributed the difference to "--fast-math we use for CUDA" and noted disabling it costs about 15% speed. https://projects.blender.org/blender/blender/issues/53348 (summary; page returned 403 to WebFetch).
|
||||
20. [web] Issue #67448 "Shader node float precision issue cause different results on CPU and GPU" exists (archived); its content was not read (redirect not followed). https://developer.blender.org/T67448
|
||||
21. [abs] No Blender issue specific to Floored Modulo and FMA/AVX2 was found; no Blender document stating a CPU/GPU numeric parity guarantee was found.
|
||||
|
||||
## 2. Shader languages
|
||||
|
||||
22. [doc] HLSL `precise`: separate operations stay separate ("where a mul and add operation might have been fused into a mad operation, precise forces the operations to remain separate"; fusion then only via the explicit `mad` intrinsic); order of operations preserved; IEEE NaN/INF honoured. It propagates only to operations that contribute to the `precise`-qualified variable; marking shader outputs is recommended. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-variable-syntax
|
||||
23. [doc] HLSL `mad`: may be fused or mul+add in hardware; for `precise` values the hardware must be consistent across all uses of that mad on that hardware. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/mad (search summary). [web] The HLSL team blog says any operation may be refactored unless `precise`, and `/Gis` makes all computations precise: https://learn.microsoft.com/ko-kr/archive/blogs/marcelolr/precise-and-ieee-strictness-in-hlsl (summary).
|
||||
24. [abs] DXC's default contraction of plain `a*b+c` was not confirmed from DXC docs or source; clang HLSL PR #119820 "Make fast math the default for HLSL" exists (https://lists.llvm.org/pipermail/cfe-commits/Week-of-Mon-20241209/654101.html), content not read.
|
||||
25. [src] Vulkan SPIR-V environment (https://registry.khronos.org/vulkan/specs/1.3/html/chap37.html, fetched and grepped): "Implementations may rearrange floating-point operations using any of the mathematical properties governing the expressions in precise arithmetic ... may involve a different number of rounding steps"; "This rearrangement can be prevented for particular operations by using the NoContraction decoration." Without NoContraction the flags AllowContract/AllowReassoc/AllowRecip/AllowTransform are assumed. Same page: OpFMod "Inherited from x - y * floor(x/y)"; OpFRem inherited from `x - y * trunc(x/y)`; OpFDiv 2.5 ULP for |y| in [2^-126, 2^126]; and the page lists `OpFmaKHR`/`shaderFmaFloat32` (a core-spec FMA capability) as new.
|
||||
26. [doc] GLSL: `precise` qualifier ("the qualifier precise will ensure that operations contributing to a variable's value are performed in the order and with the precision specified") "effectively prohibits the arbitrary use of fused multiply-add"; under `precise`, `fma()` is one operation and `a*b+c` is two. Sources: GL_ARB_gpu_shader5 / OES_gpu_shader5 text and https://registry.khronos.org/OpenGL-Refpages/gl4/html/fma.xhtml (search summary). `invariant` was not looked up.
|
||||
27. [doc] SPIR-V `NoContraction` goes on the intermediate-result instructions, not the variable; GLSL `precise` lowers to it in glslang/ANGLE. [web] LLVM-based backends have contracted NoContraction multiplies into fma before (bug report in search results, wording in item summary only).
|
||||
28. [web] Metal: `MTLCompileOptions.fastMathEnabled`/`mathMode` (fast vs safe); in safe mode MSL requires correctly rounded FP32 add, sub, mul, div, fma; `metal::precise` namespace gives precise library functions; explicit `fma()` is the advice for controlling fusion. https://developer.apple.com/documentation/metal/mtlcompileoptions/mathmode.md and https://developer.apple.com/forums/thread/90959 (summaries). Current Xcode default for fast math was not confirmed ([web] sources conflict). No Metal `fp_contract` pragma found [abs].
|
||||
29. [abs] Unity's shader compiler (FXC/DXC via HLSLcc/SPIRV-Cross) contraction behaviour: no documentation found.
|
||||
|
||||
## 3. C# / .NET
|
||||
|
||||
30. [doc] `Math.FusedMultiplyAdd` (and `MathF`): "Returns (x * y) + z, rounded as one ternary operation"; infinite-precision multiply and add then one rounding. Available .NET Core 3.0+ and .NET 5-11. https://learn.microsoft.com/en-us/dotnet/api/system.math.fusedmultiplyadd
|
||||
31. [web] dotnet/runtime issue #10137 "[Jit] Recognize FMA pattern and Emit Fused Multiply Add" closed 2024-02-05 as COMPLETED with the maintainer comment: "Closing this. We have explicit `FusedMultiplyAdd` APIs and will likely cover any future 'pick the better of MultiplyAdd or FusedMultiplyAdd' via an explicit `MultiplyAddEstimate` API." (read with `gh issue view 10137 -R dotnet/runtime`). Earlier comments there state the intent that `a*b+c` stay two IEEE roundings. PR #61037 adds an arm64 MADD fold for integers only (summary).
|
||||
32. [web] Issue #98704 "Math.FusedMultiplyAdd returns wrong results on systems without FMA3 support", closed COMPLETED 2025-05-06: the software fallback had been an estimate (suspected ucrtbase `fma`), so correct rounding was guaranteed only with hardware FMA3; `COMPlus_EnableFMA=0` did not reproduce it on FMA3 hardware. Fix details were not read. Also [web] TA-Lib issue #340 reports the same misrounding concern.
|
||||
33. [abs] A statement in .NET docs that RyuJIT never contracts floating-point `a*b+c` was not found; #10137's closure is the nearest evidence. The CultMath repo's own use of FusedMultiplyAdd/MathF.FusedMultiplyAdd was not searched.
|
||||
|
||||
## 4. Other reference implementations and tolerance policies
|
||||
|
||||
34. [src] OpenShadingLanguage testsuite (`testsuite/runtest.py` lines 82-87): image diff thresholds `failthresh = 0.004`, `hardfail = 0.01`, `failpercent = 0.02`, warn at 2x failthresh; "A test fails if more than failpercent of pixel values differ by more than failthresh ... or if even one pixel differs by more than hardfail." Raw URL: https://raw.githubusercontent.com/AcademySoftwareFoundation/OpenShadingLanguage/main/testsuite/runtest.py
|
||||
35. [src] Blender's Cycles render tests: item 10 (image thresholds, default 0.016/1%).
|
||||
36. [doc] Vulkan precision table lists per-operation ULP classes (item 25); functions defined as expressions inherit the budgets of their constituents (GLES3 CTS spec, https://fuchsia.googlesource.com/third_party/vulkan-cts/+/30e25b62d60b36b923d19a0c0759ef6cb7201f9e/doc/testspecs/GLES3/functional.shaders.builtin_functions.precision.txt, summary). Historical draft GL_ARB_shader_precision: `a*b+c` "correctly rounded single operation or sequence of two correctly rounded operations", fma same as a*b+c (https://developer.download.nvidia.com/opengl/specs/GL_ARB_shader_precision.txt, summary).
|
||||
37. [abs] Not found/not examined: MaterialX reference-vs-generated shader tolerances (only structure of MaterialXTest located); glTF sample renderer conformance; any published "bit-exact vs ulp class" policy statement from Khronos, MaterialX, OSL or Blender; the Vulkan CTS source ULP constants for mix/smoothstep/fma.
|
||||
|
||||
## 5. Floored modulo specifics
|
||||
|
||||
38. [src] Definition in Cycles: `a - floorf(a / b) * b` (item 7). For a=10000, b=0.1f the quotient a/b rounds to 100000 (float) and floorf gives 100000, so the unfused product 100000*b rounds to 10000 and the result is 0, as the oracle records; the fused form computes the residual of the exact product (b is not exactly 0.1), yielding -1.49e-4 as the finding records. [abs] This arithmetic was not re-run here; it restates the finding's `kernel_check` docstring (oracle.py).
|
||||
39. [abs] No Blender issue or commit mentioning Floored Modulo together with FMA, contraction or AVX2 was found. [web] Related reports concern modulo/wrap disagreements between Cycles and Eevee/GPU drivers (#101562, #68687, #88151).
|
||||
40. [doc] Vulkan states OpFMod is "inherited from x - y * floor(x/y)", i.e. the same formula, so a GPU implementation of it follows its own contraction/precision rules (item 25).
|
||||
|
||||
## Coverage and gaps
|
||||
|
||||
- Strong [src]: Cycles flag set per ISA and compiler, the history of `-ffp-contract`, `safe_floored_modulo`, `CYCLES_CPU_NO_AVX2` mechanism, GPU backends' fast-math flags, Cycles/OSL image-test thresholds, Vulkan NoContraction and OpFMod text, HLSL `precise`.
|
||||
- Gaps: which compiler/version builds official Blender 5.2.2 release binaries (matters for how `-ffp-contract=on` is honoured); the 4.x flags before/after 2024-09-30 at release tags; DXC/Unity default contraction; Metal contraction pragma; MaterialX, glTF and Vulkan CTS numeric tolerances; a direct statement that RyuJIT never contracts; Blender developer position on CPU/GPU parity beyond forum/issue summaries (several pages 403 to WebFetch).
|
||||
- Files of record: `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\0d5eef6e-e366-457f-bbc6-0151080ae1a8\scratchpad\eyes-cycles-fma\dl\` (CMakeLists.txt, math_base.h, math_util.h, vk.html).
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
# Decode stack safety: prior art (facts only)
|
||||
|
||||
Serves follow-up `cultlib-gaps:follow_up:decode-stack-safety` (campaign cultlib-gaps), source finding `thing2:finding:cut-contract-rs.s1.decode-stack-overflow-within-bound`.
|
||||
Gathered 2026-10-12 by eyes-decode-stack at CultLib `67c79d5c`. No recommendation. Web facts come from WebFetch/WebSearch summaries of the pages named; items marked UNVERIFIED were not confirmed against primary source.
|
||||
|
||||
## A. The local door (what exists today)
|
||||
|
||||
- `packages/cultcache-rs/src/lib.rs:503` `MAX_PAYLOAD_DEPTH = 256`; `check_payload_depth` (~526-540) is an iterative walk with a `[usize; 256]` count stack, scalars skipped by length.
|
||||
- `lib.rs:598-604` `decode_payload`: `check_payload_depth(payload)?; rmp_serde::from_slice(payload)`. Its doc comment calls rmp-serde's own counter (1024) "only a backstop".
|
||||
- Derive: `packages/cultcache-rs/cultcache-rs-derive/src/lib.rs:294-330` generates `impl Deserialize` with a `Visitor::visit_seq` over slot-ordered arrays (`deserialize_tuple`); unknown trailing elements are consumed via `IgnoredAny`.
|
||||
- rmp-serde resolved version: 1.3.1 (rmp 0.8.15; `packages/cultcache-rs/Cargo.lock`). Its depth limit is a `u16` `depth: 1024`, decremented per array/map/ext (`~/.cargo/registry/src/*/rmp-serde-1.3.1/src/decode.rs` lines 181, 213, 294, 566, 583, 616), error `DepthLimitExceeded`, tunable with `Deserializer::set_max_depth` (clamped to u16::MAX, lines 307-310). CHANGELOG line 95: "Default stack protector depth is now 1024 instead of 1000."
|
||||
- Upstream master rmp-serde (fetched summary, not checked line by line against 1.3.1): the `deserialize_newtype_struct` MSGPACK_EXT_STRUCT_NAME branch builds `ExtDeserializer` without `depth_count!`; the `any_inner` ext path does use it.
|
||||
- Existing local tests that already use sized threads: `packages/cultcache-rs/tests/union_parity.rs` ~786-830: `stack_budget_debug_2mib` (comment: "A debug build on a 2 MiB thread carries 405 containers of this document; the bound keeps every accepted payload below that") on a slim fixture (`wrap_chain(126, ...)`), and `the_walk_does_not_recurse` (100000 nested arrays refused on a 256 KiB thread). No stacker, `stack_size`, or thread spawn in `src/` or `Cargo.toml`.
|
||||
- The rmp-serde 1024 counter and the CultLib 256 bound both count containers; per-level stack cost depends on the target type (follow-up text: 15-19 KiB release, 87-95 KiB debug per level for contract-sized types).
|
||||
|
||||
## B. Rust serde ecosystem
|
||||
|
||||
### serde_json
|
||||
- Default limit 128: `Deserializer::new` sets `remaining_depth: 128` (field `u8`); `check_recursion!` returns `ErrorCode::RecursionLimitExceeded` at 0; invoked in `deserialize_any/seq/map/struct/enum`. https://github.com/serde-rs/json/blob/master/src/de.rs
|
||||
- `disable_recursion_limit()` exists only under Cargo feature `unbounded_depth`. Docs: "Parse arbitrarily deep JSON structures without any consideration for overflowing the stack"; tells the caller to "provide some other way to protect against stack overflows" and names `serde_stacker`. Docs also warn that Display, Debug and Drop on the result can overflow. https://docs.rs/serde_json/latest/serde_json/struct.Deserializer.html
|
||||
- The limit is a nesting-level count; no per-type frame accounting (code read: counter decrement only). The serde_json README mentions no recursion limit.
|
||||
|
||||
### serde_stacker (dtolnay)
|
||||
- Adapter wrapping a serde (de)serializer that grows the stack dynamically; README examples use `serde_json::Deserializer`, call `disable_recursion_limit()`, wrap with `serde_stacker::Deserializer::new`, nest 10,000 levels. README names Display/Debug/Drop as other overflow points and shows a manual iterative drop. README does not say it depends on `stacker` (UNVERIFIED). https://github.com/dtolnay/serde-stacker
|
||||
- It is a serde-level `Deserializer` wrapper, so it hooks calls routed through the wrapped deserializer.
|
||||
|
||||
### rmp-serde / rmpv
|
||||
- rmp-serde: depth counter 1024 default (section A). Search found no RUSTSEC advisory or issue for rmp / rmp-serde stack overflow (negative search, not proof). https://github.com/3Hren/msgpack-rust (rmp-serde/src/decode.rs).
|
||||
- rmpv not inspected.
|
||||
|
||||
### Other Rust deserializers
|
||||
- ciborium: `recurse: usize` default 256 in `from_reader`; `from_reader_with_recursion_limit`; doc: "Set a high recursion limit at your own risk (of stack exhaustion)!" https://github.com/enarx/ciborium (ciborium/src/de/mod.rs)
|
||||
- serde_cbor: RUSTSEC-2019-0025, tags nested without limit, <1 kB input overflows the stack, fixed in 0.10.2 by limiting nested tags. https://rustsec.org/advisories/RUSTSEC-2019-0025
|
||||
- rustc-serialize: RUSTSEC-2022-0004, JSON parser with no depth limit, "stack overflow, which aborts the process", unpatched, advises serde. https://rustsec.org/advisories/RUSTSEC-2022-0004
|
||||
- yaml-rust: RUSTSEC-2018-0006, deep nesting aborts, fixed by depth check. https://rustsec.org/advisories/RUSTSEC-2018-0006
|
||||
- serde-json-wasm: GHSA-j87p-gjr6-m4pv, overflow on deep untrusted JSON, fixed 1.0.1 by recursion depth check. https://advisories.gitlab.com/pkg/cargo/serde-json-wasm/GHSA-j87p-gjr6-m4pv/
|
||||
- quick-xml PR 982: adds a depth cap to its serde deserializer, default 128 "matching serde_json". https://github.com/tafia/quick-xml/pull/982
|
||||
- prost: `const RECURSION_LIMIT: u32 = 100`, comment "100 is the default recursion limit in the C++ implementation"; disabled by feature `no-recursion-limit`. https://github.com/tokio-rs/prost/blob/master/prost/src/lib.rs
|
||||
- bincode, postcard, simd-json: searches returned nothing on depth handling; not established either way.
|
||||
- Rust forum user report: serde overflowing the default stack beyond roughly 100 levels in some types, worked around with a 32 MiB thread. https://users.rust-lang.org/t/deserialize-large-nested-struct-without-stack-overflow/136335
|
||||
|
||||
## C. Stack-growth mechanism: stacker / psm / stacksafe
|
||||
|
||||
- `stacker::maybe_grow(red_zone, stack_size, f)`: checks whether the current stack is within `red_zone` bytes of its end; if so allocates a new stack of "at least `stack_size` bytes" and runs `f` there; else runs on the current stack. The closure runs on a stack with at least `red_zone` bytes available. https://docs.rs/stacker/latest/stacker/fn.maybe_grow.html
|
||||
- Source (rust-lang/stacker `src/lib.rs`): remaining stack = stack pointer minus thread-local `STACK_LIMIT` (lazily from `guess_os_stack_limit`); if `remaining_stack()` is `None`, `maybe_grow` "immediately grow[s]"; panics are caught on the new stack and re-raised after the segment is freed (`catch_unwind` / `resume_unwind`); on unsupported platforms `_grow` is a no-op that just calls the callback.
|
||||
- Platform: README says psm on most platforms, a Fibers-based implementation on Windows; "On all unsupported platforms this library is a noop... code will continue to hit the guard pages". https://github.com/rust-lang/stacker
|
||||
- psm README table (rust-lang/stacker/psm/README.mkd): Windows x86_64 Available Yes / Tested Yes / Callstack "Yes, but disabled"; Windows i686, aarch64, arm: No/No/No; Linux x86_64, i686, aarch64, arm: Yes/Yes/Yes; wasm: No/No/No ("callstack is not even in an address space"). Text: stacks allocated the usual way are not valid on Windows; Fibers offered as an "unnecessarily slow and inflexible" alternative.
|
||||
- CONFLICT, not reconciled: stacker's code selects a Windows backend (`backends::windows::_grow`, `cfg(all(windows, not(miri)))`) while the psm table marks Windows callstack "disabled"; stacker has a wasm32 branch (alloc-based restore guard) while psm lists wasm as No. An older docs.rs stacker 0.1.0 page lists a different verified-platform list.
|
||||
- Not found in sources read: per-call cost numbers; Windows aarch64 behavior of stacker beyond the psm table.
|
||||
- rustc usage (ensure_sufficient_stack, red zone / segment constants): two fetches returned 404; not recorded.
|
||||
- `stacksafe` (fast/stacksafe): `#[stacksafe]` attribute checks remaining stack at function entry and allocates a new segment below a threshold; defaults 128 KiB threshold, 2 MiB segment; optional `serde` feature gives stack-safe (de)serialization for the `StackSafe<T>` wrapper; MSRV 1.88.0; credits Orson Peters' `recursive` crate; says it supports platforms stacker supports (dependency on stacker not confirmed). https://github.com/fast/stacksafe
|
||||
|
||||
## D. Dedicated thread with sized stack
|
||||
|
||||
- `std::thread`: default spawned-thread stack "currently 2 MiB on all Tier-1 platforms"; `Builder::stack_size`; `RUST_MIN_STACK` (overridden by `stack_size`, "may be ignored after program start"); "the stack size of the main thread is not determined by Rust". https://doc.rust-lang.org/std/thread/index.html
|
||||
- Local use is in tests only (`union_parity.rs`, section A). No library was found in this pass that spawns its own decode thread for untrusted input; the Rust forum report (section B) and the protobuf.js advisory describe it as a caller-side or process-level workaround.
|
||||
- protobuf.js CVE-2026-44289 workarounds: reject excessively nested messages at an outer protocol boundary, or isolate decoding "in a process that can be safely restarted". https://corgea.com/advisories/vulnerabilities/CVE-2026-44289
|
||||
|
||||
## E. Non-recursive decoders
|
||||
|
||||
- `deser` crate (mitsuhiko, experimental): "does not recurse on the call stack, so deep nesting needs no recursion limit"; nested sinks/emitters kept in an arena; not serde-compatible; calls itself "an experiment". https://github.com/mitsuhiko/deser/wiki
|
||||
- `core-json`: no-std JSON parser with a fixed-size or caller-supplied state stack; typed layer is a separate crate, `core-json-traits`. https://docs.rs/core-json
|
||||
- No serde-compatible explicit-stack derive deserializer found (search negative).
|
||||
- msgpack-javascript: decoder iterative, `StackPool` array of states; DecoderOptions has no max depth, only length caps (`maxStrLength`, `maxBinLength`, `maxArrayLength`, `maxMapLength`, `maxExtLength`); pool memory grows with deepest nesting; array states allocate `new Array(size)` up front. https://github.com/msgpack/msgpack-javascript/blob/main/src/Decoder.ts
|
||||
- msgpack-python C unpacker (`unpack_template.h`): explicit `unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]`, `start_container` returns -3 at `top >= SIZE`. `unpack.h` defines the size as 1024 (`unpack_define.h` default is 32). Local probe (msgpack 1.2.2, CPython 3.13.15, `python -I`): 100, 256, 1000 nested arrays decode ok; 100000 raises `StackError`.
|
||||
- CultLib's own `check_payload_depth` walkers are iterative (Rust lib.rs ~526; TS `packages/cultcache-ts/src/payload-depth.ts`; C# `CultPayloadDepth.cs`).
|
||||
|
||||
## F. Advisories and other-language limits
|
||||
|
||||
- MessagePack-CSharp CVE-2026-48506 / GHSA-vh6j-jc39-fggf (published 2026-06-25): `MessagePackReader.TrySkip()` recursed without counting depth, bypassing `MaximumObjectGraphDepth`; reachable through generated formatters' `reader.Skip()` on unknown keys; uncatchable StackOverflowException; affected <2.5.301 and 3.0.0 to <3.1.7; fix: iterative loop with a heap pending-count. https://securelayer7.net/lab/cve-2026-48506-messagepack-csharp-tryskip-uncontrolled-recursion-dos . Related: CVE-2026-48512 (`ConvertFromJson` recursion), CVE-2026-48513 (Union deserialization not consistently under depth enforcement).
|
||||
- Default `MaximumObjectGraphDepth`: the sources read do not state it (a figure of 500 was not confirmed).
|
||||
- protobuf: documented unmarshal depth limits Java 100, C++ 100, Go 10000 (plan to lower to 100); the page gives no rationale. https://protobuf.dev/programming-guides/proto-limits/ . CVE-2024-7254: protobuf-java nested SGROUP unknown fields/maps unbounded recursion, StackOverflow, CVSS 4.0 8.7, fixed 3.25.5 / 4.27.5 / 4.28.2. https://cveawg.mitre.org/api/cve/CVE-2024-7254 (CONFLICT: RHEL reports not affected; an OpenCVE page says no fix, against the CVE record's fixed versions).
|
||||
- Go encoding/gob CVE-2022-30635: uncontrolled recursion on ignored fields; fix adds a depth limit of 10,000; fixed in 1.17.12 / 1.18.4. https://go.googlesource.com/go/+/6fa37e98ea4382bf881428ee0c150ce591500eb7 . Go encoding/json nesting constant: not confirmed.
|
||||
- .NET System.Text.Json: `JsonSerializerOptions.MaxDepth` default 0 meaning 64; exceeding throws `JsonException`. https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.maxdepth
|
||||
- Python `json`: docs say the module "does not impose any such limits beyond those of the relevant Python datatypes themselves or the Python interpreter itself"; RecursionError for deep input not mentioned. https://docs.python.org/3/library/json.html . The pure-Python msgpack fallback Unpacker recurses (`# TODO should we eliminate the recursion?`), has no depth option, converts `RecursionError` to `StackError`. https://github.com/msgpack/msgpack-python/blob/main/msgpack/fallback.py
|
||||
- msgpack5 (JS) CVE-2026-107298: no nesting limit, deep input exhausts the decoder stack (title seen only). https://vulert.com/vuln-db/CVE-2026-107298
|
||||
|
||||
## G. CultLib's other runtimes
|
||||
|
||||
- C# reference: `src/GameCult.Caching.MessagePack/CultMessagePackSecurity.cs`: `UntrustedData.WithMaximumObjectGraphDepth(256)` as backstop; the rule is `CultPayloadDepth.Check`. MessagePack package pinned 3.1.7 (`GameCult.Caching.MessagePack.csproj`), the TrySkip fix version. Other C# surfaces use bare `MessagePackSecurity.UntrustedData` (`src/GameCult.Mesh/CultMeshProviderSessionWire.cs:231`, `src/GameCult.Networking/CultNetSchemaMessageSerialization.cs:15`, `src/GameCult.Networking/MessageSerialization.cs:8`).
|
||||
- TypeScript: `@msgpack/msgpack ^3.1.3` (`packages/cultcache-ts/package.json`); decoder iterative with no depth option (section E); encode passes `maxDepth: MAX_PAYLOAD_DEPTH * 2` (`src/document-formatter.ts:21`); `checkPayloadDepth` iterative. node_modules not present, so the installed version was not read.
|
||||
- Python: `msgpack>=1.0` (`packages/cultcache-py/pyproject.toml`); `documents.py:135` and `stores.py:97,384` call `msgpack.unpackb(..., raw=False)`; the C extension has an explicit 1024 stack, so 256-bounded payloads fit; the pure-Python fallback is recursive.
|
||||
- Whether a typed (class-mapped) TS/Py/C# decode layer above these raw decoders recurses per type was not inspected in this pass.
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
# Device capture runners: how established projects capture real-GPU results, and what a browser driver can silently get wrong
|
||||
|
||||
For `cultmath-tapes:question:device-run-driver` (campaign `cultmath-tapes`) and follow-up `cultmath-tapes:follow_up:glsl-browser-parity-run`.
|
||||
Gathered by eyes-ag3-device-driver, session self-2026-10-15-ag3, 2026-10-16. Facts only; no recommendation.
|
||||
|
||||
Strength marks: [src] read from a primary source text this pass; [doc] vendor or standards documentation read this pass; [web] search-result or fetch-summary of a page (a small model summarised it; check before quoting); [abs] searched, not found.
|
||||
Chromium doc pages were read at `main` or at an old pinned revision as noted; Chromium GPU bot details drift, so the dates matter.
|
||||
|
||||
## 0. What the question asks (the record)
|
||||
|
||||
- [src] Question `device-run-driver` (Imagination, 2026-10-16): a static page (`packages/cultmath/tools/device-runner`) compiles each family's call against `CultMath.glsl`, reads back through an RGBA32UI target, emits raw bits; C# judges. Options: `committed-captures` (page + stdlib Python server, any WebGL2 browser is a driver, captures committed with renderer, revision, shader sha, args hash, result bits, judged by `DeviceCaptureTests` on Linux), `playwright-live` (Microsoft.Playwright test dependency, Windows-only, ANGLE D3D11, must refuse SwiftShader), `both`.
|
||||
- [src] Prior precedent named in the question: three Soul/Hands passes through the desktop app's built-in browser on Starfire, renderer `ANGLE (NVIDIA GeForce GTX 1070 Direct3D11)`.
|
||||
- [src] Follow-up `glsl-browser-parity-run` records both GLSL files probed on a GTX 1070 (phacelle within 3.1e-6 of max(|v|,1), frustum check 0/256).
|
||||
|
||||
## 1. How established projects capture real-GPU results for CI
|
||||
|
||||
| Project | What runs where | What is stored, and where | What identifies the device | Source |
|
||||
|---|---|---|---|---|
|
||||
| Khronos WebGL conformance suite | Static HTML pages. Open `webgl-conformance-tests.html` in the browser under test (`?run=1` auto-starts), then "display text summary". Run by hand in any browser. | Formal results go as a pull request to the `KhronosGroup/WebGLConformanceSubmissions` repo. Older README copies said email `webgl_conformance_submissions@khronos.org`. Only WebGL implementors submit. | README instruction: "Verify that the User Agent and WebGL renderer strings identify your browser and target correctly." The summary text carries them. | [src] raw.githubusercontent.com/KhronosGroup/WebGL/main/sdk/tests/README.md (read 2026-10-16); [web] search summary of CONFORMANCE_RULES.txt |
|
||||
| Khronos WebGL conformance rules | Results carry forward: "results submitted for an earlier version of the browser carry forward to later versions ... that do not cause any previously passing test to fail"; for OS versions likewise (a new OS version needs a new submission). | The carry-forward rule is the only staleness rule found. | | [web] search summary of CONFORMANCE_RULES.txt, not read in full |
|
||||
| Vulkan CTS conformance submission | Run on the vendor's driver builds, every fraction. | Submission package must contain full `TestResults.qpa` logs per driver build and fraction, `git status` and `git log --first-parent <release tag>^..HEAD` output of a clean CTS checkout, `git format-patch` output of any CTS changes over the release tag, and a statement file with a `CONFORM_VERSION` line carrying the CTS git tag, plus PRODUCT, CPU, OS. `verify_submission.py` checks the package against the current mustpass list. | PRODUCT/CPU/OS lines in the statement. | [web] search summaries of `external/vulkancts/README.md` in Fuchsia/Android/Chromium mirrors (mirror copies differ by version) |
|
||||
| Chromium GPU bots | Physical machines in Swarming; most tests via Telemetry launching the full browser; `webgl_conformance_tests` and pixel tests. `chromium.gpu.fyi` tests top-of-tree ANGLE. | Expected outcomes in per-test expectation files (`pixel_expectations.txt` under `content/test/gpu/gpu_tests/test_expectations/`, `Failure` entries). Pixel images are not in the repo (Skia Gold, below). Some pixel tests check only a few pixels with handwritten code so one check serves all vendors. | Swarming task dimensions: `gpu` = PCI vendor:device-driver version (example `10de:2184-440.100`), plus `os`, `cpu`, `pool`. The page does not describe a runtime renderer-string check. | [src] chromium.googlesource.com/chromium/src/+/main/docs/gpu/gpu_testing.md (read 2026-10-16) |
|
||||
| Skia Gold (Chromium pixel tests) | goldctl client sends the produced image, the test name and hardware/software config metadata to a service. | Approved images live in a service (storage bucket + triage UI), not git. A test passes if the hash of the produced image is in that test's approved-hash list; default exact matching, multiple baselines per test. An unknown hash fails until a human triages it. Doc says local runs "differ slightly from bot images" and uploads need allow-listing; `--local-pixel-tests` disables upload. | Config metadata uploaded with each image. | [web] search summary of `docs/gpu/gpu_pixel_testing_with_gold.md` (pinned old revisions); [src] gpu_testing.md for the local-run flags |
|
||||
| WebGPU CTS in Chromium | gpuweb/cts compiled to JS, run through WPT and Telemetry; Telemetry covers non-virtual devices with real GPUs. A pinned revision is auto-rolled. | Expectations per platform: Fail/Skip may be any CTS query; Slow/Crash/Timeout must list parameters in loader order. | | [web] search summaries of Chromium `web_tests/webgpu/README.md` and a 2022 W3C public-gpu post by Kai Ninomiya (not read in full) |
|
||||
| Dawn node runner | `dawn.node` binding runs the CTS without a browser; `run-cts` can instead drive a Chrome instance. | `--output <path>` writes a results file, `--expect <path>` diffs a later run against it. Dawn keeps `webgpu-cts/expectations.txt`, pruned when tests start passing, with wide `Failure` wildcards for flaky ones. | | [web] search summaries of Dawn `src/dawn/node/README.md` and commit `f78edf56` ("Expand CTS expectations for Win11 QC CI bot") |
|
||||
| three.js | `test/e2e/puppeteer.js` screenshots almost all examples and compares to stored references; helpers `deterministic-injection.js`, `clean-page.js`, `check-coverage.js` exist in an older release's `test/e2e`. | Reference screenshots in the repo. | A 2020 issue: screenshots were black on macOS with the GPU-path flags; a maintainer floated using SwiftShader everywhere "to avoid GPU-specific bugs". Current flags and CI setup not confirmed. | [web] search summaries of discourse.threejs.org and a lightrun mirror of the GitHub issue; [abs] current `puppeteer.js` flags not read |
|
||||
| Babylon.js / Babylon Native | Visualization tests list scenes in `tests/test/visualization/config.json` (Babylon Native: `Apps\ValidationTests\Scripts\config.json`); each entry names a reference image, optional per-scene pixel-difference threshold. | Reference images in `ReferenceImages/` in the repo; `-u` regenerates. Babylon Native CI fails on too-different images and stores rendered/error images as an artifact. | | [web] search summaries of forum.babylonjs.com and a Babylon Native `ContinuousIntegration.md` fork; [abs] an `errorRatio` key was not found |
|
||||
| glslang / SPIRV-Cross | No GPU. SPIRV-Cross round-trips `shaders/` through glslangValidator/spirv-as and SPIRV-Cross, diffs output text against committed `reference/` files; `test_shaders.py` exits non-zero on difference, `--update` regenerates references that then ride in the PR; says to use the right glslang and SPIRV-Tools versions. Only hardware option: `--malisc` static cycle counts. | Reference text in the repo. | | [src] raw.githubusercontent.com/KhronosGroup/SPIRV-Cross/main/README.md (read 2026-10-16); [abs] glslang's own test practice not read |
|
||||
| dEQP GLES3 builtin precision tests | Random inputs through built-ins in a shader; outputs via transform feedback or framebuffer pixel; compared to a reference interval, not a single value. | Spec text of tolerances (section 4). | | [src] doc/testspecs/GLES3/functional.shaders.builtin_functions.precision.txt (Fuchsia mirror, read 2026-10-16) |
|
||||
|
||||
Observations across the rows (no ranking):
|
||||
- [src/web] Where real-GPU outputs are exact or large, the approved results are stored outside a source repo (Gold, Swarming expectations) or as submission packages (WebGL, Vulkan CTS). Where outputs are text from a deterministic tool (SPIRV-Cross) the reference is committed beside the source and `--update` regenerates it.
|
||||
- [web] Only the Vulkan CTS submission package was found to record the exact source revision (tag plus `git log`) inside the result bundle; WebGL's rule is the carry-forward rule above. No project was found that refuses a committed capture automatically because a shader source hash changed. [abs]
|
||||
- [src] Chromium's GPU doc keys a bot's hardware by PCI id and driver version rather than by a renderer string read inside the page.
|
||||
|
||||
## 2. Headless browser drivers on real GPUs
|
||||
|
||||
### 2.1 Fallback to a software renderer
|
||||
|
||||
- [src] Chromium SwiftShader doc (read at `refs/heads/main`): flags `--use-gl=angle --use-angle=swiftshader` (SwANGLE as the ES driver); `--use-gl=angle --use-angle=swiftshader-webgl --enable-unsafe-swiftshader` (WebGL fallback). "Automatic fallback to SwiftShader-backed WebGL is deprecated ... WebGL context creation will soon fail instead of falling back." `--enable-unsafe-swiftshader` opts in and suppresses the console warning. The page does not describe how to verify the renderer.
|
||||
- [web] Blink-dev "Intent to Remove: SwiftShader Fallback": deprecation period began Chrome 130; estimated desktop ship Chrome 137; a later reply from Rick Byers says API owners consider it high-risk and may phase it, so the milestone was unconfirmed in the thread; Finch feature `AllowSwiftShaderFallback`; headless SwiftShader use "will still be supported by opting in"; desktop only. Sources disagree on the shipped milestone (a Winaero Chrome 137 write-up says WARP replaces it on Windows, which the blink-dev page does not mention; Samsung's browser policy page says M139). Not resolved. [web]
|
||||
- [src] Chrome developer blog "Supercharge Web AI model testing": "By default, Headless Chrome disables GPU." `chrome://gpu` showed "Software only, hardware acceleration unavailable" until drivers were fixed; "Some combinations of flags allow detection of the GPU, but don't allow you to actually use the GPU." Failing renderer string: `ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver-5.0.0)`; working: `ANGLE (NVIDIA Corporation, Tesla T4/PCIe/SSE2, OpenGL ES 3.2 NVIDIA 525.105.17)`. Linux flags there: `--headless=new --use-angle=vulkan --enable-features=Vulkan --disable-vulkan-surface --enable-unsafe-webgpu`. Setup was Colab, not CI.
|
||||
- [web] A Playwright-on-GPU-runner write-up (createit.com) reported renderer ANGLE-on-Vulkan with SwiftShader; headed Chromium under a virtual display used the GPU; a Vulkan flag set later gave `llvmpipe`, still software.
|
||||
- [src] Chromium GPU testing doc: example bot flags `--use-gl=angle --use-angle=gl --use-cmd-decoder=passthrough --force_high_performance_gpu`, passed via `--extra-browser-args`.
|
||||
- [src] Playwright `chromiumSwitches.ts` at `main` (read 2026-10-16) contains no GPU, SwiftShader, ANGLE or headless switch. [abs] What the downloaded headless shell binary itself defaults to was not found.
|
||||
- [abs] No source was found on `--use-angle=d3d11` combined with headless Playwright on Windows, and none on the renderer string a Windows GPU reports under headless.
|
||||
- [web] `--disable-gpu` is reported by one source to force software rendering.
|
||||
|
||||
### 2.2 Detecting a fallback from inside the page
|
||||
|
||||
- [doc] `WEBGL_debug_renderer_info` exposes `UNMASKED_RENDERER_WEBGL` through `gl.getExtension(...)` then `gl.getParameter(...)`; MDN says it may be limited to privileged contexts depending on privacy settings, and Firefox disables it when `privacy.resistFingerprinting` is true, so `getExtension` can return null (MDN, via search result).
|
||||
- [doc] `failIfMajorPerformanceCaveat: true` makes `getContext` return null if the browser would use a dramatically slow implementation (MDN, via search result). [web, unconfirmed here] whether Chrome treats SwiftShader as such a caveat was not found in a source this pass.
|
||||
- [src] Strings observed in sources: SwiftShader `ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) ...))` and `Google SwiftShader`; hardware `ANGLE (NVIDIA ..., Direct3D11)` form as in the question's precedent.
|
||||
- [src] Chromium's own bots do not read a string; they identify hardware by Swarming dimensions (section 1).
|
||||
- [web] `WebGLShaderPrecisionFormat` from `getShaderPrecisionFormat(FRAGMENT_SHADER, HIGH_FLOAT)`: `precision` 23 means real highp float; it reports mantissa bits, nothing about subnormal handling; Safari mis-reported it as of April 2020 (WebKit bug via webglfundamentals).
|
||||
- [web] A sample WebGL report line: `ANGLE (Intel(R) HD Graphics 4600 Direct3D11 vs_5_0 ps_5_0)`, highp/highp, precision 23.
|
||||
|
||||
### 2.3 Windows service and session 0
|
||||
|
||||
- [web] Microsoft XPDM-vs-WDDM table: on WDDM, Session 0 isolation means a Direct3D 9 HAL device is never available from a service (NULLREF, HAL, REF "No"; WARP10 "Yes").
|
||||
- [web] Microsoft support article 978635: starting with Windows 8.1 / Server 2012 R2, Direct3D 11 supports use from a Windows service. WARP is recommended by Microsoft for service/server environments.
|
||||
- [web] VTK issue 18657 (GitLab): swap-chain creation failed on Windows CI runners because the process ran in Session 0, which has no graphics hardware access; runners were moved from system services to scheduled tasks.
|
||||
- [abs] No source found on headless Chrome or ANGLE behaviour specifically inside a service. The pattern the sources support is an interactive user session or a scheduled task that runs only when the user is logged on.
|
||||
|
||||
## 3. Committed-capture designs: versioning against source hashes and refusing stale captures
|
||||
|
||||
- [src] SPIRV-Cross: references are text regenerated by the tool; staleness is detected by re-running the tool and diffing, not by a stored hash. The tool versions (glslang, SPIRV-Tools) are named in the README as the ones to use.
|
||||
- [web] Skia Gold: the approved set is keyed by test name plus image hash; an unknown hash fails until triaged. Config metadata is uploaded with the image. No source-revision binding was found.
|
||||
- [web] Vulkan CTS: the result package carries the CTS tag, `git log` and patches; a checker script validates the package against the mustpass list for the version.
|
||||
- [web] WebGL: carry-forward rule (earlier passing results hold for later browser versions that fail no previously passing test).
|
||||
- [web] Babylon / three.js: reference image names live in a config list; regeneration is a flag (`-u`); a missing reference is an error (a 404 in a Karma run, Babylon forum).
|
||||
- [src] Chromium expectation files: per-test `Failure` entries are removed once a test passes (gpu_testing.md); Dawn prunes `expectations.txt` the same way.
|
||||
- [abs] No project was found whose committed device capture embeds a hash of the shader source it ran and is rejected by CI when that hash no longer matches the checked-in source. (Search was for the pattern, not an exhaustive survey of repositories.)
|
||||
|
||||
## 4. Float behaviour that bears on judging captures across ANGLE/D3D11, native GL and mobile GPUs
|
||||
|
||||
### 4.1 What the standards guarantee
|
||||
|
||||
- [doc] Qualifiers are minimums. highp float "is at least a 32 bit value", range about +-2^62 in the WebGL2Fundamentals table (exponents stripped in extraction, inferred); mediump at least 16 bit, range +-2^14, relative precision 2^-10; lowp at least 9 bit, range +-2 (webgl2fundamentals precision lesson; asawicki.info April 2015 for the 2^-10 and 2^-8 figures). On a desktop GPU mediump "will still most likely use 32bits internally"; the lesson's author's Pixel 2 XL used 16 bit for mediump and lowp.
|
||||
- [doc] Highp in fragment shaders is optional in OpenGL ES 2.0 (a shader declaring it fails to compile where unsupported). [web] GL_ARB_shader_precision text: internal processing need not match IEEE 754; rounding mode cannot be set and is undefined; "any denormalized value input into a shader or potentially generated by an operation in a shader can be flushed to 0."
|
||||
- [src] dEQP GLES3 precision spec tolerances (ULP unless noted): `+ - *` under 1 ULP; `/` 2.5 ULP for 2^-126 <= |y| <= 2^127-1; sin/cos 2^-11 abs on [-pi, pi]; asin/acos 4; atan2 6; atan 5; exp 3+2|x| highp; inversesqrt 2; log 3 ULP highp outside the central range; exact (0 ULP) abs, sign, floor, trunc, roundEven, ceil, min, max, clamp, step; derived functions (tan, pow, sqrt, mix, smoothstep, fract, mod ...) judged by their expansions. The page does not mention flush to zero. Lowp and mediump results on overflow "must produce an infinity or the max/min value".
|
||||
- [web] Olli Etuaho on the WebGL list, 14 Nov 2014: proposes testing only at the spec minimums, with subnormals flushed to zero, since code that works there will likely work with more bits; concedes not simulating built-in internals is a gap.
|
||||
|
||||
### 4.2 Direct3D 11 (the ANGLE/D3D11 path)
|
||||
|
||||
- [doc] Microsoft Learn "Floating-point rules (Direct3D 11)" (updated 2025-04-15), 32-bit rules: "Denorms are flushed to sign-preserved zero on input and output of any floating-point mathematical operation" (exceptions: I/O or data movement that does not manipulate the data). Add/sub/mul within 0.5 ULP, and the page says hardware may truncate; sqrt and rcp 1 ULP; rcp and rsq have a separate relaxed requirement; "Fused operations (such as mad, dp3) produce results that are no less accurate than the worst possible serial ordering of evaluation of the unfused expansion", each unfused step allowed 1 ULP (D3D10 page: 1 ULP for mul/div). NaN bit patterns are not preserved. min/max flush denorms for comparison but the result "may or may not be denorm flushed". Doubles preserve denorms; 16-bit floats preserve denorms.
|
||||
- [abs] No source found that states how ANGLE's D3D11 translation, fxc/dxc flags, or NVIDIA's D3D11 driver treat denormals or contraction beyond the D3D rules above.
|
||||
- [web] ANGLE's `FeaturesD3D.h` workaround list contains HLSL-compiler-specific rewrites (for example expanding `pow` into multiplies on an affected compiler, per search summary); a Wine D3D11 trig test failed on one NVIDIA configuration with roughly 0.1% relative error, not on AMD (Wine bug thread, cause not identified).
|
||||
- [web] ANGLE D3D11 vs D3D9 rendering differences are reported in Mozilla bug 1122912 (unresolved in the excerpt).
|
||||
|
||||
### 4.3 Mobile and other native GPUs
|
||||
|
||||
- [web] ARM blog "Benchmarking floating point precision in mobile GPUs, Part III": Vivante GC4000 and Imagination SGX 554 "do not support subnormal values" (below 2^-126 treated as zero); Mali-T604 supports subnormals, and the authors say Mali Midgard is the only mobile family they know to; the measuring shader divides by 2 per exponent step and multiplies back, drawing underflow pixels red. No ULP measurements in that part; Part II (not read) is said to find GPUs taking "the easy (round-to-zero) way out".
|
||||
- [web] asawicki.info: `length`, `normalize`, `distance` and dot products in a `mediump` shader can overflow at (74,74,74) because 3*74^2 = 16428 exceeds 2^14; a GPU may pass because it uses more precision than required.
|
||||
- [abs] No source read this pass on Adreno, Apple, current Mali or PowerVR denormal and contraction behaviour; the ARM post covers 2013-era parts.
|
||||
- [abs] FMA contraction in GLSL: no WebGL or GLSL ES source was read here on whether `a*b+c` may be fused (see `docs/research/cycles-fma-contraction.md` for the Cycles CPU case and the D3D "mad" fused-op tolerance above).
|
||||
|
||||
## 5. Gaps left by this pass
|
||||
|
||||
- Current three.js `test/e2e/puppeteer.js` flags and Babylon `config.json` tolerance keys were not read from the repositories (search summaries only).
|
||||
- Gold, WebGPU CTS and Dawn node pages are search summaries, not full reads.
|
||||
- Whether Chrome's shipped (not 130-era) behaviour fails WebGL context creation when no GPU is usable, versus using WARP on Windows, is unresolved between the sources in section 2.1.
|
||||
- Nothing here was measured on Starfire; no run of any driver was made.
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# Prior art: a header flag one writer sets and content another writer cannot see
|
||||
|
||||
Serves: `variants:question:non-csharp-writers-mark-ids` (raised in
|
||||
`variants:follow_up:foreign-ids-under-v1-header`, campaign `variants`). Gathered by
|
||||
Imagination, session self-2026-10-14b-eureka, 2026-10-14. Facts with pointers; the
|
||||
recommendation lives in the question, not here.
|
||||
|
||||
The CultLib case: the store header (`cultcache.store.v1` / `v3`) says whether any record
|
||||
holds an element id, so a reader older than ids refuses the store instead of rewriting
|
||||
elements without them. An element id is an ordinary keyed string in the payload, so a
|
||||
runtime that does not know which member is the id (cultcache-py, -ts, -rs today) cannot
|
||||
tell from content whether to mark. The persistence contract
|
||||
(`src/GameCult.Caching/Contracts/cultcache-persistence-format.md:424-427` at 6afa8ed9)
|
||||
says those runtimes keep a `v3` header they found and do not decide.
|
||||
|
||||
## How established formats split the job
|
||||
|
||||
| System | Who sets the flag | What an implementation that does not understand the feature does | Pointer |
|
||||
|---|---|---|---|
|
||||
| ext4 superblock | The code that first uses a feature sets its bit in `s_feature_compat`, `_ro_compat` or `_incompat`. | Unknown `incompat` bit: refuse to mount (fsck stops too). Unknown `ro_compat` bit: mount read-only, never write. Unknown `compat` bit: read and write freely; fsck should not modify. | https://www.kernel.org/doc/html/latest/filesystems/ext4/super.html (feature fields) |
|
||||
| SQLite file header | The writer's journal mode sets the write/read version bytes (offsets 18, 19); the schema format number (offset 44) is set by the writer of the newer schema features. | Read version 1-2 with write version > 2: open read-only. Read version > 2: neither read nor write. | https://www.sqlite.org/fileformat.html section 1.3.3, 1.3.10 |
|
||||
| git repository format | A repository declares `core.repositoryformatversion = 1` and the `extensions.*` it uses; the tool enabling a feature writes its extension key. | Any unimplemented `extensions.*` key in a v1 repo: the operation must not proceed (read or write). | https://git-scm.com/docs/repository-version |
|
||||
| Protocol Buffers | No flag. New fields are self-delimited by tag. | Parsers keep unknown fields and write them back (proto2 always; proto3 again since 3.5), so an old reader does not drop a newer writer's data on rewrite. | https://protobuf.dev/programming-guides/proto3/#unknowns |
|
||||
| Avro container files | The writer embeds its full schema in the file header. | Reader resolves writer schema against its own; a writer field the reader lacks is ignored, so it is lost if the reader rewrites. | https://avro.apache.org/docs/current/specification/ ("Schema Resolution") |
|
||||
| Parquet | Writer stamps `created_by` (application and version) in FileMetaData. | Readers keep a list of known-bad writer versions and distrust their metadata (parquet-mr ignores min/max statistics from writers before 1.8.0, PARQUET-251). | https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift (`created_by`); https://issues.apache.org/jira/browse/PARQUET-251 |
|
||||
|
||||
## Patterns, as facts
|
||||
|
||||
- Flag-based formats (ext4, SQLite, git) put the duty to set the flag on the writer that
|
||||
introduces the content. None of them has a writer that carries feature content while
|
||||
not knowing it is that feature: an implementation that does not understand a feature
|
||||
either does not write (ro_compat, SQLite read-only) or does not touch the store (git).
|
||||
- Formats without a flag (protobuf, Avro) handle the older reader by preservation
|
||||
(protobuf keeps unknown fields) or accept loss (Avro drops unknown writer fields on
|
||||
rewrite). CultCache's C# 1.x readers skip unknown keyed slots and do not preserve
|
||||
them, which is why the `v3` header exists.
|
||||
- Parquet's `created_by` is a third option: readers judge content by who wrote it and
|
||||
compensate for writers known to get a flag or statistic wrong.
|
||||
- No system surveyed asks a reader to infer a feature flag from content it cannot
|
||||
recognise; where content cannot be recognised, the burden is on writers.
|
||||
|
||||
## Mechanism facts from this pass (probes at 6afa8ed9 on Yggdrasil)
|
||||
|
||||
- A C# cache that pulls the Python-written `py-store/leaf-record.cc` (v1, element ids
|
||||
`000000000021/22`) holds that record with `HoldsIds = true`, `IdsInMemoryOnly = false`;
|
||||
a C# write that adds an unrelated record copies it forward and leaves the header `v1`.
|
||||
The ids survive in C#; a pre-id reader with a type lacking the id slot would drop them.
|
||||
- cultcache-py, -ts and -rs read `v3` and keep it on rewrite; none has a declaration of
|
||||
which member is an element id.
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
# Ground motion law: prior art (facts only)
|
||||
|
||||
Serves fork: cultmath-tapes, site-ground motion law (invariant `one-motion-owner`, target `cultmath-tapes:target:r2`), blocks `cut-site-ground`.
|
||||
Gathered by Eyes, session self-2026-10-14b-eureka, 2026-10-14. No recommendation. Committed by Self before the fork was put.
|
||||
|
||||
Method: primary PDFs were downloaded to the session scratchpad and read with pdftotext ("read"). "[secondary]" marks facts taken from a web-search summary, forum or blog, not read at the primary source. "[not verified]" marks things looked for and not reached.
|
||||
|
||||
## 1. How shipped volumetric clouds move, and what breaks
|
||||
|
||||
### 1.1 Horizon Zero Dawn, Schneider and Vos, SIGGRAPH 2015
|
||||
Source: https://www.advances.realtimerendering.com/s2015/The%20Real-time%20Volumetric%20Cloudscapes%20of%20Horizon%20-%20Zero%20Dawn%20-%20ARTR.pdf (99 slides, read)
|
||||
- Density = low-frequency 3D Perlin-Worley base shape times height gradient and coverage, eroded by a high-frequency 3D Worley texture (slides ~34-37).
|
||||
- A 128^2, 3-channel 2D curl noise texture "is non divergent and is used to fake fluid motion"; it distorts the cloud shapes and distorts the sample position of the second (detail) 3D noise "to fake the swirly distortions from atmospheric turbulence" (slides 33, 37). The curl texture is a static distortion of the detail lookup, not a time-integrated flow.
|
||||
- Summary slide (48): noises are custom, "made from Perlin, Worley and Curl noise", and "all animated in a given wind direction". Wind direction varies over altitude (slide 21). High cirrus/alto layers are 2D textures "tiling and scrolling at different speeds and directions" (slide 87).
|
||||
- Large-scale evolution is not noise motion: coverage, precipitation and cloud type come from a 2D weather map driven by a simulation, with a wind-speed control (slides 39-44).
|
||||
- Reprojection (slides 92-93): quarter-res buffer updates 1 of 16 pixels per 4x4 block each frame; "We reproject the previous frame to ensure we have something persistent"; where reprojection fails (screen edges) the low-res result is substituted. Stated as making the shader "10x faster or more"; raw cost ~20 ms, target ~2 ms.
|
||||
|
||||
### 1.2 Nubis, Evolved, Schneider, SIGGRAPH 2022
|
||||
Source: https://advances.realtimerendering.com/s2022/SIGGRAPH2022-Advances-NubisEvolved-NoVideos.pdf (read)
|
||||
- Slide "Skies / Vertical Profile Model / Animating Density": the motion law for sky density is a sample-position offset, `noise_sample_position = sample_position - wind_direction * scroll_offset` (comment "Deform sample coordinates").
|
||||
- Slide family "VFX / Animating Superstorm Density / The Mesocyclone": each ring samples noise at `GetRotatedPosition(sample_position, superstorm_center, time_offset * RingRotationSpeed[n])`, later with a per-ring `ring_skew[n]`: different rotation rates per layer about one center, no flow field.
|
||||
- Slide "VFX / Animating Cloud Density For Skies" (code listing): the reprojection pass reads a stored per-pixel `cloud_distance` from a "CloudAttrWorkingBuffer", reconstructs `cloud_world_space = invView * (eyeRay * cloud_distance)`, applies the SAME motion law to that world position (rotate about the storm center blended by `superstorm_mask`; scroll `cloud_world_space + scroll_direction_2D * deltaTime`), then projects it with `inReprojectionMatrix` to get the previous-frame UV. The history lookup is displaced by the density's own motion, using one depth per pixel written by the march.
|
||||
- The 2022 and 2023 decks show an "Aero Cloud Solver" frame (Apply Buoyant Force, Advect Density, Advect Velocity, Non-Divergence, Sourcing) among the history slides, as the offline VFX approach.
|
||||
|
||||
### 1.3 Nubis3 (Nubis Cubed), Schneider, SIGGRAPH 2023
|
||||
Source: https://advances.realtimerendering.com/s2023/Nubis%20Cubed%20(Advances%202023).pdf (read)
|
||||
- Voxel clouds, slide "Sampling Density": `inSamplePosition -= float3(cCloudWindOffset.x, cCloudWindOffset.y, 0.0) * voxel_cloud_animation_speed`. Wind offset only; no curl, no flow field.
|
||||
- A comparison table lists "Evolution: Yes" for the Vertical Profile method and "Pseudomotion Only" for the Envelope method, and "Pseudomotion Only" in later tables for the voxel methods.
|
||||
- March is SDF-accelerated: "Hybrid SDF, Adaptive and Jittered samples"; jitter is `animated_hash` within 250 m of camera, `static_hash` beyond.
|
||||
- No temporal reprojection slide found in the extracted text.
|
||||
|
||||
### 1.4 Frostbite, Hillaire, SIGGRAPH 2016
|
||||
Source: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016_pbs_frostbite_sky_clouds.pdf (read; section 5.5.3 and the cloud intro)
|
||||
- 5.5.3: clouds rendered in a single pass; each frame samples are randomly offset within their step range; current frame blended with previous "according to a constant blend factor" (EMA); "For the final frame to not look blurry when the camera is moving/rotating very fast, we re-project the previous result according to previous and current camera properties". Camera-only reprojection; no density-motion term stated.
|
||||
- Text cites Guerette's visual-flow technique to give a panoramic cloud texture the illusion of motion along the wind direction, noting it has no variation of cloud shape, weather or lighting.
|
||||
- Frostbite volumetric fog temporal blend, as reproduced by Flax Engine: Halton jitter, 7% current / rest history (https://flaxengine.com/?p=941) [secondary]. Godot docs: higher blend "makes ghosting much worse", moving fog volumes and lights leave a trail (https://docs.godotengine.org/en/stable/tutorials/3d/volumetric_fog.html) [secondary].
|
||||
- Hillaire 2015 (Frostbite unified volumetrics) temporal section: [not verified] (158 MB PPTX; SlideShare copy cuts off before it).
|
||||
|
||||
### 1.5 Toft, Bowles, Zimmermann (Studio Gobo), "Optimisations for Real-Time Volumetric Cloudscapes", arXiv 2016
|
||||
Source: https://arxiv.org/abs/1609.05344 (PDF read)
|
||||
- Calls the Horizon 1/16-pixel scheme "a fairly complex technique which may introduce visual artifacts as a result of the 4x4 pattern by which pixels are updated".
|
||||
- Alternative: far fewer march steps + random per-pixel start offset + analytic integration per step + Playdead-style TAA (Pedersen, INSIDE, 2016) on a low-res buffer. Timings, 1080p GTX 1080 (Fig. 3): half res 128 steps 128.0 ms; half res 8 steps jitter+TAA 7.5 ms; quarter res 8 steps jitter+TAA 2.4 ms.
|
||||
|
||||
### 1.6 UE Volumetric Clouds
|
||||
- Official component docs list View/Reflection/Shadow sample-count scales and a transmittance stop threshold; nothing on temporal behavior: https://dev.epicgames.com/documentation/unreal-engine/volumetric-cloud-component-properties-in-unreal-engine [secondary].
|
||||
- Forum: Mode 2 traces 1 pixel per 4x4 block and reprojects 15, "lots of latency", cloud "would leak after reprojection over opaque meshes": https://forums.unrealengine.com/t/volumetriccloud/688516 [secondary].
|
||||
- Forum hypothesis (unconfirmed): a VolumetricCloud writes neither depth nor velocity, so TSR has nothing to reject history on, causing trailing and banding inside a cloud: https://forums.unrealengine.com/t/tsr-ghosting-against-a-volumetriccloud-the-player-stands-inside-every-rejection-knob-measured-none-help/2837437 [secondary].
|
||||
- Bug UE-198490 (fixed 5.4): horizontal flicker lines from the temporal component; workaround `r.VolumetricRenderTarget.Mode=1`: https://issues.unrealengine.com/issue/UE-198490 [secondary].
|
||||
|
||||
### 1.7 Red Dead Redemption 2 clouds (Bauer, SIGGRAPH 2019)
|
||||
Talk exists ("Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution", Advances in Real-Time Rendering in Games 2019). Slides [not verified]: not among the PDF/PPTX links on https://www.advances.realtimerendering.com/s2019/index.htm; a guessed URL returned 404. Nothing about its motion or reprojection was read.
|
||||
|
||||
### 1.8 Shadertoy-scale nebulae
|
||||
[not verified]. No Shadertoy source was read in this pass.
|
||||
|
||||
### 1.9 What the sources say breaks
|
||||
- Pulsing and repetition: Vlachos names these as the two problems of two-phase flow-map animation (3.1).
|
||||
- Stretching: a texture advected by a non-uniform flow stretches without bound unless regenerated (Neyret, 3.3).
|
||||
- 4x4 update-pattern artifacts (Toft et al., 1.5); latency and leaking over opaque geometry in 1/16 modes (UE forum, 1.6).
|
||||
- Camera-only reprojection fails when content moves: https://www.wayline.io/blog/altos-1-1-better-temporal-reprojection-for-volumetric-clouds-in-unity ("When the player moves their camera, the history becomes useless") [secondary]. A hobby engine skips a velocity buffer "since the clouds move slowly", uses bilinear history sampling and EMA: https://handmade.network/p/75/monter/blog/p/7201-engine_optimization__fast_high-quality_cloudscape_through_noise_and_reprojection [secondary].
|
||||
|
||||
## 2. Temporal reprojection of raymarched volumes whose density moves
|
||||
|
||||
- A volume has no single surface depth. Nubis 2022 writes a per-pixel `cloud_distance` during the march, rebuilds a world point from it, applies the density motion law to that point, then projects with the previous view-projection (1.2). This is the only shipped density-motion-aware reprojection found at a primary source.
|
||||
- Horizon 2015 and Frostbite 2016 reproject with camera properties only (1.1, 1.4).
|
||||
- AtmoPlugin (hobby project, GitHub PR #15): raymarch writes a "contribution-weighted" reprojection depth into alpha, taken where opacity exceeds 5%, with fallbacks surface depth, scene depth, mid-atmosphere, infinity; temporal pass reprojects the pixel center with previous camera and planet transforms: https://github.com/Upa2sdo2l/AtmoPlugin/pull/15 [secondary].
|
||||
- Two hybrid depth-of-field papers reproject semi-transparent regions by average world position of far-field hits, with a 3x3 neighborhood fallback when there is no hit, and lerp by latest hit ratio during motion to reduce ghosting: https://arxiv.org/pdf/2210.04981 and https://arxiv.org/pdf/2210.06158 [secondary].
|
||||
- A search for a published "transmittance-weighted depth" reprojection for clouds returned nothing; it is not an established term in anything reached.
|
||||
- Reported costs and artefacts of history: higher history weight = more ghosting for moving volumes (Godot, 1.4); EMA "stable in static scenes but performs poorly under dynamic volumetric lighting", and variance clipping plus low-discrepancy jitter tested on froxel data, implemented in Frostbite (a thesis at https://gupea.ub.gu.se/items/e05613c2-e1d4-48b0-b41e-c32f4b0ecbae, blocked by bot protection; only a search summary was seen) [secondary]; UE TSR trailing when no depth/velocity is written (1.6).
|
||||
- Playdead's TAA (Pedersen, GDC 2016) is the history-reprojection reference cited by both Frostbite and Toft et al.
|
||||
|
||||
## 3. Flow maps, curl noise, advected textures
|
||||
|
||||
### 3.1 Flow maps, Vlachos, Valve, SIGGRAPH 2010 "Water Flow in Portal 2"
|
||||
Source: https://www.advances.realtimerendering.com/s2010/Vlachos-Waterflow(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course).pdf (read)
|
||||
- Builds on Max and Becker 1995 "Flow visualization using moving textures": offset the UV by the 2D flow vector scaled by time; "distortion looks reasonable for the first 1/3 of uv space".
|
||||
- "Blend the short animated segment in two layers. Each layer is offset half a phase so we can hide the restart for each layer."
|
||||
- Two named problems: repetition ("the same normals will flow through the same point") and pulsing ("the surface appears to pulse in a repeating pattern"). Repetition solved by offsetting the second layer's sample; pulsing solved by a noise texture that offsets phase per pixel. Cost vs two scrolling normal maps: +2 texture fetches, +21 ALU instructions.
|
||||
- Colors (Portal 2): distortion interval offset to -fraction..+fraction so the peak is at rest, favouring less distortion than for normals. Listed future work: dynamic per-frame flow vectors.
|
||||
- Catlike Coding (https://catlikecoding.com/unity/tutorials/flow/texture-distortion/) [secondary]: p=frac(time), weight `1 - |1 - 2p|`, second pattern shifted half a period so weights sum to 1; Perlin noise in the flow map's A channel added to time staggers the pulse as a wave; per-phase "jump" vector times floor(time) shifts UVs only while weight is zero, limited to about +/-0.25, loop length is LCM of jump denominators; strong deformation makes compressed flow maps blocky; large-area flow maps have low effective resolution.
|
||||
- Golus flow-map notes: [not verified].
|
||||
|
||||
### 3.2 Curl noise, Bridson, Hourihan, Nordenstam, TOG 26(3) 2007
|
||||
Source: https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph2007-curlnoise.pdf (read, 3 pages)
|
||||
- Velocity = curl of a noise-valued potential (2D scalar potential rotated 90 degrees; 3D three noise functions, "the same noise function evaluated at large offsets"). Divergence-free by construction: "No sources or sinks ('gutters')"; contrasted with plain noise as velocity, which "will generally contain many sinks" where particles accumulate.
|
||||
- Derivatives by finite difference (displacement 10^-4 of the domain "works fine in single precision"). With noise at length scale L: vortices of diameter about L, speeds O(1/L). Several octaves with power-law amplitude falloff resemble Kolmogorov turbulence.
|
||||
- Time variation: "achieved very simply by using time-varying noise"; pseudo-advection of Flow Noise mentioned as untried.
|
||||
- Boundaries: potential multiplied by smooth ramp of distance to solids, `ramp(r) = 15/8 r - 10/8 r^3 + 3/8 r^5`.
|
||||
- The paper names particle advection as a primary use. It says Perlin and Neyret's Flow Noise textures "cannot be used for moving particle systems".
|
||||
- Perlin and Neyret, "Flow Noise" (SIGGRAPH 2001 sketch): rotating gradients over time plus pseudo-advection of small octaves by large ones; needs noise with analytic derivatives. https://evasion.inrialpes.fr/~Fabrice.Neyret/flownoise/index.gb.html ; Gustavson's simplex noise code exposes the derivative and a rotation parameter `t` for it (https://www.itn.liu.se/~stegu76/simplexnoise/DSOnoises.html) [secondary].
|
||||
|
||||
### 3.3 Advected textures, Neyret, SCA 2003
|
||||
Source: https://evasion.inrialpes.fr/Publications/2003/Ney03/neyret161.pdf (read)
|
||||
- A noise texture advected by a flow stretches; periodic regeneration prevents stretch from growing on steady flows, but the regeneration rate (latency) is hard to tune and blending regenerated layers causes ghosting and contrast loss.
|
||||
- Method: accumulate local deformation (time integral of the strain-tensor norm) per layer since its last regeneration; blend layers of different latency by a criterion on that accumulated deformation against a user target (analogous to MIP-mapping); each layer blends three phase-shifted regenerated textures; proposes a procedural-texture blending scheme that avoids contrast loss.
|
||||
- Needs a velocity field to compute deformation; the paper's flow came from a low-resolution fluid simulation.
|
||||
|
||||
### 3.4 Does a span / Lipschitz bound survive advection or a time-dependent warp
|
||||
No source found states it. What was read:
|
||||
- Seyb, Jacobson, Nowrouzezahrai, Jarosz, "Non-linear sphere tracing for rendering deformed signed distance fields", TOG 38(6) 2019 (PDF read): stepping in deformed space needs the Lipschitz constant of S o D^-1; "for affine transformations this is trivial [Stander and Hart 1994], but for more complicated deformations Lipschitz constants can be tedious or even impossible to compute analytically"; a global constant degrades step size where it is large; local constants "require a non-negligible computation whenever the deformation changes, i.e., every frame". Their remedy: trace in undeformed space as an ODE, avoiding the inverse deformation. https://par.nsf.gov/servlets/purl/10172295
|
||||
- Galin et al., "Segment Tracing Using Local Lipschitz Bounds", CGF 39(2) 2020: local Lipschitz bounds per ray segment, bounds derived per operator and primitive (abstract/search summary only) https://diglib.eg.org/handle/10.1111/cgf13951 [secondary].
|
||||
- Bridson 2007: speed of a noise-derived velocity is O(1/L) per octave (informal amplitude bound).
|
||||
- Neyret 2003: the quantity that grows under advection is the time-integrated strain-tensor norm.
|
||||
- A bound on the time derivative of a warped field, or a space-time Lipschitz constant: [not found].
|
||||
|
||||
## 4. Particle layers coherent with a volumetric field
|
||||
- No shipped system found that drives volumetric density and a particle layer from one stated shared motion law.
|
||||
- Niagara Curl Noise Force: reads a pre-baked, medium-resolution tiling curl-noise field by default, or computes a Perlin-derived curl at higher cost; docs do not state the divergence-free property: https://dev.epicgames.com/documentation/unreal-engine/particle-spawn-group-reference-for-niagara-effects-in-unreal-engine [secondary].
|
||||
- Frostbite 2015 couples particles to fog by voxelizing particle extinction into the same froxel volume, so they are lit together (shared volume, not shared velocity): https://www.ea.com/news/physically-based-unified-volumetric-rendering-in-frostbite [secondary].
|
||||
- Flax: volume particles write into the fog's 3D textures: https://docs.flaxengine.com/manual/graphics/fog-effects/volumetric-fog.html [secondary]. Godot demo of particle-faked fog animated with wind/attractors; "billboards make the fog appear to rotate with the camera": https://github.com/Bauxitedev/volumetric-fog-demo [secondary].
|
||||
- Bridson's curl noise is built for particle advection and the divergence-free property is its stated reason (3.2).
|
||||
- Nubis 2022 / Nubis3 slides read carry no particle layer sharing the density motion law.
|
||||
|
||||
## 5. Not reached
|
||||
RDR2 (Bauer 2019) slides; Hillaire 2015 temporal section; Golus flow-map notes; any Shadertoy nebula source; UE internals beyond forum reports; any source bounding the time derivative of a warped noise field.
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
# Inverse procedural modelling: describing messy meshes as programs
|
||||
|
||||
Eyes pass, 2026-10-10. Operator: "What if the model had some trash geo and its goal is to describe it parametrically?" Context: Tripo hard-surface hulls; CultMath intervals and affine forms on main, tape/brush grammar parked (docs/cultmath-tape-target.md); the earlier GameCult/VibeGeometry attempt (2026-05). Facts only; tags mark FETCHED, SEARCH (search summary only) and KNOWLEDGE (unverified).
|
||||
|
||||
# Eyes facts: inverse procedural modelling (mesh -> parametric program)
|
||||
Date 2026-10-10. Tags: FETCHED (page/file read this session), SEARCH (search-result digest only), KNOWLEDGE (unverified recall).
|
||||
Search digests come from a small model; SEARCH numbers are not paper-verified.
|
||||
|
||||
## 0. What exists in GameCult
|
||||
- FETCHED F:\Projects\CultLib\docs\cultmath-tape-target.md: steps 2-5 (tape, pruning, HLSL interpreter, meshing) PARKED (2026-10-02,
|
||||
ruling tape-target-unparks-when-asura-stable). Step 1 intervals + affine forms are on main (docs/cultmath-interval-ground-cut.md).
|
||||
The "grammar of nested brushes" is operator intent only (quote 2026-10-02: space station from "a grammar of nested brushes");
|
||||
doc says the station project is "a separate project with its own target". Chosen shape (c): port Fidget ideas into CultLib (Fidget =
|
||||
MPL-2.0 Rust, v0.5.1). Meshing plan: surface nets + Asura tile pass (Newton refinement onto f=0, crease snapping from field gradients);
|
||||
"Thin authored bevels can be shaded by their analytic normals". Tape gradients "reuse CultMath's analytic-gradient primitives".
|
||||
- FETCHED grep CultLib\docs for brush/tape: "brush" hits in cultmath-interval-ground-cut.md are Aetheria noise/gravity-well field brushes
|
||||
(PowerBrush (1-x^2)^16 etc.), not CSG brushes. No CSG brush grammar or tape code exists in CultLib; only the parked doc.
|
||||
- FETCHED (voidbot) separate repo GameCult/GameCult.Geometry.Csg ("CultGeometry CSG", Rust; local F:\Projects\GameCult.Geometry.Csg).
|
||||
Mesh-based convex plane-splitting CSG (not SDF), RealtimeCSG-shaped API: ordered Additive/Subtractive/Intersecting brush streams, boxes,
|
||||
oriented boxes, CylinderZ/DomeCapZ/FloretArm, dirty-frontier, OBJ output, plus domain-tree/selected-cut LOD schemas (src/cult_geometry).
|
||||
Doc text: "agents should revise intent by moving, adding, or renaming brushes, not by poking triangles"; "brush list is the durable
|
||||
design surface. The mesh is a cache."
|
||||
|
||||
## 0b. The earlier "vibe geometry" attempt
|
||||
- FETCHED voidbot gamecult-site GameCult/Projects/VibeGeometry.md: repo GameCult/VibeGeometry, started 2026-05-04, last public push
|
||||
2026-05-07; "agent-authored Blender scene generation: geometry nodes, Python builders, semantic graph contracts, render evidence, and
|
||||
the ongoing war against decorative node spaghetti". Org ledger: "Started as architecture and persistence scaffolding, then dived almost
|
||||
immediately into CSG grammar, parity fixtures, native bridge probing, mesh generation, and performance work."
|
||||
- FETCHED local git F:\Projects\VibeGeometry-cultgeometry-rename (93 commits). Commit 09bff8c (2026-05-29, "Focus VibeGeometry on vg-csg")
|
||||
deleted the scene-generation tree; recoverable at 09bff8c^: docs/research/procedural-doctrine.md (~1000-line "spatial reasoning
|
||||
playbook"), dream-machine-grammar.md, geonodes-translation-corpus.md, ircss-french-houses-* (translations of Blender node graphs into
|
||||
Geometry Script Python), builds/{aetheria-bloom-habitat, lucent-tether-habitat, dream-machine-level, industrial-maintenance-loop}.md,
|
||||
notes/architecture-rationale.md, a "reusable procedural helper library" (aa4d49d). The DSL was Blender Python via Geometry Script
|
||||
(fork F:\Projects\geometry-script; upstream PR carson-katri/geometry-script#69 linked by Metacrat 2026-05-04) emitting geometry-node
|
||||
graphs, plus the Rust brush DSL (LevelDsl: solid_box, cut_box, solid_oriented_box, dome_cap_z, floret_arm).
|
||||
- FETCHED notes/architecture-rationale.md @09bff8c^, section "Core Failure": "The central failure mode is local plausibility without global
|
||||
coherence. An agent can write plausible Blender Python, create a large node graph, and still lose the visual machine ... the script
|
||||
runs, the graph exists, the render has objects in it, and somehow the target has been replaced by decorative procedural fog".
|
||||
"Seductive proxies": valid Python, non-empty renders, high node counts, pretty materials, parameter sliders.
|
||||
procedural-doctrine.md: "Ornament in the wrong frame is not detail. It is a confident lie". Commit titles record frame errors:
|
||||
"Correct Bloom terrace slum orientation", "Correct Lucent dome spin-gravity frame", "Move Lucent ray florets outside dome".
|
||||
- FETCHED VibeGeometry README: "failed experiments are useful without being allowed to hold a steering wheel"; old mission "deleted
|
||||
from the live tree"; live mission narrowed to runtime CSG (domain tree -> LOD CSG -> selected cut -> triangles).
|
||||
- NOT FOUND: no indexed operator or agent statement that models "weren't good enough at spatial reasoning to use the DSL". voidbot
|
||||
search_history (Discord) returned only unrelated hits. The repo docs above are the only failure statements in project words
|
||||
(coherence and frame errors; the phrase "spatial reasoning" appears only in the playbook title and its body).
|
||||
|
||||
## 1. Inverse CSG / shape-program synthesis
|
||||
Most train or test on DeepCAD / ShapeNet / ABC / Infinigen; none retrieved was evaluated on noisy AI-generated hard-surface meshes.
|
||||
- InverseCSG (Du et al., SIGGRAPH Asia 2018, MIT). SEARCH: mesh/point samples -> CSG program; geometric processing (RANSAC-style primitive
|
||||
detection) turns the mixed discrete/continuous problem into a purely discrete one; careful point samples guide a discrete search;
|
||||
divide-and-conquer segmentation; program synthesis (solver) per segment. Claims >100 primitive parts, compact programs, beats prior
|
||||
methods on compactness/runtime. D2CSG authors (SEARCH) criticise it: RANSAC needs tuning, noisy initial primitives, long search.
|
||||
Project page inversecsg.csail.mit.edu lists code; licence not checked.
|
||||
- CSGNet (Sharma 2018): KNOWLEDGE only; supervised, voxel/2D -> CSG sequence.
|
||||
- UCSG-Net (Kania, NeurIPS 2020). SEARCH: unsupervised; predicts box/sphere params; differentiable indicator on SDF; several CSG layers with
|
||||
soft operator choice; structure and params learned jointly by gradient descent; autoencoding scope (2D/3D), no mesh-to-program at test
|
||||
for unseen exotic shapes beyond the trained net.
|
||||
- CAPRI-Net (CVPR 2022). SEARCH: axis-aligned quadric primitives, fixed 3-layer assembly; ABC point-cloud Chamfer 0.09 vs UCSG 1.43; ~5.3 vs
|
||||
12.92 convex shapes. D2CSG (2023), CSG-Stump (2021): SEARCH, unsupervised compact trees. SECAD-Net (CVPR 2023): SEARCH, sketch-extrude,
|
||||
editable; CD 2.94/4.20 (ABC/Fusion) vs UCSG 3.14/4.45.
|
||||
- Point2CAD (Liu et al.): SEARCH only that a repo exists (jofcodeusal/point2cad mirror, Docker toshas/point2cad); method not retrieved.
|
||||
Point2Cyl (CVPR 2022): SEARCH, supervised net to extrusion cylinders. DeepCAD (Wu 2021): KNOWLEDGE, ~178k sketch-extrude sequences.
|
||||
- ShapeAssembly (Jones 2020, TOG): SEARCH; programs of cuboid proxies attached to each other with free parameters; extraction from PartNet.
|
||||
ShapeMOD (2021): SEARCH; discovers macros across a corpus minimising calls and free parameters. Repos/licences not found by search
|
||||
(KNOWLEDGE: github rkjones4/ShapeAssembly exists; unverified).
|
||||
- CAD-Recode (Rukhovich et al., ICCV 2025, arXiv 2412.14042). FETCHED abstract: point cloud -> CadQuery Python (sketch-extrude); point-cloud
|
||||
projector + small pretrained LLM decoder; trained on 1M procedurally generated CAD sequences; eval DeepCAD, Fusion360, CC3D (real scans);
|
||||
output usable by off-the-shelf LLMs for editing and QA. SEARCH (unverified): Qwen 0.5B/1.5B decoders; DeepCAD mean CD 3.43 -> 0.30, IoU
|
||||
77.6 -> 92.0 vs CAD-SIGNet; test time: 10 sampled candidates executed, min-Chamfer kept. Code github.com/filaPro/cad-recode (SEARCH);
|
||||
HF dataset filapro/cad-recode is CC-BY-NC-4.0 (SEARCH); repo licence unchecked. Structure by autoregressive decoding, params as literal
|
||||
tokens, re-ranking by Chamfer. Abstract says nothing on fillets/chamfers.
|
||||
- MeshCoder (Dai et al., NeurIPS 2025, arXiv 2508.14879). FETCHED: 16,384-point cloud -> triplane tokenizer -> Llama-3.2-1B (LoRA) ->
|
||||
Blender Python. API: cube/cylinder/UV-sphere/cone/torus, Translation (sweep 2D section along 3D trajectory), Bridge Loop, Boolean
|
||||
(union/intersect/diff), Array, Fill Grid, Spoon, Fork. Data: ~10M part-code pairs, ~1M object-code pairs, 41 categories (Infinigen
|
||||
Indoor). Table 2 averages CD x1e-2 / IoU%: Shape2Prog 6.00/45.0, PLAD 1.87/67.6, MeshCoder 0.06/86.75. Worst: Fork 58.9 IoU, CeilingLight
|
||||
65.8, Array parts 78.9. Limits: human-made objects only; geometry only; training objects kept only if every part CD < 5e-3. LLM does not
|
||||
edit; edits are manual param changes; GPT-4o used only for QA on the code. Output is part-segmented. Licence not stated in paper.
|
||||
- CADCodeVerify (Alrashedy, ICLR 2025, arXiv 2410.05340). SEARCH: text prompt -> CadQuery (GPT-4); VLM writes/answers validation questions
|
||||
about renders and refines; CADPrompt benchmark (200 objects); GPT-4 point-cloud distance -7.30%, compile rate +5.5% (v2; v1 said 5.0%
|
||||
success). Target is a prompt, not a mesh.
|
||||
- Newer render-loop agents (SEARCH): SEIG "Thinking in Blender" (arXiv 2606.02580): single reference image -> staged Blender code (geometry,
|
||||
materials, composition, lighting), VLM generator + verifier per stage; FETCHED abstract gives no numbers, no code link. LL3M (2508.08228):
|
||||
text -> Blender with critic reading renders. ShapeCraft (2510.17603): renders bbox/part/global views as feedback. SceneCraft (2403.01248):
|
||||
render-critique-revise. Not found by search: "3D-Premise", "LLMto3D", Img2CAD/Text2CAD specifics.
|
||||
|
||||
## 2. Classical primitive fitting as a front end
|
||||
- Efficient RANSAC (Schnabel/Wahl/Klein, CGF 2007): SEARCH/FETCHED page text: planes, spheres, cylinders, cones, tori in unorganised clouds;
|
||||
multi-million-point sets "within less than a minute". CGAL Shape Detection implements it plus Region Growing (Lafarge&Mallet). CGAL is
|
||||
dual GPL/LGPL + commercial; the Shape Detection package's own licence NOT confirmed (digest guessed GPL).
|
||||
- GlobFit (Li/Wu/Chrysanthou/Sharf/Cohen-Or/Mitra, SIGGRAPH 2011): SEARCH; starts from RANSAC primitives, discovers global relations
|
||||
(orientation, placement, equality; symmetry) and enforces them by constrained optimisation; assumes man-made, repeated, globally aligned
|
||||
parts. Code: archived code.google.com/archive/p/globfit, "research use only" wording.
|
||||
- Mitra 2006 symmetry detection, SPFN (2019): KNOWLEDGE/SEARCH only. ParSeNet (2020): SEARCH; B-spline patch decomposition, beats RANSAC
|
||||
and SPFN baselines. No repos/licences retrieved.
|
||||
- What fits supply as data: exact primitive parameters with inlier sets and residuals, segment labels, relation graph (GlobFit).
|
||||
InverseCSG uses the primitives to make the problem discrete. Recorded weakness: noisy primitives, parameter tuning (SEARCH).
|
||||
|
||||
## 3. Differentiable fitting of SDF/CSG programs
|
||||
- UCSG-Net (above): soft operator selection; structure learned jointly.
|
||||
- Fuzzy Boolean differentiable CSG (dgp.toronto.edu/~hsuehtil/pdf/fuzzyBoolean.pdf): SEARCH; gradient descent over boolean and primitive
|
||||
parameters; notes prior work pre-fixes tree structure and operators and optimises only primitive params; replaces min/max with a unified
|
||||
fuzzy operator so the operation type is optimised. Costs listed: analytic SDF per primitive type, per-iteration SDF evaluation, image
|
||||
losses need differentiable sphere tracing.
|
||||
- DiffCSG (arXiv 2409.01421): SEARCH; differentiable rasterisation of mesh CSG, image-space gradients to primitive params; image-guided
|
||||
editing, not mesh fitting.
|
||||
- ResFit, Residual Primitive Fitting with SuperFrusta (arXiv 2512.09201): SEARCH; one analytic SDF primitive with 8 params (dilation, taper,
|
||||
bulge, onion hollowing, profile roundness, axial scaling); alternates global shape analysis and local primitive optimisation; code "to be
|
||||
open-sourced upon acceptance".
|
||||
- Evolutionary mesh -> CSG abstraction (SEARCH; authors not retrieved): fast, coarse. Sigmoid-over-SDF soft occupancy with alternating
|
||||
add-primitive / joint-refine loop (SEARCH, primitive-scene paper).
|
||||
- Not found: a published gradient fit of an SDF program with bevel/round parameters to a messy hard-surface mesh.
|
||||
- Handling of discrete structure across sources: soft selection (UCSG, CSG-Stump, fuzzy Boolean); pre-fixed structure (InverseCSG after
|
||||
RANSAC; CAPRI-Net fixed layers); greedy add/prune (ResFit); neural token decoding (CAD-Recode, MeshCoder).
|
||||
|
||||
## 4. LLM spatial reasoning in 3D code generation
|
||||
- BlenderGym (Gu et al., CVPR 2025, arXiv 2504.01786). FETCHED: 245 start-goal Blender scene pairs, five tasks (placement, lighting,
|
||||
procedural material, blend shape, procedural geometry); the VLM sees start and goal and edits Blender code (target-matching setting).
|
||||
Table 1 best-VLM vs human: geometry Chamfer 1.120 (GPT-4-Turbo) vs 0.334; geometry photometric loss 6.747 vs 1.269; placement CD 8.324 vs
|
||||
1.532; blend shape CD 0.904 vs 0.399. Paper: VLMs "particularly poorly in procedural material and geometry editing"; all open-source
|
||||
models except MiniCPM failed to produce executable code on >75% of geometry instances. Verifier agreement with humans: human 0.79,
|
||||
Claude 3.5 Sonnet 0.66, random 0.5. Scaling a small verifier (InternVL2-8B, k 1 -> 64 over 32 candidates) beat unscaled GPT-4o and
|
||||
Claude 3.5 on blend shape. Low compute: less verification share (0.33); high compute: more (0.62-0.73). Models are 2024-25 era. Code
|
||||
licence not in retrieved text.
|
||||
- BenchCAD (arXiv 2605.10865): SEARCH; 17,900 CadQuery programs, 106 part families; frontier models recover coarse outer geometry but fail
|
||||
faithful parametric programs; sweeps/lofts/twist-extrudes replaced by sketch-extrude; single-literal errors (5.6 vs 5.85) at IoU >= 0.9.
|
||||
BenchCAD-Edit: text instruction sets the upper bound; the image is "at best a clarifier and at worst a distractor" for thinking models.
|
||||
- Text2CAD-Bench (2605.18430): SEARCH; execution failures even at easy level; sweep/loft/shell widely fail; executing != correct.
|
||||
- Text-to-B-Rep paper (2603.11831): SEARCH; spatial misalignment of primitives, omitted fillets, geometry contradicting described layout;
|
||||
authors blame weak precise 3D spatial reasoning.
|
||||
- "Foundation Models for Automatic CAD Generation" (2607.05573): SEARCH, single source; numeric analytic feedback peaked at round 1; VLM
|
||||
critique of renders kept improving in rounds 2-3 for DeepSeek/Qwen; VLM critics struggle on rotationally symmetric geometry.
|
||||
- Multi-view conditioning beat single-view and sketch on Chamfer/F-score in a B-Rep paper (SEARCH, not CadQuery).
|
||||
- Target vs description: BlenderGym gives a target and still shows a large human gap; CAD-Recode and MeshCoder avoid prompting by training
|
||||
point-cloud encoders; no controlled prompted-LLM study of target-vs-description was found.
|
||||
- MindCube spatial study (SEARCH): errors 20% final reasoning step, 13% 3D reconstruction, 6% generated programs.
|
||||
|
||||
## 5. Hard-surface specifics
|
||||
- SDF rounding (Quilez distfunctions, SEARCH): sdRoundBox = box SDF minus r, an iso-offset so the shape grows by r. Polynomial smin with k
|
||||
as blend band; Quilez marks smin as "bound" not exact. Chamfer/fillet operators not retrieved; KNOWLEDGE: hg_sdf (Mercury) has
|
||||
fOpUnionChamfer / fOpUnionRound / fOpUnionStairs (unverified here).
|
||||
- MeshCoder (FETCHED): no fillet/chamfer primitive; booleans, sweeps, bridge-loop, arrays only.
|
||||
- BenchCAD / text-to-B-Rep (SEARCH): omitted fillets/refinement features named as failure; sweeps/lofts degrade to extrudes.
|
||||
- CAD-Recode/DeepCAD data are sketch-extrude (SEARCH/KNOWLEDGE); fillet/chamfer not covered in what was retrieved.
|
||||
- InverseCSG output: boolean of primitives, no blends (KNOWLEDGE). CAPRI-Net quadrics: smooth surface in one primitive (SEARCH). ResFit
|
||||
primitive has a profile-roundness parameter (SEARCH).
|
||||
|
||||
## Gaps
|
||||
- Operator/agent words on why VibeGeometry failed: not indexed (see 0b).
|
||||
- Not read at body level: CAD-Recode fillet handling, InverseCSG experiments, GlobFit relation list, SEIG numbers, ResFit results.
|
||||
- Licences unverified: InverseCSG, ShapeAssembly, CAD-Recode repo, MeshCoder, GlobFit, CGAL Shape Detection, Point2CAD.
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# Facts for `cultmath-tapes:question:lattice-site-weight-source`
|
||||
|
||||
Agent eyes-lattice-site-weight, session self-2026-10-14-eureka, 2026-10-14. Facts only, no recommendation.
|
||||
Marks: [src] source code, [doc] repo doc, [web] web page, [abs] absence found, [probe] measured by the scripts beside this file.
|
||||
The eureka-state view of the question id failed (`view` wants a PipelineRef, not the id string); the question text was taken from the brief.
|
||||
|
||||
## 1. What the fauna probe did (Aetheria 4354ef2c, read via `git show`)
|
||||
|
||||
- [src] `docs/research/2026-10-14-trophic-dag-probe/trophic_probe.py` `lattice_web` (lines 95-119): per lattice cell `(cx,cz)` in a (2*WIN+1)^2 window (WIN=2): jittered site `(c+.5)*LAT` plus a drifting offset; weight `wt = (hash*.5+.5) * (1 + BOOST*prox)`, `prox = max over wells of exp(-(|site - well| / (2.2*.103*r))^2)`, BOOST 1.6; `d = hypot(...) * wt`; F1, F2 and a polynomial smooth-min `m` accumulated in loop order; output `max(soft^2, exp(-((f2-f1)/.10)^2))`. Wells orbit with zone time, so the site weight is a function of time.
|
||||
- [src] The weight is read at the site's lattice position `(cx+.5)*LAT`, not at the query point and not at the drifted site.
|
||||
- [doc] `docs/aetheria-fauna-map.md` B34: largest presence step for `lattice` .124 (8 steps over .1); "not discontinuities: a traced member's web value rises .03 per step for 5 steps, a thin weighted border sweeping a fixed candidate"; window 7x7 changes nothing; boost .6 removes the steps (.047); reading the lattice drift at the member's cycle start removes them at full boost (.011, `cyc.log`).
|
||||
- [doc] Map r4 gap (1): "a 2D weighted lattice cellular with the smooth-min blend (her node) on `pcg3d` cells and zone time"; "The search window follows the weight range ... The CultMath node states the weight range it supports" (map lines 438, 444-446).
|
||||
- [doc] Prior art doc `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md` 1.1.7 [M]: power diagram evaluated pointwise by the same neighbour search "only if the search window is widened with the weight range"; marked "Neither derivation checked against a source".
|
||||
|
||||
## 2. Literature and implementations
|
||||
|
||||
- [web] Wikipedia, Weighted Voronoi diagram (https://en.wikipedia.org/wiki/Weighted_Voronoi_diagram): multiplicative = distance divided by positive site weight (circular Dirichlet tessellation, edges circular arcs and line segments); additive = weight subtracted from distance (hyperbolic Dirichlet tessellation, hyperbola arcs and segments); power diagram = weight subtracted from squared distance. A multiplicatively weighted cell may be non-convex, disconnected or hold holes. The page says nothing on continuity.
|
||||
- [web] Sangil Lee, "Understanding the Variations of Cellular Noise" (https://sangillee.com/_posts/2025-04-18-Cellular-noises/): weighted Voronoi as effective distance `(1/w)*|x - seed|` with `w = 0.2 + 0.8 * random3(cell).z`, i.e. the per-site weight is a component of the hash of the lattice cell id, computed inside the noise function; the weighted example loops 5x5 instead of 3x3 (no reason given).
|
||||
- [web] Inigo Quilez, Voronoise (https://iquilezles.org/articles/voronoise/): `hash3(cell id)` yields jitter in `.xy` and a per-cell value in `.z`; 5x5 window; notes `min()` is discontinuous and the smooth blend fixes that. No weighted variant.
|
||||
- [web] Worley noise, Wikipedia and The Book of Shaders (https://thebookofshaders.com/12/): the standard is current cell plus neighbours (3x3), with a wider pass for exact borders; Worley's original used a variable number of points per cell. Nothing found on per-point weight attributes beyond the Lee article; the continuity guidance in the search results was general-knowledge reasoning, not sourced.
|
||||
- [abs] No source found stating a continuity theorem for weighted Worley; no source found for a node that takes its per-site weight from an external field function. The only per-site weight mechanism found in shader writing is the hash of the cell id.
|
||||
- [probe] Continuity as observed (section 4): F1 stays continuous as long as the window contains the true winner; failure occurs when it does not.
|
||||
|
||||
## 3. CultMath today
|
||||
|
||||
- [src] `F:\Projects\CultLib\packages\cultmath\src\CultMath\math.cs:909` `public static CultCellular cellular(float3 p)`: one argument, 3D, unweighted; sites `neighbor + jitter` with jitter from `pcg3d(int3(neighbor))` through `cellular_unit`; fixed 5x5x5 window with a per-offset lower-bound prune (`max(0,|d|-1)`), whose exactness proof (comment above the function) relies on the jitter being in [0,1) and on equal weights; returns `CultCellular { nearest = (grad F1, F1), edge = (grad(F2-F1), F2-F1), id = pcg4d of the winning cell mapped to [0,1) }` (`CultCellular.cs`). Hash is of the integer cell, never the float bits.
|
||||
- [src] `math.cs:831-847` `smin_grad(float4 a, float4 b, float k)`: quadratic smooth-min with analytic gradient (value-and-gradient form).
|
||||
- [src] `Voronoi.cs` (`Voronoi.SampleTones`, native `cultmath_apollonian_voronoi_tones`) is a UI-tone colour sampler over a hash-jittered field with a hard-coded 5x5 loop; takes x, y, tone, span; not a weighted-cell function.
|
||||
- [abs] No function in CultMath takes a weight, a callback, or a site span; no weighted cellular, no per-site hash output exposed besides `id`. (Map B28 states the same.) HLSL twin at `shaders/CultMath.hlsl:440-503`.
|
||||
- [doc] `packages/cultmath/docs/design.md` ~265-340: `cellular` section, search-radius proof and pruned/unpruned test pin.
|
||||
- Fact for composition: `CultCellular.id` is already a per-cell hash scalar available to the caller after the call, but only for the winning cell, after F1/F2 are decided.
|
||||
|
||||
## 4. Probe: where does the weight come from, 2D, 4 orbiting wells
|
||||
|
||||
Scripts: `siteweight_probe.py`, `siteweight_probe2.py`; raw numbers `probe_results.json`, `probe_results2.json`. numpy, scratch; hash is a lowbias32-style integer hash (stand-in for pcg3d). Units: lattice cell = 1; domain [1,7]^2, 500x500 grid (600 for shape rows); wells with radii .6-1.2 cells orbiting a centre; weight `wt = base(.5..1) * (1 + B*prox)`, B = 1.6, WIN = 2, the fauna probe's smooth-min and border output.
|
||||
|
||||
Correction (2026-10-14, from the Aetheria Self's body-Voronoi probe, Aetheria 05a44541): the fauna probe's `trophic_probe.py` weights at the cell centre `(cx+.5)*LAT` (section 1 above), which is the `cellc` variant below, not `site`. An earlier label here called `site` the fauna probe's method.
|
||||
|
||||
Variants: `base` (no field); `site` (field at the jittered site position); `qpt` (field at the query point, same factor on every candidate); `cellc` (node calls the field at the integer cell centre, `cell + .5`, no jitter); `site_t0` (as `site` but field read at t=0 while wells have moved); `warp` (extra: query scaled `q*(1+B*prox(q))`).
|
||||
|
||||
### 4a. Winner identity, fraction of grid pixels whose winning cell differs (t=0 / t=8)
|
||||
|
||||
| variant | vs `site` | vs `base` |
|
||||
|---|---|---|
|
||||
| base | .147 / .156 | 0 / 0 |
|
||||
| qpt | .147 / .156 | 0 / 0 (identical argmin to base) |
|
||||
| cellc | .127 / .114 | .185 / .147 |
|
||||
| site_t0 | 0 / .226 | .147 / .147 |
|
||||
| warp | .675 / .646 | .668 / .639 |
|
||||
| site | 0 / 0 | .147 / .156 |
|
||||
|
||||
Mean absolute difference of the border output vs `site`: base .169/.186, qpt .168/.180, cellc .164/.156, site_t0 0/.234, warp .310/.280. Number of distinct cells on the grid: 53 for base, site and qpt; 54 cellc; 143/128 warp.
|
||||
|
||||
[probe] `qpt` leaves every winner unchanged because the factor is common to all candidates; it changes F1, F2 and the smooth-min scale, which moves the border line (border pixel fraction near wells .164 vs .300 for base, far .250 vs .281).
|
||||
|
||||
### 4b. Cell size and border near wells (pixels per cell, mean; "near" = within 1 well radius), t=0
|
||||
|
||||
| | near | far | border fraction near / far |
|
||||
|---|---|---|---|
|
||||
| base | 10951 | 10515 | .300 / .281 |
|
||||
| site | 9672 | 11439 | .270 / .253 |
|
||||
| qpt | 10951 | 10515 | .164 / .250 |
|
||||
| cellc | 11562 | 12274 | .274 / .262 |
|
||||
| warp | 3447 | 8566 | .291 / .302 |
|
||||
|
||||
### 4c. Pop measure, dense spatial line (6001 points over 6 cells, step 1e-3 cell), max |step| of F1 and of output, window sweep vs window 6 reference
|
||||
|
||||
- With the true winner inside the window, every variant's max F1 step is small (site .0010-.0033, qpt .0019-.0079, cellc .0009-.0041, warp .0054-.022, base .0007-.0013 per 1e-3).
|
||||
- WIN=1 (3x3): `site` F1 error vs window 6 up to .174 (diagonal line, B=1.6; max output step .95); `cellc` .155-.194; `base` .0065; `qpt` .0092-.0113; `warp` none seen. At WIN=2 and B=1.6 all variants' F1 error is 0.
|
||||
- Weight range: `site` at B=6 (weight ratio about 12:1), t=8, horizontal line: WIN=2 error .0169 (output step .371); WIN=3 error 0.
|
||||
- Note: even `base` (weights .5-1, ratio 2) shows F1 error .0065 at WIN=1.
|
||||
|
||||
### 4d. Pop measure, time at fixed query points (wells move), step refinement (worst point from a dt=.01 scan, refined)
|
||||
|
||||
| variant | dt .01 max output step | dt 1e-3 | dt 1e-4 | dt 1e-5 | max F1 step at dt 1e-5 |
|
||||
|---|---|---|---|---|---|
|
||||
| site | .1083 | .01125 | .00113 | .000113 | 2.9e-6 |
|
||||
| qpt | .00461 | .000461 | 4.6e-5 | 4.6e-6 | 4.1e-6 |
|
||||
| cellc | .1060 | .0204 | .00206 | .000207 | 5.2e-6 |
|
||||
|
||||
[probe] All three steps scale linearly with dt: no discontinuity in time for any variant when the window holds the winner. Winner-id changes over t in [0,20] at 4 query points (dt .01): base 0, qpt 0, site 17, cellc 15, site_t0 0.
|
||||
|
||||
### 4e. Caveats of the probe
|
||||
Static smooth proximity field, 4 wells, 2D, one seed of the hash; 3 boost values in the pop table only B=1.6 and 6.0; no gradient-bearing outputs measured; `cellc` field is sampled at the cell centre, so it differs from `site` by the jitter distance; no visual images rendered, only the numeric measures above. The `warp` variant is an addition outside the three asked and is reported for completeness.
|
||||
|
|
@ -1,559 +0,0 @@
|
|||
{
|
||||
"grid": {
|
||||
"t=0.0": {
|
||||
"base": {
|
||||
"winner_differs_from_site": 0.14684,
|
||||
"winner_differs_from_base": 0.0,
|
||||
"out_absdiff_vs_site_mean": 0.1692938271424024,
|
||||
"out_absdiff_vs_site_max": 0.9999999999999667,
|
||||
"n_cells": 53
|
||||
},
|
||||
"site": {
|
||||
"winner_differs_from_site": 0.0,
|
||||
"winner_differs_from_base": 0.14684,
|
||||
"out_absdiff_vs_site_mean": 0.0,
|
||||
"out_absdiff_vs_site_max": 0.0,
|
||||
"n_cells": 53
|
||||
},
|
||||
"qpt": {
|
||||
"winner_differs_from_site": 0.14684,
|
||||
"winner_differs_from_base": 0.0,
|
||||
"out_absdiff_vs_site_mean": 0.1679024745847925,
|
||||
"out_absdiff_vs_site_max": 1.0,
|
||||
"n_cells": 53
|
||||
},
|
||||
"cellc": {
|
||||
"winner_differs_from_site": 0.126984,
|
||||
"winner_differs_from_base": 0.184648,
|
||||
"out_absdiff_vs_site_mean": 0.16449204766510442,
|
||||
"out_absdiff_vs_site_max": 0.99999757371696,
|
||||
"n_cells": 54
|
||||
},
|
||||
"warp": {
|
||||
"winner_differs_from_site": 0.6748,
|
||||
"winner_differs_from_base": 0.667952,
|
||||
"out_absdiff_vs_site_mean": 0.31037303493350155,
|
||||
"out_absdiff_vs_site_max": 1.0,
|
||||
"n_cells": 143
|
||||
},
|
||||
"site_t0": {
|
||||
"winner_differs_from_site": 0.0,
|
||||
"winner_differs_from_base": 0.14684,
|
||||
"out_absdiff_vs_site_mean": 0.0,
|
||||
"out_absdiff_vs_site_max": 0.0,
|
||||
"n_cells": 53
|
||||
},
|
||||
"site_window2_vs_window5_winner_differs": 0.0
|
||||
},
|
||||
"t=8.0": {
|
||||
"base": {
|
||||
"winner_differs_from_site": 0.155996,
|
||||
"winner_differs_from_base": 0.0,
|
||||
"out_absdiff_vs_site_mean": 0.18560227222422834,
|
||||
"out_absdiff_vs_site_max": 0.9999999025356109,
|
||||
"n_cells": 53
|
||||
},
|
||||
"site": {
|
||||
"winner_differs_from_site": 0.0,
|
||||
"winner_differs_from_base": 0.155996,
|
||||
"out_absdiff_vs_site_mean": 0.0,
|
||||
"out_absdiff_vs_site_max": 0.0,
|
||||
"n_cells": 53
|
||||
},
|
||||
"qpt": {
|
||||
"winner_differs_from_site": 0.155996,
|
||||
"winner_differs_from_base": 0.0,
|
||||
"out_absdiff_vs_site_mean": 0.17977625463968147,
|
||||
"out_absdiff_vs_site_max": 0.999999999999996,
|
||||
"n_cells": 53
|
||||
},
|
||||
"cellc": {
|
||||
"winner_differs_from_site": 0.114268,
|
||||
"winner_differs_from_base": 0.146552,
|
||||
"out_absdiff_vs_site_mean": 0.1556440909167879,
|
||||
"out_absdiff_vs_site_max": 0.9999919751523085,
|
||||
"n_cells": 54
|
||||
},
|
||||
"warp": {
|
||||
"winner_differs_from_site": 0.646396,
|
||||
"winner_differs_from_base": 0.639384,
|
||||
"out_absdiff_vs_site_mean": 0.2802002446118991,
|
||||
"out_absdiff_vs_site_max": 1.0,
|
||||
"n_cells": 128
|
||||
},
|
||||
"site_t0": {
|
||||
"winner_differs_from_site": 0.226044,
|
||||
"winner_differs_from_base": 0.14684,
|
||||
"out_absdiff_vs_site_mean": 0.23360220832359216,
|
||||
"out_absdiff_vs_site_max": 1.0,
|
||||
"n_cells": 53
|
||||
},
|
||||
"site_window2_vs_window5_winner_differs": 0.0
|
||||
}
|
||||
},
|
||||
"line_pop": {
|
||||
"horizontal z=2.6 (near well 0)": {
|
||||
"base t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.000679179751586656,
|
||||
"maxjump_out_per_step": 0.011527150729375713,
|
||||
"maxjump_f1_with_window6": 0.000679179751586656,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.000679179751586656,
|
||||
"maxjump_out_per_step": 0.011527150729375713,
|
||||
"maxjump_f1_with_window6": 0.000679179751586656,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0010050426622043585,
|
||||
"maxjump_out_per_step": 0.016002826669016756,
|
||||
"maxjump_f1_with_window6": 0.0010050426622043585,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.0010050426622043585,
|
||||
"maxjump_out_per_step": 0.016002826669016756,
|
||||
"maxjump_f1_with_window6": 0.0010050426622043585,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.001918579519211816,
|
||||
"maxjump_out_per_step": 0.03595224397909913,
|
||||
"maxjump_f1_with_window6": 0.001918579519211816,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.001918579519211816,
|
||||
"maxjump_out_per_step": 0.03595224397909913,
|
||||
"maxjump_f1_with_window6": 0.001918579519211816,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0019043178713147313,
|
||||
"maxjump_out_per_step": 0.016796471326461004,
|
||||
"maxjump_f1_with_window6": 0.0019043178713147313,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.0019043178713147313,
|
||||
"maxjump_out_per_step": 0.016796471326461004,
|
||||
"maxjump_f1_with_window6": 0.0019043178713147313,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0056772408954191,
|
||||
"maxjump_out_per_step": 0.04338667429132481,
|
||||
"maxjump_f1_with_window6": 0.0056772408954191,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0056772408954191,
|
||||
"maxjump_out_per_step": 0.04338667429132481,
|
||||
"maxjump_f1_with_window6": 0.0056772408954191,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.000899555983544964,
|
||||
"maxjump_out_per_step": 0.028931107704223913,
|
||||
"maxjump_f1_with_window6": 0.000899555983544964,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.000899555983544964,
|
||||
"maxjump_out_per_step": 0.028998803253634242,
|
||||
"maxjump_f1_with_window6": 0.000899555983544964,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.001589550701590614,
|
||||
"maxjump_out_per_step": 0.02212547196092063,
|
||||
"maxjump_f1_with_window6": 0.001589550701590614,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.001589550701590614,
|
||||
"maxjump_out_per_step": 0.02212547196092063,
|
||||
"maxjump_f1_with_window6": 0.001589550701590614,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.005413713480415305,
|
||||
"maxjump_out_per_step": 0.11861272322439464,
|
||||
"maxjump_f1_with_window6": 0.005413713480415305,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.005413713480415305,
|
||||
"maxjump_out_per_step": 0.6979479036971218,
|
||||
"maxjump_f1_with_window6": 0.005413713480415305,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.019199908363967788,
|
||||
"maxjump_out_per_step": 0.28876383821796175,
|
||||
"maxjump_f1_with_window6": 0.019199908363967788,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.019199908363967788,
|
||||
"maxjump_out_per_step": 0.28876383821796175,
|
||||
"maxjump_f1_with_window6": 0.019199908363967788,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.000679179751586656,
|
||||
"maxjump_out_per_step": 0.011527150729375713,
|
||||
"maxjump_f1_with_window6": 0.000679179751586656,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.000679179751586656,
|
||||
"maxjump_out_per_step": 0.011527150729375713,
|
||||
"maxjump_f1_with_window6": 0.000679179751586656,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0011605444949153165,
|
||||
"maxjump_out_per_step": 0.03721603977227228,
|
||||
"maxjump_f1_with_window6": 0.0011605444949153165,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.0011605444949153165,
|
||||
"maxjump_out_per_step": 0.037218919278060314,
|
||||
"maxjump_f1_with_window6": 0.0011605444949153165,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.01726416929635244,
|
||||
"maxjump_out_per_step": 0.37104796823449204,
|
||||
"maxjump_f1_with_window6": 0.0024839789447830274,
|
||||
"f1_err_vs_win6": 0.016881040623005106
|
||||
},
|
||||
"site t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0024839789447830274,
|
||||
"maxjump_out_per_step": 0.045046050707719165,
|
||||
"maxjump_f1_with_window6": 0.0024839789447830274,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0019988841823728176,
|
||||
"maxjump_out_per_step": 0.01873902874992317,
|
||||
"maxjump_f1_with_window6": 0.0019988841823728176,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.0019988841823728176,
|
||||
"maxjump_out_per_step": 0.01873902874992317,
|
||||
"maxjump_f1_with_window6": 0.0019988841823728176,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.005628071367035137,
|
||||
"maxjump_out_per_step": 0.04897917252830741,
|
||||
"maxjump_f1_with_window6": 0.005628071367035137,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.005628071367035137,
|
||||
"maxjump_out_per_step": 0.04897917252830741,
|
||||
"maxjump_f1_with_window6": 0.005628071367035137,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0013893624582810293,
|
||||
"maxjump_out_per_step": 0.0186819346008138,
|
||||
"maxjump_f1_with_window6": 0.0013893624582810293,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.0013893624582810293,
|
||||
"maxjump_out_per_step": 0.02079886198517822,
|
||||
"maxjump_f1_with_window6": 0.0013893624582810293,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0033405760083177194,
|
||||
"maxjump_out_per_step": 0.07493559630083158,
|
||||
"maxjump_f1_with_window6": 0.0033405760083177194,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0033405760083177194,
|
||||
"maxjump_out_per_step": 0.07493559630083158,
|
||||
"maxjump_f1_with_window6": 0.0033405760083177194,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.005131686955684001,
|
||||
"maxjump_out_per_step": 0.10245451309831222,
|
||||
"maxjump_f1_with_window6": 0.005131686955684001,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.005131686955684001,
|
||||
"maxjump_out_per_step": 0.5048292489051344,
|
||||
"maxjump_f1_with_window6": 0.005131686955684001,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.01955931573768127,
|
||||
"maxjump_out_per_step": 0.2975581851502386,
|
||||
"maxjump_f1_with_window6": 0.01955931573768127,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.01955931573768127,
|
||||
"maxjump_out_per_step": 0.2975581851502386,
|
||||
"maxjump_f1_with_window6": 0.01955931573768127,
|
||||
"f1_err_vs_win6": 0.0
|
||||
}
|
||||
},
|
||||
"diagonal": {
|
||||
"base t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.001275910800458191,
|
||||
"maxjump_out_per_step": 0.03528728288006466,
|
||||
"maxjump_f1_with_window6": 0.001275910800458191,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.006834249882349042,
|
||||
"maxjump_out_per_step": 0.45027124784270633,
|
||||
"maxjump_f1_with_window6": 0.001275910800458191,
|
||||
"f1_err_vs_win6": 0.006469535309243879
|
||||
},
|
||||
"site t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0018160559768694018,
|
||||
"maxjump_out_per_step": 0.036994428279204405,
|
||||
"maxjump_f1_with_window6": 0.0018160559768694018,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.17389687729083692,
|
||||
"maxjump_out_per_step": 0.9512571305077048,
|
||||
"maxjump_f1_with_window6": 0.0018160559768694018,
|
||||
"f1_err_vs_win6": 0.17352236500589702
|
||||
},
|
||||
"site t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0032951968389483,
|
||||
"maxjump_out_per_step": 0.08101849084444412,
|
||||
"maxjump_f1_with_window6": 0.0032951968389483,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0032951968389483,
|
||||
"maxjump_out_per_step": 0.08049836455119364,
|
||||
"maxjump_f1_with_window6": 0.0032951968389483,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0026472302788482782,
|
||||
"maxjump_out_per_step": 0.03868824720262809,
|
||||
"maxjump_f1_with_window6": 0.0026472302788482782,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.010707146916472454,
|
||||
"maxjump_out_per_step": 0.21289370234612265,
|
||||
"maxjump_f1_with_window6": 0.0026472302788482782,
|
||||
"f1_err_vs_win6": 0.009199392603544854
|
||||
},
|
||||
"qpt t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.007883634786070015,
|
||||
"maxjump_out_per_step": 0.07213687421025916,
|
||||
"maxjump_f1_with_window6": 0.007883634786070015,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.007883634786070015,
|
||||
"maxjump_out_per_step": 0.07213687421025916,
|
||||
"maxjump_f1_with_window6": 0.007883634786070015,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0021042254469000876,
|
||||
"maxjump_out_per_step": 0.03605934023336077,
|
||||
"maxjump_f1_with_window6": 0.0021042254469000876,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.15454958335751312,
|
||||
"maxjump_out_per_step": 0.47361732317771077,
|
||||
"maxjump_f1_with_window6": 0.0021042254469000876,
|
||||
"f1_err_vs_win6": 0.15418392181872642
|
||||
},
|
||||
"cellc t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.004089971897996092,
|
||||
"maxjump_out_per_step": 0.04544931913493744,
|
||||
"maxjump_f1_with_window6": 0.004089971897996092,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.004089971897996092,
|
||||
"maxjump_out_per_step": 0.04544931913493744,
|
||||
"maxjump_f1_with_window6": 0.004089971897996092,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.007786414559647126,
|
||||
"maxjump_out_per_step": 0.10615772398185219,
|
||||
"maxjump_f1_with_window6": 0.007786414559647126,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.007786414559647126,
|
||||
"maxjump_out_per_step": 0.10615772398185219,
|
||||
"maxjump_f1_with_window6": 0.007786414559647126,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0220788526153487,
|
||||
"maxjump_out_per_step": 0.2726388272791984,
|
||||
"maxjump_f1_with_window6": 0.0220788526153487,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=0.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0220788526153487,
|
||||
"maxjump_out_per_step": 0.2726388272791984,
|
||||
"maxjump_f1_with_window6": 0.0220788526153487,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.001275910800458191,
|
||||
"maxjump_out_per_step": 0.03528728288006466,
|
||||
"maxjump_f1_with_window6": 0.001275910800458191,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"base t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.006834249882349042,
|
||||
"maxjump_out_per_step": 0.45027124784270633,
|
||||
"maxjump_f1_with_window6": 0.001275910800458191,
|
||||
"f1_err_vs_win6": 0.006469535309243879
|
||||
},
|
||||
"site t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.00155291340314212,
|
||||
"maxjump_out_per_step": 0.04226739191047357,
|
||||
"maxjump_f1_with_window6": 0.00155291340314212,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.11344119297302535,
|
||||
"maxjump_out_per_step": 0.3917028634066005,
|
||||
"maxjump_f1_with_window6": 0.00155291340314212,
|
||||
"f1_err_vs_win6": 0.11285505036112697
|
||||
},
|
||||
"site t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.002453426564246408,
|
||||
"maxjump_out_per_step": 0.03688685308461992,
|
||||
"maxjump_f1_with_window6": 0.002453426564246408,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"site t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.002453426564246408,
|
||||
"maxjump_out_per_step": 0.03688685308461992,
|
||||
"maxjump_f1_with_window6": 0.002453426564246408,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0018171966866530331,
|
||||
"maxjump_out_per_step": 0.03560130205583023,
|
||||
"maxjump_f1_with_window6": 0.0018171966866530331,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.011573594040749535,
|
||||
"maxjump_out_per_step": 0.44945714067888365,
|
||||
"maxjump_f1_with_window6": 0.0018171966866530331,
|
||||
"f1_err_vs_win6": 0.011263134181283219
|
||||
},
|
||||
"qpt t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.004178672365624969,
|
||||
"maxjump_out_per_step": 0.06700822884555246,
|
||||
"maxjump_f1_with_window6": 0.004178672365624969,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"qpt t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.004178672365624969,
|
||||
"maxjump_out_per_step": 0.06700822884555246,
|
||||
"maxjump_f1_with_window6": 0.004178672365624969,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0013657606892926166,
|
||||
"maxjump_out_per_step": 0.03848639655536479,
|
||||
"maxjump_f1_with_window6": 0.0013657606892926166,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.19448286979973972,
|
||||
"maxjump_out_per_step": 0.9778972151563999,
|
||||
"maxjump_f1_with_window6": 0.0013657606892926166,
|
||||
"f1_err_vs_win6": 0.1940193666886757
|
||||
},
|
||||
"cellc t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.0016119831862634326,
|
||||
"maxjump_out_per_step": 0.05348368104496215,
|
||||
"maxjump_f1_with_window6": 0.0016119831862634326,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"cellc t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.0016119831862634326,
|
||||
"maxjump_out_per_step": 0.05348368104496215,
|
||||
"maxjump_f1_with_window6": 0.0016119831862634326,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=1.6 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.00389655575000758,
|
||||
"maxjump_out_per_step": 0.06278112824237458,
|
||||
"maxjump_f1_with_window6": 0.00389655575000758,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=1.6 WIN=1": {
|
||||
"maxjump_f1_per_step": 0.00389655575000758,
|
||||
"maxjump_out_per_step": 0.4429137542853424,
|
||||
"maxjump_f1_with_window6": 0.00389655575000758,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=6.0 WIN=2": {
|
||||
"maxjump_f1_per_step": 0.012386023943556368,
|
||||
"maxjump_out_per_step": 0.1972931166165931,
|
||||
"maxjump_f1_with_window6": 0.012386023943556368,
|
||||
"f1_err_vs_win6": 0.0
|
||||
},
|
||||
"warp t=8.0 B=6.0 WIN=3": {
|
||||
"maxjump_f1_per_step": 0.012386023943556368,
|
||||
"maxjump_out_per_step": 0.1972931166165931,
|
||||
"maxjump_f1_with_window6": 0.012386023943556368,
|
||||
"f1_err_vs_win6": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"time_pop": {
|
||||
"base": {
|
||||
"maxjump_f1_per_dt": 0.0,
|
||||
"maxjump_out_per_dt": 0.0,
|
||||
"winner_changes_total": 0
|
||||
},
|
||||
"site": {
|
||||
"maxjump_f1_per_dt": 0.006243969970252006,
|
||||
"maxjump_out_per_dt": 0.10827554277384255,
|
||||
"winner_changes_total": 17
|
||||
},
|
||||
"qpt": {
|
||||
"maxjump_f1_per_dt": 0.004880867091880958,
|
||||
"maxjump_out_per_dt": 0.004611044852681684,
|
||||
"winner_changes_total": 0
|
||||
},
|
||||
"cellc": {
|
||||
"maxjump_f1_per_dt": 0.006179970448791816,
|
||||
"maxjump_out_per_dt": 0.10604425529368433,
|
||||
"winner_changes_total": 15
|
||||
},
|
||||
"site_t0": {
|
||||
"maxjump_f1_per_dt": 0.0,
|
||||
"maxjump_out_per_dt": 0.0,
|
||||
"winner_changes_total": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
{
|
||||
"timejump site dt=0.01": {
|
||||
"q": [
|
||||
2.3,
|
||||
2.8
|
||||
],
|
||||
"t_worst": 9.91,
|
||||
"max_out_step": 0.10827554277384255,
|
||||
"max_f1_step": 0.0028572323710521763
|
||||
},
|
||||
"timejump site dt=0.001": {
|
||||
"q": [
|
||||
2.3,
|
||||
2.8
|
||||
],
|
||||
"t_worst": 9.91,
|
||||
"max_out_step": 0.011248851985623975,
|
||||
"max_f1_step": 0.00028847749905558384
|
||||
},
|
||||
"timejump site dt=0.0001": {
|
||||
"q": [
|
||||
2.3,
|
||||
2.8
|
||||
],
|
||||
"t_worst": 9.91,
|
||||
"max_out_step": 0.00113067110268672,
|
||||
"max_f1_step": 2.8874698390524145e-05
|
||||
},
|
||||
"timejump site dt=1e-05": {
|
||||
"q": [
|
||||
2.3,
|
||||
2.8
|
||||
],
|
||||
"t_worst": 9.91,
|
||||
"max_out_step": 0.00011316362556801352,
|
||||
"max_f1_step": 2.8877387294734547e-06
|
||||
},
|
||||
"timejump qpt dt=0.01": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 16.21,
|
||||
"max_out_step": 0.00461104485268235,
|
||||
"max_f1_step": 0.004042127425204045
|
||||
},
|
||||
"timejump qpt dt=0.001": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 16.21,
|
||||
"max_out_step": 0.0004611240505579539,
|
||||
"max_f1_step": 0.000406360115853599
|
||||
},
|
||||
"timejump qpt dt=0.0001": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 16.21,
|
||||
"max_out_step": 4.611242486959877e-05,
|
||||
"max_f1_step": 4.065726355373389e-05
|
||||
},
|
||||
"timejump qpt dt=1e-05": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 16.21,
|
||||
"max_out_step": 4.611242504171109e-06,
|
||||
"max_f1_step": 4.065938649877587e-06
|
||||
},
|
||||
"timejump cellc dt=0.01": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 0.39,
|
||||
"max_out_step": 0.10604425529368433,
|
||||
"max_f1_step": 0.005110972264514513
|
||||
},
|
||||
"timejump cellc dt=0.001": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 0.39,
|
||||
"max_out_step": 0.020376801353283347,
|
||||
"max_f1_step": 0.0005172341605852449
|
||||
},
|
||||
"timejump cellc dt=0.0001": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 0.39,
|
||||
"max_out_step": 0.002064834070920063,
|
||||
"max_f1_step": 5.178482072565327e-05
|
||||
},
|
||||
"timejump cellc dt=1e-05": {
|
||||
"q": [
|
||||
5.9,
|
||||
1.9
|
||||
],
|
||||
"t_worst": 0.39,
|
||||
"max_out_step": 0.00020688477103125713,
|
||||
"max_f1_step": 5.179096147611695e-06
|
||||
},
|
||||
"shape base": {
|
||||
"mean_cell_pixels_near_wells": 10951.06215310558,
|
||||
"mean_cell_pixels_far": 10515.08654105133,
|
||||
"border_frac_near": 0.3004222385907398,
|
||||
"border_frac_far": 0.28125746844758714
|
||||
},
|
||||
"shape site": {
|
||||
"mean_cell_pixels_near_wells": 9671.733834071645,
|
||||
"mean_cell_pixels_far": 11438.643308839986,
|
||||
"border_frac_near": 0.2704401863244494,
|
||||
"border_frac_far": 0.25280623954417336
|
||||
},
|
||||
"shape qpt": {
|
||||
"mean_cell_pixels_near_wells": 10951.06215310558,
|
||||
"mean_cell_pixels_far": 10515.08654105133,
|
||||
"border_frac_near": 0.1644136900747995,
|
||||
"border_frac_far": 0.2500635410778843
|
||||
},
|
||||
"shape cellc": {
|
||||
"mean_cell_pixels_near_wells": 11562.292900789493,
|
||||
"mean_cell_pixels_far": 12273.640744435888,
|
||||
"border_frac_near": 0.27424759572989177,
|
||||
"border_frac_far": 0.2618651108270203
|
||||
},
|
||||
"shape warp": {
|
||||
"mean_cell_pixels_near_wells": 3446.9220259152826,
|
||||
"mean_cell_pixels_far": 8566.10048594699,
|
||||
"border_frac_near": 0.29084665580811486,
|
||||
"border_frac_far": 0.3016702768114897
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
"""Scratch probe: where does a weighted lattice Voronoi cell get its weight?
|
||||
2D, lattice cell = 1 unit, wells with a proximity field, numpy only.
|
||||
Variants of the per-site weight wt_i (multiplies the distance to site i, as the fauna probe's lattice_web):
|
||||
base : wt = hashed .5..1, no field (reference, the unweighted-by-field node)
|
||||
site : wt = base*(1+B*prox(site position, t)) (fauna probe, 4354ef2c)
|
||||
qpt : wt = base*(1+B*prox(query position, t)) (common factor for every candidate site)
|
||||
cellc : wt = base*(1+B*prox(lattice cell centre, t)) (node calls the field at the integer cell id)
|
||||
site_t0: as site, but field read at t=0 (stale time) while wells have moved
|
||||
warp : base weights, query warped q' = q*(1+B*prox(q)) (domain scaling, a different construct)
|
||||
"""
|
||||
import numpy as np, sys, json
|
||||
rng_seed = 7
|
||||
def hash01(ix, iz, k):
|
||||
# integer hash (lowbias32-style), stand-in for pcg3d; deterministic, vectorised
|
||||
x = (ix.astype(np.uint64)*np.uint64(73856093) ^ iz.astype(np.uint64)*np.uint64(19349663) ^ np.uint64(k*83492791+rng_seed)) & np.uint64(0xFFFFFFFF)
|
||||
x ^= x >> np.uint64(16); x = (x*np.uint64(0x7feb352d)) & np.uint64(0xFFFFFFFF)
|
||||
x ^= x >> np.uint64(15); x = (x*np.uint64(0x846ca68b)) & np.uint64(0xFFFFFFFF)
|
||||
x ^= x >> np.uint64(16)
|
||||
return (x >> np.uint64(8)).astype(np.float64)/16777216.0
|
||||
|
||||
W0 = np.array([[2.0,2.5],[5.5,4.0],[3.5,6.0],[6.5,1.5]]); WS = np.array([0.8,1.2,0.6,1.0])
|
||||
OM = np.array([0.30,-0.22,0.17,-0.41]) # well orbit rate, rad per unit t (wells orbit a centre)
|
||||
CEN = np.array([4.0,4.0])
|
||||
def wells(t):
|
||||
r = W0-CEN; c, s = np.cos(OM*t), np.sin(OM*t)
|
||||
return CEN + np.stack([r[:,0]*c-r[:,1]*s, r[:,0]*s+r[:,1]*c], 1)
|
||||
def prox(x, z, t):
|
||||
w = wells(t); out = 0
|
||||
for k in range(len(w)):
|
||||
out = np.maximum(out, np.exp(-(np.hypot(x-w[k,0], z-w[k,1])/WS[k])**2))
|
||||
return out
|
||||
|
||||
KW=.35
|
||||
def node(qx, qz, variant, t=0.0, B=1.6, WIN=2, tfield=None):
|
||||
"""returns f1, f2, winning cell id (ix*10007+iz), soft border value"""
|
||||
qx0, qz0 = qx, qz
|
||||
if variant == 'warp':
|
||||
s = 1+B*prox(qx, qz, t); qx, qz = qx*s, qz*s
|
||||
nx, nz = np.floor(qx), np.floor(qz)
|
||||
m = np.full(qx.shape, 8.0); f1 = m.copy(); f2 = m.copy(); wid = np.zeros(qx.shape)
|
||||
pq = prox(qx0, qz0, t)
|
||||
tf = t if tfield is None else tfield
|
||||
for j in range(-WIN, WIN+1):
|
||||
for i in range(-WIN, WIN+1):
|
||||
cx, cz = nx+i, nz+j
|
||||
ox, oz = hash01(cx, cz, 11), hash01(cx, cz, 12)
|
||||
base = hash01(cx, cz, 13)*.5+.5
|
||||
sx, sz = cx+ox, cz+oz
|
||||
if variant in ('base','warp'): wt = base
|
||||
elif variant == 'site': wt = base*(1+B*prox(sx, sz, tf))
|
||||
elif variant == 'site_t0': wt = base*(1+B*prox(sx, sz, 0.0))
|
||||
elif variant == 'cellc': wt = base*(1+B*prox(cx+.5, cz+.5, tf))
|
||||
elif variant == 'qpt': wt = base*(1+B*pq)
|
||||
d = np.hypot(sx-qx, sz-qz)*wt
|
||||
upd1 = d < f1; upd2 = (~upd1) & (d < f2)
|
||||
wid = np.where(upd1, cx*10007+cz, wid)
|
||||
f2 = np.where(upd1, f1, np.where(upd2, d, f2)); f1 = np.where(upd1, d, f1)
|
||||
h = (lambda u: u*u*(3-2*u))(np.clip(.5+.5*(m-d)/KW, 0, 1))
|
||||
m = m+(d-m)*h-h*(1-h)*KW/(1+3*KW)
|
||||
gap0 = .25*KW/(1+3*KW)
|
||||
soft = np.clip((f1-m)/gap0, 0, 1)
|
||||
out = np.maximum(soft**2, np.exp(-((f2-f1)/.10)**2))
|
||||
return f1, f2, wid, out
|
||||
|
||||
def grid(n=500, lo=1.0, hi=7.0):
|
||||
xs = np.linspace(lo, hi, n); X, Z = np.meshgrid(xs, xs); return X, Z
|
||||
|
||||
def main():
|
||||
R = {}
|
||||
X, Z = grid()
|
||||
for t in (0.0, 8.0):
|
||||
ref = {v: node(X, Z, v, t) for v in ('base','site','qpt','cellc','warp')}
|
||||
ref['site_t0'] = node(X, Z, 'site_t0', t)
|
||||
# large-window truth for 'site' (exact argmin unless window too small)
|
||||
truth = node(X, Z, 'site', t, WIN=5)[2]
|
||||
key = f"t={t}"; R[key] = {}
|
||||
for v, (f1, f2, wid, out) in ref.items():
|
||||
R[key][v] = dict(
|
||||
winner_differs_from_site=float((wid != ref['site'][2]).mean()),
|
||||
winner_differs_from_base=float((wid != ref['base'][2]).mean()),
|
||||
out_absdiff_vs_site_mean=float(np.abs(out-ref['site'][3]).mean()),
|
||||
out_absdiff_vs_site_max=float(np.abs(out-ref['site'][3]).max()),
|
||||
n_cells=int(len(np.unique(wid))))
|
||||
R[key]['site_window2_vs_window5_winner_differs'] = float((ref['site'][2] != truth).mean())
|
||||
# cell count density near wells for site vs base
|
||||
# pop measure: dense line, step 1e-3 units; also jumps in time at fixed q
|
||||
ts = np.linspace(1.0, 7.0, 6001) # 1e-3
|
||||
lines = {'horizontal z=2.6 (near well 0)': (ts, np.full_like(ts, 2.6)),
|
||||
'diagonal': (ts, 1.0+ts*0.9-0.5)}
|
||||
P = {}
|
||||
for ln, (lx, lz) in lines.items():
|
||||
P[ln] = {}
|
||||
for t in (0.0, 8.0):
|
||||
for v in ('base','site','qpt','cellc','warp'):
|
||||
for B, WIN in ((1.6,2),(1.6,1),(6.0,2),(6.0,3)):
|
||||
if v in ('base',) and B != 1.6: continue
|
||||
f1, f2, wid, out = node(lx, lz, v, t, B, WIN)
|
||||
ref = node(lx, lz, v, t, B, 6)
|
||||
P[ln][f"{v} t={t} B={B} WIN={WIN}"] = dict(
|
||||
maxjump_f1_per_step=float(np.abs(np.diff(f1)).max()),
|
||||
maxjump_out_per_step=float(np.abs(np.diff(out)).max()),
|
||||
maxjump_f1_with_window6=float(np.abs(np.diff(ref[0])).max()),
|
||||
f1_err_vs_win6=float(np.abs(f1-ref[0]).max()))
|
||||
# time pop: fixed query points, t stepped at 1e-2; wells move
|
||||
tt = np.linspace(0, 20, 2001)
|
||||
qs = [(2.3,2.8),(4.7,3.9),(3.5,5.5),(5.9,1.9)]
|
||||
T = {}
|
||||
for v in ('base','site','qpt','cellc','site_t0'):
|
||||
mj = 0; mo = 0; changes = 0
|
||||
for (qx, qz) in qs:
|
||||
f1s=[];outs=[];wids=[]
|
||||
for t in tt:
|
||||
a,b,w,o = node(np.array([qx]), np.array([qz]), v, t)
|
||||
f1s.append(a[0]); outs.append(o[0]); wids.append(w[0])
|
||||
mj=max(mj,np.abs(np.diff(f1s)).max()); mo=max(mo,np.abs(np.diff(outs)).max())
|
||||
changes += int((np.diff(wids)!=0).sum())
|
||||
T[v] = dict(maxjump_f1_per_dt=float(mj), maxjump_out_per_dt=float(mo), winner_changes_total=changes)
|
||||
json.dump(dict(grid=R, line_pop=P, time_pop=T), open('probe_results.json','w'), indent=1)
|
||||
print(json.dumps(R, indent=1)); print(json.dumps(T, indent=1))
|
||||
for ln in P:
|
||||
print(ln)
|
||||
for k,v in P[ln].items(): print(' ', k, {a: round(b,5) for a,b in v.items()})
|
||||
main()
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"""Follow-up: (1) continuity refinement: does the max time-jump of 'site' scale with dt? (2) cell size near vs far from wells,
|
||||
border-pixel fraction per variant."""
|
||||
import numpy as np, json, importlib.util, sys, io, contextlib
|
||||
spec = importlib.util.spec_from_file_location('p', 'siteweight_probe.py')
|
||||
src = open('siteweight_probe.py').read().replace('\nmain()\n', '\n') # import without running main
|
||||
ns = {}; exec(compile(src, 'p', 'exec'), ns)
|
||||
node, prox, grid, wells = ns['node'], ns['prox'], ns['grid'], ns['wells']
|
||||
res = {}
|
||||
# (1) worst time step at dt=.01 for each variant, then refine dt inside +-.02 of it (continuity: step should shrink ~ with dt)
|
||||
qs = [(2.3,2.8),(4.7,3.9),(3.5,5.5),(5.9,1.9)]
|
||||
def series(v, qx, qz, tt):
|
||||
f1 = np.empty(len(tt)); out = np.empty(len(tt))
|
||||
for k, t in enumerate(tt):
|
||||
a,b,w,o = node(np.array([qx]), np.array([qz]), v, t); f1[k]=a[0]; out[k]=o[0]
|
||||
return f1, out
|
||||
for v in ('site','qpt','cellc'):
|
||||
tt = np.arange(0, 20.0001, 0.01); best = (0, None, None)
|
||||
for qx, qz in qs:
|
||||
f1, out = series(v, qx, qz, tt); i = int(np.argmax(np.abs(np.diff(out))))
|
||||
if abs(out[i+1]-out[i]) > best[0]: best = (abs(out[i+1]-out[i]), (qx,qz), tt[i])
|
||||
_, q, t0 = best
|
||||
for dt in (0.01, 0.001, 0.0001, 0.00001):
|
||||
t2 = np.arange(t0-0.02, t0+0.03, dt); f1, out = series(v, q[0], q[1], t2)
|
||||
res[f"timejump {v} dt={dt}"] = dict(q=q, t_worst=float(t0), max_out_step=float(np.abs(np.diff(out)).max()), max_f1_step=float(np.abs(np.diff(f1)).max()))
|
||||
print(v, dt, res[f"timejump {v} dt={dt}"], flush=True)
|
||||
# (2) cell area near vs far wells, border fraction
|
||||
X, Z = grid(600)
|
||||
w = wells(0.0)
|
||||
near = np.min([np.hypot(X-w[k,0], Z-w[k,1])/ns['WS'][k] for k in range(4)], axis=0) < 1.0
|
||||
for v in ('base','site','qpt','cellc','warp'):
|
||||
f1,f2,wid,out = node(X, Z, v, 0.0)
|
||||
ids, cnt = np.unique(wid, return_counts=True); area = dict(zip(ids, cnt))
|
||||
pa = np.array([area[i] for i in wid.ravel()]).reshape(wid.shape)
|
||||
res[f"shape {v}"] = dict(mean_cell_pixels_near_wells=float(pa[near].mean()), mean_cell_pixels_far=float(pa[~near].mean()),
|
||||
border_frac_near=float((out[near]>.5).mean()), border_frac_far=float((out[~near]>.5).mean()))
|
||||
print(v, res[f"shape {v}"])
|
||||
json.dump(res, open('probe_results2.json','w'), indent=1)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import numpy as np, probe as P
|
||||
g = np.linspace(-3200, 3200, 260); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
|
||||
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
|
||||
for boost, S in [(1.6, 1), (1.6, 2), (4, 1), (4, 2), (9, 1)]:
|
||||
ref = P.node(q, 100.0, 9, boost, S, nrm)
|
||||
row = []
|
||||
for W in (2, 3, 4, 5):
|
||||
r = P.node(q, 100.0, W, boost, S, nrm)
|
||||
miss = np.mean((np.abs(r[0]-ref[0])>1e-12)|(np.abs(r[1]-ref[1])>1e-12))
|
||||
row.append(f"W{W}: miss {miss:.5f} dOut {np.abs(r[3]-ref[3]).max():.3f}")
|
||||
print(f"boost {boost} S {S} ratio {2*(1+boost):.1f}x{S}:", "; ".join(row))
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import numpy as np, probe as P
|
||||
g = (np.arange(-12, 12) + .5) * 260; X, Z = np.meshgrid(g, g); c = np.stack([X, Z], -1).reshape(-1, 2)
|
||||
for ex in (False, True):
|
||||
prev = None; worst = 0; big = 0
|
||||
for t in np.arange(0, 300, 1/60):
|
||||
Pw = np.exp(-(P.border(c, P.bodies(t), ex) / P.FALL) ** 2)
|
||||
if prev is not None:
|
||||
s = np.abs(Pw - prev); worst = max(worst, s.max()); big += (s > .01).sum()
|
||||
prev = Pw
|
||||
print("exact" if ex else "first-order", f"cell-centre profile: max step per 1/60 s {worst:.4f}, steps > .01: {big} (576 centres x 18000 ticks)")
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import numpy as np, probe as P
|
||||
rng = np.random.default_rng(9); n = 15000
|
||||
kx = rng.integers(-12, 12, n); z = rng.uniform(-3100, 3100, n); eps = 1e-6
|
||||
qa = np.stack([kx * 260.0 - eps, z], -1)[None]; qb = np.stack([kx * 260.0 + eps, z], -1)[None]
|
||||
for W, fold, boost, S in [(6,"f1kw",1.6,2),(6,"her",1.6,2),(6,"f1kw",4,2)]:
|
||||
ang = rng.uniform(0, 6.283, n); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)[None]
|
||||
a = P.node(qa, 100.0, W, boost, S, nrm, True, fold); b = P.node(qb, 100.0, W, boost, S, nrm, True, fold)
|
||||
j = np.abs(a[3] - b[3]); js = np.abs(a[2]-b[2]); jf = np.abs(a[0]-b[0])
|
||||
print(f"W{W} {fold} B{boost} S{S}: cell-boundary jump out max {j.max():.4f} (>.01: {np.mean(j>.01):.4f}), soft max {js.max():.4f}, F1 max {jf.max():.2e}")
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import numpy as np, probe as P
|
||||
g = np.linspace(-3200, 3200, 200); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
|
||||
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
|
||||
wmin = .5
|
||||
for boost, S in [(1.6, 1), (1.6, 2), (4, 2), (9, 1)]:
|
||||
f1, f2, soft, out = P.node(q, 100.0, 6, boost, S, nrm)
|
||||
C = np.maximum(f2, f1 + P.KW)
|
||||
k = np.floor(C / wmin).astype(int) + 1 # first ring whose lower bound (k-1)*wmin >= C is excluded... rings visited 0..k
|
||||
k = np.ceil(C / wmin).astype(int) # rings 0..k visited (ring k+1 has lb k*wmin >= C)
|
||||
cells = (2 * k + 1) ** 2
|
||||
wmax = 1.0 * (1 + boost); Usound = 1.58 * wmax * S
|
||||
Wsound = int(np.ceil((Usound + P.KW) / wmin))
|
||||
print(f"B{boost} S{S}: rings needed p50 {np.median(k):.0f} p99 {np.percentile(k,99):.0f} max {k.max()} -> cells p50 {np.median(cells):.0f} max {cells.max()}; worst-case sound W {Wsound} ({(2*Wsound+1)**2} cells)")
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import numpy as np, probe as P, sys
|
||||
fold = sys.argv[1]; rng = np.random.default_rng(2); jm = js = 0; nsw = 0
|
||||
for k in range(3):
|
||||
o = rng.uniform(-2500, 2500, 2); a = rng.uniform(0, 3.1416); d = np.array([np.cos(a), np.sin(a)])
|
||||
s = np.arange(0, 15 * 260, .5); P.node((o + s[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); a1 = P.node.a1[0].copy()
|
||||
idx = np.nonzero(np.diff(a1))[0][:40]
|
||||
lo = s[idx]; hi = s[idx + 1]; ref = a1[idx]
|
||||
for _ in range(40):
|
||||
mid = .5 * (lo + hi); P.node((o + mid[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold)
|
||||
same = P.node.a1[0] == ref; lo = np.where(same, mid, lo); hi = np.where(same, hi, mid)
|
||||
ra = P.node((o + lo[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); ma = P.node.m[0].copy()
|
||||
rb = P.node((o + hi[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); mb = P.node.m[0]
|
||||
jm = max(jm, np.abs(ma - mb).max()); js = max(js, np.abs(ra[2] - rb[2]).max()); nsw += len(idx)
|
||||
print(fold, f"F1 swaps {nsw}: max jump m {jm:.2e}, soft {js:.2e}")
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import numpy as np, probe as P
|
||||
g = np.linspace(-3200, 3200, 200); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
|
||||
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
|
||||
for S in (1, 2):
|
||||
a = P.node(q, 100.0, 6, 1.6, S, nrm, True, "her"); b = P.node(q, 100.0, 6, 1.6, S, nrm, True, "f1kw")
|
||||
d = np.abs(a[3] - b[3]); print(f"S{S} look her vs f1kw: mean {d.mean():.4f} p99 {np.percentile(d,99):.3f} max {d.max():.3f}; mean out {a[3].mean():.3f}")
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
"""Weighted lattice cellular probe (Imagination, lattice-weighted-cellular). numpy, float64.
|
||||
Bodies orbit (period = distance s). Cell weight = base(hash)*(1+BOOST*exp(-(border(centre)/FALL)^2)),
|
||||
border over the bodies' power scores (site.w = sigma). Her fold: row-major, m from 8; variant m0f1 starts m at F1."""
|
||||
import numpy as np, sys
|
||||
LAT, FALL, KW, EW, OMEGA = 260.0, 120.0, .35, .10, .05
|
||||
GAP0 = .25 * KW / (1 + 3 * KW)
|
||||
rng = np.random.default_rng(7)
|
||||
NB = 6
|
||||
DIST = np.array([0.0, 700, 1200, 1800, 2400, 3000]); PH = rng.uniform(0, 6.283, NB)
|
||||
SIG = np.array([1500, 300, 450, 350, 900, 600]) / np.sqrt(128.0) * 4 # sigma ratio 5:1, edges well inside
|
||||
def bodies(t):
|
||||
a = PH + np.where(DIST > 0, 2 * np.pi * t / np.maximum(DIST, 1), 0)
|
||||
return np.stack([np.cos(a) * DIST, np.sin(a) * DIST], -1)
|
||||
|
||||
def h32(x):
|
||||
x = x.astype(np.uint32)
|
||||
x ^= x >> 16; x *= np.uint32(0x7feb352d); x ^= x >> 15; x *= np.uint32(0x846ca68b); x ^= x >> 16
|
||||
return x
|
||||
def hash01(cx, cz, s):
|
||||
k = ((cx.astype(np.int64) * 73856093) ^ (cz.astype(np.int64) * 19349663) ^ (s * 83492791)) & 0xffffffff
|
||||
return (h32(h32(k)) >> 8) / 16777216.0
|
||||
|
||||
def border(q, B, exact):
|
||||
"""q: (...,2). Power border distance over bodies B."""
|
||||
sc = ((q[..., None, :] - B) ** 2).sum(-1) - SIG ** 2
|
||||
o = np.argsort(sc, -1); i1 = o[..., 0]; i2 = o[..., 1]
|
||||
s1 = B[i1]
|
||||
if not exact:
|
||||
s2 = B[i2]; return (np.take_along_axis(sc, i2[..., None], -1)[..., 0] - np.take_along_axis(sc, i1[..., None], -1)[..., 0]) / (2 * np.linalg.norm(s2 - s1, axis=-1))
|
||||
best = np.full(q.shape[:-1], 1e30)
|
||||
w1 = SIG[i1]
|
||||
for k in range(NB):
|
||||
d = B[k] - s1; L = np.linalg.norm(d, axis=-1)
|
||||
pl = np.maximum((((s1 - q) * d).sum(-1) + .5 * ((d * d).sum(-1) + w1 ** 2 - SIG[k] ** 2)) / np.where(L > 0, L, 1), 0)
|
||||
best = np.where((L > 0) & (pl < best), pl, best)
|
||||
return best
|
||||
|
||||
def node(q, t, W, boost, S=1.0, nrm=None, exact=True, fold="her"):
|
||||
B = bodies(t); p = q / LAT; n = np.floor(p); f = p - n
|
||||
if nrm is None: nrm = np.zeros_like(q); nrm[..., 0] = 1
|
||||
ds = []
|
||||
for j in range(-W, W + 1):
|
||||
for i in range(-W, W + 1):
|
||||
cx, cz = n[..., 0] + i, n[..., 1] + j
|
||||
base = .5 + .5 * hash01(cx, cz, 1)
|
||||
P = np.exp(-(border(np.stack([(cx + .5) * LAT, (cz + .5) * LAT], -1), B, exact) / FALL) ** 2)
|
||||
wt = base * (1 + boost * P)
|
||||
ox = .5 + .5 * np.sin(OMEGA * t + 6.2831 * hash01(cx, cz, 2)); oz = .5 + .5 * np.sin(OMEGA * t + 6.2831 * hash01(cx, cz, 3))
|
||||
vx, vz = i - f[..., 0] + ox, j - f[..., 1] + oz
|
||||
a = vx * nrm[..., 0] + vz * nrm[..., 1]; c = -vx * nrm[..., 1] + vz * nrm[..., 0]
|
||||
ds.append(np.hypot(S * a, c) * wt)
|
||||
D = np.stack(ds, -1); srt = np.sort(D, -1); f1, f2 = srt[..., 0], srt[..., 1]; a1 = np.argmin(D, -1)
|
||||
m = np.full(f1.shape, 8.0) if fold == "her" else (f1 + KW if fold == "f1kw" else f1.copy())
|
||||
for k in range(D.shape[-1]):
|
||||
d = D[..., k]
|
||||
h = np.clip(.5 + .5 * (m - d) / KW, 0, 1); h = h * h * (3 - 2 * h)
|
||||
mn = m + (d - m) * h - h * (1 - h) * KW / (1 + 3 * KW)
|
||||
m = np.where((fold == "m0f1") & (a1 == k), m, mn)
|
||||
soft = np.clip((f1 - m) / GAP0, 0, 1); node.m = m; node.a1 = a1
|
||||
return f1, f2, soft, np.maximum(soft ** 2, np.exp(-((f2 - f1) / EW) ** 2))
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# Shader runner probes: the HLSL-to-GLSL ES toolchain, lavapipe on Yggdrasil, native ANGLE on Starfire
|
||||
|
||||
For the cultmath-tapes questions that replace `device-run-driver`: `glsl-es-translation-owner`,
|
||||
`conforming-shader-run` and `real-device-shader-run`. These are body facts measured by
|
||||
imagination-ag4-shader-runner (session self-2026-10-16-ag4) on 2026-10-16, between about 14:10 and
|
||||
15:20 UTC (16:10 to 17:20 CEST). Every probe read CultLib at f697e03f (origin/main). Companion to
|
||||
`docs/research/native-shader-testing.md` (f697e03f) and `docs/research/device-capture-runners.md` (c9d3e672).
|
||||
|
||||
The probe scripts are in this directory under `p/`: `probe.sh` and `lib.sh` (Yggdrasil jobs), `fam.py`
|
||||
(the per-family shader generator and bit judge), `vkrun.c` (an 87-line Vulkan compute host) and
|
||||
`angle_native.py` (a 96-line native ANGLE host). They are throwaway probe code, not a proposed
|
||||
implementation. The logs are `q12-run3.log` (lavapipe) and `lib-run4.log` (library route). The earlier
|
||||
runs are superseded: run1 used a raw-ulp metric, run2 died on CRLF, and lib-run1 to lib-run3 failed on
|
||||
the capability and grep problems recorded below.
|
||||
|
||||
## Metric
|
||||
|
||||
Each fixture family has 256 cases, and the fixture records the C# result bits. "Exact" means all 256
|
||||
cases are bit-identical to the C# fixture. Otherwise the worst error is given in units of
|
||||
2^-23 * max(|expected|, 1) (written "u1" below), the scale the fixture's ulp-bounded checks use. NaN and
|
||||
Inf cases are compared only as NaN-versus-not-NaN. These figures describe the data. They are not the
|
||||
fixture's own judge, which is C# and per family: GlslMirrorTests tolerances, and the `check` text on 10
|
||||
families.
|
||||
|
||||
## The fixture surface (glsl-parity.json at f697e03f)
|
||||
|
||||
- 109 families, each with 256 cases, encoded `args -> result` as hex 32-bit words. There are at most 32
|
||||
words per case.
|
||||
- 105 families use only float, int, vecN, ivecN and mat3 (mat3 appears as a return only). The other 4
|
||||
take or return structs: `CultCellular cultmath_cellular(vec3)`, `CultPhasor cultmath_phacelle(vec3,
|
||||
vec3, float, float)`, and `SpanNoise cultmath_span_fbm/ridged(SpanPath, SpanStage, int, float, float)`.
|
||||
Any runner needs per-struct flatteners in field order for these 4.
|
||||
- Tolerance classes: 89 are `ulp-bounded`, 2 are `exact`, 7 are `platform` (exp, pow, log, asin, acos,
|
||||
atan, and phacelle's sin and cos), and 10 carry a `check` (enclosure).
|
||||
- What a harness needs: generate one shader per family that reads the argument words, bit-casts them to
|
||||
the signature's types, calls the function and writes the result words. The probe generator does this
|
||||
for both HLSL and GLSL in 112 lines of Python, judge included.
|
||||
|
||||
## Q1. Can dxc emit a SPIR-V library that SPIRV-Cross lowers to GLSL ES for CultMath's real surface?
|
||||
|
||||
Yggdrasil, ubuntu:24.04. Tools: dxc v1.9.2607 Linux (`linux_dxc_2026_07_29`, the version get-dxc.ps1
|
||||
pins on Windows), and SPIRV-Cross built from HEAD 300681bb (2026-10-14).
|
||||
|
||||
| Step | Result |
|
||||
|---|---|
|
||||
| `dxc -T lib_6_6 -spirv -fspv-target-env=vulkan1.2 CultMath.hlsl`, no change to the source | fails: "No OpEntryPoint ... only allowed if the Linkage ... capability is being used" |
|
||||
| `-default-linkage external` (lib_6_3, universal1.5), no `export` | fails the same way: no exports in SPIR-V |
|
||||
| `export` added to every top-level `cultmath_` definition, vulkan1.2 | fails: "Capability Linkage is not allowed by Vulkan 1.2" |
|
||||
| same, `-fspv-target-env=universal1.5` | compiles, but declares 14 capabilities, ray query, interlock, quad control and partitioned subgroups among them |
|
||||
| SPIRV-Cross `--es --version 310` on that module | fails: "RayQuery requires Vulkan GLSL 460" |
|
||||
| ray query stripped | fails: "Cannot find mapping for combined sampler parameter". The Texture2D/SamplerState functions were exported |
|
||||
| `export` only on the 173 non-texture definitions, all capabilities but Shader and Linkage stripped, every OpExtension stripped, then `spirv-as` | assembles; SPIRV-Cross `--es --version 310` exits 0 |
|
||||
| SPIRV-Cross `--es --version 300` | fails: "At least ESSL 3.10 required for compute shaders" (a library gets a GLCompute placeholder entry) |
|
||||
| ES 3.10 output, `#version` guarded by `#ifdef SPIRV_CROSS_LIBRARY_HEADER`, placeholder main removed, compiled by glslang as a `#version 300 es` fragment shader | compiles |
|
||||
|
||||
Shape of the output against the committed lowering:
|
||||
|
||||
| | committed (CultMath.glsl, .Cycles, .Phacelle) | dxc + SPIRV-Cross |
|
||||
|---|---|---|
|
||||
| lines | 1035 + Cycles + Phacelle | 3458, one file (the MPL and Apache files are merged) |
|
||||
| function definitions | 173 (121 + 51 + 1) | 127 |
|
||||
| overloads | kept | **46 dropped**: one definition per name survives (for example `cultmath_snoise(vec2)` and `cultmath_iv_snoise_ball(vec2, float)` are gone) |
|
||||
| SSA temporaries `_NNN` | 0 | 5632 |
|
||||
| min and max | as written | NaN-guarded `mix(mix(min(a,b),b,isnan(a)),a,isnan(b))` |
|
||||
|
||||
In `cultmath_snoise(vec3)` as emitted, the body is one temporary per SSA value (`vec3 _1157 = floor(...)`)
|
||||
and constants are spelt to 20 digits.
|
||||
|
||||
**Answer:** yes, but only with three owned workarounds: an `export` annotation on 173 definitions,
|
||||
which FXC (Unity's D3D11 compiler) does not accept, so it would have to be a macro; a capability and
|
||||
extension strip pass; and unique names for 46 overloads, which breaks the public API for both the HLSL
|
||||
and the GLSL consumers. The output is generated text, not a readable library.
|
||||
|
||||
## Q2. Does a conforming Vulkan implementation run the families in a Linux container on Yggdrasil?
|
||||
|
||||
Yggdrasil, ubuntu:24.04, apt packages: mesa-vulkan-drivers 25.2.8 (lavapipe), glslang-tools 15.1.0,
|
||||
spirv-tools 2025.1~rc1, libvulkan-dev, and the same dxc. There is no GPU.
|
||||
|
||||
- Device: `llvmpipe (LLVM 20.1.2, 256 bits)`, Vulkan 1.4. Float controls for f32: denormPreserve 0,
|
||||
flushToZero 0, RTE 1, RTZ 0, signedZeroInfNanPreserve 1.
|
||||
- HLSL route: a generated `cs_6_0` compute shader that includes CultMath.hlsl, through `dxc -spirv
|
||||
-fspv-target-env=vulkan1.2`. 105 of 105 families compile and run. Fma instructions in the SPIR-V: 0.
|
||||
- GLSL route: the committed CultMath.glsl, .Cycles.glsl and .Phacelle.glsl concatenated after `#version
|
||||
310 es` and highp precision, as a compute shader through `glslangValidator -V`. 105 of 105 compile and
|
||||
run.
|
||||
- Library route: the dxc + SPIRV-Cross library from Q1 through glslang. 103 of 105 run; the 2 missing
|
||||
ones are the dropped overloads. Against the fixture: 75 exact, 4 within 4 u1, 7 within 64, 17 above
|
||||
64. Its worst rows are the same figures as the HLSL route's.
|
||||
- Time: all 105 dispatches together take 8 s on a re-run. Each job runs about 4 minutes, almost all of
|
||||
it apt, the dxc download and the SPIRV-Cross build.
|
||||
- Determinism: re-running all 105 SPIR-V modules gave bit-identical outputs.
|
||||
- The HLSL and GLSL routes are bit-identical to each other in 85 of 105 families.
|
||||
- Against the C# fixture, HLSL route: 75 exact, 6 within 4 u1, 7 within 64, 17 above 64. GLSL route:
|
||||
75, 4, 8 and 18. The largest errors are in the `coordinate bands 1e2 to 4e6` families of snoise_grad
|
||||
and snoise_hessian (13e6 and 1.6e8 u1), then fbm_grad and ridged_grad (about 3.5e3 to 5e3 u1), Cycles
|
||||
arcsine (2.7e3) and arccosine (1.0e3), and the other hessian families (1e3 to 2.4e3).
|
||||
|
||||
**Answer:** yes. The image needs nothing beyond distro packages and the pinned dxc. It is
|
||||
deterministic and fast, and it runs both the HLSL and the lowered GLSL on the same implementation.
|
||||
|
||||
## Q3. A real-device run on Starfire without a browser: native ANGLE
|
||||
|
||||
Starfire. The host is `angle_native.py`, Python ctypes over the libEGL.dll and libGLESv2.dll that ship
|
||||
with Docker Desktop 4.71.0 (an Electron app): ANGLE 2.1.26626, git hash a4490148bd3d. Display:
|
||||
`EGL_PLATFORM_ANGLE_TYPE_D3D11`. Context: ES 3.0, optionally `EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE`.
|
||||
Each family is a `#version 300 es` fragment shader, with arguments read by texelFetch from an RGBA32UI
|
||||
texture and results written to up to 3 RGBA32UI targets over a 16x16 grid, then read back with
|
||||
glReadPixels. The same RGBA32UI readback is used by the scratchpad browser pages (gpu.js).
|
||||
|
||||
- `GL_RENDERER`: `ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)`.
|
||||
`GL_VERSION`: `OpenGL ES 3.0 (ANGLE 2.1.26626 git hash: a4490148bd3d)`.
|
||||
- 105 of 105 families compile and run. All 105 together take 6.3 s wall, process start included. The GL
|
||||
error at the end is 0.
|
||||
- The WebGL-compatibility context and the plain ES 3.0 context are bit-identical in 105 of 105 families.
|
||||
- Against the C# fixture: 38 exact, 31 within 4 u1, 17 within 64, 19 above 64.
|
||||
- It agrees with the earlier browser measurements on the same GPU. `cultmath_iv_exp`: 19 of 256 exact,
|
||||
worst 12 ulp; the browser run of finding iv-exp-gpu-not-enclosed measured "up to 12 ulp".
|
||||
`cultmath_iv_frustum_ball`: 213 of 256 exact, so 43 off, by at most 2 ulp; finding
|
||||
frustum-exact-unsound-on-webgl2 measured "43/256 cases by up to 2 ulp".
|
||||
- The dxc + SPIRV-Cross library from Q1 also runs on this ANGLE, as a WebGL-compatibility ES 3.00
|
||||
fragment shader: 103 of 105 families (the 2 dropped overloads fail to compile), with 41 exact. It is
|
||||
bit-identical to the committed lowering's ANGLE output in 71 of 103 families.
|
||||
- Largest differences: the `coordinate bands 1e2 to 4e6` families again. 173 cases of snoise_grad and
|
||||
415 component values of snoise_hessian differ by more than 1e-3 relative, up to 2 and 20. These
|
||||
families exist for the span allowance's device margin (follow-up span-allowance-gpu-margin, cut
|
||||
stage-shaders-device). Their judge is the allowance check, not an ulp bound.
|
||||
|
||||
**Answer:** yes. A pinned ANGLE build, loaded natively, runs the site's real translation path
|
||||
(ES 3.00, then ANGLE, then HLSL, then D3DCompile, then the NVIDIA D3D11 driver) with no browser and no
|
||||
SwiftShader fallback, because the backend is named explicitly and the renderer string is recorded. The
|
||||
result agrees bit for bit with the WebGL-compatibility context, and it reproduces the two browser
|
||||
measurements on record.
|
||||
|
||||
## What was not probed
|
||||
|
||||
- SwiftShader as a second conforming implementation.
|
||||
- Amber as the dispatch host instead of a small owned one.
|
||||
- WARP and native D3D11 HLSL on Starfire, which is the Unity consumer path.
|
||||
- naga with dxc SPIR-V as input.
|
||||
- Chrome's own ANGLE feature flags, which the native host does not reproduce.
|
||||
- Mobile or mediump devices.
|
||||
- An Electron release's ANGLE pinned by download. The probe used the copy Docker Desktop happens to
|
||||
ship.
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
"""Probe: run the GLSL fixture families through a native ANGLE (DLLs shipped with an Electron app), D3D11
|
||||
backend, ES 3.0 context, fragment-shader readback through RGBA32UI targets. No browser.
|
||||
usage: py -3 -I angle_native.py <angle-dll-dir> <fixture.json> <shaders-dir> <outdir> [webgl]"""
|
||||
import ctypes as C, json, os, struct, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import fam
|
||||
|
||||
dll, fx, sh, out = sys.argv[1:5]; webgl = len(sys.argv) > 5
|
||||
os.add_dll_directory(dll)
|
||||
egl = C.CDLL(os.path.join(dll, 'libEGL.dll')); gl = C.CDLL(os.path.join(dll, 'libGLESv2.dll'))
|
||||
egl.eglGetProcAddress.restype = C.c_void_p
|
||||
for n in ['eglInitialize', 'eglChooseConfig', 'eglCreatePbufferSurface', 'eglCreateContext', 'eglMakeCurrent', 'eglBindAPI', 'eglGetError']:
|
||||
getattr(egl, n).restype = C.c_void_p if n.startswith('eglCreate') else C.c_uint
|
||||
egl.eglCreatePbufferSurface.argtypes = [C.c_void_p, C.c_void_p, C.POINTER(C.c_int)]
|
||||
egl.eglCreateContext.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p, C.POINTER(C.c_int)]
|
||||
egl.eglMakeCurrent.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p]
|
||||
egl.eglInitialize.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p]
|
||||
egl.eglChooseConfig.argtypes = [C.c_void_p, C.POINTER(C.c_int), C.POINTER(C.c_void_p), C.c_int, C.POINTER(C.c_int)]
|
||||
GPD = C.CFUNCTYPE(C.c_void_p, C.c_uint, C.c_void_p, C.POINTER(C.c_int))(egl.eglGetProcAddress(b'eglGetPlatformDisplayEXT'))
|
||||
def ia(*v): return (C.c_int * (len(v) + 1))(*v, 0x3038)
|
||||
disp = GPD(0x3202, None, ia(0x3203, 0x3208)) # EGL_PLATFORM_ANGLE_TYPE_ANGLE = D3D11
|
||||
assert egl.eglInitialize(disp, None, None), 'eglInitialize %x' % egl.eglGetError()
|
||||
cfg = C.c_void_p(); ncfg = C.c_int()
|
||||
egl.eglChooseConfig(disp, ia(0x3033, 0x1, 0x3040, 0x40), C.byref(cfg), 1, C.byref(ncfg)) # PBUFFER, ES3
|
||||
surf = egl.eglCreatePbufferSurface(disp, cfg, ia(0x3057, 16, 0x3056, 16))
|
||||
egl.eglBindAPI(0x30A0)
|
||||
ctx_attr = (0x3098, 3) + ((0x33AC, 1) if webgl else ()) # CLIENT_VERSION 3 [, WEBGL_COMPATIBILITY_ANGLE]
|
||||
ctx = egl.eglCreateContext(disp, cfg, None, ia(*ctx_attr))
|
||||
assert ctx and egl.eglMakeCurrent(disp, surf, surf, ctx), 'context %x' % egl.eglGetError()
|
||||
gl.glGetString.restype = C.c_char_p
|
||||
print('GL_RENDERER', gl.glGetString(0x1F01).decode()); print('GL_VERSION', gl.glGetString(0x1F02).decode())
|
||||
print('webgl compatibility context', webgl)
|
||||
|
||||
gl.glCreateShader.restype = C.c_uint; gl.glCreateProgram.restype = C.c_uint
|
||||
def shader(kind, src):
|
||||
s = gl.glCreateShader(kind); b = C.c_char_p(src.encode()); gl.glShaderSource(s, 1, C.byref(b), None); gl.glCompileShader(s)
|
||||
ok = C.c_int(); gl.glGetShaderiv(s, 0x8B81, C.byref(ok))
|
||||
if not ok.value:
|
||||
buf = C.create_string_buffer(4000); gl.glGetShaderInfoLog(s, 4000, None, buf); raise RuntimeError(buf.value.decode()[:300])
|
||||
return s
|
||||
VS = '#version 300 es\nvoid main() { vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }\n'
|
||||
lib = open(os.environ['LIBGLSL']).read() if os.environ.get('LIBGLSL') else ''.join(open(os.path.join(sh, f)).read() for f in ['CultMath.glsl', 'CultMath.Cycles.glsl', 'CultMath.Phacelle.glsl'])
|
||||
vao = C.c_uint(); gl.glGenVertexArrays(1, C.byref(vao)); gl.glBindVertexArray(vao)
|
||||
d = json.load(open(fx)); man = []; skipped = 0
|
||||
for idx, f in enumerate(d['functions']):
|
||||
r, name, ps = fam.parse(f['glsl']); rt = fam.ty(r); pts = [fam.ty(p) for p in ps]
|
||||
if rt is None or any(p is None for p in pts): skipped += 1; continue
|
||||
nin = sum(p[1] for p in pts); nout = rt[1]; tw = (nin + 3) // 4; nt = (nout + 3) // 4; tag = 'f%03d' % idx
|
||||
words = [int(x, 16) for c in f['cases'] for x in c.split('->')[0].split()]
|
||||
tex = []
|
||||
for ci in range(256):
|
||||
row = words[ci * nin:(ci + 1) * nin]; tex += row + [0] * (tw * 4 - nin)
|
||||
lines, off = [], 0
|
||||
for j, (p, (ht, n, k)) in enumerate(zip(ps, pts)):
|
||||
src = ', '.join('w[%d]' % (off + t) for t in range(n)); v = ('uvec%d(%s)' % (n, src)) if n > 1 else src
|
||||
conv = {'f': 'uintBitsToFloat', 'i': ('ivec%d' % n if n > 1 else 'int'), 'u': ''}[k]
|
||||
lines.append(' %s a%d = %s(%s);' % (p, j, conv, v)); off += n
|
||||
lines.append(' %s r = %s(%s);' % (r, name, ', '.join('a%d' % j for j in range(len(ps)))))
|
||||
comps = ['r[%d][%d]' % (a, b) for a in range(3) for b in range(3)] if r == 'mat3' else ['r'] if nout == 1 else ['r[%d]' % i for i in range(nout)]
|
||||
for i, c in enumerate(comps):
|
||||
lines.append(' ow[%d] = %s(%s);' % (i, {'f': 'floatBitsToUint', 'i': 'uint', 'u': ''}[rt[2]], c))
|
||||
outs = ''.join('layout(location = %d) out highp uvec4 o%d;\n' % (t, t) for t in range(nt))
|
||||
wr = ''.join(' o%d = uvec4(ow[%d], ow[%d], ow[%d], ow[%d]);\n' % (t, 4 * t, 4 * t + 1, 4 * t + 2, 4 * t + 3) for t in range(nt))
|
||||
FS = ('#version 300 es\nprecision highp float;\nprecision highp int;\n' + lib + '\nuniform highp usampler2D In;\n' + outs +
|
||||
'void main() {\n int i = int(gl_FragCoord.y) * 16 + int(gl_FragCoord.x);\n uint w[%d];\n for (int k = 0; k < %d; k++) w[k] = texelFetch(In, ivec2(k / 4, i), 0)[k %% 4];\n'
|
||||
' uint ow[%d];\n' % (tw * 4, nin, nt * 4) + '\n'.join(lines) + '\n' + ''.join(' ow[%d] = 0u;\n' % z for z in range(nout, nt * 4)) + wr + '}\n')
|
||||
try:
|
||||
prog = gl.glCreateProgram(); gl.glAttachShader(prog, shader(0x8B31, VS)); gl.glAttachShader(prog, shader(0x8B30, FS)); gl.glLinkProgram(prog)
|
||||
except RuntimeError as e:
|
||||
print('compile fail', f['glsl'], str(e).replace('\n', ' ')[:200]); continue
|
||||
gl.glUseProgram(prog)
|
||||
t_in = C.c_uint(); gl.glGenTextures(1, C.byref(t_in)); gl.glActiveTexture(0x84C0); gl.glBindTexture(0x0DE1, t_in)
|
||||
gl.glTexParameteri(0x0DE1, 0x2801, 0x2600); gl.glTexParameteri(0x0DE1, 0x2800, 0x2600)
|
||||
buf = (C.c_uint * len(tex))(*tex); gl.glTexImage2D(0x0DE1, 0, 0x8D70, tw, 256, 0, 0x8D99, 0x1405, buf)
|
||||
gl.glUniform1i(gl.glGetUniformLocation(prog, b'In'), 0)
|
||||
fbo = C.c_uint(); gl.glGenFramebuffers(1, C.byref(fbo)); gl.glBindFramebuffer(0x8D40, fbo)
|
||||
tos = (C.c_uint * nt)(); gl.glGenTextures(nt, tos)
|
||||
for t in range(nt):
|
||||
gl.glBindTexture(0x0DE1, tos[t]); gl.glTexImage2D(0x0DE1, 0, 0x8D70, 16, 16, 0, 0x8D99, 0x1405, None)
|
||||
gl.glFramebufferTexture2D(0x8D40, 0x8CE0 + t, 0x0DE1, tos[t], 0)
|
||||
gl.glDrawBuffers(nt, (C.c_uint * nt)(*[0x8CE0 + t for t in range(nt)]))
|
||||
gl.glActiveTexture(0x84C0); gl.glBindTexture(0x0DE1, t_in)
|
||||
gl.glViewport(0, 0, 16, 16); gl.glDrawArrays(0x0004, 0, 3)
|
||||
res = [[0] * 1024 for _ in range(nt)]
|
||||
for t in range(nt):
|
||||
gl.glReadBuffer(0x8CE0 + t); px = (C.c_uint * 1024)(); gl.glReadPixels(0, 0, 16, 16, 0x8D99, 0x1405, px); res[t] = list(px)
|
||||
flat = []
|
||||
for ci in range(256):
|
||||
allw = sum((res[t][ci * 4:ci * 4 + 4] for t in range(nt)), []); flat += allw[:nout]
|
||||
open(os.path.join(out, tag + '.in'), 'wb').write(b'\0')
|
||||
open(os.path.join(out, tag + '.angle.out'), 'wb').write(struct.pack('<%dI' % len(flat), *flat))
|
||||
man.append({'tag': tag, 'glsl': f['glsl'], 'n': 256, 'nout': nout, 'kind': rt[2], 'tol': f['tolerance'], 'platform': f.get('platform')})
|
||||
gl.glDeleteFramebuffers(1, C.byref(fbo)); gl.glDeleteTextures(nt, tos); gl.glDeleteTextures(1, C.byref(t_in)); gl.glDeleteProgram(prog)
|
||||
print('gl error at end 0x%x' % gl.glGetError(), '; ran', len(man), 'skipped struct', skipped)
|
||||
json.dump(man, open(os.path.join(out, 'manifest.json'), 'w'))
|
||||
fam.cmp(fx, out, 'angle')
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Probe: per fixture family, generate an HLSL and a GLSL ES 3.10 compute shader that call the family's
|
||||
function on the fixture's argument bits, and compare device results with the fixture's result bits.
|
||||
usage: fam.py gen <fixture> <outdir> | fam.py cmp <fixture> <outdir> <tag>"""
|
||||
import json, re, struct, sys, os
|
||||
|
||||
SCAL = {'float': ('float', 1, 'f'), 'int': ('int', 1, 'i'), 'uint': ('uint', 1, 'u')}
|
||||
def ty(t):
|
||||
if t in SCAL: return SCAL[t]
|
||||
m = re.fullmatch(r'(i|u|)vec([234])', t)
|
||||
if m:
|
||||
k = {'': 'f', 'i': 'i', 'u': 'u'}[m.group(1)]; n = int(m.group(2))
|
||||
return ({'f': 'float', 'i': 'int', 'u': 'uint'}[k] + str(n), n, k)
|
||||
if t == 'mat3': return ('float3x3', 9, 'f')
|
||||
return None
|
||||
|
||||
def parse(sig):
|
||||
m = re.fullmatch(r'(\w+)\s+(\w+)\((.*)\)', sig)
|
||||
ps = [p.strip() for p in m.group(3).split(',') if p.strip()]
|
||||
return m.group(1), m.group(2), ps
|
||||
|
||||
def hlsl_read(k, words):
|
||||
return {'f': 'asfloat', 'i': 'asint', 'u': ''}[k] + '(' + words + ')'
|
||||
|
||||
def gen(fx, out):
|
||||
d = json.load(open(fx)); man = []; skipped = []
|
||||
for idx, f in enumerate(d['functions']):
|
||||
r, name, ps = parse(f['glsl'])
|
||||
rt = ty(r); pts = [ty(p) for p in ps]
|
||||
if rt is None or any(p is None for p in pts):
|
||||
skipped.append(f['glsl']); continue
|
||||
nin = sum(p[1] for p in pts); nout = rt[1]
|
||||
tag = 'f%03d' % idx
|
||||
words = []
|
||||
for c in f['cases']:
|
||||
a, b = c.split('->'); aw = a.split(); assert len(aw) == nin, (f['glsl'], len(aw), nin)
|
||||
words += [int(x, 16) for x in aw]
|
||||
open(os.path.join(out, tag + '.in'), 'wb').write(struct.pack('<%dI' % len(words), *words) if words else b'\0\0\0\0')
|
||||
# HLSL
|
||||
hl, gl, off = [], [], 0
|
||||
for j, (p, (ht, n, k)) in enumerate(zip(ps, pts)):
|
||||
idxs = ', '.join('In[b + %d]' % (off + t) for t in range(n))
|
||||
gidx = ', '.join('In.a[b + %du]' % (off + t) for t in range(n))
|
||||
if ht == 'float3x3': raise SystemExit('mat3 arg unsupported')
|
||||
vec = ('uint%d(%s)' % (n, idxs)) if n > 1 else idxs
|
||||
hl.append(' %s a%d = %s;' % (ht, j, hlsl_read(k, vec) if k != 'u' else vec))
|
||||
gvec = ('uvec%d(%s)' % (n, gidx)) if n > 1 else gidx
|
||||
gfun = {'f': 'uintBitsToFloat', 'i': ('ivec%d' % n if n > 1 else 'int'), 'u': ''}[k]
|
||||
gl.append(' %s a%d = %s(%s);' % (p, j, gfun, gvec))
|
||||
off += n
|
||||
args = ', '.join('a%d' % j for j in range(len(ps)))
|
||||
hl.append(' %s r = %s(%s);' % (rt[0], name, args))
|
||||
gl.append(' %s r = %s(%s);' % (r, name, args))
|
||||
if rt[0] == 'float3x3':
|
||||
comps = ['r[%d][%d]' % (i, j) for i in range(3) for j in range(3)]
|
||||
elif rt[1] == 1: comps = ['r']
|
||||
else: comps = ['r[%d]' % i for i in range(rt[1])]
|
||||
for i, c in enumerate(comps):
|
||||
hl.append(' Out[o + %d] = %s;' % (i, {'f': 'asuint', 'i': 'asuint', 'u': ''}[rt[2]] + '(' + c + ')'))
|
||||
gl.append(' Out.a[o + %du] = %s;' % (i, {'f': 'floatBitsToUint', 'i': 'uint', 'u': ''}[rt[2]] + '(' + c + ')'))
|
||||
n = len(f['cases'])
|
||||
open(os.path.join(out, tag + '.hlsl'), 'w').write(
|
||||
'#include "CultMath.hlsl"\n[[vk::binding(0)]] StructuredBuffer<uint> In;\n[[vk::binding(1)]] RWStructuredBuffer<uint> Out;\n'
|
||||
'[numthreads(64, 1, 1)] void main(uint3 id : SV_DispatchThreadID) {\n uint i = id.x; if (i >= %du) return; uint b = i * %du; uint o = i * %du;\n%s\n}\n'
|
||||
% (n, nin, nout, '\n'.join(hl)))
|
||||
open(os.path.join(out, tag + '.body.glsl'), 'w').write(
|
||||
'layout(local_size_x = 64) in;\nlayout(std430, binding = 0) readonly buffer IB { uint a[]; } In;\nlayout(std430, binding = 1) buffer OB { uint a[]; } Out;\n'
|
||||
'void main() {\n uint i = gl_GlobalInvocationID.x; if (i >= %du) return; uint b = i * %du; uint o = i * %du;\n%s\n}\n'
|
||||
% (n, nin, nout, '\n'.join(gl)))
|
||||
man.append({'tag': tag, 'glsl': f['glsl'], 'n': n, 'nout': nout, 'kind': rt[2], 'tol': f['tolerance'], 'platform': f.get('platform')})
|
||||
json.dump(man, open(os.path.join(out, 'manifest.json'), 'w'))
|
||||
print('generated', len(man), 'families; skipped (struct types)', len(skipped), ':', '; '.join(skipped))
|
||||
|
||||
def ordered(u): # float bits -> monotonic int for ulp distance
|
||||
return u if u < 0x80000000 else 0x80000000 - u
|
||||
|
||||
def cmp(fx, out, tag):
|
||||
d = json.load(open(fx)); man = json.load(open(os.path.join(out, 'manifest.json')))
|
||||
rows = []; buckets = {'exact': 0, '<=4': 0, '<=64': 0, '>64': 0, 'missing': 0}
|
||||
for m in man:
|
||||
p = os.path.join(out, m['tag'] + '.' + tag + '.out')
|
||||
if not os.path.exists(p): buckets['missing'] += 1; rows.append((m['glsl'], 'MISSING', '')); continue
|
||||
got = struct.unpack('<%dI' % (m['n'] * m['nout']), open(p, 'rb').read())
|
||||
f = d['functions'][int(m['tag'][1:])]
|
||||
worst = 0; exact = 0; nanmis = 0
|
||||
for ci, c in enumerate(f['cases']):
|
||||
exp = [int(x, 16) for x in c.split('->')[1].split()]
|
||||
g = got[ci * m['nout']:(ci + 1) * m['nout']]
|
||||
if list(g) == exp: exact += 1; continue
|
||||
for e, h in zip(exp, g):
|
||||
if m['kind'] != 'f': worst = max(worst, 1 << 40); continue
|
||||
en = (e & 0x7f800000) == 0x7f800000 and e & 0x7fffff; hn = (h & 0x7f800000) == 0x7f800000 and h & 0x7fffff
|
||||
if en or hn:
|
||||
if not (en and hn): nanmis += 1
|
||||
continue
|
||||
fe = struct.unpack('<f', struct.pack('<I', e))[0]; fh = struct.unpack('<f', struct.pack('<I', h))[0]
|
||||
if fe != fe or fh != fh or abs(fe) == float('inf') or abs(fh) == float('inf'):
|
||||
if not (fe == fh or (fe != fe and fh != fh)): nanmis += 1
|
||||
continue
|
||||
worst = max(worst, abs(fe - fh) / max(abs(fe), 1.0) / 2.0 ** -23)
|
||||
b = 'exact' if exact == m['n'] else '<=4' if worst <= 4 and not nanmis else '<=64' if worst <= 64 and not nanmis else '>64'
|
||||
if worst >= 1 << 40: b = '>64'
|
||||
buckets[b] += 1
|
||||
rows.append((m['glsl'], '%d/%d exact' % (exact, m['n']), 'max err %s ulp(max(|v|,1))%s' % (('%.1f' % worst) if worst < 1 << 40 else 'int-mismatch', ', nan-mismatch %d' % nanmis if nanmis else '')))
|
||||
print(tag, 'buckets', buckets)
|
||||
for r in sorted(rows, key=lambda r: r[1])[:0]: pass
|
||||
rows.sort(key=lambda r: -float(re.search(r'max err ([0-9.]+)', r[2]).group(1)) if re.search(r'max err ([0-9.]+)', r[2]) else -1e18)
|
||||
for r in rows[:int(os.environ.get('CMP_TOP', '25'))]:
|
||||
if not r[1].startswith('256/256'): print(' ', r[0], '|', r[1], '|', r[2])
|
||||
|
||||
if __name__ == '__main__':
|
||||
{'gen': lambda: gen(sys.argv[2], sys.argv[3]), 'cmp': lambda: cmp(sys.argv[2], sys.argv[3], sys.argv[4])}[sys.argv[1]]()
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Probe job, runs in ubuntu:24.04 with /src = CultLib at the probed revision. Q1 library path, Q2 lavapipe runs.
|
||||
set -u
|
||||
P=/tmp/p; W=/tmp/w; mkdir -p $W/fam $W/tools; cd $W
|
||||
step() { echo; echo "=== $*"; }
|
||||
step apt
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
|
||||
mesa-vulkan-drivers libvulkan-dev vulkan-tools glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
|
||||
dpkg-query -W -f='${Package} ${Version}\n' mesa-vulkan-drivers glslang-tools spirv-tools libvulkan-dev
|
||||
step dxc
|
||||
url=$(curl -s https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/tags/v1.9.2607 | python3 -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if 'linux' in a['name'] and a['name'].endswith('.tar.gz')][0])")
|
||||
echo "$url"; curl -sL --fail -o dxc.tgz "$url" && mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc; DXC=$(find tools/dxc -name dxc -type f | head -1); echo "DXC=$DXC"; $DXC --version 2>&1 | head -2
|
||||
step spirv-cross
|
||||
git clone -q --depth 1 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc && git -C tools/sc log -1 --format='SPIRV-Cross %H %cs' && \
|
||||
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls -la $SC | cut -c1-120
|
||||
SH=/src/packages/cultmath/shaders
|
||||
python3 - "$SH" <<'PY'
|
||||
import re,sys,os,shutil
|
||||
src=sys.argv[1]; dst='/tmp/w/shx'; shutil.copytree(src,dst,dirs_exist_ok=True); n=0
|
||||
for fn in os.listdir(dst):
|
||||
if fn.endswith('.hlsl'):
|
||||
t=open(os.path.join(dst,fn)).read(); t2,k=re.subn(r'(?m)^(?!static|export)(\w+(?:\dx\d)?\s+cultmath_\w+\s*\(([^)]*)\))', lambda m: m.group(1) if re.search('Texture2D|SamplerState', m.group(2)) else 'export ' + m.group(1), t); k=len(re.findall(r'(?m)^export ', t2)); n+=k; open(os.path.join(dst,fn),'w').write(t2)
|
||||
print('export keywords added', n)
|
||||
PY
|
||||
step "L1 dxc lib_6_3 universal1.5, export on non-texture defs"
|
||||
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -I shx shx/CultMath.hlsl -Fo lib.spv 2>&1 | head -3; echo "exit ${PIPESTATUS[0]}"
|
||||
spirv-dis lib.spv -o lib.spvasm; echo "exports $(grep -c 'LinkageAttributes.*Export' lib.spvasm); capabilities $(grep -c OpCapability lib.spvasm)"
|
||||
step "L1b -default-linkage external, no export keyword"
|
||||
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -default-linkage external -I $SH $SH/CultMath.hlsl -Fo libd.spv 2>&1 | head -3; echo "exit ${PIPESTATUS[0]}"
|
||||
[ -s libd.spv ] && { spirv-dis libd.spv -o libd.spvasm; echo "exports $(grep -c 'LinkageAttributes.*Export' libd.spvasm)"; }
|
||||
step "L2 strip every capability but Shader and Linkage, every extension; SPIRV-Cross ES 310 and 300"
|
||||
grep -vP '^\s*OpCapability (?!Shader$|Linkage$)|^\s*OpExtension ' lib.spvasm > s.spvasm; spirv-as --target-env spv1.5 s.spvasm -o s.spv; echo "as exit $?"
|
||||
for v in 310 300; do $SC s.spv --es --version $v --output s.$v.glsl 2>&1 | head -2; echo "es$v exit ${PIPESTATUS[0]}"; done
|
||||
G=s.310.glsl; echo "G=$G"; [ -s "$G" ] || { echo PROBE-DONE; exit 0; }
|
||||
step "L3 shape of $G"
|
||||
echo "lines $(wc -l < $G); defs cultmath_: $(grep -cE '^\w+ cultmath_\w+\(' $G); distinct: $(grep -oE '^\w+ cultmath_\w+\(' $G | sed -E 's/^\w+ //' | sort -u | wc -l); renamed _N: $(grep -oE '^\w+ cultmath_\w+_[0-9]+\(' $G | wc -l); _NNN temporaries: $(grep -oE '\b_[0-9]+\b' $G | wc -l); non-cultmath defs: $(grep -cE '^\w+ (?!cultmath_)\w+\(' -P $G)"
|
||||
grep -nE '^#|^layout|precision|^struct|^void main' $G | head -12
|
||||
echo "--- renamed defs sample:"; grep -oE '^\w+ cultmath_\w+_[0-9]+\([^)]*\)' $G | head -6
|
||||
echo "--- snoise(vec3):"; awk '/^(highp )?float cultmath_snoise\((highp )?vec3/{p=1} p{print; if(++n>=22) exit}' $G
|
||||
sed -E '/^#version/d; /^layout\(local_size/d' $G | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}' > lib.body.glsl
|
||||
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl; printf 'out highp vec4 c;\nvoid main() { c = vec4(cultmath_snoise(gl_FragCoord.xyz)); }\n'; } > es300.frag
|
||||
glslangValidator es300.frag > es300.txt 2>&1; echo "glslang ES 3.00 fragment with the library: exit $? $(grep -m3 ERROR es300.txt | tr '\n' ' ' | cut -c1-300)"
|
||||
step "L4 library through glslang on lavapipe"
|
||||
gcc -O2 -o vkrun $P/vkrun.c -lvulkan 2>/dev/null; python3 $P/fam.py gen /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam >/dev/null
|
||||
okl=0; for b in fam/*.body.glsl; do t=${b%.body.glsl}; tag=$(basename $t); n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl $b; } > $t.l.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $t.l.spv $t.l.comp >$t.l.log 2>&1 && ./vkrun $t.l.spv $t.in $t.lib.out $n && okl=$((okl+1)); done
|
||||
echo "library path ran $okl of 105"; grep -h -m1 ERROR fam/*.l.log | sed -E 's/[0-9]+:/N:/g' | sort | uniq -c | sort -rn | head -6
|
||||
CMP_TOP=8 python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam lib
|
||||
echo "LIBGZ-BEGIN"; gzip -9c lib.body.glsl | base64 -w0; echo; echo "LIBGZ-END"
|
||||
echo PROBE-DONE
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Probe: by-value parameter lowering on a SPIR-V module. dxc -Od passes every parameter as a Function pointer and the
|
||||
caller copies in; a callee that assigns its parameter then comes out of SPIRV-Cross as `inout`. This pass makes each
|
||||
Function-pointer parameter a value parameter: the old param id becomes a local variable in the entry block, stored from the
|
||||
new value parameter; each call loads the pointer it passed. usage: byvalue.py <in.spv> <out.spv>"""
|
||||
import struct, sys
|
||||
def load(p):
|
||||
b = open(p, 'rb').read(); w = list(struct.unpack('<%dI' % (len(b) // 4), b)); ins = []; i = 5
|
||||
while i < len(w): n = w[i] >> 16; ins.append(w[i:i + n]); i += n
|
||||
return w[:5], ins
|
||||
def op(x): return x[0] & 0xffff
|
||||
def mk(o, *ws): return [((len(ws) + 1) << 16) | o, *ws]
|
||||
h, ins = load(sys.argv[1]); bound = h[3]
|
||||
ptr = {x[1]: (x[2], x[3]) for x in ins if op(x) == 32} # id -> (storage, pointee)
|
||||
ftype = {x[1]: x for x in ins if op(x) == 33}
|
||||
fnparams = {}; cur = None; fntype = {}
|
||||
for x in ins: # analysis: per function, which params are Function pointers
|
||||
if op(x) == 54: cur = x[2]; fnparams[cur] = []; fntype[cur] = x[4]
|
||||
elif op(x) == 55 and cur is not None:
|
||||
t = x[1]; conv = t in ptr and ptr[t][0] == 7
|
||||
fnparams[cur].append((x[2], t, ptr[t][1] if conv else None, bound if conv else None)); bound += conv
|
||||
newtype = {}; typeins = []; sig = {tuple(x[2:]): x[1] for x in ins if op(x) == 33}
|
||||
for f, ps in fnparams.items():
|
||||
if any(p[2] for p in ps):
|
||||
ft = ftype[fntype[f]]; key = (ft[2], *[p[2] if p[2] else p[1] for p in ps])
|
||||
if key not in sig: bound += 1; sig[key] = bound; typeins.append(mk(33, bound, *key))
|
||||
newtype[f] = sig[key]
|
||||
names = {x[1]: x for x in ins if op(x) == 5}
|
||||
out = []; cur = None; pend = []; need_stores = False; first = True; pc = 0
|
||||
for x in ins:
|
||||
o = op(x)
|
||||
if o == 54 and first: out += typeins; first = False
|
||||
if need_stores and o != 59:
|
||||
out += [mk(62, p, q) for p, _, q in pend]; pend = []; need_stores = False
|
||||
if o == 54:
|
||||
cur = x[2]; pc = 0
|
||||
if cur in newtype: x = x[:4] + [newtype[cur]] + x[5:]
|
||||
elif o == 55:
|
||||
_, t, pointee, q = fnparams[cur][pc]; pc += 1
|
||||
if pointee: out.append(mk(55, pointee, q)); pend.append((x[2], t, q)); continue
|
||||
elif o == 248 and pend:
|
||||
out.append(x); out += [mk(59, t, p, 7) for p, t, _ in pend]; need_stores = True; continue
|
||||
elif o == 57:
|
||||
callee = x[3]; args = x[4:]; new = []
|
||||
for a, (_, _, pointee, _) in zip(args, fnparams[callee]):
|
||||
if pointee: bound += 1; out.append(mk(61, pointee, bound, a)); new.append(bound)
|
||||
else: new.append(a)
|
||||
x = x[:4] + new; x[0] = (len(x) << 16) | 57
|
||||
out.append(x)
|
||||
if o == 5 and x[1] in [p[0] for ps in fnparams.values() for p in ps if p[2]]:
|
||||
q = next(p[3] for ps in fnparams.values() for p in ps if p[0] == x[1]); out.append([x[0], q] + x[2:])
|
||||
h[3] = bound + 1; w = h + [v for x in out for v in x]; open(sys.argv[2], 'wb').write(struct.pack('<%dI' % len(w), *w))
|
||||
print('byvalue: %d functions changed, %d params' % (len(newtype), sum(1 for ps in fnparams.values() for p in ps if p[2])))
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Imagination probe: what dxc -Od leaves in the SPIRV-Cross GLSL and what cleans it. ubuntu:24.04, /src = CultLib, /cache persists tools.
|
||||
set -u; P=/src/docs/research/native-shader-probes; O=$P/overload-wrappers; W=/tmp/w; mkdir -p $W /cache; cd $W
|
||||
step() { echo; echo "=== $*"; }
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 gcc \
|
||||
mesa-vulkan-drivers libvulkan-dev glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
|
||||
dpkg-query -W -f='${Package} ${Version}\n' spirv-tools glslang-tools mesa-vulkan-drivers
|
||||
if [ ! -x /cache/sc/spirv-cross ]; then
|
||||
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Cross.git /cache/scsrc
|
||||
cmake -S /cache/scsrc -B /cache/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF -DSPIRV_CROSS_CLI=ON >/dev/null && cmake --build /cache/scb --target spirv-cross -j8 >/dev/null 2>&1
|
||||
mkdir -p /cache/sc; cp /cache/scb/spirv-cross /cache/sc/
|
||||
fi
|
||||
if [ ! -x /cache/dxc/bin/dxc ]; then
|
||||
curl -sL --fail -o /cache/dxc.tgz https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz
|
||||
mkdir -p /cache/dxc && tar xzf /cache/dxc.tgz -C /cache/dxc
|
||||
fi
|
||||
DXC=$(find /cache/dxc -name dxc -type f | head -1); SC=/cache/sc/spirv-cross; export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:${LD_LIBRARY_PATH:-}
|
||||
if [ ! -x /cache/st/spirv-opt ]; then
|
||||
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Tools.git /cache/stsrc
|
||||
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Headers.git /cache/stsrc/external/spirv-headers
|
||||
cmake -S /cache/stsrc -B /cache/stb -DCMAKE_BUILD_TYPE=Release -DSPIRV_SKIP_TESTS=ON -DSPIRV_WERROR=OFF >/dev/null && cmake --build /cache/stb --target spirv-opt spirv-val spirv-dis spirv-as -j8 >/dev/null 2>&1
|
||||
mkdir -p /cache/st; cp /cache/stb/tools/spirv-* /cache/st/
|
||||
fi
|
||||
export PATH=/cache/st:$PATH; echo "spirv-opt from $(which spirv-opt)"
|
||||
echo "dxc $DXC"; $DXC --version 2>&1|head -2; spirv-opt --version | head -1
|
||||
SH=/src/packages/cultmath/shaders; FX=/src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
|
||||
step "1 build the -Od library as in Pass 9b"
|
||||
python3 $O/wrapgen.py $SH shw
|
||||
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od -I shw shw/cm_wrappers.hlsl -Fo w.spv 2>&1 | head -3
|
||||
python3 $O/spvedit.py edit w.spv e.spv shw/wrappers.tsv export; spirv-val --target-env spv1.5 e.spv && echo "val ok"
|
||||
nf() { spirv-dis "$1" | grep -c ' = OpFunction '; }; echo "functions in e.spv: $(nf e.spv)"
|
||||
step "2 variants: by-value lowering, then spirv-opt (never an inliner)"
|
||||
cp e.spv v0.spv
|
||||
python3 $P/od-temporaries/byvalue.py e.spv b0.spv; spirv-val --target-env spv1.5 b0.spv && echo "b0 val ok"
|
||||
spirv-opt --target-env=spv1.5 --ssa-rewrite --eliminate-dead-code-aggressive --simplify-instructions e.spv -o v2.spv
|
||||
SSA="--ssa-rewrite --eliminate-dead-code-aggressive"
|
||||
declare -A VP=([b1]="--eliminate-local-single-store --eliminate-dead-code-aggressive" [b2]="$SSA" [b3]="$SSA --simplify-instructions" [b4]="--eliminate-local-single-block --eliminate-local-single-store --eliminate-local-multi-store --eliminate-dead-code-aggressive --merge-blocks")
|
||||
for v in b1 b2 b3 b4; do spirv-opt --target-env=spv1.5 ${VP[$v]} b0.spv -o $v.spv 2>$v.err; echo "$v opt exit $? functions $(nf $v.spv 2>/dev/null) $(head -c 160 $v.err)"; done
|
||||
cp b1.spv b1r.spv; VS="v0 b0 b1 b1r b3"
|
||||
for v in $VS; do [ -s $v.spv ] || continue
|
||||
spirv-val --target-env spv1.5 $v.spv >$v.val 2>&1; vx=$?
|
||||
$SC $v.spv --es --version 310 $([ $v = b1r ] && echo --relax-nan-checks) --output $v.glsl >$v.sc 2>&1; sx=$?
|
||||
[ -s $v.glsl ] || { echo "$v val $vx sc exit $sx"; head -3 $v.val $v.sc; continue; }
|
||||
python3 - $v $vx $sx <<'PY'
|
||||
import re,sys
|
||||
v,vx,sx=sys.argv[1:4]; g=open(f'/tmp/w/{v}.glsl').read()
|
||||
defs=re.findall(r'(?m)^(?:\w+ )*?(\w+) (cultmath_\w+)\(([^)]*)\)\n\{',g)
|
||||
ids=re.findall(r'\b_\d+\b',g)
|
||||
print(f'{v}: val={vx} sc={sx} lines={g.count(chr(10))} bytes={len(g)} cultmath_defs={len(defs)} distinct={len(set(d[1] for d in defs))} id_tokens={len(ids)} distinct_ids={len(set(ids))} param_var={g.count("param_var_")} inout={len(re.findall(r"inout",g))} suffixed={len(re.findall(r"cultmath_\w+_\d+\(",g))}')
|
||||
PY
|
||||
done
|
||||
mkdir -p /cache/out; cp *.glsl *.spv /cache/out/ 2>/dev/null
|
||||
step "3 SPIRV-Cross options"
|
||||
for opt in "--remove-unused-variables"; do for v in v0 b2; do $SC $v.spv --es --version 310 $opt --output $v.o.glsl >/dev/null 2>&1; echo "$v + $opt: lines $(wc -l < $v.o.glsl)"; done; done
|
||||
step "4 lerp(vec3) as emitted: v0 vs b2, vs committed"
|
||||
for v in v0 b2; do echo "--- $v lerp(vec3)"; awk '/^(highp )?vec3 cultmath_lerp\((highp )?vec3/{p=1} p{print} p&&/^}/{exit}' $v.glsl; done
|
||||
echo "--- committed lerp(vec3)"; grep -n -A3 -E '^(highp )?vec3 cultmath_lerp\(' $SH/CultMath.glsl | head -6
|
||||
for v in v0 b2; do echo "--- $v snoise(vec3) head"; awk '/^(highp )?float cultmath_snoise\((highp )?vec3/{p=1} p{print; if(++n>=40) exit}' $v.glsl; done
|
||||
step "5 conformance on lavapipe, every family, per variant vs committed GLSL and HLSL"
|
||||
mkdir -p fam; gcc -O2 -o vkrun $P/vkrun.c -lvulkan && python3 $P/fam.py gen $FX fam >/dev/null
|
||||
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
|
||||
body() { sed -E '/^#version/d; /^#ifdef SPIRV_CROSS/d; /^#endif/d; /^layout\(local_size/d' $1 | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}'; }
|
||||
for v in $VS; do [ -s $v.glsl ] && body $v.glsl > $v.body; done
|
||||
run() { local t=fam/$1 n; n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$1'];print(m['n'],m['nout'])")
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat $3 $t.body.glsl; } > $t.$2.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $t.$2.spv $t.$2.comp >$t.$2.log 2>&1 && ./vkrun $t.$2.spv $t.in $t.$2.out $n; }
|
||||
for b in fam/*.body.glsl; do tag=$(basename ${b%.body.glsl})
|
||||
n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH fam/$tag.hlsl -Fo fam/$tag.h.spv >/dev/null 2>&1 && ./vkrun fam/$tag.h.spv fam/$tag.in fam/$tag.h.out $n
|
||||
run $tag g all.glsl; for v in $VS; do [ -s $v.body ] && run $tag $v $v.body; done; done
|
||||
export VS; python3 - <<'PY'
|
||||
import os,filecmp
|
||||
d='/tmp/w/fam'; tags=sorted(f[:-10] for f in os.listdir(d) if f.endswith('.body.glsl'))
|
||||
def same(a,b): return [t for t in tags if os.path.exists(f'{d}/{t}.{a}.out') and os.path.exists(f'{d}/{t}.{b}.out') and filecmp.cmp(f'{d}/{t}.{a}.out',f'{d}/{t}.{b}.out',shallow=False)]
|
||||
print('families',len(tags),'hlsl ran',len([t for t in tags if os.path.exists(f'{d}/{t}.h.out')]),'committed glsl ran',len([t for t in tags if os.path.exists(f'{d}/{t}.g.out')]))
|
||||
for v in os.environ['VS'].split():
|
||||
ran=[t for t in tags if os.path.exists(f'{d}/{t}.{v}.out')]
|
||||
sg=same(v,'g'); print(v,'ran',len(ran),'bit-identical to committed glsl',len(sg),'to hlsl',len(same(v,'h')),'| differs from g:',[t for t in ran if t not in sg][:8])
|
||||
print('committed vs hlsl',len(same('g','h')),'of',len(tags))
|
||||
PY
|
||||
for v in v0 b2; do { printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat $v.body; printf 'out highp vec4 c;\nvoid main() { vec3 p = gl_FragCoord.xyz; c = vec4(cultmath_lerp(p, p.yzx, vec3(0.5)), cultmath_lerp(1.0, 2.0, p.x)) + cultmath_saturate(vec4(p, 1.0)) + vec4(cultmath_snoise(p)); }\n'; } > $v.es300.frag; glslangValidator $v.es300.frag >$v.es300.txt 2>&1; echo "$v ES 3.00 glslang exit $? $(grep -m2 ERROR $v.es300.txt | tr '\n' ' ' | cut -c1-200)"; done
|
||||
for t in g $VS; do echo "--- cmp vs C# fixture: $t"; CMP_TOP=3 python3 $P/fam.py cmp $FX fam $t 2>&1 | grep -v "^ *$" | head -8; done
|
||||
echo PROBE-DONE
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Imagination probe, lavapipe leg: every fixture family through three routes on lavapipe.
|
||||
# h = HLSL by dxc cs_6_0, g = committed GLSL by glslang, r = restored overloaded library GLSL (lib.body.glsl) by glslang.
|
||||
set -u; P=/tmp/p; W=/tmp/w; cd $W; SH=/src/packages/cultmath/shaders; FX=/src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
|
||||
echo; echo "=== 2b lavapipe: same computation"
|
||||
mkdir -p fam; gcc -O2 -o vkrun $P/vkrun.c -lvulkan && python3 $P/fam.py gen $FX fam >/dev/null
|
||||
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
|
||||
ok=(0 0 0); first=1
|
||||
for b in fam/*.body.glsl; do t=${b%.body.glsl}; tag=$(basename $t)
|
||||
n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
|
||||
$DXC_PATH -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $t.hlsl -Fo $t.h.spv >$t.h.log 2>&1 && ./vkrun $t.h.spv $t.in $t.h.out $n && ok[0]=$((ok[0]+1))
|
||||
for r in g r; do src=all.glsl; [ $r = r ] && src=lib.body.glsl
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat $src $b; } > $t.$r.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $t.$r.spv $t.$r.comp >$t.$r.log 2>&1 && ./vkrun $t.$r.spv $t.in $t.$r.out $n && { [ $r = g ] && ok[1]=$((ok[1]+1)) || ok[2]=$((ok[2]+1)); }
|
||||
done
|
||||
done
|
||||
echo "ran on lavapipe: hlsl ${ok[0]}, committed glsl ${ok[1]}, restored glsl ${ok[2]} (of $(ls fam/*.body.glsl | wc -l))"
|
||||
grep -h -m1 ERROR fam/*.r.log | sed -E 's/[0-9]+:/N:/g' | sort | uniq -c | sort -rn | head -5
|
||||
python3 - <<'PY'
|
||||
import os,filecmp
|
||||
d='/tmp/w/fam'; tags=sorted(f[:-10] for f in os.listdir(d) if f.endswith('.body.glsl'))
|
||||
def same(a,b): return [t for t in tags if os.path.exists(f'{d}/{t}.{a}.out') and os.path.exists(f'{d}/{t}.{b}.out') and filecmp.cmp(f'{d}/{t}.{a}.out',f'{d}/{t}.{b}.out',shallow=False)]
|
||||
rg=same('r','g'); rh=same('r','h'); gh=same('g','h')
|
||||
print('bit-identical: restored vs committed glsl',len(rg),'| restored vs hlsl',len(rh),'| committed glsl vs hlsl',len(gh),'of',len(tags))
|
||||
print('restored differs from committed glsl in:',[t for t in tags if t not in rg][:12])
|
||||
print('restored differs from hlsl but committed glsl matches hlsl:',[t for t in gh if t not in rh][:12])
|
||||
PY
|
||||
|
||||
CMP_TOP=10 python3 $P/fam.py cmp $FX fam r 2>&1 | head -16
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import re,sys,os,collections
|
||||
sh,g,out=sys.argv[1:4]; os.makedirs(out,exist_ok=True)
|
||||
origin={}; owner={'CultMath.hlsl':'core','CultMath.Interval.hlsl':'core','CultMath.Affine.hlsl':'core','CultMath.Cycles.hlsl':'cycles','CultMath.Phacelle.hlsl':'phacelle'}
|
||||
for fn,o in owner.items():
|
||||
t=open(os.path.join(sh,fn)).read()
|
||||
for m in re.finditer(r'(?m)^(?:export\s+)?\w+\s+(cultmath_\w+)\s*\(',t): origin.setdefault(m.group(1),o)
|
||||
for m in re.finditer(r'(?m)^struct\s+(\w+)',t): origin.setdefault(m.group(1),o)
|
||||
text=open(g).read()
|
||||
# top-level chunks: struct blocks and function definitions; everything else is prologue
|
||||
lines=text.split('\n'); chunks=[]; i=0; pro=[]
|
||||
while i<len(lines):
|
||||
l=lines[i]
|
||||
m=re.match(r'^struct (\w+)$',l) or re.match(r'^\w+ (\w+)\(.*\)$',l)
|
||||
if m and m.group(1)!='main':
|
||||
j=i
|
||||
while lines[j]!='};' and lines[j]!='}': j+=1
|
||||
chunks.append((m.group(1),'\n'.join(lines[i:j+1]))); i=j+1
|
||||
elif m:
|
||||
j=i
|
||||
while lines[j]!='}': j+=1
|
||||
i=j+1
|
||||
else:
|
||||
if l.strip(): pro.append(l)
|
||||
i+=1
|
||||
print('prologue lines',len(pro),':',' | '.join(pro[:6])[:300])
|
||||
cnt=collections.Counter(); unknown=[]; bad=collections.Counter(); files=collections.defaultdict(list)
|
||||
rank={'core':0,'cycles':1,'phacelle':1}
|
||||
for name,body in chunks:
|
||||
o=origin.get(name)
|
||||
if o is None: unknown.append(name); o='core'
|
||||
cnt[o]+=1; files[o].append(body)
|
||||
for callee in set(re.findall(r'\b(cultmath_\w+|[A-Z]\w+)\b',body))-{name}:
|
||||
co=origin.get(callee)
|
||||
if co and co!=o and not (rank[co]==0): bad[(o,co,name,callee)]+=1
|
||||
print('chunks',len(chunks),dict(cnt),'unknown-origin',len(unknown),unknown[:8])
|
||||
print('cross-licence calls (non-core callee):',len(bad),list(bad)[:6])
|
||||
seen=collections.Counter(n for n,_ in chunks); print('duplicate chunk names',[n for n,c in seen.items() if c>1][:6])
|
||||
for o,b in files.items(): open(os.path.join(out,o+'.glsl'),'w').write('\n'.join(b)+'\n')
|
||||
print('split sizes', {o:len('\n'.join(b).split('\n')) for o,b in files.items()})
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Probe: instruction-level SPIR-V edit (binary, no text regex).
|
||||
stats <spv> <wrappers.tsv> how each wrapper looks after dxc
|
||||
edit <in> <out> <wrappers.tsv> <mode> mode drop: strip caps (keep Shader, Linkage) and extensions, delete
|
||||
wrapper functions and their names/decorations, set each callee's OpName to the overloaded source name.
|
||||
mode export: as drop, but each wrapper's Export decoration moves to its callee instead of being deleted."""
|
||||
import struct, sys, collections
|
||||
def load(p):
|
||||
b = open(p, 'rb').read(); w = list(struct.unpack('<%dI' % (len(b) // 4), b)); ins = []; i = 5
|
||||
while i < len(w): n = w[i] >> 16; ins.append(w[i:i + n]); i += n
|
||||
return w[:5], ins
|
||||
def s_dec(ws):
|
||||
b = struct.pack('<%dI' % len(ws), *ws); return b[:b.index(0)].decode()
|
||||
def s_enc(s):
|
||||
b = s.encode() + b'\0'; b += b'\0' * (-len(b) % 4); return list(struct.unpack('<%dI' % (len(b) // 4), b))
|
||||
def op(x): return x[0] & 0xffff
|
||||
def funcs(ins):
|
||||
fs = {}; cur = None
|
||||
for k, x in enumerate(ins):
|
||||
if op(x) == 54: cur = x[2]; fs[cur] = [k, None, [], set()]
|
||||
if cur is not None:
|
||||
if op(x) == 57: fs[cur][2].append(x[3])
|
||||
o = op(x)
|
||||
if o == 248: fs[cur][3].add(x[1])
|
||||
elif o not in (8, 317, 62, 246, 247, 249, 250, 253, 254, 252) and len(x) > 2: fs[cur][3].add(x[2])
|
||||
if op(x) == 56: fs[cur][1] = k; cur = None
|
||||
return fs
|
||||
def names(ins): return {x[1]: s_dec(x[2:]) for x in ins if op(x) == 5}
|
||||
def wrappers(tsv): return [l.split('\t') for l in open(tsv).read().splitlines() if l]
|
||||
def analyse(ins, tsv):
|
||||
nm = names(ins); fs = funcs(ins); byname = {v: k for k, v in nm.items() if k in fs}; res = []
|
||||
for wn, name, fn, ps in wrappers(tsv):
|
||||
fid = byname.get(wn); callees = fs[fid][2] if fid else None
|
||||
res.append((wn, name, fid, callees))
|
||||
return nm, fs, res
|
||||
if sys.argv[1] == 'stats':
|
||||
h, ins = load(sys.argv[2]); nm, fs, res = analyse(ins, sys.argv[3])
|
||||
missing = [r[0] for r in res if r[2] is None]; inl = [r[0] for r in res if r[2] and not r[3]]
|
||||
multi = [r[0] for r in res if r[3] and len(r[3]) != 1]
|
||||
callee = collections.Counter(r[3][0] for r in res if r[3] and len(r[3]) == 1)
|
||||
wrong = [(r[0], nm.get(r[3][0])) for r in res if r[3] and len(r[3]) == 1 and nm.get(r[3][0]) != r[1]]
|
||||
print('functions %d, wrappers found %d of %d, missing %d, inlined (no call) %d, not-one-call %d' % (len(fs), len(res) - len(missing), len(res), len(missing), len(inl), len(multi)))
|
||||
print('distinct callees %d, callee shared by >1 wrapper %d' % (len(callee), sum(1 for c in callee.values() if c > 1)))
|
||||
print('callee OpName != source name: %d %s' % (len(wrong), wrong[:4]))
|
||||
calls = sum(len(f[2]) for f in fs.values()); print('total OpFunctionCall %d; non-wrapper functions %d' % (calls, len(fs) - len(res) + len(missing)))
|
||||
print('sample', [(r[0], nm.get(r[3][0]) if r[3] else None) for r in res[:3]], 'missing sample', missing[:3], 'inlined sample', inl[:3])
|
||||
sys.exit(0)
|
||||
_, src, dst, tsv, mode = sys.argv[1:6]
|
||||
h, ins = load(src); nm, fs, res = analyse(ins, tsv)
|
||||
drop_k = set(); drop_ids = set(); setname = {}; retarget = {}
|
||||
for wn, name, fid, callees in res:
|
||||
if fid is None or not callees or len(callees) != 1: sys.exit('refuse: wrapper %s is not one call' % wn)
|
||||
a, b, _, ids = fs[fid]; drop_k.update(range(a, b + 1)); drop_ids |= ids | {fid}
|
||||
setname[callees[0]] = name; retarget[fid] = callees[0]
|
||||
drop_ids |= set(retarget)
|
||||
outi = []; named = set()
|
||||
for k, x in enumerate(ins):
|
||||
if k in drop_k: continue
|
||||
o = op(x)
|
||||
if o == 17 and x[1] not in (1, 5): continue
|
||||
if o == 10: continue
|
||||
if o == 5 and x[1] in setname: x = [0] + [x[1]] + s_enc(setname[x[1]]); x[0] = (len(x) << 16) | 5; named.add(x[1])
|
||||
elif o in (5, 6) and x[1] in drop_ids: continue
|
||||
if o == 71 and x[1] in drop_ids:
|
||||
if mode == 'export' and x[1] in retarget and x[2] == 41: x = [x[0], retarget[x[1]]] + x[2:]
|
||||
else: continue
|
||||
outi.append(x)
|
||||
for fid, name in setname.items():
|
||||
if fid not in named: outi.insert(next(i for i, x in enumerate(outi) if op(x) == 5), [((2 + len(s_enc(name))) << 16) | 5, fid] + s_enc(name))
|
||||
w = h + [v for x in outi for v in x]; open(dst, 'wb').write(struct.pack('<%dI' % len(w), *w))
|
||||
print('edit %s: dropped %d wrapper functions, renamed %d callees, kept %d instructions' % (mode, len(res), len(setname), len(outi)))
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Imagination probe: build-only export wrappers -> dxc lib_6_3 -spirv -> SPIR-V edit -> SPIRV-Cross ES. /src = CultLib.
|
||||
set -u; P=/tmp/p; W=/tmp/w; mkdir -p $W/tools; cd $W
|
||||
step() { echo; echo "=== $*"; }
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
|
||||
mesa-vulkan-drivers libvulkan-dev glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
|
||||
curl -sL --fail -o dxc.tgz https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz
|
||||
echo "dxc sha ok: $(sha256sum dxc.tgz | cut -c1-64 | grep -c 55665c87824051ed4774ff3280a79ccbbb7d39243b9736ca5e98222134112d54)"
|
||||
mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc; DXC=$(find tools/dxc -name dxc -type f | head -1)
|
||||
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc
|
||||
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF -DSPIRV_CROSS_CLI=ON >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls $SC >/dev/null && echo "spirv-cross built"
|
||||
SH=/src/packages/cultmath/shaders
|
||||
step "1 wrappers"
|
||||
python3 $P/wrapgen.py $SH shw; head -3 shw/cm_wrappers.hlsl | cut -c1-160; grep -c '' shw/wrappers.tsv
|
||||
echo "source defs per overloaded name: $(cut -f2 shw/wrappers.tsv | sort | uniq -c | awk '$1>1{n++; d+=$1} END{print n" names, "d" defs"}')"
|
||||
step "2 dxc lib_6_3 on source plus wrappers"
|
||||
for v in default Od; do f=""; [ $v = Od ] && f="-Od"
|
||||
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 $f -I shw shw/cm_wrappers.hlsl -Fo w.$v.spv 2>&1 | head -3; echo "[$v] dxc exit ${PIPESTATUS[0]}"
|
||||
[ -s w.$v.spv ] && python3 $P/spvedit.py stats w.$v.spv shw/wrappers.tsv; done
|
||||
V=default; python3 $P/spvedit.py stats w.default.spv shw/wrappers.tsv | grep -q 'inlined (no call) 0,' || V=Od; echo "using variant $V"
|
||||
step "3 SPIR-V edit, validate, SPIRV-Cross ES 310"
|
||||
for m in drop export; do python3 $P/spvedit.py edit w.$V.spv e.$m.spv shw/wrappers.tsv $m
|
||||
spirv-val --target-env spv1.5 e.$m.spv 2>&1 | head -2; echo "[$m] val exit ${PIPESTATUS[0]}"
|
||||
$SC e.$m.spv --es --version 310 --output e.$m.glsl 2>&1 | head -2; echo "[$m] spirv-cross exit ${PIPESTATUS[0]}, lines $(wc -l < e.$m.glsl 2>/dev/null)"
|
||||
spirv-dis e.$m.spv | grep -E 'OpCapability|OpExtension' | tr -s ' ' | sort | uniq -c | tr '\n' ' '; echo; done
|
||||
cmp -s e.drop.glsl e.export.glsl && echo "drop and export outputs identical" || echo "drop and export outputs DIFFER"
|
||||
for m in drop export; do G=e.$m.glsl; [ -s $G ] || continue; python3 - $G shw/wrappers.tsv <<'PY'
|
||||
import re,sys,collections
|
||||
g=open(sys.argv[1]).read(); rows=[l.split('\t') for l in open(sys.argv[2]).read().splitlines() if l]
|
||||
src=collections.Counter(r[1] for r in rows)
|
||||
defs=re.findall(r'(?m)^(?:\w+ )*?(\w+) (\w+)\(([^)]*)\)\n\{',g); out=collections.Counter(d[1] for d in defs)
|
||||
print(sys.argv[1],'defs',len(defs),'cultmath_ defs',sum(v for k,v in out.items() if k.startswith('cultmath_')),'source defs',sum(src.values()))
|
||||
print(' names whose def count differs from source:',[(k,src[k],out.get(k,0)) for k in src if src[k]!=out.get(k,0)][:8])
|
||||
print(' non-source names emitted:',[k for k in out if k not in src][:10],' cmw in text:',g.count('cmw'),' suffixed _N cultmath names:',len(re.findall(r'\bcultmath_\w+_\d+\(',g)))
|
||||
print(' prologue:',' | '.join(l for l in g.split('\n')[:6] if l.startswith('#'))[:200], ' main:', g.count('void main'))
|
||||
PY
|
||||
done
|
||||
G=e.export.glsl; echo "--- cultmath_lerp and cultmath_saturate as emitted:"; grep -E '^\w+ cultmath_(lerp|saturate)\(' $G
|
||||
step "3b glslang ES 3.00 fragment, call sites resolve per overload"
|
||||
sed -E '/^#version/d; /^#ifdef SPIRV_CROSS/d; /^#endif/d; /^layout\(local_size/d' $G | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}' > lib.body.glsl
|
||||
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl; printf 'out highp vec4 c;\nvoid main() { vec3 p = gl_FragCoord.xyz; float a = cultmath_lerp(1.0, 2.0, p.x); vec3 b = cultmath_lerp(p, p.yzx, vec3(0.5)); vec4 s = cultmath_saturate(vec4(p, a)); c = vec4(b, cultmath_saturate(a)) + s + vec4(cultmath_snoise(p)); }\n'; } > es300.frag
|
||||
glslangValidator es300.frag > es300.txt 2>&1; echo "glslang ES 3.00 fragment: exit $? $(grep -m3 ERROR es300.txt | tr '\n' ' ' | cut -c1-300)"
|
||||
sed -i 's/cultmath_saturate(a))/cultmath_saturate(ivec2(1)))/' es300.frag; glslangValidator es300.frag > neg.txt 2>&1; echo "negative (no ivec2 overload) refused: exit $? $(grep -m1 ERROR neg.txt | cut -c1-160)"
|
||||
step "4 split, constants, capability strip"
|
||||
python3 $P/split.py $SH $G $W/split 2>&1 | head -6
|
||||
for combo in "core" "core cycles" "core phacelle" "core cycles phacelle"; do f=$W/combo.frag; { printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; for c in $combo; do cat $W/split/$c.glsl; done; printf 'out highp vec4 c;\nvoid main() { c = vec4(cultmath_snoise(gl_FragCoord.xyz)) + vec4(cultmath_lerp(gl_FragCoord.xyz, vec3(1.0), vec3(0.5)), 1.0); }\n'; } > $f; glslangValidator $f > $W/c.txt 2>&1; echo "glslang [$combo]: exit $? $(grep -m2 ERROR $W/c.txt | tr '\n' ' ' | cut -c1-200)"; done
|
||||
echo "static const lines: $(grep -hcE '^static const' $SH/*.hlsl | awk '{s+=$1}END{print s}'); of form 'static const float CULTMATH_X = lit;': $(grep -hcE '^static const float CULTMATH_\w+ = [-0-9.eEfx]+;' $SH/*.hlsl | awk '{s+=$1}END{print s}'); CULTMATH_ in output $(grep -c 'CULTMATH_' $G); in committed glsl $(cat $SH/*.glsl | grep -cE '^(const )?\w* ?float CULTMATH_')"
|
||||
DXC_PATH=$DXC bash $P/lava.sh
|
||||
echo PROBE-DONE
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Probe: build-only export wrappers. One exported, uniquely named wrapper per top-level cultmath_ definition
|
||||
(texture-taking ones skipped). Source is copied unchanged. usage: wrapgen.py <shaders> <outdir>
|
||||
Writes <outdir>/cm_wrappers.hlsl and <outdir>/wrappers.tsv (wrapper, name, file, params)."""
|
||||
import os, re, sys, shutil
|
||||
sh, out = sys.argv[1:3]; shutil.copytree(sh, out, dirs_exist_ok=True)
|
||||
SIG = re.compile(r'(?m)^(\w+)\s+(cultmath_\w+)\s*\(([^)]*)\)')
|
||||
files = ['CultMath.hlsl', 'CultMath.Interval.hlsl', 'CultMath.Affine.hlsl', 'CultMath.Cycles.hlsl', 'CultMath.Phacelle.hlsl']
|
||||
w = ['#include "CultMath.hlsl"']; rows = []; k = 0
|
||||
for fn in files:
|
||||
for m in SIG.finditer(open(os.path.join(sh, fn)).read()):
|
||||
ret, name, ps = m.group(1), m.group(2), m.group(3)
|
||||
if re.search(r'Texture|Sampler', ps): continue
|
||||
params = [p.strip() for p in ps.split(',') if p.strip()]
|
||||
args = [re.split(r'\s+', p)[-1] for p in params]
|
||||
wn = 'cmw%03d_%s' % (k, name); k += 1
|
||||
call = '%s(%s)' % (name, ', '.join(args))
|
||||
w.append('export %s %s(%s) { %s%s; }' % (ret, wn, ', '.join(params), '' if ret == 'void' else 'return ', call))
|
||||
rows.append('\t'.join([wn, name, fn, ', '.join(params)]))
|
||||
open(os.path.join(out, 'cm_wrappers.hlsl'), 'w').write('\n'.join(w) + '\n')
|
||||
open(os.path.join(out, 'wrappers.tsv'), 'w').write('\n'.join(rows) + '\n')
|
||||
print('wrappers', k)
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Probe job, runs in ubuntu:24.04 with /src = CultLib at the probed revision. Q1 library path, Q2 lavapipe runs.
|
||||
set -u
|
||||
P=/tmp/p; W=/tmp/w; mkdir -p $W/fam $W/tools; cd $W
|
||||
step() { echo; echo "=== $*"; }
|
||||
step apt
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
|
||||
mesa-vulkan-drivers libvulkan-dev vulkan-tools glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
|
||||
dpkg-query -W -f='${Package} ${Version}\n' mesa-vulkan-drivers glslang-tools spirv-tools libvulkan-dev
|
||||
step dxc
|
||||
url=$(curl -s https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/tags/v1.9.2607 | python3 -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if 'linux' in a['name'] and a['name'].endswith('.tar.gz')][0])")
|
||||
echo "$url"; curl -sL --fail -o dxc.tgz "$url" && mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc; DXC=$(find tools/dxc -name dxc -type f | head -1); echo "DXC=$DXC"; $DXC --version 2>&1 | head -2
|
||||
step spirv-cross
|
||||
git clone -q --depth 1 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc && git -C tools/sc log -1 --format='SPIRV-Cross %H %cs' && \
|
||||
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls -la $SC | cut -c1-120
|
||||
SH=/src/packages/cultmath/shaders
|
||||
step "Q1a dxc library SPIR-V, no wrapper (lib_6_6, vulkan1.2)"
|
||||
$DXC -T lib_6_6 -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $SH/CultMath.hlsl -Fo lib.spv 2>&1 | head -8; echo "dxc exit ${PIPESTATUS[0]}"
|
||||
[ -f lib.spv ] && { spirv-dis lib.spv -o lib.spvasm; echo "functions: $(grep -c ' = OpFunction ' lib.spvasm) exports: $(grep -c 'LinkageAttributes.*Export' lib.spvasm) entrypoints: $(grep -c OpEntryPoint lib.spvasm)"; }
|
||||
step "Q1b dxc library with 'export' added to every top-level cultmath_ definition"
|
||||
python3 - "$SH" <<'PY'
|
||||
import re,sys,os,shutil
|
||||
src=sys.argv[1]; dst='/tmp/w/shx'; shutil.copytree(src,dst,dirs_exist_ok=True)
|
||||
n=0
|
||||
for fn in os.listdir(dst):
|
||||
if not fn.endswith('.hlsl'): continue
|
||||
t=open(os.path.join(dst,fn)).read()
|
||||
t2,k=re.subn(r'(?m)^(?!static|export)(\w+(?:\dx\d)?\s+cultmath_\w+\s*\()', r'export \1', t); n+=k
|
||||
open(os.path.join(dst,fn),'w').write(t2)
|
||||
print('export keywords added', n)
|
||||
PY
|
||||
for v in "universal1.5" "vulkan1.2 -Vd" "vulkan1.3 -Vd"; do echo "--- variant: -fspv-target-env=$v"; rm -f libx.spv
|
||||
$DXC -T lib_6_6 -HV 2021 -spirv -fspv-target-env=$v -I shx shx/CultMath.hlsl -Fo libx.spv 2>&1 | head -4; echo "dxc exit ${PIPESTATUS[0]}"
|
||||
[ -s libx.spv ] && { spirv-dis libx.spv -o libx.spvasm; echo "functions: $(grep -c ' = OpFunction ' libx.spvasm) exports: $(grep -c 'LinkageAttributes.*Export' libx.spvasm) OpName cultmath_: $(grep -c 'OpName.*"cultmath_' libx.spvasm) Fma: $(grep -c ' Fma ' libx.spvasm) NoContraction: $(grep -c NoContraction libx.spvasm)"; spirv-val --target-env vulkan1.2 libx.spv 2>&1 | head -2; break; }
|
||||
done
|
||||
LIB=libx.spv
|
||||
step "Q1c SPIRV-Cross ES output of $LIB"
|
||||
for v in 300 310; do $SC $LIB --es --version $v --output lib$v.glsl 2>&1 | head -3; echo "spirv-cross --es --version $v exit ${PIPESTATUS[0]}"; done
|
||||
for v in 300 310; do [ -s lib$v.glsl ] || continue
|
||||
echo "lib$v.glsl: $(wc -l < lib$v.glsl) lines; defs named cultmath_: $(grep -cE '^\w+ cultmath_\w+\(' lib$v.glsl); distinct names: $(grep -oE '^\w+ cultmath_\w+\(' lib$v.glsl | sed -E 's/^\w+ //' | sort -u | wc -l); suffixed _N names: $(grep -oE '^\w+ cultmath_\w+_[0-9]+\(' lib$v.glsl | wc -l); _NNN temporaries: $(grep -oE '\b_[0-9]+\b' lib$v.glsl | wc -l); precision lines: $(grep -c precision lib$v.glsl); main: $(grep -c 'void main' lib$v.glsl)"; done
|
||||
echo "committed CultMath.glsl: $(wc -l < $SH/CultMath.glsl) lines; defs $(grep -cE '^\w+ cultmath_\w+\(' $SH/CultMath.glsl); distinct names $(grep -oE '^\w+ cultmath_\w+\(' $SH/CultMath.glsl | sed -E 's/^\w+ //' | sort -u | wc -l)"
|
||||
F=$(ls lib310.glsl lib300.glsl 2>/dev/null | head -1); [ -n "$F" ] && { echo "--- head of $F"; head -12 $F; echo "--- cultmath_snoise(vec3) as emitted (first 25 lines)"; awk '/^float cultmath_snoise\(/{p=1} p{print; if(++n>=25) exit}' $F; }
|
||||
for v in 300 310; do [ -s lib$v.glsl ] && { glslangValidator -S comp lib$v.glsl >gv$v.txt 2>&1; echo "glslang -S comp lib$v.glsl exit $? : $(head -3 gv$v.txt | tr '\n' ' ' | cut -c1-300)"; }; done
|
||||
step "Q2 lavapipe runner"
|
||||
gcc -O2 -o vkrun $P/vkrun.c -lvulkan && echo "vkrun built"
|
||||
python3 $P/fam.py gen /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam
|
||||
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
|
||||
python3 - <<'PY'
|
||||
import json; print('families', len(json.load(open('/tmp/w/fam/manifest.json'))))
|
||||
PY
|
||||
ok_h=0; ok_g=0; fail=0; first=1
|
||||
for h in fam/*.hlsl; do t=${h%.hlsl}; tag=$(basename $t)
|
||||
n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
|
||||
if $DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $h -Fo $t.h.spv >$t.h.log 2>&1; then
|
||||
if [ $first = 1 ]; then VKRUN_INFO=1 ./vkrun $t.h.spv $t.in $t.hlsl.out $n; first=0; else ./vkrun $t.h.spv $t.in $t.hlsl.out $n; fi && ok_h=$((ok_h+1)) || echo "run fail hlsl $tag"
|
||||
else fail=$((fail+1)); echo "dxc fail $tag: $(head -2 $t.h.log | tr '\n' ' ' | cut -c1-200)"; fi
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat all.glsl $t.body.glsl; } > $t.comp
|
||||
if glslangValidator -V --target-env vulkan1.2 -o $t.g.spv $t.comp >$t.g.log 2>&1; then ./vkrun $t.g.spv $t.in $t.glsl.out $n && ok_g=$((ok_g+1)) || echo "run fail glsl $tag"
|
||||
else echo "glslang fail $tag: $(grep -m2 ERROR $t.g.log | tr '\n' ' ' | cut -c1-200)"; fi
|
||||
done
|
||||
echo "ran: hlsl->dxc->lavapipe $ok_h, committed glsl->glslang->lavapipe $ok_g, dxc compile failures $fail"
|
||||
echo "dxc-compute Fma total: $(for s in fam/*.h.spv; do spirv-dis $s | grep -c ' Fma '; done | python3 -c "import sys;print(sum(int(l) for l in sys.stdin))")"
|
||||
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam hlsl
|
||||
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam glsl
|
||||
python3 - <<'PY2'
|
||||
import os,filecmp
|
||||
fs=sorted(f[:-9] for f in os.listdir('/tmp/w/fam') if f.endswith('.hlsl.out'))
|
||||
same=sum(filecmp.cmp('/tmp/w/fam/%s.hlsl.out'%f,'/tmp/w/fam/%s.glsl.out'%f,shallow=False) for f in fs if os.path.exists('/tmp/w/fam/%s.glsl.out'%f))
|
||||
print('lavapipe: HLSL-path and GLSL-path outputs bit-identical in',same,'of',len(fs),'families')
|
||||
PY2
|
||||
t0=$(date +%s); for h in fam/*.h.spv; do t=${h%.h.spv}; n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$(basename $t)'];print(m['n'],m['nout'])"); ./vkrun $h $t.in $t.rerun.out $n; cmp -s $t.rerun.out $t.hlsl.out || echo "NONDETERMINISTIC $(basename $t)"; done; echo "rerun of 105 SPIR-V modules on lavapipe: $(( $(date +%s) - t0 ))s, determinism check done"
|
||||
step "Q2b SPIRV-Cross ES 3.10 library through glslang on lavapipe (only if Q1 produced one)"
|
||||
if [ -s lib310.glsl ]; then sed -E '/^#version/d; /^void main\(\)/,/^\}/d' lib310.glsl > lib.body.glsl; okl=0
|
||||
for b in fam/*.body.glsl; do t=${b%.body.glsl}; tag=$(basename $t); n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl $b; } > $t.l.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $t.l.spv $t.l.comp >$t.l.log 2>&1 && ./vkrun $t.l.spv $t.in $t.lib.out $n && okl=$((okl+1))
|
||||
done; echo "lib path ran $okl"; grep -h -m1 ERROR fam/*.l.log | sort | uniq -c | sort -rn | head -5
|
||||
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam lib | head -15
|
||||
fi
|
||||
echo PROBE-DONE
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work
|
||||
for t in f103 f104; do for r in h g; do
|
||||
spirv-dis --raw-id $W/$t.$r.spv -o /tmp/$t.$r.dis
|
||||
echo "== $t $r: NoContraction $(grep -c NoContraction /tmp/$t.$r.dis) Fma $(grep -c ' Fma ' /tmp/$t.$r.dis) FMul $(grep -c OpFMul /tmp/$t.$r.dis) FAdd $(grep -c OpFAdd /tmp/$t.$r.dis) FSub $(grep -c OpFSub /tmp/$t.$r.dis) lines $(wc -l </tmp/$t.$r.dis)"
|
||||
done; done
|
||||
mkdir -p /tmp/dump; cp /tmp/f10?.?.dis /tmp/dump/; cp $W/f103.hlsl $W/f103.comp /tmp/dump/; cp /tmp/glslout/*.glsl /tmp/dump/
|
||||
echo DUMP-BEGIN; tar czf - -C /tmp dump | base64 -w0; echo; echo DUMP-END
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
mkdir -p $R/nc/lavapipe-hlsl $R/nc/lavapipe-glsl
|
||||
while IFS=$'\t' read -r tag _ _ n _ wout; do
|
||||
for r in h g; do
|
||||
spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis
|
||||
python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis
|
||||
spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv
|
||||
spirv-val --target-env vulkan1.2 $W/$tag.$r.nc.spv
|
||||
d=hlsl; [ $r = g ] && d=glsl
|
||||
$R/vkrun $W/$tag.$r.nc.spv $W/$tag.in $R/nc/lavapipe-$d/$tag.out $n $wout >/dev/null
|
||||
done
|
||||
echo "$tag nc: hlsl==glsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/nc/lavapipe-glsl/$tag.out && echo YES || echo NO); nc-hlsl==plain-hlsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/results/lavapipe-hlsl/$tag.out && echo YES || echo NO); nc-glsl==plain-glsl $(cmp -s $R/nc/lavapipe-glsl/$tag.out $R/results/lavapipe-glsl/$tag.out && echo YES || echo NO)"
|
||||
done <$W/manifest.txt
|
||||
cp /tmp/device/vkrun /tmp/ 2>/dev/null || true
|
||||
export CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
|
||||
for v in results nc; do
|
||||
export CULTMATH_DEVICE_RESULTS=$R/$v
|
||||
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
|
||||
echo "== judge $v"; grep -E "^(lavapipe-)" packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt | grep -E " 10[34] " | cut -f1,2,5,6 || tail -5 /tmp/judge.$v.log
|
||||
tail -3 /tmp/judge.$v.log
|
||||
done
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
CM=packages/cultmath
|
||||
cat >/tmp/cnt.py <<'PYEOF'
|
||||
import sys,struct
|
||||
a=open(sys.argv[1],'rb').read();b=open(sys.argv[2],'rb').read()
|
||||
n=len(a)//4;x=struct.unpack('<%dI'%n,a);y=struct.unpack('<%dI'%n,b)
|
||||
d=[i for i in range(n) if x[i]!=y[i]]
|
||||
print(len(d),'of',n,'cols',sorted(set(i%6 for i in d)))
|
||||
PYEOF
|
||||
run() { # name spv tag n wout
|
||||
$R/vkrun $2 $W/$3.in /tmp/o.$1.$3 $4 $5 >/dev/null; }
|
||||
OPT="--target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive"
|
||||
while IFS=$' ' read -r tag _ _ n _ wout; do
|
||||
for r in h g; do spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis; python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv; done
|
||||
# h variants
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo $W/$tag.hOd.spv >/dev/null 2>&1
|
||||
spirv-opt $OPT $W/$tag.hOd.spv -o $W/$tag.hOdO.spv
|
||||
for v in hOd hOdO; do spirv-dis --raw-id $W/$tag.$v.spv -o /tmp/$tag.$v.dis; python3 /tmp/nc.py /tmp/$tag.$v.dis /tmp/$tag.$v.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$v.nc.dis -o $W/$tag.$v.nc.spv; done
|
||||
for v in h g hOd hOdO; do run $v $W/$tag.$v.spv $tag $n $wout; run $v.nc $W/$tag.$v.nc.spv $tag $n $wout; done
|
||||
for v in h hOd hOdO; do
|
||||
echo "$tag $v vs g: $(python3 /tmp/cnt.py /tmp/o.$v.$tag /tmp/o.g.$tag)"
|
||||
echo "$tag $v.nc vs g.nc: $(python3 /tmp/cnt.py /tmp/o.$v.nc.$tag /tmp/o.g.nc.$tag)"
|
||||
done
|
||||
echo "$tag h.nc vs h: $(python3 /tmp/cnt.py /tmp/o.h.nc.$tag /tmp/o.h.$tag)"
|
||||
echo "$tag g.nc vs g: $(python3 /tmp/cnt.py /tmp/o.g.nc.$tag /tmp/o.g.$tag)"
|
||||
echo "$tag hOd.nc vs hOd: $(python3 /tmp/cnt.py /tmp/o.hOd.nc.$tag /tmp/o.hOd.$tag)"
|
||||
done <$W/manifest.txt
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\\1\\2/g" "$W"/work/*.comp\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
set +e
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
CM=packages/cultmath
|
||||
for d in plain ncod gis; do mkdir -p $R/$d/lavapipe-hlsl $R/$d/lavapipe-glsl; done
|
||||
while IFS=$'\t' read -r tag _ _ n _ wout; do
|
||||
spirv-dis --raw-id $W/$tag.g.spv -o /tmp/g.dis; python3 /tmp/nc.py /tmp/g.dis /tmp/g.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/g.nc.dis -o /tmp/g.nc.spv
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hOd.spv >/dev/null 2>&1
|
||||
spirv-dis --raw-id /tmp/hOd.spv -o /tmp/hOd.dis; python3 /tmp/nc.py /tmp/hOd.dis /tmp/hOd.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hOd.nc.dis -o /tmp/hOd.nc.spv
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Gis -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hGis.spv >/dev/null 2>&1
|
||||
spirv-dis --raw-id /tmp/hGis.spv -o /tmp/hGis.dis; python3 /tmp/nc.py /tmp/hGis.dis /tmp/hGis.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hGis.nc.dis -o /tmp/hGis.nc.spv
|
||||
cp $R/results/lavapipe-hlsl/$tag.out $R/plain/lavapipe-hlsl/; cp $R/results/lavapipe-glsl/$tag.out $R/plain/lavapipe-glsl/
|
||||
$R/vkrun /tmp/hOd.nc.spv $W/$tag.in $R/ncod/lavapipe-hlsl/$tag.out $n $wout >/dev/null
|
||||
$R/vkrun /tmp/g.nc.spv $W/$tag.in $R/ncod/lavapipe-glsl/$tag.out $n $wout >/dev/null
|
||||
$R/vkrun /tmp/hGis.nc.spv $W/$tag.in $R/gis/lavapipe-hlsl/$tag.out $n $wout >/dev/null
|
||||
cp $R/ncod/lavapipe-glsl/$tag.out $R/gis/lavapipe-glsl/
|
||||
for d in plain ncod gis; do cmp -s $R/$d/lavapipe-hlsl/$tag.out $R/$d/lavapipe-glsl/$tag.out || echo "EQ-FAIL $d $tag"; done
|
||||
done <$W/manifest.txt
|
||||
export CULTMATH_DEVICE_WORK=$W CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
|
||||
B=packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt
|
||||
for v in plain ncod gis; do
|
||||
export CULTMATH_DEVICE_RESULTS=$R/$v
|
||||
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
|
||||
grep -aE "Error Message" -A8 /tmp/judge.$v.log | head -14; ls -la $B; cp $B /tmp/bounds.$v; echo "judge $v: $(tail -3 /tmp/judge.$v.log | tr '\n' ' ')"
|
||||
done
|
||||
for v in plain ncod gis; do awk -F'\t' -v v=$v '{k=$1; u[k]+=$5; if($5+0>m[k]+0)m[k]=$5; c[k]++} END{for(k in c) print "BOUNDS",v,k,"families",c[k],"maxulp",m[k],"sumulp",u[k]}' /tmp/bounds.$v; done
|
||||
for v in plain ncod; do awk -F'\t' '{print $1"/"$2"\t"$5}' /tmp/bounds.$v | sort > /tmp/b.$v; done
|
||||
join -t$'\t' /tmp/b.plain /tmp/b.ncod | awk -F'\t' '$2!=$3{print "ULPDIFF",$1,"plain",$2,"ncod",$3}' | head -60
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
// Probe runner: one compute dispatch, binding 0 = input uints, binding 1 = output uints.
|
||||
// usage: vkrun <spv> <in.bin> <out.bin> <ncases> <out_words_per_case> [entry]
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define CK(x) do { VkResult r_ = (x); if (r_ != VK_SUCCESS) { fprintf(stderr, "FAIL %s = %d\n", #x, r_); exit(2); } } while (0)
|
||||
|
||||
static void *slurp(const char *p, size_t *n) {
|
||||
FILE *f = fopen(p, "rb"); if (!f) { perror(p); exit(2); }
|
||||
fseek(f, 0, SEEK_END); *n = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
void *b = malloc(*n ? *n : 4); fread(b, 1, *n, f); fclose(f); return b;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 6) { fprintf(stderr, "usage\n"); return 2; }
|
||||
size_t spvn, inn; void *spv = slurp(argv[1], &spvn); void *in = slurp(argv[2], &inn);
|
||||
unsigned ncases = atoi(argv[4]), ow = atoi(argv[5]); const char *entry = argc > 6 ? argv[6] : "main";
|
||||
size_t outn = (size_t)ncases * ow * 4;
|
||||
|
||||
VkApplicationInfo ai = { VK_STRUCTURE_TYPE_APPLICATION_INFO, 0, "probe", 1, "probe", 1, VK_API_VERSION_1_2 };
|
||||
VkInstanceCreateInfo ici = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, 0, 0, &ai };
|
||||
VkInstance inst; CK(vkCreateInstance(&ici, 0, &inst));
|
||||
uint32_t np = 1; VkPhysicalDevice pd; vkEnumeratePhysicalDevices(inst, &np, &pd); if (!np) { fprintf(stderr, "no device\n"); return 2; }
|
||||
VkPhysicalDeviceFloatControlsProperties fc = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT_CONTROLS_PROPERTIES };
|
||||
VkPhysicalDeviceProperties2 pp = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &fc };
|
||||
vkGetPhysicalDeviceProperties2(pd, &pp);
|
||||
if (getenv("VKRUN_INFO"))
|
||||
printf("device=%s api=%u.%u driver=%u f32: denormPreserve=%u ftz=%u rte=%u rtz=%u signedZeroInfNan=%u\n",
|
||||
pp.properties.deviceName, VK_VERSION_MAJOR(pp.properties.apiVersion), VK_VERSION_MINOR(pp.properties.apiVersion),
|
||||
pp.properties.driverVersion, fc.shaderDenormPreserveFloat32, fc.shaderDenormFlushToZeroFloat32,
|
||||
fc.shaderRoundingModeRTEFloat32, fc.shaderRoundingModeRTZFloat32, fc.shaderSignedZeroInfNanPreserveFloat32);
|
||||
|
||||
float prio = 1; VkDeviceQueueCreateInfo qci = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, 0, 0, 0, 1, &prio };
|
||||
VkDeviceCreateInfo dci = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, 0, 0, 1, &qci };
|
||||
VkDevice dev; CK(vkCreateDevice(pd, &dci, 0, &dev)); VkQueue q; vkGetDeviceQueue(dev, 0, 0, &q);
|
||||
|
||||
VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(pd, &mp);
|
||||
VkBuffer buf[2]; VkDeviceMemory mem[2]; size_t sz[2] = { inn, outn }; void *map[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 0, 0, sz[i], VK_BUFFER_USAGE_STORAGE_BUFFER_BIT };
|
||||
CK(vkCreateBuffer(dev, &bci, 0, &buf[i])); VkMemoryRequirements mr; vkGetBufferMemoryRequirements(dev, buf[i], &mr);
|
||||
uint32_t t = 0; VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
for (; t < mp.memoryTypeCount; t++) if ((mr.memoryTypeBits & (1u << t)) && (mp.memoryTypes[t].propertyFlags & want) == want) break;
|
||||
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, 0, mr.size, t };
|
||||
CK(vkAllocateMemory(dev, &mai, 0, &mem[i])); CK(vkBindBufferMemory(dev, buf[i], mem[i], 0));
|
||||
CK(vkMapMemory(dev, mem[i], 0, sz[i], 0, &map[i]));
|
||||
}
|
||||
memcpy(map[0], in, inn); memset(map[1], 0xEE, outn);
|
||||
|
||||
VkDescriptorSetLayoutBinding b[2] = { { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT },
|
||||
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT } };
|
||||
VkDescriptorSetLayoutCreateInfo lci = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, 0, 0, 2, b };
|
||||
VkDescriptorSetLayout dsl; CK(vkCreateDescriptorSetLayout(dev, &lci, 0, &dsl));
|
||||
VkPipelineLayoutCreateInfo plci = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, 0, 0, 1, &dsl };
|
||||
VkPipelineLayout pl; CK(vkCreatePipelineLayout(dev, &plci, 0, &pl));
|
||||
VkShaderModuleCreateInfo smci = { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, 0, 0, spvn, spv };
|
||||
VkShaderModule sm; CK(vkCreateShaderModule(dev, &smci, 0, &sm));
|
||||
VkComputePipelineCreateInfo cpci = { VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, 0, 0,
|
||||
{ VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 0, 0, VK_SHADER_STAGE_COMPUTE_BIT, sm, entry }, pl };
|
||||
VkPipeline pipe; CK(vkCreateComputePipelines(dev, 0, 1, &cpci, 0, &pipe));
|
||||
|
||||
VkDescriptorPoolSize ps = { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2 };
|
||||
VkDescriptorPoolCreateInfo dpci = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, 0, 0, 1, 1, &ps };
|
||||
VkDescriptorPool dp; CK(vkCreateDescriptorPool(dev, &dpci, 0, &dp));
|
||||
VkDescriptorSetAllocateInfo dsai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, 0, dp, 1, &dsl };
|
||||
VkDescriptorSet ds; CK(vkAllocateDescriptorSets(dev, &dsai, &ds));
|
||||
VkDescriptorBufferInfo dbi[2] = { { buf[0], 0, VK_WHOLE_SIZE }, { buf[1], 0, VK_WHOLE_SIZE } };
|
||||
VkWriteDescriptorSet w[2];
|
||||
for (int i = 0; i < 2; i++) { VkWriteDescriptorSet x = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 0, ds, i, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, &dbi[i] }; w[i] = x; }
|
||||
vkUpdateDescriptorSets(dev, 2, w, 0, 0);
|
||||
|
||||
VkCommandPoolCreateInfo cpi = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, 0, 0, 0 };
|
||||
VkCommandPool cp; CK(vkCreateCommandPool(dev, &cpi, 0, &cp));
|
||||
VkCommandBufferAllocateInfo cbai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, 0, cp, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 };
|
||||
VkCommandBuffer cb; CK(vkAllocateCommandBuffers(dev, &cbai, &cb));
|
||||
VkCommandBufferBeginInfo cbbi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||||
CK(vkBeginCommandBuffer(cb, &cbbi));
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
|
||||
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pl, 0, 1, &ds, 0, 0);
|
||||
vkCmdDispatch(cb, (ncases + 63) / 64, 1, 1);
|
||||
CK(vkEndCommandBuffer(cb));
|
||||
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO, 0, 0, 0, 0, 1, &cb };
|
||||
CK(vkQueueSubmit(q, 1, &si, 0)); CK(vkQueueWaitIdle(q));
|
||||
FILE *f = fopen(argv[3], "wb"); fwrite(map[1], 1, outn, f); fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
# Native shader testing: how established projects check shader numerics without a browser, and what each path proves
|
||||
|
||||
For `cultmath-tapes:question:device-run-driver` (campaign `cultmath-tapes`). Companion to `docs/research/device-capture-runners.md` (CultLib c9d3e672), which covered browser drivers only; not repeated here.
|
||||
Also serves the operator's question of 2026-10-16 on why CultMath lowers HLSL to GLSL with its own text lowering (section on HLSL to GLSL ES / SPIR-V toolchains).
|
||||
Gathered by an Eyes pass for Eureka Self, session self-2026-10-16-ag4, 2026-10-16. Facts only; no recommendation.
|
||||
|
||||
Strength marks: [src] read from a primary source text this pass (raw file fetched and read or grepped locally, or a spec PDF converted to text); [doc] vendor or project documentation page read this pass (via a fetch summariser, so check before quoting); [web] search-result summary only, not read in full; [abs] searched, not found.
|
||||
Nothing was built or run. Dates matter: several repos below are moving; read at HEAD on 2026-10-16.
|
||||
|
||||
## 0. The record
|
||||
|
||||
- [src] Operator, 2026-10-16, verbatim: "There has to be a better way to test shader code than getting a browser involved". An observation that sets research direction, not a design ruling.
|
||||
- [src] Question `device-run-driver` (viewed in the mind): options committed-captures / playwright-live / both; the runner is a static page that compiles each family's call against `CultMath.glsl`, reads back via RGBA32UI, emits raw bits; C# judges.
|
||||
- [src] CultMath shader files: `packages/cultmath/shaders/` has `CultMath.hlsl` (673 lines), `CultMath.Affine.hlsl`, `.Cycles.hlsl`, `.Interval.hlsl`, `.Phacelle.hlsl`, and `CultMath.glsl` (900), `.Cycles.glsl`, `.Phacelle.glsl`. HLSL intrinsics seen by grep across the `.hlsl` files: `asuint` (13), `asfloat` (6), `saturate` (2); no `mad`, `fma`, `frexp`, `ldexp`, `precise`, `sincos`, `rcp`, `groupshared`, `#include` hits in the grep (the `GlslLowering.cs` doc comment says the HLSL has "includes" that are inlined).
|
||||
- [src] The `.glsl` files contain no `precision` statement and no `#version` line (grep found none); a consumer supplies them.
|
||||
- [src] `GlslLowering.cs` header: the GLSL ES 3.00 files are a regex text transformation of the HLSL; a dialect gap is fixed in the HLSL "in the subset both languages share".
|
||||
|
||||
## 1. Google Amber
|
||||
|
||||
- [doc] README (github.com/google/amber): runs on Vulkan (fully supported); Dawn listed "WIP". A command-line `amber` tool runs script files, reports pass/fail by exit code, can write output buffers and images to files. Says "not an officially supported Google product". AmberScript is the format getting new features; VkScript (the VkRunner syntax) is kept for history, no new features. Build bots check that Amber compiles on several targets; "integration tests don't run on the bots because of bot limitations, so only unit tests run in CI".
|
||||
- [src] Maintenance: GitHub API `google/amber`: not archived, created 2018-11-13, pushed 2026-09-24, 76 open issues. Last five commits 2026-05-28 to 2026-09-24 (ray query support, acceleration structures, alpha-to-coverage, barrier fixes).
|
||||
- [doc] AmberScript (docs/amber_script.md): starts `#!amber`. `SHADER compute <name> <format>` where format is GLSL, HLSL, SPIRV-ASM, SPIRV-HEX, SPIRV-BIN or OPENCL-C, inline, `FILE`, or `VIRTUAL_FILE`; optional `TARGET_ENV`. `PIPELINE compute <name> ... END`. `BUFFER <name> DATA_TYPE <type> [STD140|STD430] DATA ... END | FILL v | SERIES_FROM s INC_BY i`, types include int8..int64, uint8..uint64, float16, float, double, vectors, matrices. `COPY a TO b`. `RUN <pipeline> x y z`.
|
||||
- [doc] Expectations: `EXPECT <buf> IDX <n> EQ|NE|LT|LE|GT|GE <values>`; with tolerance `EXPECT <buf> IDX <n> TOLERANCE <t1..t4> EQ <values>` (1 to 4 floats, a trailing `%` makes one relative; components without one use a default); whole-buffer `EQ_BUFFER`, `RMSE_BUFFER ... TOLERANCE <x>`, `EQ_HISTOGRAM_EMD_BUFFER ... TOLERANCE <x>`. Pixel forms `EQ_RGB`, `EQ_RGBA` use 0-255. Comparison is on typed values, not raw bit patterns; a uint32 buffer compared with `EQ` is an exact integer compare. [abs] A ULP-based tolerance form was not found in the AmberScript doc.
|
||||
- [doc] VkScript (docs/vk_script.md): `probe ssbo` has a `~=` fuzzy-equal comparator and a `tolerance t0 [t1 t2 t3]` command.
|
||||
- [web] Amber with SwiftShader: a build option `AMBER_ENABLE_SWIFTSHADER`/`AMBER_USE_SWIFTSHADER` builds SwiftShader as a Vulkan ICD; run with `VK_ICD_FILENAMES` pointing at its manifest; `amber -d` style device listing confirms it. [abs] Amber's own CI workflow files were not found at the paths tried (404); whether Amber CI uses lavapipe or SwiftShader was not established.
|
||||
- [web] Amber lineage: shader test files mirror VkRunner's format; Amber is shipped inside Android's `external/deqp-deps` and Vulkan CTS.
|
||||
|
||||
## 2. Software Vulkan: Mesa lavapipe and SwiftShader
|
||||
|
||||
### 2.1 Conformance and float behaviour
|
||||
|
||||
- [web] Lavapipe announced formally Vulkan 1.2 conformant (Phoronix, May 2022) and Vulkan 1.3 conformant (July 2022; Khronos submission 696, AMD Ryzen 7 1800X, Mesa 22.1). A Mesa issue (#5472, older) complained lavapipe reported a conformance version while not yet conformant; resolution not checked.
|
||||
- [web] Mesa MR 11739 "WIP: lavapipe: float_controls" (author's words as summarised): passes about 97% of CTS, fails `float_controls...denorm_*_preserve` cases, "llvmpipe/gallivm have no concept of setting a specific denorm mode"; the author said they probably would not continue. Current merged state of float controls in lavapipe: [abs] (the Mesa GitLab pages and `lvp_device.c` were behind bot protection or not at the tried path).
|
||||
- [web] A separate llvmpipe MR disables denorms for compute shaders to align with fragment shaders, because OpenCL CTS tests "don't expect working denorms".
|
||||
- [web] SwiftShader commit "Implement VK_KHR_shader_float_controls": at that time SwiftShader exposed no preserve, denorm or rounding controls and set all denorm-preserve and flush-to-zero flags false. Current values: [abs].
|
||||
- [doc] Vulkan float controls (core in 1.2): a flush-to-zero flag set means denormals may be flushed, not that they are; preservation needs `shaderDenormPreserveFloat32` and a `DenormPreserve` execution mode on the entry point. Rounding-mode controls do not apply to float to int conversions (always toward zero).
|
||||
- [web] Mesa lavapipe docs page (docs.mesa3d.org/drivers/lavapipe.html) 404s at the tried URL; the `llvmpipe.html` page was read and says nothing of lavapipe floats.
|
||||
- [src] D3D11 floating-point rules (learn.microsoft.com, doc dated 2025-02/04): 32-bit ops are within 1 ULP (add, subtract, multiply: 0.5 ULP "applies only to addition, subtraction, and multiplication", hardware may truncate); "Denorms are flushed to sign-preserved zero on input and output of any floating-point mathematical operation" (data movement exempt); min/max flush for comparison but the result may or may not be flushed; fused operations (mad, dp3) are no less accurate than the worst serial ordering with each step allowed 1 ULP; sqrt and rcp 1 ULP, rcp/rsq have their own relaxed requirement; x*1.0, x/1.0, x+-0.0 return x "except denorm flushed"; NaN payload bits not preserved; no exceptions or traps; double precision preserves denorms and follows IEEE 754R; float16 preserves denorms.
|
||||
- [src] GLSL ES 3.00 spec section 4.5.1 (Khronos PDF, converted locally): highp float is "defined by the IEEE 754 standard for 32-bit" in storage; "Any subnormal (denormalized) value input into a shader or potentially generated by any operation in a shader can be flushed to 0. The rounding mode cannot be set and is undefined." Required precisions: `a+b, a-b, a*b` correctly rounded; `a/b` and `1.0/b` 2.5 ULP for b in [2^-126, 2^126]; `a*b+c` "correctly rounded single operation or sequence of two correctly rounded operations"; `exp`, `exp2` (3 + 2|x|) ULP; `log`, `log2` 3 ULP outside [0.5, 2.0] and absolute error < 2^-21 inside; `inversesqrt` 2 ULP; `sqrt` inherits from `1.0/inversesqrt`; trig and `determinant` have undefined precision; explicit conversions correctly rounded; "rounding mode ... must not affect the result by more than 1 ULP". NaNs and Infs "are not required to be generated".
|
||||
- [src] GLSL ES 3.10 spec (NVIDIA-hosted copy of the Khronos PDF) repeats the same flush-to-zero sentence (line 3061 of the converted text).
|
||||
- [src] Table of required ranges in ES 3.00: highp relative precision 2^-24 (as IEEE); mediump minimum range (-2^14, 2^14), relative 2^-10; lowp minimum range (-2, 2), absolute 2^-8. Integers: highp 32-bit; mediump 16 to 32 bits; lowp 9 to 32 bits. "Mediump and lowp ... have maximum range and precision as defined by IEEE 754", so an implementation may run them at 32 bits.
|
||||
|
||||
### 2.2 Who uses software Vulkan in CI
|
||||
|
||||
- [src] wgpu `.github/workflows/ci.yml` (trunk, read via fetch): job `gpu-test` on `windows-2022`, `macos-14`, `ubuntu-24.04`. Linux installs the Vulkan SDK and Mesa (driver named only via the composite action, not visible), sets `LVP_POISON_MEMORY: true`; Windows installs DXC and WARP (`install-warp` action), Agility SDK, Mesa, Vulkan SDK, sets `WGPU_DX12_COMPILER: dxc`. Tests run with `cargo xtask test` (nextest).
|
||||
- [doc] wgpu `docs/testing.md`: names `LVP_POISON_MEMORY` ("fills all newly initialized memory with a non-zero value"); describes a parameter system that skips or expects failure per adapter; says nothing on float or denorm differences.
|
||||
- [web] onnxruntime commit replaced Mesa lavapipe with SwiftShader in headless CI because an old lavapipe returned `VK_ERROR_INCOMPATIBLE_DRIVER` when Dawn asked for a Vulkan 1.3 instance; the SwiftShader ICD is built from source there and used by Dawn.
|
||||
- [web] Dawn/Chromium use the SwiftShader Vulkan ICD for headless runs; ANGLE end2end has a `VULKAN_SWIFTSHADER` config and a `--deqp-egl-display-type=angle-swiftshader` mode (section 4).
|
||||
- [web] Wine/vkd3d: tests marked broken on WARP; a November 2025 commit disabled 64-bit wave bitwise ops on WARP "known to be buggy"; a merge request says WARP "doesn't emulate faithfully a hardware device".
|
||||
- [abs] No project was found that commits raw float bit patterns from lavapipe or SwiftShader as a reference. Projects found compare with tolerance, by pixel, or by exact integer.
|
||||
|
||||
## 3. GLSL/HLSL to SPIR-V
|
||||
|
||||
### 3.1 glslang
|
||||
|
||||
- [src] glslang README (read at main): ESSL is a supported front end; compute shaders use `.comp`; stage is inferred from the file extension; C++ and C interfaces exist; JS/WASM build via `-DENABLE_GLSLANG_JS=ON`. Tests: gtest under `gtests/` plus a `runtests` script, expected outputs in `Test/baseResults/`.
|
||||
- [src] The HLSL front end of glslang is deprecated: README states deprecation, slated for removal in the next major version with at least 18 months' notice, no bug reports accepted. Commit "Deprecate the HLSL front-end." dated 2026-04-16 (GitHub API). This does not touch `dxc -spirv`, a separate compiler.
|
||||
- [web] Compute shaders need desktop GLSL 4.30 or GLSL ES 3.10 (Mesa error text "Compute shaders require GLSL 4.30 or GLSL ES 3.10"). In ES 3.10 only the last member of a shader storage block may be a runtime-sized array; desktop is looser.
|
||||
- [src] SPIRV-Cross (section 3.3) throws "At least ESSL 3.10 required for compute shaders." for ES versions under 310. So a `#version 300 es` compute shader does not exist: a GLSL ES 3.00 fragment function ported to compute becomes a 3.10 shader.
|
||||
- [src] ES 3.10 section 4.7.4: vertex and compute languages have predeclared `precision highp float; precision highp int;`; the fragment language has "no default precision qualifier for floating point types" and `mediump int`. So an unqualified float in a compute shader is highp, while a fragment shader must declare a float precision.
|
||||
- [src] ES 3.10 and 3.00 share the flush-to-zero sentence and the ULP table for highp (the 3.10 table was not diffed line by line; the denorm sentence was).
|
||||
- [abs] A published comparison of ES compute against desktop 4.30 numerics (same function, fragment vs compute) was not found. The two specs' highp rules, as read, are the same sentence; what a given driver does per stage is not stated by either spec.
|
||||
|
||||
### 3.2 dxc -spirv
|
||||
|
||||
- [src] DXC `docs/SPIR-V.rst` (235 KB, read by grep): default target environment `vulkan1.0` via `-fspv-target-env`; `-fspv-entrypoint-name=<name>` exists; `min16float`, `min10float`, `min16int`, `min12int`, `min16uint` map to 32-bit types with `RelaxedPrecision` unless `-enable-16bit-types`, which gives real 16-bit types and the `Float16` capability. `lib_6_x` appears only in the raytracing section ("must be compiled as library using lib_6_3/lib_6_4"). [abs] No statement in the doc that a non-raytracing `lib_6_x` library compiles to SPIR-V with exported functions; no statement on denormal execution modes.
|
||||
- [src] `SPV_KHR_float_controls` is in the supported-extension list; no description of when dxc emits denorm modes (not found by grep of the doc).
|
||||
- [src] HLSL exposes `asuint`/`asfloat` as bitcasts; in SPIR-V these are `OpBitcast`. (CultMath's use is section 0.)
|
||||
|
||||
### 3.3 SPIRV-Cross (SPIR-V to GLSL ES/HLSL/MSL)
|
||||
|
||||
- [src] README (main): ES example `spirv-cross --version 310 --es test.spv`; sections on HLSL-derived SPIR-V; names glslangValidator's `-e` entry-point flag in an error hint. Active: commits 2026-09-18, 2026-09-22, 2026-10-14.
|
||||
- [src] `spirv_glsl.cpp`: `#version` is written as `#version <n> es` when `options.es && version > 100`; ES is allowed from 100 up (the `300 es` form is the emitted string); buffer blocks need ES >= 300, `binding` needs >= 310.
|
||||
- [src] Precision: `backend.allow_precision_qualifiers = options.vulkan_semantics || options.es;` `flags_to_qualifiers_glsl` emits `mediump ` for ids with `RelaxedPrecision`, else `highp `, and omits either when it equals the fragment default; structs and doubles get none. `Options::Fragment` defaults are `default_float_precision = Mediump`, `default_int_precision = Highp` (`spirv_glsl.hpp` lines 196-197). `emit_header()` writes `precision <p> float;` and `precision <p> int;` only for fragment shaders when `options.es`. So a function-only library run through `--es` with default options would, in a fragment context, assume mediump float defaults and tag non-relaxed values `highp`.
|
||||
- [src] Library modules: `spirv_parser.cpp` (main) handles a module with no `OpEntryPoint` but with `OpDecorate ... LinkageAttributes ... Export`: marks `is_library_module`, uses the first exported function as a placeholder entry point with execution model GLCompute, and `spirv_glsl.cpp` emits "each exported function as a normal free function" plus the callees. A module with neither throws "There is no entry point in the SPIR-V module." The code paths exist at HEAD; which release first had them, and whether the output is valid for ES, were not checked.
|
||||
- [web] A Unity issue reports min16float hints (`min16f as def32` modifier in FXC bytecode) not honoured in SPIR-V/ESSL output (mediump produced where highp was expected). One blog summary says SPIRV-Cross may pick a low default precision unless told otherwise (not authoritative).
|
||||
- [src] Readability: [abs] no source characterising readability of the emitted GLSL was found; SPIRV-Cross output is generated text with temporaries named `_<id>` unless debug names survive (names come from OpName; not verified for dxc output).
|
||||
|
||||
## 4. Native ANGLE without a browser
|
||||
|
||||
- [doc] ANGLE README: implements EGL 1.5 and GLES 2.0/3.0/3.1 over Vulkan, desktop GL, GLES, D3D9, D3D11, Metal; "Chrome uses ANGLE for all graphics rendering on Windows" and it is the default WebGL backend for Chrome and Firefox on Windows; its shader compiler can emit Desktop GLSL, Vulkan GLSL, HLSL, ESSL.
|
||||
- [doc] ANGLE `doc/DevSetup.md`: build standalone with `fetch angle`, `gn gen out/Debug`, `autoninja -C out/Debug`; tests, libraries and samples land in `out/Debug`; on Windows default backend is D3D11 (D3D9 otherwise); backend chosen at runtime through `EGL_ANGLE_platform_angle` via `eglGetPlatformDisplayEXT` (`util/EGLWindow.cpp` as example); backends removable via GN args `angle_enable_d3d11` etc. [abs] prebuilt binaries not covered in the doc.
|
||||
- [src] ANGLE `doc/dEQP.md`: targets `angle_deqp_gles2_tests`, `angle_deqp_gles3_tests`, `angle_deqp_gles31_tests`, `angle_deqp_egl_tests`; renderer chosen with `--deqp-egl-display-type=angle-d3d11 | angle-d3d9 | angle-d3d11-fl93 | angle-gl | angle-gles | angle-metal | angle-swiftshader | angle-vulkan` (same as `--use-angle=X`); default is Vulkan except Apple (OpenGL); test lists come from `third_party/VK-GL-CTS/.../gles3-main.txt`; GoogleTest builds of the same targets run on the bots with expectation files in `src/tests/deqp_support`.
|
||||
- [src] ANGLE `doc/TestingAndProcesses.md`: angle_end2end_tests expectations live in `src/tests/angle_end2end_tests_expectations.txt` (`SKIP`, `TIMEOUT` only); the WebGL conformance expectations live in the Chromium tree, not the ANGLE tree.
|
||||
- [web] `angle_end2end_tests` configs include `ES2_D3D11_WARP`, `ES2_D3D11_REFERENCE`, feature-level variants and `VULKAN_SWIFTSHADER`; end2end tests subsume the older angle_tests and standalone tests.
|
||||
- [doc] ANGLE `EGL_ANGLE_create_context_webgl_compatibility` / `ANGLE_webgl_compatibility` (spec text, Draft): `eglCreateContext` attribute `EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE` adds WebGL validation; shaders with `#version 100` get WebGL 1 rules and `#version 300 es` get WebGL 2 rules.
|
||||
- [src] Path in D3D11 (`src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp`, main): ANGLE translator emits HLSL, then `D3DCompile` with profile `vs_5_0`/`ps_5_0` style (`getMajorShaderModel`), flags `D3DCOMPILE_OPTIMIZATION_LEVEL2`, plus `D3DCOMPILE_IEEE_STRICTNESS` only `if (workarounds.enableIEEEStrictness)`; on failure retries with `SKIP_VALIDATION`, then `SKIP_OPTIMIZATION`. The HLSL is generated at link time (`DynamicShaderCompilation.md`) because signatures between stages must be known.
|
||||
- [src] ANGLE `doc/CompilingTranslatorWithEmscripten.md`: the translator alone can be compiled for use from a page (preliminary notes).
|
||||
- [abs] The `enableIEEEStrictness` workaround's default value in ANGLE and in Chrome was not found. [abs] Whether Chrome's WebGL2 context and a standalone ANGLE EGL context share all translator options and workaround flags was not established (same repository; Chrome applies its own feature flags and the WebGL-compatibility context).
|
||||
- [abs] No source found for a project that uses a standalone ANGLE `libEGL`/`libGLESv2` as a compute-readback harness; ANGLE's own tests use it as the system under test.
|
||||
- [src] Context: GLES 3.0 cannot run compute (3.1 adds it); ANGLE advertises 3.1 (README). A WebGL2 fragment readback and an ES 3.1 compute readback are different ANGLE shader paths (pixel-shader HLSL vs compute-shader HLSL).
|
||||
|
||||
## 5. D3D11/D3D12 compute harnesses for HLSL
|
||||
|
||||
- [src] DXC repo `tools/clang/unittests/HLSLExec/` files: `ExecutionTest.cpp`, `HlslExecTestUtils.cpp`, `LongVectors.cpp`, `ShaderOpArithTable.xml`, `ShaderOpTest.cpp` and others; the test project uses TAEF (WEX) test methods.
|
||||
- [src] `HlslExecTestUtils.cpp`: `useWarpByDefault()` returns true unless `_HLK_CONF` is defined; when the `UseWARP` parameter is set it creates a device from `EnumWarpAdapter` at feature level 11_0, with an optional `WARP_DLL` path to load a specific WARP build.
|
||||
- [src] `ExecutionTest.cpp`: `DenormBinaryFloatOpTest`/`DenormTertiaryFloatOpTest` read tables from `ShaderOpArithTable.xml#DenormBinaryFloatOpTable`/`DenormTertiaryFloatOpTable` (rows like `FDivDenormFTZ`; columns `ShaderOp.Target`, `ShaderOp.Arguments`, `Validation.Type`, `Validation.Tolerance`, inputs and expected values); a helper `ifdenorm_flushf_eq` accepts either flushed or unflushed results; derivative tests use `CompareFloatULP(..., 1)` with alternative acceptable values.
|
||||
- [src] `LongVectors.cpp`: validation config `Epsilon(tol)` or `Ulp(tol)`; default is ULP 1.0 for floating-point types, exact for others; failures print the validation type and tolerance.
|
||||
- [src] DXC's `HLSLExec` tests thus run on WARP by default and on hardware via a flag; tolerances are per-row in data tables.
|
||||
- [web] Shader Test Framework (KStocky, D3D12, Catch2, HLSL 202x): README says "very experimental"; tests are HLSL functions run through a `ShaderTestFixture`; a device can be hardware or WARP (README read by fetch did not state WARP; a search summary and the Skyrim Community Shaders DeepWiki page did); README does not state its float comparison. Used by Skyrim Community Shaders per a DeepWiki page (AI-generated, unverified).
|
||||
- [web] An LLVM 2025 dev meeting talk lists "accuracy tolerances" and "NaN/INF/denorm handling" among hard areas for HLSL test writing.
|
||||
- [web] WARP is Microsoft's recommended D3D11 device for service/server environments; Session 0 services cannot use hardware D3D9 HAL on WDDM (also in the earlier file).
|
||||
- [abs] WARP's own float conformance statement (denorm flushing, mad fusion) was not found in a primary document; the D3D11 rules in 2.1 are the contract WARP implements.
|
||||
- [web] vkd3d-proton and Wine's `d3d12` tests mark WARP-specific broken cases (see 2.2).
|
||||
|
||||
## 6. Other paths
|
||||
|
||||
### 6.1 Slang
|
||||
|
||||
- [doc] Targets page (shader-slang.org/slang/user-guide/targets.html): D3D11 (DXBC), D3D12 (DXIL), Vulkan (SPIR-V), OpenGL (GLSL source, SPIR-V, vendor binaries; "only limited support for compiling code for OpenGL"), Metal, CUDA and OptiX, CPU compute ("compute only", "functional, but not feature- or performance-complete"), WebGPU (WGSL, "work in progress"). GLSL ES: not mentioned.
|
||||
- [doc] `a2-04-glsl-target-specific.md`: version selected by `-profile glsl_<version>` (example `glsl_450`); the GLSL target "is currently less mature than the SPIR-V target and has several known limitations"; known issue with 3-element vector constant-buffer packing. GLSL ES / `300 es`: not mentioned. [abs] A search for Slang emitting `#version 300 es` found nothing.
|
||||
- [doc] `docs/cpu-target.md`: outputs C++ (`-target cpp`) or executables, shared libraries, host-callable code, object code; in-process execution through `getTargetHostCallable`; default host-callable backend is `slang-llvm` (no file system, no C compiler needed, shader style only), alternatives are system Clang/GCC/Visual Studio via temp files; limitations: no barriers, no user atomics (except compiler-generated 32/64-bit int add), entry points cannot be named `main`, `float16_t` unsupported, limited bounds-check handling; "no content on floating-point semantics".
|
||||
- [web] Slang can compile to SPIR-V directly or via GLSL (`-emit-spirv-via-glsl`, glslang downstream).
|
||||
|
||||
### 6.2 wgpu / naga
|
||||
|
||||
- [src] naga README table: inputs SPIR-V (primary), WGSL (primary, "fully validated"), GLSL (secondary, "GLSL 440+ and Vulkan semantics only"); outputs SPIR-V, Metal, HLSL ("Shader Model 5.0+"), WGSL (secondary), GLSL (secondary, "GLSL 330+ and GLSL ES 300+"). HLSL is not an input language. naga is a different translator from ANGLE's; wgpu's DX12 path uses naga HLSL output compiled with DXC or FXC (`WGPU_DX12_COMPILER`, ci.yml).
|
||||
- [src] wgpu CI deletes `naga/tests/out` snapshots before test runs (ci.yml), i.e. naga keeps snapshot-compared output files.
|
||||
|
||||
### 6.3 Tint (Dawn's WGSL compiler)
|
||||
|
||||
- [web] Search snapshots (2020-2021, a CMake-only Dawn fork): Tint reads WGSL (default on) and SPIR-V (`TINT_BUILD_SPV_READER`, off by default); writers for SPIR-V, WGSL, HLSL, MSL, GLSL (GLSL writer added October 2021, validated with glslang). [abs] The current upstream README was not retrievable (404 at tried paths), so current readers and writers are unconfirmed.
|
||||
|
||||
### 6.4 Shader-function unit-test frameworks and GPU-free paths
|
||||
|
||||
- [web] `shadertest` (Kupoman, Python): load a GLSL file, call its functions from pytest; supports bool, int, uint, float and vectors; implementation (GPU context) not read.
|
||||
- [web] `shdr` (cdave1): experimental CPU emulator for GLSL fragment shaders, translates GLSL to JavaScript, "does not quite match the glsl". `rpu` (Rust crate): a GLSL-like language compiled to WebAssembly for CPU rendering, not full GLSL. HlslUnit (.NET): an interpreter that runs HLSL on the CPU (search summary).
|
||||
- [web] Blender added infrastructure to run GLSL shader unit tests and shares C++/GLSL source in the GPU module, compiled through the real GPU backend.
|
||||
- [web] Khronos forum "GLSL Unit" (Google MapsGL team) unit-testing framework for shaders, old, likely dormant.
|
||||
- [doc] Vulkan Validation Layers `debug_printf`: shaders print through `debugPrintfEXT`; output goes to the validation layer message stream; [web] each a runtime Vulkan layer, not a numeric assertion framework.
|
||||
- [abs] No GPU-free GLSL ES interpreter with IEEE-faithful float semantics, maintained, and used by a notable project was found.
|
||||
|
||||
## 7. Cross-cutting: tools that produce SPIR-V for sections 1-3 (item 8)
|
||||
|
||||
| Tool | Input | Output | Library with no entry point | Maintenance |
|
||||
|---|---|---|---|---|
|
||||
| glslang | GLSL, ESSL, HLSL (deprecated 2026-04-16) | SPIR-V | not covered in README | active (commits 2026-04) |
|
||||
| dxc `-spirv` | HLSL | SPIR-V | `lib_6_x` only documented for raytracing; `-E` entry otherwise [abs] export behaviour | active repo, wiki and docs |
|
||||
| SPIRV-Cross | SPIR-V | GLSL (incl. `300 es`, `310 es`), HLSL, MSL | HEAD has library-module path keyed on `LinkageAttributes Export` | active (2026-10-14) |
|
||||
| Slang | Slang (accepts HLSL-like syntax) | SPIR-V, HLSL, GLSL desktop (`-profile glsl_N`), WGSL (WIP), CPU, others | not examined | active |
|
||||
| Unity HLSLcc | DX bytecode (DXBC) | GLSL 3.2+, GLSL ES 2.0+ ("OpenGL ES 3.0+" used for Unity), Metal, Vulkan GLSL | n/a (bytecode) | `pushed_at` 2024-07-16, not archived, 30 open issues, README "used to generate all Unity shaders for OpenGL, OpenGL ES 3.0+, Metal, Vulkan"; supports `min16float`, infers sampler precision |
|
||||
| naga | WGSL, SPIR-V, GLSL 440+ | SPIR-V, WGSL, GLSL ES 300+, HLSL SM5+, MSL | n/a | active (wgpu) |
|
||||
| Tint | WGSL (SPIR-V reader optional) | SPIR-V, HLSL, MSL, GLSL (per 2021 snapshot) | n/a | unconfirmed |
|
||||
|
||||
- [src] Which can also feed Amber/Vulkan runs: glslang and dxc produce SPIR-V from GLSL/HLSL; Amber itself accepts HLSL and GLSL source in `SHADER` blocks directly (section 1), choosing compilers internally [web: Amber builds against glslang/shaderc; which is used for HLSL not read].
|
||||
- [src] Which can emit GLSL ES 3.00 for the web from one source: SPIRV-Cross (`--es --version 300`, with the fragment precision options above), naga (GLSL ES 300+), HLSLcc (GLSL ES from DXBC; `ES 3.0+` per README), Slang (not documented).
|
||||
- [web] WGSL for the web: naga and Tint write WGSL (naga: "secondary"); Slang WGSL target "work in progress".
|
||||
|
||||
## 8. What each path exercises (stated as facts about the scope of each check)
|
||||
|
||||
| Path | Executes | Compiler/translator in the chain | Float rules in force | What a pass establishes | What it does not |
|
||||
|---|---|---|---|---|---|
|
||||
| Amber on lavapipe | SPIR-V on Mesa llvmpipe JIT (CPU) | glslang or dxc/other to SPIR-V, then Mesa NIR/LLVM | Vulkan spec; GLSL-derived SPIR-V denorm handling per Mesa; FMA contraction per Mesa/LLVM | The shader source, as compiled to SPIR-V, computes within the stated tolerance on a Vulkan 1.3 conformant implementation | Behaviour of ANGLE's HLSL, fxc/D3DCompile, NVIDIA D3D11 |
|
||||
| Amber on SwiftShader | SPIR-V on SwiftShader's JIT (Reactor/Subzero) | same front end | Vulkan spec; float controls reported off (older commit) | same, on a second conforming implementation | same |
|
||||
| Amber on a real Vulkan GPU | SPIR-V on the driver | same | vendor | The SPIR-V on that driver | WebGL path |
|
||||
| glslang/SPIRV-Cross text snapshot tests | nothing executed | translators only | none | translation is stable between versions | numerics |
|
||||
| ANGLE `angle_end2end`/dEQP on D3D11 | GLSL ES via ANGLE translator, HLSL, `D3DCompile` (fxc), NVIDIA D3D11 driver, as in Chrome on Windows | ANGLE as built in the harness (the same codebase Chrome vendors) | D3D11 rules (flush to zero, 1 ULP) over ESSL rules | The ES shader behaviour of that ANGLE build on that driver | Chrome's own flags/feature overrides unless reproduced |
|
||||
| ANGLE on SwiftShader (`angle-swiftshader`) | GLSL ES via ANGLE to SPIR-V, SwiftShader | ANGLE Vulkan backend | Vulkan | ANGLE's Vulkan translation path, software | ANGLE's D3D11 path |
|
||||
| D3D11/12 compute on WARP | HLSL via dxc/fxc, WARP CPU rasterizer | Microsoft compiler | D3D rules as implemented by WARP | The HLSL source on a D3D conformant software device | NVIDIA hardware or the WebGL route |
|
||||
| D3D11/12 compute on GTX 1070 | HLSL via dxc/fxc, NVIDIA driver | | D3D rules, vendor | The HLSL source on that GPU | other vendors |
|
||||
| wgpu/naga on lavapipe or WARP | WGSL (or SPIR-V in) via naga | naga | WGSL/Vulkan or D3D12 | naga's translation of that source on that device | ANGLE or glslang/dxc paths |
|
||||
| Slang CPU | Slang to C++ or LLVM, run on the host CPU | Slang | host CPU IEEE with the host compiler's flags (page silent) | Slang source semantics on a CPU | HLSL or GLSL as consumers compile them |
|
||||
| GLSL-to-CPU emulators/interpreters | the interpreter | the interpreter | the interpreter's host floats | the interpreted source | any real driver |
|
||||
| Browser WebGL2 (earlier file) | GLSL ES via browser's ANGLE | | | the exact consumer path on that browser/GPU | other browsers/drivers |
|
||||
|
||||
- [src] Spec positions that bound what a conforming-implementation pass can claim: GLSL ES 3.00/3.10 allow flush of subnormals to zero at any time, undefined rounding, `a*b+c` either fused or two roundings, and undefined trig precision; D3D11 requires flushing subnormals on math input/output and allows fused ops up to the unfused tolerance; Vulkan lets an implementation flush unless a preserve mode is requested and supported.
|
||||
- [src] Where the same GLSL ES source goes through ANGLE→HLSL→`D3DCompile`→driver, the compiler flags in `Renderer11.cpp` (OPTIMIZATION_LEVEL2, retry configurations, conditional IEEE_STRICTNESS) are part of the path; a SPIR-V route (glslang or dxc, then a Vulkan implementation) has none of those stages.
|
||||
- [abs] No source found that quantifies disagreement between ANGLE→D3D11 and a Vulkan software implementation for the same GLSL ES function.
|
||||
|
||||
## 9. Gaps (searched, not resolved)
|
||||
|
||||
- Lavapipe and SwiftShader current `VkPhysicalDeviceFloatControlsProperties` values; FMA contraction policy in either; the lavapipe docs page.
|
||||
- Amber's own CI jobs and which compilers it uses for HLSL and GLSL.
|
||||
- Whether `dxc -spirv` can emit an exported-function library that SPIRV-Cross's library path accepts; whether `dxc` flush/preserve execution modes are emitted by default.
|
||||
- Tint's current readers and writers.
|
||||
- Slang GLSL ES output; Slang CPU float flags.
|
||||
- ANGLE: default of `enableIEEEStrictness`; prebuilt standalone `libEGL`/`libGLESv2` availability for Windows outside Chrome/Electron; Chrome-specific translator options.
|
||||
- WARP numerics statement; DXC test tolerance values per intrinsic row (the XML table structure was read, not every row).
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
# Payload nesting limits in comparable libraries
|
||||
|
||||
Fork: `cultlib-gaps:question:payload-depth-limit` (options reference-500, shared-256, shared-128).
|
||||
Raised in `cultlib-gaps:cut_spec:cut-union-bounds-cs.r1`. Gathered 2026-10-11 by Eyes
|
||||
(`eyes-depth-limit-prior-art`). Facts only; no recommendation. Source files were fetched
|
||||
from each project's default branch on 2026-10-11 (raw.githubusercontent.com); line numbers
|
||||
refer to those fetches and move with the branch. "(inferred)" marks a reading of code or
|
||||
text that the source does not state.
|
||||
|
||||
Terms: "read" = decode/parse; "write" = encode/serialize; "skipped" = content a decoder
|
||||
walks past without materialising (ignored fields, unknown fields).
|
||||
|
||||
## 1. Summary table
|
||||
|
||||
| Library | Default | Counts | Read / write | Source |
|
||||
|---|---|---|---|---|
|
||||
| MessagePack-CSharp (UntrustedData and TrustedData) | 500 (`MaximumObjectGraphDepth`) | each `DepthStep` call: formatter-driven, one step per container a formatter enters; check is `Depth >= max` before increment | read only; skipped content not counted unless a formatter calls DepthStep (see 2.1) | MessagePackSecurity.cs:31,41,92,266-273 |
|
||||
| rmp-serde | 1024 (`depth: u16`, `set_max_depth`) | containers (array, map, ext newtype) via `depth_count!`; counter decremented, error at 0 | read: yes; write: error variant exists but is never constructed | decode.rs:51-60,213,294,309-310,566,583,616; encode.rs:37,49,64 |
|
||||
| serde_json | 128 (`remaining_depth`, `disable_recursion_limit`) | each `[` and `{` entered by `deserialize_any`-style paths; error when the counter reaches 0 (deepest enterable container is 127, inferred from the decrement-then-test-zero code) | read only; serializer has no depth code; skipped values (`ignore_value`) are iterative and contain no `check_recursion!` call | de.rs:63,1357-1387,1434,1445,1102-1215; ser.rs (no match) |
|
||||
| msgpack-python (C unpacker) | 1024 (`MSGPACK_EMBED_STACK_SIZE`) | each container pushed on a fixed stack, before the zero-length check; raises `StackError` | read: yes (including `skip()`); write: `Packer` `DEFAULT_RECURSE_LIMIT = 1024` (`ValueError`) | unpack.h:19; unpack_template.h:117-127; _unpacker.pyx:208-211,524; _packer.pyx:42,259-262 |
|
||||
| msgpack-python (pure-Python fallback) | Python's recursion limit; `RecursionError` is mapped to `StackError` | recursion frames | read; write via `DEFAULT_RECURSE_LIMIT = 1024` | fallback.py:66,586-606 |
|
||||
| @msgpack/msgpack | decode: no depth limit found (heap-allocated `StackPool`, iterative); encode `maxDepth` 100 | encode: depth argument to `doEncode` | read: unbounded; write: yes (`Too deep objects in depth N`) | Decoder.ts:111-190,640-643; Encoder.ts:9,97,173; README.md:119 |
|
||||
| msgpackr | no depth option found | recursive `readObject` (inferred from the single recursion comment at unpack.js:506) | neither | unpack.js, pack.js (grep for depth/maxDepth: no hit) |
|
||||
| System.Text.Json | 64 (reader and serializer); writer default 1000 | `JsonReaderOptions.MaxDepth`; `JsonSerializerOptions.MaxDepth` default is the minimum of reader and writer defaults | read: yes; write: yes (writer 1000, serializer 64) | JsonReaderOptions.cs:13,43; JsonWriterOptions.cs:18; JsonSerializerOptions.cs:27-28,580-593 |
|
||||
| Newtonsoft.Json | 64 (`JsonReader` ctor; `MaxDepth` nullable) | reader `Depth + 1 > _maxDepth` | read; `JsonWriter.cs` has no MaxDepth match | JsonReader.cs:332,356-359 |
|
||||
| protobuf C++ | 100 (`default_recursion_limit_`) | recursion budget decremented per nested message/group | read | coded_stream.h:630,1588-1602,1637 |
|
||||
| protobuf C# | 100 (`DefaultRecursionLimit`) | CodedInputStream recursion | read | CodedInputStream.cs:57,120-147 |
|
||||
| protobuf Java | 100 (`defaultRecursionLimit`) | CodedInputStream recursion | read | CodedInputStream.java:45,52,521 |
|
||||
| protobuf upb | 100 (`kUpb_WireFormat_DefaultDepthLimit`) | `d->depth` per decode; max configurable up to a uint16 | read | upb/wire/internal/constants.h:11; decode.c:105,140,192,619,1306-1312 |
|
||||
| ciborium | 256 (`recurse` counter) | `recurse()` wrapper around array/map/tag decode | read only (cib_ser.rs has no depth/recurs match) | ciborium/src/de/mod.rs:54-74,812,854 |
|
||||
| cbor2 (Python) | 400 `max_depth` since 5.9.0 (CVE-2026-26209 fix); 6.0.0 (2026-04-28) rewrote the decoder in Rust as iterative, "limited only by the available memory" | containers | read | docs/versionhistory.rst:120-142,172-173 |
|
||||
| BSON (MongoDB) | 100 levels | "Each object or array adds a level" | the doc page does not say read vs write | mongodb.com/docs/manual/reference/limits/ |
|
||||
| Go encoding/json | 10000 (`maxNestingDepth`) | scanner `parseState` stack length; the scanner validates the whole input before decoding (`checkValid`) | read; encoder has no depth limit, only pointer-cycle detection after 1000 pointer levels (`startDetectingCyclesAfter`) | scanner.go:146-186; encode.go:298-305,764 |
|
||||
| simdjson | 1024 (`DEFAULT_MAX_DEPTH`) | parser allocation parameter | read | common_defs.h:49; dom/parser.h:596,599 |
|
||||
|
||||
## 2. Detail by library
|
||||
|
||||
### 2.1 MessagePack-CSharp (the C# reference's decoder)
|
||||
|
||||
- `UntrustedData` and `TrustedData` both set `MaximumObjectGraphDepth = 500`
|
||||
(MessagePackSecurity.cs:31, 41; property initialiser 92). `TrustedData` was lowered to 500
|
||||
by commit 6b025eab "Limit stack depth to 500 even for trusted data" (2024-10-29) and
|
||||
set from `UntrustedData` by 7c087905 (2024-11-28), per the commit history of
|
||||
MessagePackSecurity.cs (api.github.com/repos/MessagePack-CSharp/MessagePack-CSharp/commits?path=src/MessagePack/MessagePackSecurity.cs).
|
||||
The commit messages give no rationale for the value 500.
|
||||
- `DepthStep(ref MessagePackReader)` throws `InsufficientExecutionStackException` when
|
||||
`reader.Depth >= MaximumObjectGraphDepth`, else increments (MessagePackSecurity.cs:266-273).
|
||||
The doc comment says callers must decrement after leaving the edge (:254-260).
|
||||
- Counting is by contract with formatters, not by the reader: README.md says `DepthStep` and
|
||||
`Depth--` "should surround the bulk of any `IMessagePackFormatter<T>.Deserialize` method"
|
||||
(README lines ~1161-1175). So content no formatter reads is not counted (inferred from the
|
||||
README contract; the CultLib measurement in the question confirms it: an extra trailing slot
|
||||
of 100000 nested arrays is accepted).
|
||||
- Write: no depth check in the serializer or writer in the code fetched (MessagePackReader.cs
|
||||
holds `Depth` at :53-75,133; the writer was not fetched). The CultLib question reports a
|
||||
3001-deep payload written and then refused on read.
|
||||
- Advisory GHSA-7q36-4xx7-xcxf / CVE-2020-5234 (published 2020-01-31): "Untrusted data can lead
|
||||
to DoS attack due to hash collisions and stack overflow in MessagePack". Affected < 1.9.11 and
|
||||
>= 2.0.0 < 2.1.90. Mitigation is opting into `UntrustedData`; the advisory text does not state
|
||||
the depth value.
|
||||
- Advisory GHSA-cj9g-3mj2-g8vv / CVE-2026-48512 (published 2026-06-25), fixed in 2.5.301 and
|
||||
3.1.7: the JSON conversion helpers (`ConvertFromJson`, `TinyJsonReader.ReadNextToken`, the
|
||||
ext-100 branch of `ConvertToJson`) recursed without consulting `MaximumObjectGraphDepth`; the
|
||||
advisory says `UntrustedData` "does not fully mitigate" them. This is the class of defect
|
||||
"a depth limit exists but one recursive path skips it".
|
||||
|
||||
### 2.2 rmp-serde (the Rust runtime's decoder)
|
||||
|
||||
- `Deserializer` has `depth: u16`, initialised to 1024 (decode.rs:181, 213, 294);
|
||||
`set_max_depth` clamps to `u16::MAX` (:309-310).
|
||||
- `depth_count!` decrements, returns `DepthLimitExceeded` when the counter reaches 0, runs the
|
||||
body, increments (decode.rs:51-62). Used for arrays (:566), maps (:583) and ext newtype (:616).
|
||||
So the usable nesting is 1023 levels (inferred from decrement-then-test-zero).
|
||||
- `ignored_any` is forwarded to `deserialize_any` (decode.rs:~749-753 `forward_to_deserialize_any!`),
|
||||
which goes through the same counted paths: skipped containers are counted.
|
||||
- Write: `Error::DepthLimitExceeded` exists in encode.rs (:37, 49, 64) but a grep finds no site
|
||||
that constructs it (only definition, `source`, `Display`). The writer has no depth check
|
||||
(inferred from the three matches).
|
||||
- Crate version at fetch: 1.3.1 (Cargo.toml). No RUSTSEC advisory about depth was found for
|
||||
rmp-serde, rmpv or ciborium in a web search (search-engine result, not a database query).
|
||||
|
||||
### 2.3 serde_json
|
||||
|
||||
- `remaining_depth: u8 = 128` (de.rs:34, 63); `check_recursion!` decrements and errors with
|
||||
`RecursionLimitExceeded` when it reaches 0 (de.rs:1372-1387), used at the `[`/`{` branches of
|
||||
`deserialize_any` and the typed deserialize methods (de.rs:1434, 1445, 1750, 1801, 1838, 1849,
|
||||
1882). `disable_recursion_limit()` exists behind the `unbounded_depth` feature (de.rs:197-216,
|
||||
1357-1370). A third-party analysis (search result, not fetched) states the deepest enterable
|
||||
container is level 127.
|
||||
- `deserialize_ignored_any` calls `ignore_value()` (de.rs:1910-1916). `ignore_value` (:1102-~1215)
|
||||
is an explicit loop with a heap `scratch` stack of frames and contains no `check_recursion!`
|
||||
(the `check_recursion!` lines in the file are all at 1372+). Reading: ignored values in
|
||||
serde_json are walked iteratively and are not depth-limited (inferred from the code; not
|
||||
tested). This differs from the question's statement that "serde_json ... count ignored values
|
||||
the same way".
|
||||
- Serializer: grep of ser.rs for depth/recurs returns no match.
|
||||
- Related: serde-json-wasm GHSA-rr69-rxr6-8qwf / RUSTSEC-2024-0012 (2024-02-09), fixed in 1.0.1
|
||||
and 0.5.2: "This was fixed by adding a check for recursion depth"; the advisory text gives no value.
|
||||
rustc_serialize RUSTSEC-2022-0004: parsing had no stack-depth limit and "can cause a stack
|
||||
overflow, which aborts the process".
|
||||
- serde_stacker (crates.io description) grows the stack dynamically and is documented as paired
|
||||
with `disable_recursion_limit()`; its README warns that Drop, Debug and Display on deep values
|
||||
can still overflow. A forum thread reports dropping a deeply nested `Value` (~10 million
|
||||
levels) overflowing after a successful parse.
|
||||
|
||||
### 2.4 msgpack-python
|
||||
|
||||
- C unpacker: `MSGPACK_EMBED_STACK_SIZE` 1024 (unpack.h:19); `start_container` returns -3 when
|
||||
`top >= 1024` before it checks for an empty container (unpack_template.h:117-127), and -3 is
|
||||
raised as `StackError` (_unpacker.pyx:208-211, 521-524). The docstring says "Raises `StackError`
|
||||
when `packed` contains too nested" (_unpacker.pyx:158, 313). Empty containers at depth 1025 are
|
||||
therefore also refused (inferred from check order). `skip()` runs the same state machine
|
||||
(fallback.py:599-606 shows `skip` raising `StackError`; the C path is the same unpack function,
|
||||
inferred).
|
||||
- Pure-Python fallback: depth is Python's recursion limit; `RecursionError` is converted to
|
||||
`StackError` (fallback.py:586-606).
|
||||
- Packer: `DEFAULT_RECURSE_LIMIT = 1024`; `_pack` raises `ValueError("recursion limit exceeded.")`
|
||||
when `nest_limit < 0` and decrements per container (_packer.pyx:42, 259-262; fallback.py:66, 699-711, 801-804).
|
||||
|
||||
### 2.5 JavaScript
|
||||
|
||||
- @msgpack/msgpack Decoder: iterative, with a heap `StackPool` (Decoder.ts:111-190, loop :640-643);
|
||||
no depth option appears in the decoder (grep of "depth" returns only stack-pool lines). The
|
||||
README option table lists `maxDepth` default 100 (README.md:119) and Encoder.ts uses it
|
||||
(`DEFAULT_MAX_DEPTH = 100`, :9; `doEncode` throws `Too deep objects in depth N` when
|
||||
`depth > maxDepth`, :173). So the library is asymmetric: encode bounded, decode unbounded in
|
||||
depth (limited by memory). The CultLib question states TS "parse the whole payload"; consistent.
|
||||
- msgpackr: grep of unpack.js and pack.js finds no maxDepth option or depth counter.
|
||||
- msgpack5 (another npm package) GHSA-24ch-f2g6-9hhh (2026-10-08): "The decoder has no
|
||||
nesting-depth limit for arrays and maps ... exhaust the JavaScript call stack"; fix in 6.1.0
|
||||
limits depth to 100 by default, option `maxDepth`, error "Maximum decode depth exceeded".
|
||||
- msgpack-java: CVE record at vulncheck.com ("through 0.9.12", GitHub issue #1015):
|
||||
`MessageUnpacker.unpackValue()` "recursively deserializes arrays and maps without nesting
|
||||
depth limits", causing StackOverflowError. Fix value not retrieved.
|
||||
|
||||
### 2.6 .NET JSON
|
||||
|
||||
- System.Text.Json: `JsonReaderOptions.DefaultMaxDepth = 64` (JsonReaderOptions.cs:13);
|
||||
`JsonWriterOptions.DefaultMaxDepth = 1000` (JsonWriterOptions.cs:18);
|
||||
`JsonSerializerOptions.DefaultMaxDepth` = the reader's 64, with a comment that it is the
|
||||
minimum of the reader and writer defaults (JsonSerializerOptions.cs:27-28). So the serializer
|
||||
limit applies to both directions at 64; the bare writer at 1000.
|
||||
- Newtonsoft.Json: `JsonReader` constructor sets `_maxDepth = 64` (JsonReader.cs:332); exceeding
|
||||
throws `JsonReaderException` "The reader's MaxDepth of {0} has been exceeded" (:356-359).
|
||||
`JsonWriter.cs` has no MaxDepth match. Advisory GHSA-5crp-9r3c-p9vr / CVE-2024-21907
|
||||
(published 2022-06-22, affects < 13.0.1): "Insecure Defaults ... improper handling of
|
||||
expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM
|
||||
usage". Advisory figures: deserialising needs >10 million nesting (9.5 MB of `{a:{a:`) for >10 s
|
||||
of CPU; serialising (`SerializeObject`, `JObject.ToString`) overflowed the stack at about
|
||||
20,000 levels. The advisory's own mitigation example sets `MaxDepth = 128`; the 13.0.1 default
|
||||
is 64 (the changelog line was not retrieved; the 64 is read from the code above).
|
||||
|
||||
### 2.7 Protobuf
|
||||
|
||||
- Default recursion limit 100 in C++ (coded_stream.h:630 comment "100 by default"), C#
|
||||
(CodedInputStream.cs:57), Java (CodedInputStream.java:45) and upb (constants.h:11). C# rejects a
|
||||
non-positive limit (cs:142-144); upb stores it in a 16-bit field (`upb_DecodeOptions_MaxDepth`).
|
||||
- Skipped content: GHSA-735f-pc8j-v9w8 / CVE-2024-7254 (published 2024-09-19): in protobuf Java
|
||||
(full and lite), Kotlin and JRuby, "Parsing nested groups as unknown fields with
|
||||
DiscardUnknownFieldsParser or Java Protobuf Lite parser, or against Protobuf map fields,
|
||||
creates unbounded recursions". Fixed in 3.25.5, 4.27.5, 4.28.2. The advisory implies unknown
|
||||
(skipped) content had escaped the recursion limit until the fix.
|
||||
- No write-side limit was looked up.
|
||||
|
||||
### 2.8 CBOR, BSON, Go, simdjson
|
||||
|
||||
- ciborium: `recurse: 256` in `from_reader` and `deserializer_from_reader`
|
||||
(de/mod.rs:812, 854); `from_reader_with_recursion_limit` documents "Set a high recursion limit at
|
||||
your own risk (of stack exhaustion)!" (:819-822, 859-862). `recurse()` errors
|
||||
`RecursionLimitExceeded` at 0 (:64-74). No depth/recurs match in ser/mod.rs.
|
||||
- cbor2: advisory GHSA-3c37-wwvx-h642 / CVE-2026-26209 (published 2026-03-23, fixed 5.9.0):
|
||||
"uncontrolled recursion" in both the pure-Python and C decoders; PoC is 1000 nested `0x81`
|
||||
bytes raising `RecursionError`. The C decoder used `Py_EnterRecursiveCall` (Python's
|
||||
interpreter limit). Fix: `max_depth` parameter, default 400, in 5.9.0 (versionhistory.rst:172-173).
|
||||
6.0.0 (2026-04-28): decoder rewritten in Rust, iterative, depth "limited only by the available
|
||||
memory" (versionhistory.rst:138-140).
|
||||
- BSON: MongoDB documents "no more than 100 levels of nesting for BSON documents. Each object or
|
||||
array adds a level." The page does not state read vs write.
|
||||
- Go encoding/json: `maxNestingDepth = 10000` "limits the max nesting depth to prevent stack
|
||||
overflow" (scanner.go:146-148); `pushParseState` errors "exceeded max depth" when the parse stack
|
||||
exceeds it (:181-185). `checkValid` scans the whole input before decode (scanner.go:30), so every
|
||||
container, including ones the target type ignores, is counted (inferred from `checkValid` being
|
||||
the first step of `Unmarshal`; not read in decode.go). Encoder: `ptrLevel` over 1000 starts
|
||||
cycle detection, which is not a depth limit (encode.go:298-305).
|
||||
- simdjson: `constexpr size_t DEFAULT_MAX_DEPTH = 1024` (common_defs.h:49); `allocate(capacity,
|
||||
max_depth = DEFAULT_MAX_DEPTH)`. (The PHP binding defaults to 512 per the PHP manual.)
|
||||
|
||||
## 3. Read-side vs write-side, and skipped content
|
||||
|
||||
Enforces on write: rmp-serde (variant defined, no construction found: no); msgpack-python Packer
|
||||
(1024); @msgpack/msgpack Encoder (100); System.Text.Json (serializer 64, writer 1000); Go
|
||||
encoding/json (no depth limit; cycle detection only); MessagePack-CSharp (no); serde_json (no);
|
||||
ciborium (no); Newtonsoft (no match in JsonWriter.cs).
|
||||
|
||||
Counts skipped/ignored content: rmp-serde (yes, via `ignored_any` to `deserialize_any`);
|
||||
msgpack-python C unpacker (yes, `skip()` shares the stack); Go (scanner pre-pass, inferred);
|
||||
serde_json (apparently no: `ignore_value` is iterative and uncounted, inferred);
|
||||
MessagePack-CSharp (no: counting is by formatter contract); protobuf Java (unknown groups were
|
||||
unbounded before CVE-2024-7254).
|
||||
|
||||
## 4. Stack use per nesting level
|
||||
|
||||
No library documents bytes of stack per nesting level. What exists:
|
||||
- ciborium: "Set a high recursion limit at your own risk (of stack exhaustion)!"
|
||||
- Newtonsoft advisory: serialisation overflowed the default stack at about 20,000 levels
|
||||
(a count, not bytes; thread stack size not stated in the advisory).
|
||||
- cbor2 advisory PoC: 1000 nested arrays reached the Python recursion limit.
|
||||
- Rhai docs (search snippet, a different system): default call depth 64 in release, 8 in debug,
|
||||
an illustration that debug frames are larger.
|
||||
- CultLib's own measurement (from the question, not a prior-art source): Rust debug on 2 MiB
|
||||
aborts at about 408 containers, which is about 5 KiB per level (2 MiB / 408, inferred);
|
||||
release carries 1200+; Python near 1000; TS near 4000.
|
||||
- serde_json and rmp-serde pick 128 and 1024 respectively; neither source documents how the value
|
||||
was derived. Arrow Parquet Variant (docs.rs search result) says it picked MAX_NESTING_DEPTH 128
|
||||
"to match serde_json" because validation recurses.
|
||||
|
||||
## 5. Not retrieved
|
||||
|
||||
The rationale for 500 in MessagePack-CSharp; the Newtonsoft 13.0.1 release note; the fix depth
|
||||
chosen by msgpack-java and serde-json-wasm; protobuf write-side behaviour; decode.go's use of
|
||||
`checkValid`; MessagePack-CSharp writer code; simdjson's depth-error semantics.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Python MessagePack default payload: history and probes (2026-10-11)
|
||||
|
||||
Serves `cultlib-gaps:ruling:py-msgpack-default` and `cultlib-gaps:cut_spec:cut-py-msgpack-default.r1`.
|
||||
|
||||
## How a JSON payload default got into .cc
|
||||
|
||||
1. Ghostlight `2531b42` (2026-05-06), the first consumer: "The JSONL store is deliberate bootstrap plumbing", msgpack optional.
|
||||
2. CultLib `b214daab` (2026-05-24) squashed in `python/cultcache-py` from GameCult/cultcache-py@9b4bbb8a with
|
||||
`define_document_type` already defaulting to `_json_payload_encoder`; msgpack was an optional extra and the README
|
||||
promised a standard-library-only bootstrap. JSON was the default because it was the only codec without msgpack.
|
||||
3. CultLib `a1ef58cb` (2026-06-13) made msgpack a hard dependency but kept the JSON default and the optional-import
|
||||
guards, so the default outlived its reason.
|
||||
4. Earlier history is unreachable (GameCult/cultcache-py no longer exists; CultLib holds only the squash). voidbot
|
||||
`search_history` found no discussion of the choice.
|
||||
|
||||
## Probes
|
||||
|
||||
- `probe1.py`, `probe2.py`: no runtime records a payload's format in the store, so a reader cannot tell JSON from
|
||||
MessagePack except by trying; a one-byte JSON payload `0`-`9` is valid MessagePack (48-57); a MessagePack map with
|
||||
a non-str/bytes key packs but `unpackb` refuses it.
|
||||
- `rmpprobe/`: `rmp_serde::from_slice` reads a JSON object's bytes as `Number(123)`, which is why Brokkr's daemon must
|
||||
read exactly one MessagePack value with a trailing-bytes check.
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import json, sys, msgpack
|
||||
print(sys.version.split()[0], msgpack.version)
|
||||
def pk(v): return msgpack.packb(v, use_bin_type=True)
|
||||
def up(b): return msgpack.unpackb(b, raw=False)
|
||||
def jd(v): return json.dumps(v, ensure_ascii=False, sort_keys=True).encode()
|
||||
cases = {"dict": {"schema":"x","n":1}, "list":[1,2], "str":"hi", "digit":5, "neg":-3, "true":True, "null":None, "float":1.5, "bigdigit":12}
|
||||
print("== msgpack.unpackb on JSON payloads")
|
||||
for k,v in cases.items():
|
||||
b = jd(v)
|
||||
try: r = up(b); print(k, b, "->ACCEPTED", repr(r))
|
||||
except Exception as e: print(k, b, "->", type(e).__module__+"."+type(e).__name__, isinstance(e, ValueError), str(e)[:60])
|
||||
print("== json.loads on msgpack payloads")
|
||||
for k,v in cases.items():
|
||||
b = pk(v)
|
||||
try: r = json.loads(b.decode("utf-8")); print(k, b, "->ACCEPTED", repr(r))
|
||||
except Exception as e: print(k, b, "->", type(e).__name__, str(e)[:50])
|
||||
print("== semantic differences, value -> json roundtrip vs msgpack roundtrip")
|
||||
diffs = {"intkeys": {1:"a"}, "tuple": (1,2), "bytes": b"ab", "bigint": 2**70, "nan": float("nan"), "boolkey": {True:1}, "nested_intkey": {"a":{2:3}}, "set": {1,2}, "order": {"b":1,"a":2}}
|
||||
for k,v in diffs.items():
|
||||
out=[]
|
||||
try: out.append("json:"+repr(json.loads(jd(v))))
|
||||
except Exception as e: out.append("json:"+type(e).__name__)
|
||||
try: b=pk(v)
|
||||
except Exception as e: out.append("pack:"+type(e).__name__+":"+str(e)[:40]); print(k, out); continue
|
||||
try: out.append("mp:"+repr(up(b)))
|
||||
except Exception as e: out.append("unpack:"+type(e).__name__+":"+str(e)[:60])
|
||||
try: out.append("mp_nonstrict:"+repr(msgpack.unpackb(b, raw=False, strict_map_key=False)))
|
||||
except Exception as e: out.append("ns:"+type(e).__name__)
|
||||
print(k, out)
|
||||
print("== custom node classes")
|
||||
import collections, types
|
||||
class OnlyIter:
|
||||
def __iter__(self): return iter([1])
|
||||
for k,v in {"deque":collections.deque([1]), "mappingproxy":types.MappingProxyType({"a":1}), "userlist":collections.UserList([1]), "onlyiter":OnlyIter(), "frozenset":frozenset([1]), "userdict":collections.UserDict({"a":1})}.items():
|
||||
r=[]
|
||||
for name,f in (("pack",pk),("json",jd)):
|
||||
try: f(v); r.append(name+":ok")
|
||||
except Exception as e: r.append(name+":"+type(e).__name__)
|
||||
print(k, r)
|
||||
print("== JSON read recursion threshold")
|
||||
for d in (900, 1000, 3000, 5000, 10000, 100000):
|
||||
try: json.loads("["*d + "]"*d); print(d, "ok")
|
||||
except RecursionError: print(d, "RecursionError")
|
||||
print("== msgpack read deep")
|
||||
for d in (300, 1000, 2000, 100000):
|
||||
try: up(b"\x91"*d + b"\xc0"); print(d,"ok")
|
||||
except Exception as e: print(d, type(e).__module__, type(e).__name__)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Brokkr-shaped flow at CultLib 9d7b5842: a store written by today's JSON default, then read by a MessagePack-default definition.
|
||||
import sys, tempfile, os, json, msgpack, timeit, dataclasses
|
||||
sys.path.insert(0, r"F:/Projects/CultLib/packages/cultcache-py/src")
|
||||
import cultcache_py as cc
|
||||
from cultcache_py.stores import SingleFileMessagePackBackingStore
|
||||
d = tempfile.mkdtemp(); path = os.path.join(d, "blender-editor.ccmp")
|
||||
def open_cache(defn):
|
||||
c = cc.CultCache.builder().register_document_type(defn).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
|
||||
c.pull_all_backing_stores(); return c
|
||||
old = cc.define_document_type("brokkr.sync.var.v0")
|
||||
value = {"schema": "brokkr.sync.var.v0", "syncVarId": "v1", "enabled": True, "kind": "transform", "updatedAt": "2026-10-11T00:00:00Z"}
|
||||
c = open_cache(old); c.put(old, "sync/vars/v1", value)
|
||||
raw = msgpack.unpackb(open(path, "rb").read(), raw=False)
|
||||
print("store header", raw[0], "catalog entry", raw[1][0][:5] if raw[1] else None)
|
||||
print("record", raw[2][0][:3], "payload", raw[2][0][3][:40])
|
||||
# MessagePack-default reader, today's _messagepack pair standing in for the proposed default
|
||||
new = cc.define_document_type("brokkr.sync.var.v0", payload_encoder=lambda t: msgpack.packb(t, use_bin_type=True), payload_decoder=lambda b: msgpack.unpackb(b, raw=False))
|
||||
try: open_cache(new); print("new reader: ACCEPTED")
|
||||
except Exception as e: print("new reader refused:", type(e).__module__, type(e).__name__, str(e)[:80])
|
||||
# opt-in JSON pair reads it
|
||||
print("json opt-in reads:", open_cache(old).get(old, "sync/vars/v1") == value)
|
||||
# migration: read with JSON definition, write with MessagePack definition, same key
|
||||
v = open_cache(old).get(old, "sync/vars/v1"); c2 = cc.CultCache.builder().register_document_type(new).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
|
||||
c2._state # not pulled: pull would fail on the JSON doc
|
||||
try:
|
||||
c2.put(new, "sync/vars/v1", v); print("put without pull ok")
|
||||
except Exception as e: print("put without pull:", type(e).__name__, str(e)[:80])
|
||||
print("after migration new reader reads:", open_cache(new).get(new, "sync/vars/v1") == value)
|
||||
# size and speed, one Brokkr-shaped document
|
||||
jb = json.dumps(value, ensure_ascii=False, sort_keys=True).encode(); mb = msgpack.packb(value, use_bin_type=True)
|
||||
print("bytes json", len(jb), "msgpack", len(mb))
|
||||
print("encode us json %.2f msgpack %.2f" % (timeit.timeit(lambda: json.dumps(value, ensure_ascii=False, sort_keys=True).encode(), number=100000)*10, timeit.timeit(lambda: msgpack.packb(value, use_bin_type=True), number=100000)*10))
|
||||
print("decode us json %.2f msgpack %.2f" % (timeit.timeit(lambda: json.loads(jb.decode()), number=100000)*10, timeit.timeit(lambda: msgpack.unpackb(mb, raw=False), number=100000)*10))
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "rmpprobe"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[dependencies]
|
||||
rmp-serde = "=1.3.1"
|
||||
serde_json = "1"
|
||||
serde = "1"
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
fn main() {
|
||||
let json_obj = br#"{"enabled":true,"kind":"transform","schema":"brokkr.sync.var.v0"}"#;
|
||||
let r: Result<serde_json::Value, _> = rmp_serde::from_slice(json_obj);
|
||||
println!("rmp_serde on a JSON object payload: {:?}", r);
|
||||
let mp = rmp_serde::to_vec(&serde_json::json!({"schema":"x","n":1})).unwrap();
|
||||
println!("rmp_serde::to_vec of a json! object, first byte {:#04x} len {}", mp[0], mp.len());
|
||||
let j: Result<serde_json::Map<String, serde_json::Value>, _> = serde_json::from_slice(&mp);
|
||||
println!("serde_json on that MessagePack map: {:?}", j.map(|_| ()).map_err(|e| e.to_string()));
|
||||
let mut de = rmp_serde::Deserializer::from_read_ref(&json_obj[..]);
|
||||
let v: serde_json::Value = serde::Deserialize::deserialize(&mut de).unwrap();
|
||||
println!("one_value(json obj) = {:?}; one_value(mp) = {:?}", one_value(json_obj), one_value(&mp).map(|_| ()));
|
||||
println!("consumed {} of {} bytes, value {:?}", de.get_ref().len() as usize, json_obj.len(), v);
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn one_value(bytes: &[u8]) -> Result<serde_json::Value, String> {
|
||||
let mut de = rmp_serde::Deserializer::new(std::io::Cursor::new(bytes));
|
||||
let v: serde_json::Value = serde::Deserialize::deserialize(&mut de).map_err(|e| e.to_string())?;
|
||||
let used = de.get_ref().position() as usize;
|
||||
if used != bytes.len() { return Err(format!("{} trailing bytes after the first MessagePack value", bytes.len() - used)); }
|
||||
Ok(v)
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
# Python slot scalar types vs the C# reference: facts and pointers
|
||||
|
||||
Fork served: cultlib-gaps: Python slot scalar types vs the C# reference's refusals, raised by cut-union-bounds-py.h1 undone.
|
||||
Date: 2026-10-11. Gathered by eyes-py-slot-scalar-types (read-only). Facts and pointers only; no recommendation.
|
||||
|
||||
Checkout caveat: line numbers below are the operator checkout at `main` 4ea296ae. cultcache-py's bounds cut lives on
|
||||
branch `hands/union-bounds-py` (head 4328ddb4); cultcache-ts's union codec lives on `hands/union-codec-ts`
|
||||
(absent from `main`: `git ls-tree HEAD packages/cultcache-ts/src` has no `codec.ts`).
|
||||
|
||||
## 1. In CultLib
|
||||
|
||||
### 1.1 The measured gap (the trigger)
|
||||
|
||||
Verdict `cultlib-gaps:verdict:cut-union-bounds-py.s1`, claim `scalar-type-parity-unproven`, outcome Unproven, no finding
|
||||
(spec did not promise it). Probe `cross.py g-` files, 168 cases, job1.log:
|
||||
- 13 scalar slots x 8 wrong values; agree 86; C# refuses and Python accepts 82; C# accepts and Python refuses 0.
|
||||
- String slots (Id, Box.Id, Text.Id, Text.Content, Image.Uri, Image.Alt, Comp.Name, Pending.Message, Tool.Name):
|
||||
Python accepts int, float, true, bin, array, map (refuses neither str nor nil, as C#).
|
||||
- Int slots (Version, Completed.SourceVersion, Tool.Uses, Part.Count): Python accepts float, str, true, bin, array, map
|
||||
and nil; int5 agrees.
|
||||
- All 8 container-shape slots (children list, record, slot list, outcomes list, arm slots, union x2, document) agree on
|
||||
every wrong value (64 shape replacements agree). So only scalar leaves differ.
|
||||
- Related earlier finding: `cultlib-gaps:finding:cut-union-codec-py.s2.python-decode-slot-strictness-differs`
|
||||
(Python accepts Tool wire value [1,['hammer',12]] as Text(content=12), C# refuses; Python refuses short Gem value,
|
||||
C# accepts with default). Resolved Deferred to `cultlib-gaps:cut_spec:cut-union-bounds-py.r1`.
|
||||
|
||||
### 1.2 C# reference: how a slot's scalar type is declared and refused
|
||||
|
||||
- Declaration is the C# member type plus `[Key(n)]`; there is no CultLib scalar-type vocabulary.
|
||||
`src/GameCult.Caching/README.md:16-27` (example), `src/GameCult.Caching/Contracts/cultcache-schema-compatibility.md:31-35`
|
||||
("A member's slot is its MessagePack `[Key(n)]` integer").
|
||||
- Registry checks are structural only (CultCache.cs:1015-1053, messages 1430-1461: needs [MessagePackObject], [Key] on every
|
||||
public member, no string keys, distinct keys, constructor shape).
|
||||
- The wrong-scalar refusal is made by MessagePack-CSharp's per-type formatter (external library, not CultLib code); CultLib
|
||||
contains no scalar-type check of its own. (Grep of `src/` for scalar/wrong-type checks found none; unions get a guard:
|
||||
`src/GameCult.Caching.MessagePack/CultUnionGuard.cs:44-50` refuses non-integer key, key outside Int32, undeclared key,
|
||||
nil arm slots.)
|
||||
- Published schema: each member's `TypeName` is generated from the member's CLR type,
|
||||
`CultCache.cs:647` `TypeName = CultSchemaTypeNames.FromType(member.MemberType)`; `CultSchemaTypeNames.cs:9-31` yields
|
||||
`System.String`, `System.Int32`, `System.String[]`, `System.Collections.Generic.List<...>`.
|
||||
Catalog members persisted with `Slot, MemberName, TypeName, IsReference, IsMany, TargetSchemaName, IndexAlias, IsName`
|
||||
(`CultDocumentMessagePackSerialization.cs:465-496`; `CultCache.cs:39,326`).
|
||||
- The catalog TypeName is load-bearing at open: `CultCache.cs:874-877` a persisted member with a different TypeName is an
|
||||
error (`slot N changed type 'A' -> 'B'`); an absent member is only a warning (`:863-872`). The catalog names a type; no
|
||||
code reads it to validate payload scalars on read.
|
||||
|
||||
### 1.3 cultcache-rs
|
||||
|
||||
- Slot types are the Rust field types, enforced by serde. `packages/cultcache-rs/cultcache-rs-derive/src/lib.rs`:
|
||||
`#[cultcache(key = N)]` per field (`:126-171`), `slot_codec` (`:177`) generates `Serialize` as a positional tuple with nil
|
||||
gaps and `Deserialize` by slot (`:213-258`), extra trailing slots skipped with `IgnoredAny` (`:233`, `:305`). `bytes` and
|
||||
`default` attributes are the only extras (`:175-176`). Union: `CultUnion` derive, arm key Int32, unknown key a decode error
|
||||
(`:87-97`, `:326-340`; `packages/cultcache-rs/README.md:187-196`).
|
||||
- No separate scalar-type vocabulary; no catalog-type derivation was looked for in rs here (not read).
|
||||
|
||||
### 1.4 cultcache-ts
|
||||
|
||||
- On `main`: no slot codec. `packages/cultcache-ts/src/document.ts:51-64` takes author-declared schema members and refuses
|
||||
an empty `typeName`; `types.ts:22-24` member has `slot`, `typeName`. `swarm-documents.ts:91-95` hand-writes
|
||||
`typeName: "string"|"long"|"object"`. Payload encode/decode is a user function (`types.ts:17-18`).
|
||||
- On branch `hands/union-codec-ts`, `packages/cultcache-ts/src/codec.ts`: typed leaf codecs `string`, `boolean`, `bytes`,
|
||||
`int32`, `int64`, `float32`, `float64`, `list`, `array`, `nullable`, each with the reference's `typeName`
|
||||
(`"System.String"`, `"System.Int32"`, ... lines 39-128) and `expect`/`refuse` on a wrong kind in both encode and decode
|
||||
(lines 31-42, 60-79). `SlotsCodec.members` derives the schema catalog from the slot codecs (lines 17-20). Header
|
||||
comment lines 5-8 say the reference refuses a store whose slot names another type.
|
||||
- Precedent in the mind: finding `cultlib-gaps:finding:cut-union-codec-ts.s1.codec-catalog-refused-by-reference` (High,
|
||||
Fixed 0882eee9): TS codec first derived `System.Object` for every slot; C# refused the whole store (7 slots).
|
||||
|
||||
### 1.5 cultcache-py today
|
||||
|
||||
- Slot = `DatabaseEntryField(name, key, default, codec, many)`; `codec` is a `SlotCodec` (encode/decode callables) only for
|
||||
nested record or union (`documents.py:61-92`). A scalar slot has no codec and no type: `encode_slot`/`decode_slot`
|
||||
return the value unchanged when `field.codec is None` (`:370-386`). Consumers write `("name", 0)` tuples, 3-tuples with a
|
||||
default, or `database_entry_field("body", 1, codec=..., many=True)` (`:276-283, 417-`;
|
||||
`packages/cultcache-py/tests/test_union_parity.py:25-59`; README `packages/cultcache-py/README.md:30,114-148`).
|
||||
- Python-side checks that exist: record is a list (`:394-396`), missing required slot (`:398-409`), union shape/arm key
|
||||
(`:313-325`, bool excluded by `_is_key` `:346-347`), many-slot is a list (`:383-384`), key >= 0 (`:88-89`).
|
||||
- Catalog: `define_database_entry_type` emits every member with `type_name="System.Object"` (`documents.py:244`) and
|
||||
`canonical_schema_json` built from the members (`:140-160`). `interop.py:38-46` instead hard-codes C# names
|
||||
(`System.String`, `System.String[]`) for its one fixed document.
|
||||
Same `System.Object` default earlier caused the TS finding in 1.4 (finding text: "the codec's default copies Python's
|
||||
define_database_entry_type, whose own stores would hit the same refusal").
|
||||
- Published schema carrying slot types for Python to read: the C# catalog carries `TypeName` per slot (1.2), persisted in
|
||||
the store and compared at open; `interop.py` shows Python reading catalog entries. No JSON Schema / CultNet schema with
|
||||
scalar types was found by grep in cultcache-py; `canonical_schema_json` in Python is built from `type_name` members.
|
||||
Whether a CultNet schema catalog outside the store (cultnet-py) exposes slot scalar types was not checked.
|
||||
|
||||
### 1.6 Probe: the C# reference opening Python-written stores (2026-10-11, imagination-cl-py-slot-types)
|
||||
|
||||
Section 1.5 left this untested. Python 3.13 and cultcache-py at main 2037dd44 wrote four single-file stores of
|
||||
`cultcache.interop-note`. The C# reference at 2037dd44 read them through
|
||||
`tests/GameCult.Caching.InteropPeer read --file` (dotnet sdk 10.0 on Yggdrasil, ygg-verify job1). The scripts and
|
||||
log are in the Imagination scratchpad `imagination-cl-py-slot-types/` (`write_stores.py`, `cmd.txt`, `job1.log`).
|
||||
|
||||
| Store | Python definition | Payload | C# result |
|
||||
|---|---|---|---|
|
||||
| a | `interop.py` (`define_document_type`, catalog members hand-typed `System.String`, `System.String[]`) | correct | opens, record read |
|
||||
| b | `define_database_entry_type`, same schema name and version as C# | correct | refused: `Persisted schema 'cultcache.interop-note' ... is incompatible ...: slot 0 changed type 'System.Object' -> 'System.String'; slot 1 ...` (every slot) |
|
||||
| c | as b | `title = 12` | refused at the catalog, as b |
|
||||
| d | as a (typed catalog) | `title = 12`, accepted by Python on write | refused at decode: `Record 'note:python-d' ... cannot be decoded ... Unexpected msgpack code 12 (positive fixint)` |
|
||||
|
||||
Mechanism (source read): the C# schema id is a SHA-256 of the semantic fingerprint (`CultCache.cs:656-658`), while
|
||||
Python's defaults to the type name. A Python-written entry therefore never matches by id. It resolves by schema name
|
||||
(`CultCache.cs:518-521`), and the content hashes differ, so `CompareSchemaShapes` runs and refuses any TypeName mismatch
|
||||
(`:874-877`). The refusal is thrown from the pull, so the C# cache does not open the store at all.
|
||||
|
||||
Consequences:
|
||||
- `define_database_entry_type` has no `members` or per-slot type parameter (`documents.py:214-229`). An ordinary
|
||||
Python consumer cannot write a store that a C# peer with the same schema name will open. The only way to get one is
|
||||
to hand-build `define_document_type` with typed members, as `interop.py` does. This is the TS finding
|
||||
`cut-union-codec-ts.s1.codec-catalog-refused-by-reference` again, this time in Python.
|
||||
- Store d: a wrong scalar that Python accepts on write makes the store unreadable to the C# reference.
|
||||
- A C# `object` member is also cataloged `System.Object`, and MessagePack-CSharp's object formatter accepts any
|
||||
value. Python's current default is therefore true of a C# `object` slot, and false for every typed C# slot.
|
||||
|
||||
Real consumer definitions for scale (source read): `cultnet_py/cultmesh_contracts.py:626-698`
|
||||
(`CultMeshSimulationFact`, a frozen dataclass annotated `str`, `int`, `float`, `str | None`, passed as `cls=`), and
|
||||
`repixelizer/src/repixelizer/verse_state.py:77-118` (dict values with no `cls`; `("value", 0)` and `("surface", 4)`
|
||||
hold whole nested structures in one slot, which is a genuine "any" slot). Python annotations do not carry the C# widths:
|
||||
`int` cannot say Int32 or Int64, and `float` cannot say Single or Double.
|
||||
|
||||
## 2. Prior art (dynamic host, declared field types)
|
||||
|
||||
| System | Types come from | Write | Read | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| protobuf python | .proto IDL, generated classes | type-checked on field assignment (`TypeError`; `ValueError` out of range; bool rejected for int) | wire-type mismatch handling not stated in docs | protobuf.dev/reference/python/python-generated ("setting `foo` to a value of the wrong type will raise a `TypeError`"); `type_checkers.py` (IntValueChecker rejects bool `TypeError`, out of range `ValueError`; UnicodeValueChecker non-UTF-8 `ValueError`) https://raw.githubusercontent.com/protocolbuffers/protobuf/main/python/google/protobuf/internal/type_checkers.py ; proto3 guide: wire format "doesn't provide a way to detect fields encoded using one definition and decoded using another" https://protobuf.dev/programming-guides/proto3/ |
|
||||
| Apache Avro python | Avro schema (JSON), not host code | `DatumWriter.write` calls `validate(..., raise_on_error=True)` -> `AvroTypeException`; union branch picked by non-raising validate | reader does not validate values; checks schema resolution (`SchemaResolutionException`) and decodes by writer schema | https://raw.githubusercontent.com/apache/avro/main/lang/py/avro/io.py ; fastavro writer: `validator` parameter "validation will be done on the records" (opt-in) https://fastavro.readthedocs.io/en/latest/writer.html |
|
||||
| Thrift python | .thrift IDL, generated code | generated `write` calls `validate()` which checks required-field presence only (not scalar types); `writeFieldBegin` writes the given type unchecked | generated read emits `if ftype == TType.X:` ... `else: iprot.skip(ftype)`: a wrong wire type is silently skipped, field left unset | t_py_generator.cc `generate_py_struct_reader`, `generate_py_struct_required_validator` https://raw.githubusercontent.com/apache/thrift/master/compiler/cpp/src/thrift/generate/t_py_generator.cc ; TBinaryProtocol.py `writeFieldBegin`/`readFieldBegin` have no type check |
|
||||
| pydantic v2 | Python annotations in host code | validates input at construction / `validate_python` / `validate_json`; lax coercion default, strict opt-in per call, field or config | same entry points | https://pydantic.dev/docs/validation/latest/concepts/strict_mode/ ("Lax mode (default) coerces"; page does not say assignment or serialization validates) |
|
||||
| msgspec | Python annotations (Struct) in host code | docs (not retrieved; search snippet only): encode serializes values without enforcing annotations | decode validates against the `type=` and raises `ValidationError`; `array_like=True` encodes Struct as positional array | search result only, unverified: https://jcristharif.com/msgspec/usage.html (fetch returned 404 for usage/structs/supported-types pages) |
|
||||
| Cap'n Proto (pycapnp) | .capnp schema loaded at runtime (`capnp.load`) | assignment type-error behavior not documented on the fetched page | n/a (pointer-based, no parse step) | https://capnproto.github.io/pycapnp/capnp.html (schema loading: "much like loading a Python module") |
|
||||
| FlatBuffers python | .fbs schema, `flatc --python` generated typed accessors/builders | no runtime type validation stated on the fetched page | typed accessors read at fixed offsets; no value validation stated | https://flatbuffers.dev/languages/python/ |
|
||||
| MessagePack (msgpack-python) | none | none | none | no IDL; the format is schemaless (msgpack-python itself not fetched) |
|
||||
|
||||
Observed pattern in the fetched sources, stated as fact only:
|
||||
- Type source is an IDL/schema in protobuf, Avro, Thrift, Cap'n Proto, FlatBuffers; host-code annotations in pydantic and
|
||||
msgspec.
|
||||
- Write-side type checking: protobuf (assignment), Avro (default on), pydantic/msgspec (on construction/decode of input);
|
||||
Thrift write checks required presence only; fastavro opt-in.
|
||||
- Read-side: Avro reader trusts the writer schema; Thrift generated reader skips mismatched wire types; protobuf docs silent.
|
||||
|
||||
Not retrieved / unverified: pycapnp assignment error; FlatBuffers runtime checks; Thrift generated `validate` on type;
|
||||
msgspec authoritative statements; protobuf python parse behavior for wrong wire type.
|
||||
|
||||
## 3. Precedent in the mind
|
||||
|
||||
Tool `query` (`eureka-state`), semantic "python slot scalar type validation parity with C# reference", top_k 10, no schema
|
||||
filter (a `schemas` filter named Ruling/Resolution/Finding was refused: no such schema; schema names are
|
||||
`<campaign>.<kind>`-style, not enumerated here). Roots variants and cultlib-gaps both appeared; matched 40. Relevant hits:
|
||||
- `cultlib-gaps:finding:cut-union-codec-py.s2.python-decode-slot-strictness-differs` (PreExisting, Low, Deferred to
|
||||
union-bounds-py.r1): first record of Python decode strictness differing in both directions.
|
||||
- `cultlib-gaps:finding:cut-union-codec-ts.s1.codec-catalog-refused-by-reference` (High, Fixed): `System.Object` catalog
|
||||
types refused by the reference; evidence note says Python's `define_database_entry_type` default has the same shape.
|
||||
- `cultlib-gaps:finding:cut-union-codec-py.s1.missing-field-typed-item` (Fixed 36aa1a44): Python fixture declared slot 6 as
|
||||
Item union where C# says Node?; golden carried nil so tests passed.
|
||||
- `aetheria-release:follow_up:py-float-width-parity`: Python msgpack round-trip widens float32 to float64 (204 -> 284 bytes);
|
||||
float width is another undeclared-scalar-type consequence in Python (owner CultLib cultcache-py).
|
||||
- `cultlib-gaps:cut_spec:cut-slot-readonly-parity.r2` and `cut-slot-tolerance.r*`: read-only/unread-slot rules, adjacent.
|
||||
- No ruling or follow_up was returned that decides scalar-type parity for Python. A dedicated Ruling query was not run
|
||||
(schema id unknown; the only attempted names were refused).
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Caching.Tests.PyLeavesProbe
|
||||
{
|
||||
[MessagePackObject] public sealed class ProbeInner { [Key(0)] public string N = ""; [Key(1)] public float W; }
|
||||
[Union(0, typeof(ProbeCircle))] public interface IProbeShape { }
|
||||
[MessagePackObject] public sealed class ProbeCircle : IProbeShape { [Key(0)] public float R; }
|
||||
|
||||
[CultDocument("cultcache.py-leaves-probe", "cultcache.py_leaves_probe.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class ProbeDoc
|
||||
{
|
||||
[Key(0)] [CultName] public string Id = "";
|
||||
[Key(1)] public bool Flag;
|
||||
[Key(2)] public byte[] Blob = Array.Empty<byte>();
|
||||
[Key(3)] public int I;
|
||||
[Key(4)] public long L;
|
||||
[Key(5)] public float F;
|
||||
[Key(6)] public double D;
|
||||
[Key(7)] public int? MaybeI;
|
||||
[Key(8)] public List<string> Tags = new();
|
||||
[Key(9)] public string[] Names = Array.Empty<string>();
|
||||
[Key(10)] public object? Any;
|
||||
[Key(11)] public ProbeInner? Nested;
|
||||
[Key(12)] public List<ProbeInner> Items = new();
|
||||
[Key(13)] public IProbeShape? Shape;
|
||||
[Key(14)] public float[] Floats = Array.Empty<float>();
|
||||
[Key(15)] public float? MaybeF;
|
||||
}
|
||||
|
||||
[MessagePackObject] public sealed class S1 { [Key(0)] public string? V; }
|
||||
[MessagePackObject] public sealed class B1 { [Key(0)] public bool V; }
|
||||
[MessagePackObject] public sealed class Y1 { [Key(0)] public byte[]? V; }
|
||||
[MessagePackObject] public sealed class O1 { [Key(0)] public object? V; }
|
||||
[MessagePackObject] public sealed class LS1 { [Key(0)] public List<string>? V; }
|
||||
[MessagePackObject] public sealed class NI1 { [Key(0)] public int? V; }
|
||||
[MessagePackObject] public sealed class R1 { [Key(0)] public ProbeInner? V; }
|
||||
[MessagePackObject] public sealed class U1 { [Key(0)] public IProbeShape? V; }
|
||||
[MessagePackObject] public sealed class LI1 { [Key(0)] public List<int>? V; }
|
||||
|
||||
public class PyLeavesProbeTests
|
||||
{
|
||||
static readonly MessagePackSerializerOptions Options = CultDocumentMessagePackSerialization.OptionsFor(typeof(PyLeavesProbeTests).Assembly);
|
||||
|
||||
[Test]
|
||||
public void TypeNames()
|
||||
{
|
||||
var names = typeof(CultCache).Assembly.GetType("GameCult.Caching.CultSchemaTypeNames")!.GetMethod("FromType")!;
|
||||
foreach (var t in new[] { typeof(string), typeof(bool), typeof(byte[]), typeof(int), typeof(long), typeof(float), typeof(double),
|
||||
typeof(int?), typeof(bool?), typeof(float?), typeof(List<string>), typeof(string[]), typeof(List<int?>), typeof(object),
|
||||
typeof(ProbeInner), typeof(List<ProbeInner>), typeof(IProbeShape), typeof(float[]), typeof(List<List<string>>),
|
||||
typeof(Dictionary<string, int>), typeof(byte[][]) , typeof(List<byte[]>)})
|
||||
TestContext.Out.WriteLine($"TYPENAME {t} => {names.Invoke(null, new object[] { t })}");
|
||||
}
|
||||
|
||||
[TestCase("a-leaf-catalog.cc")]
|
||||
[TestCase("b-wrong-leaf.cc")]
|
||||
public async Task OpenPythonStore(string file)
|
||||
{
|
||||
var path = Path.Combine("/tmp/pyst", file);
|
||||
try
|
||||
{
|
||||
var cache = new CultCache();
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var doc = cache.AllEntries.OfType<ProbeDoc>().Single();
|
||||
var bytes = MessagePackSerializer.Serialize(doc, Options);
|
||||
TestContext.Out.WriteLine($"OPEN {file} OK id={doc.Id} F={doc.F:R} Shape={doc.Shape?.GetType().Name} Any={doc.Any?.GetType().Name} items={doc.Items.Count}");
|
||||
TestContext.Out.WriteLine($"CSHARP-REWRITE {file} {Convert.ToHexString(bytes).ToLowerInvariant()}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TestContext.Out.WriteLine($"OPEN {file} REFUSED {e.GetType().Name}: {e.Message.Split('\n')[0]}");
|
||||
}
|
||||
}
|
||||
|
||||
static readonly string[] Values = { "05", "ff", "cb3ff8000000000000", "ca3fc00000", "c0", "c2", "c3", "a161", "d90161", "c40100", "90", "9101", "91a161", "80", "81a161a162", "920091ca3f800000" };
|
||||
|
||||
static string Try<T>(byte[] p) { try { var v = MessagePackSerializer.Deserialize<T>(p, Options); return "ok"; } catch { return "--"; } }
|
||||
|
||||
[Test]
|
||||
public void ScalarReads()
|
||||
{
|
||||
foreach (var v in Values)
|
||||
{
|
||||
var p = Convert.FromHexString("91" + v);
|
||||
TestContext.Out.WriteLine($"READ {v,-22} string={Try<S1>(p)} bool={Try<B1>(p)} bytes={Try<Y1>(p)} object={Try<O1>(p)} list<string>={Try<LS1>(p)} int?={Try<NI1>(p)} record={Try<R1>(p)} union={Try<U1>(p)} list<int>={Try<LI1>(p)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Python typed leaves: probe evidence (2026-10-11)
|
||||
|
||||
Serves `cultlib-gaps:ruling:py-slot-typed-leaves` and the specs
|
||||
`cultlib-gaps:cut_spec:cut-leaf-vectors-cs.r1` and `cut-py-typed-leaves.r1`.
|
||||
Run by the Imagination pass `imagination-cl-py-typed-leaves` on Yggdrasil at CultLib main 4ec4bfb7.
|
||||
|
||||
- `write_probe_stores.py` + `PyLeavesProbe.cs`: a Python store whose catalog uses the leaf type names
|
||||
(from `CultSchemaTypeNames.FromType`) is opened by the C# reference and its payload rewritten byte-identical,
|
||||
float32 included (except inside an `object` slot, where C# itself rewrites a fixint as uint8). Negative control:
|
||||
one slot named Int64 where C# has Int32 is refused ("slot 3 changed type").
|
||||
- `proto_leaves.py` + `run_vectors.py`: prototype leaves reproduce the C# numeric-leaves vectors (160 reads, 46 writes,
|
||||
the mixed record). `truth.py`: what C# accepts per slot type (byte[] accepts str; bool only false/true; record
|
||||
accepts []; nil in an int slot refused).
|
||||
- `perf.py`: float32 via a marker with packb-first fallback; a 1.5k-node record with float32 about 0.8 ms vs 0.1 ms.
|
||||
- `job1.log`: the verify run.
|
||||
|
||||
Scripts are probes, not a suite; they are kept so the next pass can rerun them.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import sys, time, msgpack
|
||||
sys.path.insert(0, sys.argv[1]); import proto_leaves as L
|
||||
# a HullData-like record: 20 float32 slots, strings, nested list of 200 small records
|
||||
rec = lambda i: ["part%d" % i, i, [L.Float32(0.5*i), L.Float32(1.5), L.Float32(2.5)], None]
|
||||
tree = ["id", 3, [L.Float32(float(i)) for i in range(20)], [rec(i) for i in range(200)]]
|
||||
plain = ["id", 3, [float(i) for i in range(20)], [["part%d" % i, i, [0.5*i,1.5,2.5], None] for i in range(200)]]
|
||||
N=300
|
||||
t=time.perf_counter(); [L.write(tree) for _ in range(N)]; a=time.perf_counter()-t
|
||||
t=time.perf_counter(); [msgpack.packb(plain, use_bin_type=True) for _ in range(N)]; b=time.perf_counter()-t
|
||||
t=time.perf_counter(); [msgpack.unpackb(msgpack.packb(plain)) for _ in range(N)]; c=time.perf_counter()-t
|
||||
print(f"walk-writer {a/N*1e6:.0f} us/rec, packb {b/N*1e6:.0f} us/rec, ratio {a/b:.1f}x; size {len(L.write(tree))} vs {len(msgpack.packb(plain))}")
|
||||
def pack_tree(t):
|
||||
try: return msgpack.packb(t, use_bin_type=True)
|
||||
except TypeError: return L.write(t)
|
||||
assert pack_tree(plain) == L.write(plain) == msgpack.packb(plain, use_bin_type=True)
|
||||
assert pack_tree(tree) == L.write(tree)
|
||||
t=time.perf_counter(); [pack_tree(plain) for _ in range(N)]; p=time.perf_counter()-t
|
||||
t=time.perf_counter(); [pack_tree(tree) for _ in range(N)]; q=time.perf_counter()-t
|
||||
print(f"pack_tree plain {p/N*1e6:.0f} us, float32 tree {q/N*1e6:.0f} us")
|
||||
try:
|
||||
pack_tree([object()])
|
||||
except TypeError as e: print("custom object:", e)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
"""Scratch prototype of cultcache_py.leaves, to establish mechanisms against the C# vectors. Not for the repo."""
|
||||
import math, struct, msgpack
|
||||
|
||||
class Refused(ValueError): pass
|
||||
|
||||
def kind(v):
|
||||
if v is None: return "nil"
|
||||
if isinstance(v, bool): return "a boolean"
|
||||
if isinstance(v, int): return "an integer"
|
||||
if isinstance(v, float): return "a float"
|
||||
if isinstance(v, str): return "a string"
|
||||
if isinstance(v, (bytes, bytearray, memoryview)): return "bytes"
|
||||
if isinstance(v, (list, tuple)): return "an array"
|
||||
if isinstance(v, dict): return "a map"
|
||||
return type(v).__name__
|
||||
|
||||
def refuse(what, v): raise Refused(f"expected {what}, found {kind(v)}")
|
||||
|
||||
class Float32: # tree marker: written as MessagePack float 32
|
||||
__slots__ = ("value",)
|
||||
def __init__(self, v): self.value = v
|
||||
|
||||
def int_to_f32(v: int) -> float:
|
||||
if abs(v) <= 2**53:
|
||||
return f32(float(v))
|
||||
m = abs(v); excess = m.bit_length() - 24
|
||||
kept = m >> excess; rest = m - (kept << excess); half = 1 << (excess - 1)
|
||||
if rest > half or (rest == half and kept & 1): kept += 1
|
||||
r = float(kept) * 2.0**excess # kept<=2^24 exact; scaling exact unless overflow
|
||||
return f32(-r if v < 0 else r)
|
||||
|
||||
def f32(x: float) -> float:
|
||||
try:
|
||||
return struct.unpack(">f", struct.pack(">f", x))[0]
|
||||
except OverflowError:
|
||||
return math.copysign(math.inf, x)
|
||||
|
||||
class Leaf:
|
||||
def __init__(self, type_name, enc, dec, value_type=False):
|
||||
self.type_name, self.encode, self.decode, self.value_type = type_name, enc, dec, value_type
|
||||
|
||||
def _int_leaf(name, what, lo, hi):
|
||||
def enc(v):
|
||||
if not isinstance(v, int) or isinstance(v, bool): refuse(what, v)
|
||||
if not lo <= v <= hi: raise Refused(f"expected {what}, found an integer out of range")
|
||||
return v
|
||||
return Leaf(name, enc, enc, True)
|
||||
|
||||
int32 = _int_leaf("System.Int32", "an int32", -2**31, 2**31-1)
|
||||
int64 = _int_leaf("System.Int64", "an int64", -2**63, 2**63-1)
|
||||
|
||||
def _f32_enc(v):
|
||||
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float32", v)
|
||||
return Float32(int_to_f32(v) if isinstance(v, int) else f32(v))
|
||||
def _f32_dec(v):
|
||||
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float32", v)
|
||||
return int_to_f32(v) if isinstance(v, int) else f32(v)
|
||||
float32 = Leaf("System.Single", _f32_enc, _f32_dec, True)
|
||||
def _f64(v):
|
||||
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float64", v)
|
||||
return float(v) # int > 2^53 rounds once (Python int->float is correctly rounded)
|
||||
float64 = Leaf("System.Double", _f64, _f64, True)
|
||||
def _str(v):
|
||||
if v is not None and not isinstance(v, str): refuse("a string", v)
|
||||
return v
|
||||
string = Leaf("System.String", _str, _str)
|
||||
def _bool(v):
|
||||
if not isinstance(v, bool): refuse("a boolean", v)
|
||||
return v
|
||||
boolean = Leaf("System.Boolean", _bool, _bool, True)
|
||||
|
||||
# writer: msgpack-python per value, float32 through a single-float packer, containers by header
|
||||
_P = msgpack.Packer(use_bin_type=True)
|
||||
_P32 = msgpack.Packer(use_bin_type=True, use_single_float=True)
|
||||
def write(tree) -> bytes:
|
||||
out = bytearray()
|
||||
stack = [tree]
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
if isinstance(n, Float32): out += _P32.pack(n.value)
|
||||
elif isinstance(n, list):
|
||||
out += _P.pack_array_header(len(n)); stack.extend(reversed(n))
|
||||
elif isinstance(n, dict):
|
||||
out += _P.pack_map_header(len(n))
|
||||
for k, v in reversed(list(n.items())): stack.append(v); stack.append(k)
|
||||
else: out += _P.pack(n)
|
||||
return bytes(out)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import sys, struct, msgpack, math
|
||||
sys.path.insert(0, sys.argv[2])
|
||||
import proto_leaves as L
|
||||
d = sys.argv[1]
|
||||
leaves = {"int32": L.int32, "int64": L.int64, "float32": L.float32, "float64": L.float64}
|
||||
rows = msgpack.unpackb(open(d+"/reads.msgpack","rb").read(), raw=False)
|
||||
bad = 0
|
||||
for leaf, payload, ok, bits in rows:
|
||||
raw = msgpack.unpackb(payload, raw=False)[0]
|
||||
try:
|
||||
v = leaves[leaf].decode(raw); got = True
|
||||
b = struct.pack(">q", v) if leaf.startswith("int") else struct.pack(">d", v)
|
||||
except L.Refused: got, b = False, b""
|
||||
if got != ok or (ok and b != bits):
|
||||
bad += 1; print("READ DIFF", leaf, payload.hex(), "C#", ok, bits.hex(), "py", got, b.hex())
|
||||
print("reads", len(rows), "diffs", bad)
|
||||
wr = msgpack.unpackb(open(d+"/writes.msgpack","rb").read(), raw=False)
|
||||
bad = 0
|
||||
for leaf, text, want in wr:
|
||||
v = int(text) if leaf.startswith("int") else struct.unpack(">d", bytes.fromhex(text))[0]
|
||||
got = L.write([leaves[leaf].encode(v)])
|
||||
if got != want: bad += 1; print("WRITE DIFF", leaf, text, want.hex(), got.hex())
|
||||
print("writes", len(wr), "diffs", bad)
|
||||
mixed = L.write([L.string.encode("n1"), L.int32.encode(7), L.int64.encode(8), L.float32.encode(1.5), L.float64.encode(2.5)])
|
||||
print("mixed equal:", mixed == open(d+"/mixed-record.msgpack","rb").read(), mixed.hex())
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import msgpack, struct, sys
|
||||
d = sys.argv[1]
|
||||
rows = msgpack.unpackb(open(d+"/reads.msgpack","rb").read(), raw=False)
|
||||
from collections import defaultdict
|
||||
t = defaultdict(list)
|
||||
for leaf, payload, ok, bits in rows:
|
||||
tok = payload[1:].hex()
|
||||
t[leaf].append((tok, ok, bits.hex()))
|
||||
for leaf, cases in t.items():
|
||||
print(leaf, "accepts", sum(ok for _,ok,_ in cases), "of", len(cases))
|
||||
print(" refused:", [tok for tok,ok,_ in cases if not ok])
|
||||
# float accepted from integer? int from float?
|
||||
f = msgpack.unpackb(open(d+"/float32-nan.msgpack","rb").read(), raw=False)
|
||||
for p, b in f:
|
||||
v = msgpack.unpackb(p)[0]
|
||||
bits = struct.pack(">d", v).hex()
|
||||
repacked = msgpack.packb([v], use_single_float=True).hex()
|
||||
print("nan in", p.hex(), "C# out", b.hex(), "py double", bits, "py repack f32", repacked)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""Writes Python stores whose catalog carries the C# type names the proposed leaves would derive, for the C# reference to open."""
|
||||
import sys, os
|
||||
sys.path.insert(0, r"F:\Projects\CultLib\packages\cultcache-py\src"); sys.path.insert(0, os.path.dirname(__file__))
|
||||
import proto_leaves as L
|
||||
from cultcache_py import CultCache, SingleFileMessagePackBackingStore
|
||||
from cultcache_py.documents import define_document_type
|
||||
from cultcache_py.backing_store import CultCacheSchemaCatalogMember as M
|
||||
|
||||
NS = "GameCult.Caching.Tests.PyLeavesProbe"
|
||||
LIST = "System.Collections.Generic.List<{}>"
|
||||
MEMBERS = [ # slot, member, C# type name the leaf derives
|
||||
(0, "Id", "System.String"), (1, "Flag", "System.Boolean"), (2, "Blob", "System.Byte[]"),
|
||||
(3, "I", "System.Int32"), (4, "L", "System.Int64"), (5, "F", "System.Single"), (6, "D", "System.Double"),
|
||||
(7, "MaybeI", "System.Nullable<System.Int32>"), (8, "Tags", LIST.format("System.String")),
|
||||
(9, "Names", "System.String[]"), (10, "Any", "System.Object"), (11, "Nested", NS + ".ProbeInner"),
|
||||
(12, "Items", LIST.format(NS + ".ProbeInner")), (13, "Shape", NS + ".IProbeShape"),
|
||||
(14, "Floats", "System.Single[]"), (15, "MaybeF", "System.Nullable<System.Single>"),
|
||||
]
|
||||
F32 = lambda x: L.Float32(L.f32(x))
|
||||
def payload(doc_id):
|
||||
inner = lambda n, w: [n, F32(w)]
|
||||
return [doc_id, True, b"\x01\x02", 7, 2**40, F32(0.1), 0.1, None, ["a", "b"], ["x"], {"k": [1, "two"]},
|
||||
inner("n", 1.25), [inner("i0", 0.5), inner("i1", 2.0)], [0, [F32(3.5)]], [F32(1.0), F32(0.1)], F32(2.5)]
|
||||
|
||||
def write(path, members, tree_for):
|
||||
doc = define_document_type(
|
||||
"cultcache.py-leaves-probe", encode=lambda v: v, decode=lambda v: v,
|
||||
payload_encoder=L.write, payload_decoder=lambda b: b, name=lambda v: v[0],
|
||||
schema_name="cultcache.py-leaves-probe", schema_version="cultcache.py_leaves_probe.v1",
|
||||
members=[M(slot=s, member_name=n, type_name=t, is_name=(s == 0)) for s, n, t in members])
|
||||
if os.path.exists(path): os.remove(path)
|
||||
cache = CultCache.builder().register_document_type(doc).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
|
||||
cache.pull_all_backing_stores()
|
||||
tree = tree_for(os.path.basename(path))
|
||||
cache.put(doc, tree[0], tree)
|
||||
print(path, "payload", L.write(tree).hex())
|
||||
|
||||
out = sys.argv[1]
|
||||
write(os.path.join(out, "a-leaf-catalog.cc"), MEMBERS, payload)
|
||||
# negative control: the int32 slot cataloged as the int64 leaf would name it
|
||||
wrong = [(s, n, "System.Int64" if s == 3 else t) for s, n, t in MEMBERS]
|
||||
write(os.path.join(out, "b-wrong-leaf.cc"), wrong, payload)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Prior art: refuse or walk a container the identity walk does not own
|
||||
|
||||
For `variants:question:c2a-unowned-holders` (campaign `variants`, cut `c2a-element-id-holes` r5).
|
||||
These are from Imagination's reading. No web fetch was made in this pass, so check the
|
||||
pointers before quoting them outside the mind.
|
||||
|
||||
| System | Unknown or unsupported type in a payload | Extension point | Pointer |
|
||||
|---|---|---|---|
|
||||
| Protocol Buffers | Not expressible. A field is a scalar, a message, `repeated`, or `map<scalar, V>`. No other container exists. | None. You model the data as messages. | protobuf.dev, Language Guide (proto3), "Repeated fields" and "Maps" |
|
||||
| Microsoft Bond | Fixed container set: `list`, `vector`, `set`, `map`, `nullable`, `blob`, `bonded`. Any other type is a codegen error. | Type aliases with explicit converters, declared per type. | microsoft.github.io/bond/manual/bond_cs.html, "Type aliases" and "Custom type mappings" |
|
||||
| Orleans 7+ serializer | Throws `CodecNotFoundException` when it serializes a type with no codec (not `[GenerateSerializer]`, not registered). | Surrogates via `[RegisterConverter]`, or codecs registered per type. Built-in codecs cover the BCL immutable collections. | learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization |
|
||||
| MessagePack-CSharp 3.1.7 | `DynamicObjectResolver` refuses an unannotated type (formatter null, the serializer throws) unless the caller opts into contractless. | Resolvers, `[MessagePackFormatter]`. | Probed: `scratchpad/imag-r5/probe1.log` (NkHold: std=null, contractless builds one) |
|
||||
| System.Text.Json (source gen) | `NotSupportedException` for types outside the generated metadata. Polymorphism needs `[JsonDerivedType]` registration. | `JsonConverter<T>`, `[JsonDerivedType]`. | learn.microsoft.com/dotnet/standard/serialization/system-text-json/polymorphism |
|
||||
|
||||
## What the table says for CultCache's id walk
|
||||
|
||||
1. Every system that guarantees something about a payload's structure fails closed on a
|
||||
shape it does not own. The failure is loud and comes at build or first use. It never
|
||||
drops data silently. Protobuf and Bond refuse when the schema is written, and Orleans,
|
||||
MessagePack and STJ refuse when the type is first serialized. None of them walks an
|
||||
arbitrary wrapper on the chance that it is a container.
|
||||
2. Each system supports a fixed set of containers and walks those natively. Extension is
|
||||
explicit and per type: Bond aliases, Orleans surrogates, STJ converters. The walker
|
||||
never infers it from a type's shape.
|
||||
3. Orleans covers the BCL immutable collections with built-in codecs. Each added container
|
||||
is a deliberate, tested entry. A reasonable consumer therefore expects ImmutableArray to
|
||||
be supported when the library says so, and refused loudly when it does not. Nobody
|
||||
expects silent nil ids.
|
||||
|
||||
CultCache's id walk today is open-world: a shape it does not recognise passes as a leaf.
|
||||
That is the opposite of every row above, and it is the common cause of findings s1-s4.
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
# Shader variable-length inputs: how shader libraries take a span, and what CultMath's build route allows
|
||||
|
||||
For `cultmath-tapes:follow_up:shader-weighted-lattice-loop` and `cultmath-tapes:follow_up:lattice-shader-pins` (campaign `cultmath-tapes`).
|
||||
Imagination pass, agent `imagination-ag4-shader-loop`, session `self-2026-10-16-ag4`, 2026-10-17, against CultLib `origin/main` `e2051c5a`
|
||||
and the build branch `hands/cultmath-glsl-build-dxc` `23ac4e89`. Probes are in `docs/research/shader-variable-inputs/`.
|
||||
|
||||
Tags: [src] primary source text read this pass; [web] search summary only; [probe] run this pass, output quoted below.
|
||||
|
||||
## 1. The question
|
||||
|
||||
C#'s `weighted_lattice_cells` reads the body span once per lattice cell (the weight is the exact power border of the
|
||||
cell centre), and the ring search around it is the part a consumer cannot safely write by hand: the exit bound,
|
||||
the two passes and the box-independent fold order. The shader port has the four steps only. A shader function cannot
|
||||
take `ReadOnlySpan`, the library targets GLSL ES 3.00 fragment shaders (WebGL2: no storage buffers), and the GLSL is
|
||||
built from the HLSL by dxc `lib_6_3` SPIR-V plus SPIRV-Cross (ruling `glsl-es-via-dxc-spirv-cross`).
|
||||
So how do bodies reach a shader `weighted_lattice_cells`?
|
||||
|
||||
## 2. Prior art: how shader code that ships as a library takes a variable number of inputs
|
||||
|
||||
- [src] Unity URP (`ShaderLibrary/Input.hlsl`, `RealtimeLights.hlsl`, Graphics `master`): a fixed capacity
|
||||
`MAX_VISIBLE_LIGHTS` chosen per platform (`MAX_VISIBLE_LIGHT_COUNT_LOW_END_MOBILE` under `SHADER_API_GLES30`,
|
||||
`_MOBILE` for GLES3/WebGPU, `_DESKTOP` otherwise), constant-buffer arrays `float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS]`
|
||||
and the like, with `StructuredBuffer<LightData>` only behind `USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA` (defined 0). The
|
||||
count is a uniform read through `GetAdditionalLightsCount()`, and `GetAdditionalLight(uint i, float3 positionWS)` is
|
||||
the accessor; `LIGHT_LOOP_BEGIN(lightCount)` / `LIGHT_LOOP_END` macros hide the cluster versus plain loop. The
|
||||
pipeline owns the storage and the accessor; user shaders call them. Capacity plus count, storage owned by the include.
|
||||
- [src] Filament (`shaders/src/surface_light_punctual.fs`, `main`): the loop is the engine's, over a froxel record
|
||||
(`froxelsUniforms.records`, then `froxelRecordUniforms.records`) giving `recordOffset` and `count`; each light is
|
||||
`lightsUniforms.lights[lightIndex]`, a uniform-block array. Material code never sees the loop. The engine owns both
|
||||
the storage and the loop because it also owns the uniform layout.
|
||||
- [src] three.js (`ShaderChunk/lights_fragment_begin.glsl.js`, `dev`): `for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ )`
|
||||
under `#pragma unroll_loop_start`, reading `pointLights[ i ]`; `NUM_POINT_LIGHTS` is a define the renderer writes,
|
||||
so the shader is regenerated per light count. Count as a compile-time constant; the library is source text.
|
||||
- [web] Bevy: clustered forward needs storage buffers; on WebGL2 it falls back to a uniform buffer with a capped
|
||||
object count (`MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS` 204 in an older `bevy_pbr`, asserted to fit 16384 bytes; the
|
||||
Many Lights example says 256 lights under `WGPU_SETTINGS_PRIO=webgl2`). Capacity plus count where storage is absent.
|
||||
- [web] lygia (README conventions, via search): reuse by `#ifndef` / `#define` options the caller sets before the
|
||||
include (`SAMPLER_FNC(TEX, UV)`, `MYFUNC_TYPE`, per-function sampler overrides). Caller-supplied accessor macros; the
|
||||
library ships as source text, which is what lets a macro reach inside a function body.
|
||||
|
||||
The pattern that splits them: a caller-supplied accessor (lygia, URP's accessor functions, three.js's define) works
|
||||
where the library is source text the caller's preprocessor sees. Where the library is compiled or its storage layout is
|
||||
fixed (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks), the input is a fixed-capacity array plus a
|
||||
count, and the capacity is a named library constant.
|
||||
|
||||
## 3. What the build route allows (probes)
|
||||
|
||||
| Candidate | HLSL (dxc) | Library SPIR-V and SPIRV-Cross GLSL | C# mirror (`TransformHlslToCSharp`) | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| Fixed-size array parameter plus count | compiles, `lib_6_3` and `cs_6_0` [probe P1, P3] | see P2 | needs one transform (`T x[N]` parameter to `T[] x`) | see section 4 |
|
||||
| Caller accessor macro (`CULTMATH_BODY(i)`) | the library build defines no accessor, so the function is absent [probe P4] | absent from the built GLSL: SPIRV-Cross output carries no consumer macro | the mirror would need the macro too | refused by the route |
|
||||
| Bodies packed into library-owned constants | a library cbuffer or uniform | a uniform block in a function-only library; a binding the consumer does not own | a static field | refused (P5, by construction, not run): a library may not own a consumer's binding |
|
||||
|
||||
Probes (files in `shader-variable-inputs/`):
|
||||
|
||||
- [probe] P1, local, dxc 2026_07_29 (`.tools/dxc`): `lattice-array.hlsl` (CultMath.hlsl, `prototype.hlsl`, one export)
|
||||
compiles with `-T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od`. The SPIR-V types the array as
|
||||
`OpTypeArray %v4float %uint_16` behind a Function pointer, and `-Od` copies it at each call (`param_var_bodies`).
|
||||
- [probe] P3, local: `entry-cbuffer.hlsl` passes a cbuffer array directly and a `StructuredBuffer` copied into a local
|
||||
array; both compile to SPIR-V (`cs_6_0`) and to DXIL.
|
||||
- [probe] P4, local: `macro-accessor.hlsl` with no `CULTMATH_BODY` defined fails, `use of undeclared identifier`: a
|
||||
macro-parameterised function does not exist in a library compiled without the caller's macros, so the built GLSL
|
||||
cannot carry it. Shipping it would need a hand-kept GLSL text beside the build, a second owner the ruling removed.
|
||||
- [probe] P2, Yggdrasil job (`probe.sh` at `23ac4e89`, sdk:10.0, 2026-10-17): with the prototype appended to
|
||||
`CultMath.hlsl` and two build-tool edits (`wrappers.py` strips `[N]` from an argument name; `partition.py` accepts
|
||||
`static const int`), `build-glsl.sh` is green ("build shape ok", "suffixed ids: 0; inout: 0") and emits
|
||||
`const int CULTMATH_MAX_SITES = 16;`, `vec4 cultmath_power_border(vec3 p, vec4 sites[16], int count)` and
|
||||
`vec4 cultmath_weighted_lattice_cells(vec2 p, float time, vec4 bodies[16], int count, ...)`. A `#version 300 es`
|
||||
fragment consumer passing `uniform vec4 zone[CULTMATH_MAX_SITES]` compiles with glslang.
|
||||
- [probe] Same job, lavapipe (`llvmpipe (LLVM 20.1.2, 256 bits)`), 257 cases from `ref/Program.cs` (the ring-17 point
|
||||
of `TheCapIsReachedAndMatchesTheFullBox` and 7 neighbours, four looks, five normals including (3e-30, -4e-30) and
|
||||
(1e30, 2e30), a zero normal at stretch 1, counts 0, 1, 3, 6, 16), judged against C#'s bits at 2.2e-6 max(|v|, 1):
|
||||
|
||||
| Variant (HLSL route unless named) | bit-exact | over bound | worst |
|
||||
|---|---|---|---|
|
||||
| built GLSL | 237 | 0 | 1.19e-6 |
|
||||
| HLSL (equal to built GLSL bit for bit) | 237 | 0 | 1.19e-6 |
|
||||
| M1 exit at `0.5 k` | 236 | 1 | 4.28e-3 |
|
||||
| M2 exit ignores the smooth band (`>= f2`) | 226 | 13 | 8.33e-2 |
|
||||
| M3 normaliser drops the divide by the largest component | 148 | 96 | 1e30 |
|
||||
| M4 normaliser drops the divide by length | 1 | 208 | 0.259 |
|
||||
| M5 smooth step loses its `min` cap | 153 | 85 | 2.26e-3 |
|
||||
| M6 box pass skips its first row | 228 | 9 | 0.807 |
|
||||
| M7 exit one ring late (`0.5 (k - 2)`) | 237 | 0 | 1.19e-6 |
|
||||
|
||||
M7 is equivalent: the result does not depend on the box (the proof above `weighted_lattice_cells`), so a late exit
|
||||
costs rings and changes nothing. M1 is killed by one case of 257, the ring-17 point: an early exit only matters where
|
||||
a far ring wins, so the device family must carry several such points.
|
||||
|
||||
## 4. Decision
|
||||
|
||||
The fixed-size array plus count is the only candidate the route admits, and it is the shape the compiled-library
|
||||
prior art uses (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks). The capacity is a named library
|
||||
constant, `CULTMATH_MAX_SITES`, shared by every span-taking shader function. Its value is
|
||||
`cultmath-tapes:question:shader-site-capacity` (Aetheria zones: p50 17, p99 57, max 80 bodies, gravity map B28).
|
||||
|
||||
The shader recomputes each distance in the box pass instead of caching it: C#'s 75 x 75 stack cache is not a shader
|
||||
option, and recomputing gives the same value, as the probe's bit-exact HLSL and built-GLSL agreement shows.
|
||||
Reflection cannot pass a `ReadOnlySpan`, so the mirror and the fixture reach the C# span functions through one
|
||||
explicit adapter table rather than `MethodInfo.Invoke`.
|
||||
|
||||
## 5. lattice-shader-pins
|
||||
|
||||
- The smooth-step family draws m in [f1, f1 + smoothing), so the cap never binds. The cap binds when m = f1 + smoothing,
|
||||
d = f1 + x smoothing with x around 1.3 to 1.9, and smoothing above about 0.25: next - m = (d - m) h - h (1 - h)
|
||||
smoothing / (1 + 3 smoothing) is positive there (for smoothing 1 and d - m = 0.5, h = 0.156 and next - m = +0.045).
|
||||
M5 above is killed by the whole function already; the step family should kill it on its own.
|
||||
- Platform values: `8b31e47e` rewrote 7 platform-family entries of `glsl-parity.json` from "Ubuntu 24.04.5 LTS" to
|
||||
"Microsoft Windows 10.0.26200", while `7ddb1040` recorded the lavapipe bounds on Yggdrasil against that fixture.
|
||||
The bounds, the lavapipe job and the build route all run in Yggdrasil's sdk:10.0 Ubuntu image, so that image
|
||||
owns the fixture's platform values too. A write on another OS is refused rather than reviewed.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Probe C: a compute entry passing a constant-buffer array, and one passing a StructuredBuffer copied to a local array.
|
||||
#include "lattice-array.hlsl"
|
||||
cbuffer Zone : register(b0) { float4 zoneBodies[CULTMATH_MAX_SITES]; int zoneCount; };
|
||||
StructuredBuffer<float4> bufferBodies : register(t1);
|
||||
RWStructuredBuffer<float4> result : register(u2);
|
||||
[numthreads(64, 1, 1)]
|
||||
void main(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
float2 p = float2(id.x * 7.0, id.y * 11.0);
|
||||
float4 local[CULTMATH_MAX_SITES];
|
||||
for (int i = 0; i < CULTMATH_MAX_SITES; i++)
|
||||
local[i] = i < zoneCount ? bufferBodies[i] : float4(0.0, 0.0, 0.0, 1.0);
|
||||
result[id.x] = cultmath_weighted_lattice_cells(p, 1.0, zoneBodies, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5)
|
||||
+ cultmath_weighted_lattice_cells(p, 1.0, local, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
layout(local_size_x = 64) in;
|
||||
layout(std430, binding = 0) readonly buffer IB { uint a[]; } In;
|
||||
layout(std430, binding = 1) buffer OB { uint a[]; } Out;
|
||||
void main()
|
||||
{
|
||||
uint i = gl_GlobalInvocationID.x; if (i >= NCASES) return; uint b = i * 76u;
|
||||
vec4 bodies[CULTMATH_MAX_SITES];
|
||||
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
|
||||
bodies[k] = uintBitsToFloat(uvec4(In.a[b + 2u + 4u * uint(k)], In.a[b + 3u + 4u * uint(k)], In.a[b + 4u + 4u * uint(k)], In.a[b + 5u + 4u * uint(k)]));
|
||||
uint c = b + 67u;
|
||||
vec4 r = cultmath_weighted_lattice_cells(uintBitsToFloat(uvec2(In.a[b], In.a[b + 1u])), uintBitsToFloat(In.a[c]), bodies, int(In.a[b + 66u]),
|
||||
uintBitsToFloat(In.a[c + 1u]), uintBitsToFloat(In.a[c + 2u]), uintBitsToFloat(In.a[c + 3u]), uintBitsToFloat(In.a[c + 4u]),
|
||||
uintBitsToFloat(In.a[c + 5u]), uintBitsToFloat(uvec2(In.a[c + 6u], In.a[c + 7u])), uintBitsToFloat(In.a[c + 8u]));
|
||||
uvec4 w = floatBitsToUint(r);
|
||||
Out.a[i * 4u] = w.x; Out.a[i * 4u + 1u] = w.y; Out.a[i * 4u + 2u] = w.z; Out.a[i * 4u + 3u] = w.w;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// Probe compute entry (HLSL route): 76 words in per case (p, 16 bodies, count, 9 parameters), 4 words out.
|
||||
#include "CultMath.hlsl"
|
||||
[[vk::binding(0)]] StructuredBuffer<uint> In;
|
||||
[[vk::binding(1)]] RWStructuredBuffer<uint> Out;
|
||||
[numthreads(64, 1, 1)] void main(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
uint i = id.x; if (i >= NCASES) return; uint b = i * 76u;
|
||||
float4 bodies[CULTMATH_MAX_SITES];
|
||||
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
|
||||
bodies[k] = float4(asfloat(In[b + 2 + 4 * k]), asfloat(In[b + 3 + 4 * k]), asfloat(In[b + 4 + 4 * k]), asfloat(In[b + 5 + 4 * k]));
|
||||
uint c = b + 67u;
|
||||
float4 r = cultmath_weighted_lattice_cells(float2(asfloat(In[b]), asfloat(In[b + 1])), asfloat(In[c]), bodies, asint(In[b + 66]),
|
||||
asfloat(In[c + 1]), asfloat(In[c + 2]), asfloat(In[c + 3]), asfloat(In[c + 4]), asfloat(In[c + 5]),
|
||||
float2(asfloat(In[c + 6]), asfloat(In[c + 7])), asfloat(In[c + 8]));
|
||||
Out[i * 4] = asuint(r.x); Out[i * 4 + 1] = asuint(r.y); Out[i * 4 + 2] = asuint(r.z); Out[i * 4 + 3] = asuint(r.w);
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compare a device output with C#'s reference bits: cases over the lavapipe bound 2.2e-6 max(|v|, 1), and the largest.
|
||||
usage: judge.py <ref.bin> <out.bin> <label>"""
|
||||
import math, struct, sys
|
||||
ref, out = open(sys.argv[1], 'rb').read(), open(sys.argv[2], 'rb').read()
|
||||
n = len(ref) // 16
|
||||
r, o = struct.unpack('<%df' % (4 * n), ref), struct.unpack('<%df' % (4 * n), out)
|
||||
over, exact, worst = 0, 0, 0.0
|
||||
for i in range(n):
|
||||
s = max((abs(o[4*i+j] - r[4*i+j]) / max(abs(r[4*i+j]), 1.0) if math.isfinite(o[4*i+j]) else math.inf) for j in range(4))
|
||||
exact += ref[16*i:16*i+16] == out[16*i:16*i+16]
|
||||
over += s > 2.2e-6
|
||||
worst = max(worst, s)
|
||||
print('%s: cases %d, bit-exact %d, over 2.2e-6 %d, worst %.3g' % (sys.argv[3], n, exact, over, worst))
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// Probe A (local dxc): the prototype as a lib_6_3 SPIR-V library export, bodies as a fixed-size array parameter plus a count.
|
||||
#include "CultMath.hlsl"
|
||||
#include "prototype.hlsl"
|
||||
export float4 probe_lattice(float2 p, float4 bodies[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
return cultmath_weighted_lattice_cells(p, 1.0, bodies, count, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// Probe B: bodies through a caller-supplied accessor macro. The library build defines no accessor.
|
||||
#include "CultMath.hlsl"
|
||||
#ifdef CULTMATH_BODY
|
||||
float4 cultmath_power_border_m(float3 p)
|
||||
{
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
for (int k = 1; k < CULTMATH_BODY_COUNT; k++)
|
||||
border = cultmath_power_border_step(p, CULTMATH_BODY(0), CULTMATH_BODY(k), border);
|
||||
return border;
|
||||
}
|
||||
#endif
|
||||
export float4 probe_macro(float3 p) { return cultmath_power_border_m(p); }
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Probe for shader-weighted-lattice-loop: the prototype through the dxc + SPIRV-Cross build and on lavapipe, against C#.
|
||||
# Run from a CultLib clone at a revision with packages/cultmath/tools/glsl/build-glsl.sh (hands/cultmath-glsl-build-dxc
|
||||
# 23ac4e89), inside mcr.microsoft.com/dotnet/sdk:10.0, with this directory at $P. The clone is disposable: the
|
||||
# prototype is appended to its CultMath.hlsl, and two build-tool lines are patched (array arguments, int constants).
|
||||
set -euo pipefail
|
||||
P=${P:-/tmp/probe}; ROOT=$(pwd); CM=$ROOT/packages/cultmath; W=/tmp/pw; mkdir -p $W
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends python3 >/dev/null
|
||||
cat $P/prototype.hlsl >> $CM/shaders/CultMath.hlsl
|
||||
python3 - $CM/tools/glsl/wrappers.py <<'PY'
|
||||
import sys; f = sys.argv[1]; t = open(f).read()
|
||||
t = t.replace(r"args = [re.split(r'\s+', p)[-1] for p in params]", r"args = [re.split(r'\s+', re.sub(r'\[.*\]', '', p))[-1] for p in params]")
|
||||
open(f, 'w').write(t)
|
||||
PY
|
||||
grep -c "re.sub(r'" $CM/tools/glsl/wrappers.py
|
||||
python3 - $CM/tools/glsl/partition.py <<'PY'
|
||||
import sys; f = sys.argv[1]; t = open(f).read()
|
||||
t = t.replace(r"r'^static const float (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$'", r"r'^static const (float|int) (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$'")
|
||||
t = t.replace("consts[o].append('const float %s = %s;' % m.groups())", "consts[o].append('const %s %s = %s;' % m.groups())")
|
||||
open(f, 'w').write(t)
|
||||
PY
|
||||
grep -c "(float|int)" $CM/tools/glsl/partition.py
|
||||
echo "== build"
|
||||
bash $CM/tools/glsl/build-glsl.sh $W/built 2>&1 | tail -12
|
||||
echo "== array functions in the built core.glsl"
|
||||
grep -nE "^(const int CULTMATH_MAX_SITES|vec4 cultmath_power_border\(|float cultmath_lattice_cell_distance\(|vec4 cultmath_weighted_lattice_cells\()" $W/built/core.glsl
|
||||
echo "== ES 3.00 fragment consumer passing a uniform array"
|
||||
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat $W/built/core.glsl
|
||||
printf 'uniform vec4 zone[CULTMATH_MAX_SITES];\nuniform int zoneCount;\nout highp vec4 c;\nvoid main() { c = cultmath_weighted_lattice_cells(gl_FragCoord.xy, 1.0, zone, zoneCount, 260.0, 0.7, 1.6, 120.0, 0.35, vec2(0.6, 0.8), 1.5); }\n'; } > $W/consumer.frag
|
||||
glslangValidator $W/consumer.frag && echo "consumer ES 3.00 ok"
|
||||
echo "== lavapipe"
|
||||
apt-get install -y -qq --no-install-recommends mesa-vulkan-drivers libvulkan-dev gcc libc6-dev >/dev/null
|
||||
gcc -O2 -o $W/vkrun $CM/tools/device/vkrun.c -lvulkan
|
||||
cp -r $P/ref $W/ref && sed -i 's#../../../../packages#'"$CM"'/..#' $W/ref/Ref.csproj
|
||||
N=$(dotnet run --project $W/ref -c Release -- $W/in.bin $W/ref.bin | tail -1); echo "cases $N"
|
||||
DXC=$(find $CM/.tools/glsl/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n#define NCASES %su\n' $N; cat $W/built/core.glsl $P/entry.comp.tail; } > $W/e.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $W/g.spv $W/e.comp >$W/g.log || { head $W/g.log; exit 1; }
|
||||
$W/vkrun $W/g.spv $W/in.bin $W/g.out $N 4
|
||||
python3 $P/judge.py $W/ref.bin $W/g.out built-glsl
|
||||
run_hlsl() { # $1 label, $2 sed expression applied to a copy of the shaders
|
||||
rm -rf $W/s && cp -r $CM/shaders $W/s && sed -i "$2" $W/s/CultMath.hlsl
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -D NCASES=${N}u -I $W/s $P/entry.hlsl -Fo $W/h.spv >$W/h.log 2>&1 || { head -5 $W/h.log; exit 1; }
|
||||
$W/vkrun $W/h.spv $W/in.bin $W/h.out $N 4 >/dev/null
|
||||
python3 $P/judge.py $W/ref.bin $W/h.out "$1"
|
||||
}
|
||||
run_hlsl hlsl 's/^$//'
|
||||
cmp $W/h.out $W/g.out && echo "hlsl equals built glsl bit for bit" || echo "hlsl and built glsl differ (plain form; the gate is strict form)"
|
||||
run_hlsl "M1 exit at 0.5k" 's/0\.5 \* (k - 1) >= max/0.5 * k >= max/'
|
||||
run_hlsl "M2 exit ignores the smooth band" 's/>= max(f2, f1 + 2\.0 \* smoothing))$/>= f2)/'
|
||||
run_hlsl "M3 normaliser drops the pre-scale" 's/normal = stretchNormal \/ scale;/normal = stretchNormal;/'
|
||||
run_hlsl "M4 normaliser drops the length" 's/normal = normal \/ length(normal);//'
|
||||
run_hlsl "M5 smooth step loses its cap" 's/return min(next, f1 + smoothing);/return next;/'
|
||||
run_hlsl "M6 box pass skips its first row" 's/for (int dz = -reach; dz <= reach; dz++)/for (int dz = -reach + 1; dz <= reach; dz++)/'
|
||||
run_hlsl "M7 late exit (equivalent: window-independent)" 's/0\.5 \* (k - 1) >= max/0.5 * (k - 2) >= max/'
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// The prototype, appended to CultMath.hlsl by probe.sh: the whole weighted_lattice_cells with bodies as a fixed-size array and a count.
|
||||
static const int CULTMATH_MAX_SITES = 16;
|
||||
|
||||
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
CultSiteCells cells = cultmath_site_cells_empty();
|
||||
for (int i = 0; i < count; i++)
|
||||
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
int owner = (int)cells.nearestSite;
|
||||
if (owner < 0)
|
||||
return border;
|
||||
for (int k = 0; k < count; k++)
|
||||
if (k != owner)
|
||||
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
return border;
|
||||
}
|
||||
|
||||
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
|
||||
float cellSize, float boost, float falloff, float2 normal, float stretch)
|
||||
{
|
||||
float3 site = cultmath_lattice_site(cell, time, drift);
|
||||
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
|
||||
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
|
||||
}
|
||||
|
||||
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
|
||||
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
|
||||
{
|
||||
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
|
||||
float2 normal = float2(1.0, 0.0);
|
||||
if (scale > 0.0)
|
||||
{
|
||||
normal = stretchNormal / scale;
|
||||
normal = normal / length(normal);
|
||||
}
|
||||
float2 q = p / cellSize;
|
||||
float2 origin = floor(q);
|
||||
int cx0 = (int)origin.x;
|
||||
int cz0 = (int)origin.y;
|
||||
float f1 = 1.0e30;
|
||||
float f2 = 1.0e30;
|
||||
int reach = 0;
|
||||
for (int k = 0; k <= 37; k++)
|
||||
{
|
||||
if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing))
|
||||
break;
|
||||
for (int dz = -k; dz <= k; dz++)
|
||||
{
|
||||
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
|
||||
for (int dx = -k; dx <= k; dx += step)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < f1) { f2 = f1; f1 = d; }
|
||||
else if (d < f2) f2 = d;
|
||||
}
|
||||
}
|
||||
reach = k;
|
||||
}
|
||||
float m = f1 + smoothing;
|
||||
float best = 1.0e30;
|
||||
float second = 1.0e30;
|
||||
int winDx = 0;
|
||||
int winDz = 0;
|
||||
for (int dz = -reach; dz <= reach; dz++)
|
||||
for (int dx = -reach; dx <= reach; dx++)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
|
||||
else if (d < second) second = d;
|
||||
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
|
||||
}
|
||||
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
|
||||
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// Probe reference: writes the lattice cases (76 words each) and C#'s weighted_lattice_cells bits (4 words each).
|
||||
// usage: Ref <in.bin> <ref.bin>; prints the case count.
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
const int Max = 16;
|
||||
var cases = new List<(float2 p, float t, float4[] b, float cell, float drift, float boost, float falloff, float s, float2 n, float st)>();
|
||||
float4[] Bodies(float t, int count)
|
||||
{
|
||||
var b = new float4[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var a = 0.001f * (i + 1) * t + i; var r = 300.0f + 220.0f * i;
|
||||
b[i] = new float4(r * MathF.Cos(a), r * MathF.Sin(a), 0.0f, 40.0f + 40.0f * i);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
// The ring-17 point of LatticeCellsTests.TheCapIsReachedAndMatchesTheFullBox, and its neighbours.
|
||||
for (var j = 0; j < 8; j++)
|
||||
cases.Add((new float2(1446.7345f + 3.0f * j, -680.7637f), 97.69269f, Bodies(97.69269f, 6), 260.0f, 0.7f, 4.0f, 1.0e6f, 1.0f, new float2(0.6f, 0.8f), 2.0f));
|
||||
var looks = new[] { (1.6f, 1.0f, 0.35f), (1.6f, 2.0f, 0.35f), (4.0f, 2.0f, 1.0f), (0.0f, 1.0f, 0.05f) };
|
||||
var normals = new[] { new float2(0.6f, 0.8f), new float2(3.0f, 4.0f), new float2(3.0e-30f, -4.0e-30f), new float2(1.0e30f, 2.0e30f), new float2(-1.0f, 1.0e-3f) };
|
||||
var rng = new System.Random(20261017);
|
||||
float U(float a, float b) => a + (b - a) * (float)rng.NextDouble();
|
||||
for (var i = 0; i < 248; i++)
|
||||
{
|
||||
var (boost, stretch, s) = looks[i % 4];
|
||||
var n = normals[(i / 4) % normals.Length];
|
||||
var count = new[] { 6, 0, 1, 16, 6, 3 }[(i / 20) % 6];
|
||||
var t = U(0.0f, 200.0f);
|
||||
cases.Add((new float2(U(-3000, 3000), U(-3000, 3000)), t, Bodies(t, count), 260.0f, 0.7f, boost, 120.0f, s, n, stretch));
|
||||
}
|
||||
// Zero normal at stretch 1 is (1, 0).
|
||||
cases.Add((new float2(10.0f, 20.0f), 1.0f, Bodies(1.0f, 6), 260.0f, 0.7f, 1.6f, 120.0f, 0.35f, new float2(0.0f, 0.0f), 1.0f));
|
||||
using var input = new BinaryWriter(File.Create(args[0]));
|
||||
using var reference = new BinaryWriter(File.Create(args[1]));
|
||||
foreach (var c in cases)
|
||||
{
|
||||
input.Write(c.p.x); input.Write(c.p.y);
|
||||
for (var k = 0; k < Max; k++)
|
||||
{
|
||||
var v = k < c.b.Length ? c.b[k] : new float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
input.Write(v.x); input.Write(v.y); input.Write(v.z); input.Write(v.w);
|
||||
}
|
||||
input.Write(c.b.Length);
|
||||
foreach (var f in new[] { c.t, c.cell, c.drift, c.boost, c.falloff, c.s, c.n.x, c.n.y, c.st }) input.Write(f);
|
||||
var r = weighted_lattice_cells(c.p, c.t, c.b, c.cell, c.drift, c.boost, c.falloff, c.s, c.n, c.st);
|
||||
reference.Write(r.x); reference.Write(r.y); reference.Write(r.z); reference.Write(r.w);
|
||||
}
|
||||
Console.WriteLine(cases.Count);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue