Compare commits

..

No commits in common. "main" and "claude/strange-ardinghelli-d810cd" have entirely different histories.

4297 changed files with 29511 additions and 3363692 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
.github/workflows/*.lock.yml linguist-generated=true merge=ours

View file

@ -1,227 +0,0 @@
# Gitea Actions CI gate for the self-hosted runners.
#
# Deliberately does NOT use actions/setup-dotnet: data.forgejo.org (the mirror
# Gitea resolves actions from) does not host that action at all, and the
# self-hosted runners carry the pinned SDK band from global.json already.
# actions/checkout IS mirrored, so it is used normally.
#
# The suite runs through tools/run-release-gate.ps1 rather than a bare
# `dotnet test`: that script owns the xUnit trait-lane filter which excludes
# the InstalledDat / Live / Manual / OS-specific lanes. A bare `dotnet test`
# fails ~36 tests by design because those lanes assert their own preconditions.
name: CI
on:
push:
branches: [main]
# Docs-only pushes change nothing a test can fail on, and each gate run is
# ~7 minutes of clean build + 14k tests + a 121 MB release. Skip them; a
# code push (or manual dispatch) still runs everything from scratch —
# deliberately uncached, so the gate keeps proving a from-nothing build.
paths-ignore:
- 'docs/**'
- 'claude-memory/**'
- 'memory/**'
- '**.md'
workflow_dispatch:
jobs:
windows-gate:
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- name: Verify the pinned SDK band resolves
shell: pwsh
run: |
dotnet --version
dotnet --list-sdks
# NOT tools/run-release-gate.ps1 here. That script redirects every child
# process to its own log file, so the step emits nothing for minutes at a
# time; Forgejo treats a task that stops reporting as a zombie and fails
# it while the work is still running (observed: job marked failed with 20
# dotnet processes still alive and a complete 8.7 MB TRX on disk). Running
# the projects directly keeps output streaming. The script stays the
# canonical LOCAL gate; the trait filter below is copied from its default.
- name: Build
shell: pwsh
run: dotnet build AcDream.slnx -c Release --nologo
- name: Test (lane-filtered, streaming)
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$filter = 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure'
$failed = @()
foreach ($proj in Get-ChildItem tests -Directory | Sort-Object Name) {
$csproj = Join-Path $proj.FullName "$($proj.Name).csproj"
if (-not (Test-Path $csproj)) { continue }
Write-Host "::group::$($proj.Name)"
dotnet test $csproj -c Release --no-build --nologo --filter $filter
if ($LASTEXITCODE -ne 0) { $failed += $proj.Name }
Write-Host "::endgroup::"
}
if ($failed.Count) { throw "Failed test projects: $($failed -join ', ')" }
linux-portable:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- name: Portable closure (Linux lanes run here, not on Windows)
run: |
set -e
dotnet --version
# Core.Net runs SINGLE-THREADED here, on its own, and the split is
# measured rather than defensive: on this 6-core container the
# assembly FAILS in 40 s with default parallelism and PASSES in 10 s
# with one thread. Its sessions do real socket work on background
# threads, so contention both breaks and slows them. Windows has 18
# cores, passes in ~7 s parallel, and REGRESSED when serialized, so
# this stays scoped to Linux.
echo '::group::AcDream.Core.Net.Tests (single-threaded)'
dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj \
-c Release --nologo \
--filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure' \
-- xUnit.MaxParallelThreads=1
echo '::endgroup::'
for p in \
tests/AcDream.Platform.Tests \
tests/AcDream.Core.Tests \
tests/AcDream.Content.Tests \
tests/AcDream.Runtime.Tests \
tests/AcDream.Headless.Tests \
tests/AcDream.Launcher.Core.Tests \
tests/AcDream.UI.Abstractions.Tests ; do
echo "::group::$p"
dotnet test "$p" -c Release --nologo \
--filter 'Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure'
echo "::endgroup::"
done
release:
# Same workflow rather than a workflow_run trigger: workflow_run is a
# GitHub feature whose Forgejo support is unreliable, while `needs` is
# guaranteed. A red gate therefore cannot publish.
needs: [windows-gate, linux-portable]
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Compute release version
id: ver
shell: pwsh
run: |
$v = '0.1.0-build.{0}' -f ([DateTime]::UtcNow.ToString('yyyyMMddHHmm'))
"version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Write-Host "release version: $v"
- name: Build payloads with release-attachment URLs
shell: pwsh
env:
TAG: ${{ steps.ver.outputs.version }}
run: |
./tools/publish-bin.ps1 -Version $env:TAG -BaseUrl "${{ github.server_url }}/${{ github.repository }}/releases/download/$env:TAG"
- name: Create the release and upload payloads
shell: pwsh
env:
TAG: ${{ steps.ver.outputs.version }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
$headers = @{ Authorization = "token $env:TOKEN" }
$body = @{
tag_name = $env:TAG
name = "acdream alpha $env:TAG"
body = "Automated alpha build from ${{ github.sha }}."
draft = $false
prerelease = $true
target_commitish = 'main'
} | ConvertTo-Json
$release = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers -ContentType 'application/json' -Body $body
Write-Host "created release id=$($release.id)"
foreach ($f in Get-ChildItem bin -File) {
Write-Host ("uploading {0} ({1:N1} MB)" -f $f.Name, ($f.Length/1MB))
Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases/$($release.id)/assets?name=$($f.Name)" -Form @{ attachment = Get-Item $f.FullName } | Out-Null
}
- name: Republish the `latest` pointer release
shell: pwsh
env:
TAG: ${{ steps.ver.outputs.version }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
$headers = @{ Authorization = "token $env:TOKEN" }
# Forgejo has no /releases/latest/download/ route, so the launcher
# needs a pointer at a URL that never changes. A one-asset release on
# the fixed `latest` tag is that pointer. Keeping it in a release
# rather than in git means no payload branch, no bot commits on main,
# and no push that would retrigger this workflow.
$existing = Invoke-RestMethod -Method Get -Headers $headers `
-Uri "$api/releases/tags/latest" -SkipHttpErrorCheck
if ($existing.id) {
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($existing.id)" | Out-Null
# The tag outlives its release and would block recreation.
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/latest" -SkipHttpErrorCheck | Out-Null
Write-Host "removed the previous latest pointer"
}
$body = @{
tag_name = 'latest'
name = "Update feed -> $env:TAG"
body = "**Download ``launcher-win-x64.zip``**, unzip it, and run ``acdream-launcher.exe``. It installs the game and keeps itself and the client up to date.`n`nThis is build ``$env:TAG``."
draft = $false
prerelease = $false
target_commitish = 'main'
} | ConvertTo-Json
$pointer = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers `
-ContentType 'application/json' -Body $body
# Upload the payloads here too, not just the manifest. `latest` is the
# top of the Releases page and the first thing a person sees; a
# pointer-only release gives them nothing to click and makes them hunt
# for a build tagged with a timestamp. The launcher only needs
# manifest.json, but a friend needs launcher-win-x64.zip.
foreach ($f in Get-ChildItem bin -File) {
Invoke-RestMethod -Method Post -Headers $headers `
-Uri "$api/releases/$($pointer.id)/assets?name=$($f.Name)" `
-Form @{ attachment = Get-Item $f.FullName } | Out-Null
}
Write-Host "latest now carries $env:TAG and its downloads"
- name: Prune old releases
shell: pwsh
env:
KEEP: '5'
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
$headers = @{ Authorization = "token $env:TOKEN" }
$keep = [int]$env:KEEP
# Each build is ~121 MB of attachments, so without this the server
# grows by that much on EVERY push to main. Keep the newest $keep
# versioned releases: enough to grab a previous build or bisect a
# regression, bounded at well under a gigabyte.
$releases = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases?limit=100"
# Never touch the `latest` pointer — it is the launcher's feed, not a build.
$builds = @($releases | Where-Object { $_.tag_name -ne 'latest' } |
Sort-Object -Property created_at -Descending)
Write-Host "$($builds.Count) versioned release(s); keeping $keep"
foreach ($old in ($builds | Select-Object -Skip $keep)) {
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($old.id)" | Out-Null
# The tag survives its release and would otherwise accumulate.
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/$($old.tag_name)" -SkipHttpErrorCheck | Out-Null
Write-Host " pruned $($old.tag_name)"
}

View file

@ -1,236 +0,0 @@
---
description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing
disable-model-invocation: true
---
# GitHub Agentic Workflows Agent
This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files.
## What This Agent Does
This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task:
- **Creating new workflows**: Routes to `create` prompt
- **Updating existing workflows**: Routes to `update` prompt
- **Debugging workflows**: Routes to `debug` prompt
- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt
- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments
- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt
- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes
- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs
- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions
- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command
- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments
- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows
> [!IMPORTANT]
> For architecture/pattern-selection requests, load `https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/patterns.md` first.
Workflows may optionally include:
- **Project tracking / monitoring** (GitHub Projects updates, status reporting)
- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows)
## Files This Applies To
- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md`
- Workflow lock files: `.github/workflows/*.lock.yml`
- Shared components: `.github/workflows/shared/*.md`
- Configuration: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/github-agentic-workflows.md
## Problems This Solves
- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions
- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues
- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes
- **Component Design**: Create reusable shared workflow components that wrap MCP servers
## How to Use
When you interact with this agent, it will:
1. **Understand your intent** - Determine what kind of task you're trying to accomplish
2. **Route to the right prompt** - Load the specialized prompt file for your task
3. **Execute the task** - Follow the detailed instructions in the loaded prompt
## Available Prompts
### Create New Workflow
**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/create-agentic-workflow.md
**Use cases**:
- "Create a workflow that triages issues"
- "I need a workflow to label pull requests"
- "Design a weekly research automation"
### Update Existing Workflow
**Load when**: User wants to modify, improve, or refactor an existing workflow
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/update-agentic-workflow.md
**Use cases**:
- "Add web-fetch tool to the issue-classifier workflow"
- "Update the PR reviewer to use discussions instead of issues"
- "Improve the prompt for the weekly-research workflow"
### Debug Workflow
**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/debug-agentic-workflow.md
**Use cases**:
- "Why is this workflow failing?"
- "Analyze the logs for workflow X"
- "Investigate missing tool calls in run #12345"
### Upgrade Agentic Workflows
**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/upgrade-agentic-workflows.md
**Use cases**:
- "Upgrade all workflows to the latest version"
- "Fix deprecated fields in workflows"
- "Apply breaking changes from the new release"
### Create a Report-Generating Workflow
**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/report.md
**Use cases**:
- "Create a weekly CI health report"
- "Post a daily security audit to Discussions"
- "Add a status update comment to open PRs"
### Create Shared Agentic Workflow
**Load when**: User wants to create a reusable workflow component or wrap an MCP server
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/create-shared-agentic-workflow.md
**Use cases**:
- "Create a shared component for Notion integration"
- "Wrap the Slack MCP server as a reusable component"
- "Design a shared workflow for database queries"
### Fix Dependabot PRs
**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`)
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/dependabot.md
**Use cases**:
- "Fix the open Dependabot PRs for npm dependencies"
- "Bundle and close the Dependabot PRs for workflow dependencies"
- "Update @playwright/test to fix the Dependabot PR"
### Analyze Test Coverage
**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy.
**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/test-coverage.md
**Use cases**:
- "Create a workflow that comments coverage on PRs"
- "Analyze coverage trends over time"
- "Add a coverage gate that blocks PRs below a threshold"
### Render ASCII Charts in Markdown
**Load when**: The workflow needs in-markdown charts (sparklines, bars, table+trend views) that must align cleanly and render reliably across GitHub surfaces, including mobile.
**Reference file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/asciicharts.md
**Use cases**:
- "Show a compact trend chart in an issue comment"
- "Render a dashboard table with sparkline trends"
- "Generate aligned ASCII bars for service metrics"
### CLI Commands Reference
**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access.
**Reference file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/cli-commands.md
**Use cases**:
- "How do I trigger workflow X on the main branch?"
- "What's the MCP equivalent of `gh aw logs`?"
- "I'm in Copilot Cloud — how do I compile a workflow?"
- "Show me all available gh aw commands"
### Token Consumption Optimization
**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes.
**Reference file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/token-optimization.md
**Use cases**:
- "How do I reduce the token cost of this workflow?"
- "My workflow is too expensive — how do I optimize it?"
- "How do I compare token usage between two runs?"
- "Should I use gh-proxy or the MCP server?"
- "How do I use sub-agents to reduce costs?"
- "How do I measure the impact of a prompt change?"
### Workflow Pattern Selection
**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows.
**Reference file**: https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/patterns.md
**Use cases**:
- "Which pattern should I use for multi-repo rollout?"
- "How should I structure this workflow architecture?"
- "What pattern fits slash-command triage?"
- "Should this be DispatchOps or DailyOps?"
## Instructions
When a user interacts with you:
1. **Identify the task type** from the user's request
2. **Load the appropriate prompt** from the GitHub repository URLs listed above
3. **Follow the loaded prompt's instructions** exactly
4. **If uncertain**, ask clarifying questions to determine the right prompt
## Quick Reference
```bash
# Initialize repository for agentic workflows
gh aw init
# Generate the lock file for a workflow
gh aw compile [workflow-name]
# Trigger a workflow on demand (preferred over gh workflow run)
gh aw run <workflow-name> # interactive input collection
gh aw run <workflow-name> --ref main # run on a specific branch
# Debug workflow runs
gh aw logs [workflow-name]
gh aw audit <run-id>
# Upgrade workflows
gh aw fix --write
gh aw compile --validate
```
## Key Features of gh-aw
- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter
- **AI Engine Support**: Copilot, Claude, Codex, or custom engines
- **MCP Server Integration**: Connect to Model Context Protocol servers for tools
- **Safe Outputs**: Structured communication between AI and GitHub API
- **Strict Mode**: Security-first validation and sandboxing
- **Shared Components**: Reusable workflow building blocks
- **Repo Memory**: Persistent git-backed storage for agents
- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default
## Important Notes
- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/github-agentic-workflows.md for complete documentation
- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud
- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions
- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF
- Follow security best practices: minimal permissions, explicit network access, no template injection
- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns.
- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself.
- **Triggering runs**: Always use `gh aw run <workflow-name>` to trigger a workflow on demand — not `gh workflow run <file>.lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref <branch>` to run on a specific branch.
- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see https://github.com/github/gh-aw/blob/v0.74.8/.github/aw/cli-commands.md

11
.github/mcp.json vendored
View file

@ -1,11 +0,0 @@
{
"mcpServers": {
"github-agentic-workflows": {
"command": "gh",
"args": [
"aw",
"mcp-server"
]
}
}
}

View file

@ -1,3 +0,0 @@
{
"ghes": false
}

View file

@ -1,23 +0,0 @@
name: "Copilot Setup Steps"
# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
on:
workflow_dispatch:
jobs:
# The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent
copilot-setup-steps:
runs-on: ubuntu-latest
# Set minimal permissions for setup steps
# Copilot Agent receives its own token with appropriate permissions
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8

View file

@ -1,485 +0,0 @@
name: Headless portability
on:
workflow_dispatch:
permissions:
contents: read
jobs:
portable-headless:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pinned .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
# No apt step here on purpose. This job's whole claim is that the closure
# below is presentation-free: it builds Bake, Plugin.Abstractions, Core,
# Core.Net, Content, Runtime and Headless, runs their tests, and invokes
# the Headless CLI. Nothing in it opens a display, links GL, or calls
# xvfb-run, so an "install the graphical smoke dependencies" step here was
# both unnecessary and, being unconditional `sudo apt-get` in a
# two-operating-system matrix, fatal on the windows-latest leg (exit 127,
# `sudo: command not found`). The graphical jobs that do use xvfb-run and
# jq run only on ubuntu-latest and take both from the runner image.
- name: Build presentation-free closure
shell: pwsh
run: |
$projects = @(
"src/AcDream.Platform/AcDream.Platform.csproj",
"src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj",
"src/AcDream.Bake/AcDream.Bake.csproj",
"src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj",
"src/AcDream.Core/AcDream.Core.csproj",
"src/AcDream.Core.Net/AcDream.Core.Net.csproj",
"src/AcDream.Content/AcDream.Content.csproj",
"src/AcDream.Runtime/AcDream.Runtime.csproj",
"src/AcDream.Headless/AcDream.Headless.csproj"
)
foreach ($project in $projects) {
dotnet build $project -c Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
# AcDream.Core.Tests still contains historical App integration fixtures,
# so it is deliberately not a member of this no-App restore lane. Core is
# built directly above and exercised through every portable downstream
# test project below; the complete solution lane retains Core.Tests.
- name: Test presentation-free closure
shell: pwsh
run: |
$projects = @(
"tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj",
"tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj",
"tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj",
"tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj",
"tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj",
"tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj",
"tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj"
)
foreach ($project in $projects) {
dotnet test $project -c Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
- name: Verify CLI without connecting
shell: pwsh
run: |
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- --help
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Set-Content -LiteralPath headless-k0.json -Value '{"version":1,"sessions":[]}'
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- validate --config headless-k0.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Verify Linux headless host executable permission
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
test -x src/AcDream.Headless/bin/Release/net10.0/acdream-headless
portable-launcher:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pinned .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Build and test the portable launcher
shell: pwsh
run: |
dotnet build src/AcDream.Launcher/AcDream.Launcher.csproj -c Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet test tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj -c Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Publish the self-contained launcher distribution
shell: pwsh
run: |
$rid = if ($IsWindows) { "win-x64" } else { "linux-x64" }
dotnet publish src/AcDream.Launcher/AcDream.Launcher.csproj `
-c Release `
-r $rid `
-o "artifacts/acdream-launcher-$rid"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Verify self-contained Windows launcher and bake artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$root = "artifacts/acdream-launcher-win-x64"
if (-not (Test-Path -LiteralPath "$root/acdream-launcher.exe" -PathType Leaf)) { throw "launcher executable missing" }
if (-not (Test-Path -LiteralPath "$root/acdream-bake.exe" -PathType Leaf)) { throw "bake executable missing" }
if (Test-Path -LiteralPath "$root/acdream-launcher.dll") { throw "launcher is not single-file" }
if (Test-Path -LiteralPath "$root/acdream-bake.dll") { throw "bake is not single-file" }
$env:DOTNET_ROOT = "Z:\definitely-not-installed"
$env:DOTNET_ROOT_X64 = "Z:\definitely-not-installed"
$env:DOTNET_MULTILEVEL_LOOKUP = "0"
& "$root/acdream-launcher.exe" --verify-publish
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& "$root/acdream-bake.exe" --help
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Verify self-contained Linux launcher and bake artifacts
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
root=artifacts/acdream-launcher-linux-x64
self_contained=$(dotnet msbuild \
src/AcDream.Launcher/AcDream.Launcher.csproj \
-nologo \
-property:RuntimeIdentifier=linux-x64 \
-getProperty:SelfContained | tr -d '\r\n ')
test "$self_contained" = true
test -x "$root/acdream-launcher"
test -x "$root/acdream-bake"
test ! -f "$root/acdream-launcher.dll"
test ! -f "$root/acdream-bake.dll"
DOTNET_ROOT=/definitely-not-installed \
DOTNET_ROOT_X64=/definitely-not-installed \
DOTNET_MULTILEVEL_LOOKUP=0 \
"$root/acdream-launcher" --verify-publish
DOTNET_ROOT=/definitely-not-installed \
DOTNET_ROOT_X64=/definitely-not-installed \
DOTNET_MULTILEVEL_LOOKUP=0 \
"$root/acdream-bake" --help
linux-graphical:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pinned .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Build and publish Linux graphical client
shell: pwsh
run: |
dotnet build src/AcDream.App/AcDream.App.csproj -c Release -r linux-x64
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet publish src/AcDream.App/AcDream.App.csproj `
-c Release `
-r linux-x64 `
--self-contained false `
-o artifacts/acdream-linux-x64
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Test portable graphical boundary
shell: pwsh
run: |
dotnet test `
tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj `
-c Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet test `
tests/AcDream.App.Tests/AcDream.App.Tests.csproj `
-c Release `
--filter "FullyQualifiedName~LinuxMonotonicFramePacingWaiterTests|FullyQualifiedName~LinuxPlatformBoundaryTests|FullyQualifiedName~GraphicalHostPlatformServicesTests|FullyQualifiedName~GraphicalLegacyConfigurationMigratorTests|FullyQualifiedName~GraphicalWindowBackendSelectionTests"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Verify Linux package contract
shell: bash
run: |
set -euo pipefail
root=artifacts/acdream-linux-x64
test -x "$root/AcDream.App"
test -f "$root/AcDream.App.dll"
test -f "$root/libglfw.so.3"
test -f "$root/libopenal.so"
test -f "$root/Rendering/Shaders/mesh_modern.vert"
test -f "$root/plugins/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.dll"
test -f "$root/plugins/AcDream.Plugins.Smoke/plugin.json"
test "$(grep -RIl --include='*.cs' 'LibraryImport(\"kernel32.dll\"' \
src/AcDream.App | wc -l)" -eq 1
grep -q 'WindowsHighResolutionFramePacingWaiter.cs' < <(
grep -RIl --include='*.cs' 'LibraryImport(\"kernel32.dll\"' \
src/AcDream.App)
! grep -RIn --include='*.cs' 'LocalApplicationData' \
src/AcDream.App src/AcDream.UI.Abstractions
! grep -RIn --include='*.cs' \
'WindowsHighResolutionFramePacingWaiter.Create()' \
src/AcDream.App \
--exclude='FramePacingWaiterFactory.cs' \
--exclude='WindowsHighResolutionFramePacingWaiter.cs'
# Campaign V slice V9 built this job to prove lavapipe (Mesa's software
# Vulkan) passes the capability gate: every Vulkan feature acdream requires
# is core 1.3 or a descriptor-indexing feature lavapipe implements. That made
# it the first CI job in the project's history to render a frame.
#
# Until Campaign V slice V11, linux-graphical (above) carried the mirror
# case: llvmpipe (Mesa's software OpenGL) FAILING the capability gate,
# because mandatory GL_ARB_bindless_texture has no llvmpipe implementation.
# V11 deleted the GL backend entirely, so there is no more GL capability
# gate for any driver to pass or fail — that job's "Verify actionable
# unsupported-driver gate" step went with it. "Verify the forced-unsupported
# gate exits 4" below is what now proves the exit-code-4 contract still
# fires, forcing an unsupported VULKAN feature instead.
#
# NOT DONE HERE, deliberately: a GL-versus-Vulkan pixel comparison. It was
# never viable in CI even before V11 — the probe harness renders synthetic
# verification scenes rather than the world, and the world needs retail DATs
# that CI does not have and cannot be given. The real GL-versus-Vulkan
# differential was V7's, on the developer machine, against the DATs, with
# both clocks pinned, before V11 deleted the GL arm it depended on.
linux-vulkan:
runs-on: ubuntu-latest
# Redirect the portable per-user roots into the workspace so the capability
# report, which the app writes to its own diagnostics directory rather than
# to a path a caller chooses, lands somewhere collectable. Slice L0 made
# these XDG-driven precisely so a host could place them.
env:
XDG_CONFIG_HOME: ${{ github.workspace }}/artifacts/xdg/config
XDG_DATA_HOME: ${{ github.workspace }}/artifacts/xdg/data
XDG_CACHE_HOME: ${{ github.workspace }}/artifacts/xdg/cache
VULKAN_REPORT: ${{ github.workspace }}/artifacts/xdg/cache/acdream/diagnostics/graphical-capabilities-vulkan.json
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pinned .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install lavapipe, the Vulkan loader and Xvfb
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
jq \
libvulkan1 \
mesa-vulkan-drivers \
vulkan-tools \
xauth \
xvfb
- name: Record the software Vulkan device
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
# Evidence, not configuration: no ICD is forced, because on a runner
# with no GPU lavapipe is the only one the loader can find. If that
# ever stops being true the DeviceType assertion below turns red
# rather than silently measuring different hardware, which is the
# outcome this row wants; its whole point is that the gate passes on
# the weakest conformant device in existence.
ls -l /usr/share/vulkan/icd.d/ || true
vulkaninfo --summary 2>&1 | tee artifacts/vulkaninfo-summary.txt
- name: Build and publish the Linux graphical client
shell: pwsh
run: |
dotnet publish src/AcDream.App/AcDream.App.csproj `
-c Release `
-r linux-x64 `
--self-contained false `
-o artifacts/acdream-linux-x64
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Test the Vulkan backend's platform-independent decisions
shell: pwsh
run: |
dotnet test `
tests/AcDream.App.Tests/AcDream.App.Tests.csproj `
-c Release `
--filter "FullyQualifiedName~AcDream.App.Tests.Rendering.Gpu.Vk"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# (a) + (b): one run, two gates. The harness opens a real window, runs the
# capability gate against a real device, and presents the V6c/V6d
# verification scenes through the real RHI. ACDREAM_VULKAN_PROBE_FRAMES is
# what makes it terminate: nothing in CI ever closes a window.
- name: Probe the Vulkan capability gate on lavapipe
shell: bash
run: |
set -euo pipefail
root=artifacts/acdream-linux-x64
out="$PWD/artifacts/vulkan-probe"
mkdir -p "$out"
# 24-bit depth explicitly: xvfb-run's default screen is 8-bit, which
# leaves the X11 WSI without a usable visual.
ACDREAM_RENDER_BACKEND=vulkan \
ACDREAM_VULKAN_PROBE=1 \
ACDREAM_VULKAN_PROBE_FRAMES=30 \
ACDREAM_AUTOMATION_ARTIFACT_DIR="$out" \
ACDREAM_DISPLAY_PROTOCOL=x11 \
ACDREAM_NO_AUDIO=1 \
xvfb-run -a -s "-screen 0 1920x1080x24" \
"$root/AcDream.App" /tmp/not-needed 2>&1 | tee artifacts/vulkan-probe.log
test -f "$VULKAN_REPORT"
cp "$VULKAN_REPORT" artifacts/vulkan-capabilities-pass.json
jq -r '"device: \(.DeviceName) (\(.DeviceType)), \(.DeviceApiVersion), \(.DriverInfo)"' \
"$VULKAN_REPORT"
# The gate accepted the device outright.
jq -e '.SupportFailures | length == 0' "$VULKAN_REPORT"
test "$(jq -r '.ActiveDisplayProtocol' "$VULKAN_REPORT")" = X11
# A software device, which is the whole point of this row.
test "$(jq -r '.DeviceType' "$VULKAN_REPORT")" = Cpu
# Vulkan 1.3 floor, unpacked from VK_MAKE_API_VERSION: major is bits
# 22+, minor is bits 12-21. Evaluate() already rejects anything lower,
# so this asserts the report agrees with the verdict rather than
# re-deriving it.
jq -e '
((.DeviceApiVersionPacked / 4194304) | floor) as $major
| (((.DeviceApiVersionPacked % 4194304) / 4096) | floor) as $minor
| $major > 1 or ($major == 1 and $minor >= 3)' "$VULKAN_REPORT"
# Advertisement is not evidence: the active probe created the device,
# built the descriptor layouts and a pipeline from committed .spv, drew
# an offscreen triangle and read the pixels back.
jq -e '.FunctionProbe.Failures | length == 0' "$VULKAN_REPORT"
jq -e '.FunctionProbe.DeviceCreation and .FunctionProbe.OffscreenReadback' \
"$VULKAN_REPORT"
- name: Assert the offline render produced a real frame
shell: bash
run: |
set -euo pipefail
png=artifacts/vulkan-probe/vulkan-bringup.png
test -f "$png"
# PNG IHDR carries the dimensions at bytes 16..23, big-endian. Reading
# them proves the capture path returned a full backbuffer rather than a
# stub, without depending on a rasterizer's pixel values.
width=$(od -An -tu4 -j16 -N4 --endian=big "$png" | tr -d ' ')
height=$(od -An -tu4 -j20 -N4 --endian=big "$png" | tr -d ' ')
bytes=$(stat -c%s "$png")
echo "captured ${width}x${height}, ${bytes} bytes"
test "$width" -ge 640
test "$height" -ge 360
# A uniform-colour frame at this size encodes to a few kilobytes. This
# threshold is the "the scene actually drew" line, and is deliberately
# a byte count rather than a pixel baseline: lavapipe and any other
# rasterizer are free to disagree about shading, and nothing in CI has
# a reference frame to disagree with.
test "$bytes" -gt 8192
# (c) The gate's failure path, exercised on a device that actually
# supports everything, so the exit-code-4 contract is proven rather than
# assumed. Same knob slice V5 built for exactly this.
- name: Verify the forced-unsupported gate exits 4
shell: bash
run: |
set -euo pipefail
root=artifacts/acdream-linux-x64
out="$PWD/artifacts/vulkan-forced"
mkdir -p "$out"
set +e
ACDREAM_RENDER_BACKEND=vulkan \
ACDREAM_VULKAN_PROBE=1 \
ACDREAM_VULKAN_PROBE_FRAMES=30 \
ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore \
ACDREAM_AUTOMATION_ARTIFACT_DIR="$out" \
ACDREAM_DISPLAY_PROTOCOL=x11 \
ACDREAM_NO_AUDIO=1 \
xvfb-run -a -s "-screen 0 1920x1080x24" \
"$root/AcDream.App" /tmp/not-needed \
> artifacts/vulkan-forced.log 2>&1
code=$?
set -e
cat artifacts/vulkan-forced.log
test -f "$VULKAN_REPORT"
cp "$VULKAN_REPORT" artifacts/vulkan-capabilities-forced.json
test "$code" -eq 4
test "$(jq -r '.ForcedUnsupportedFeature' "$VULKAN_REPORT")" = timelineSemaphore
test "$(jq -r '.Features.TimelineSemaphore' "$VULKAN_REPORT")" = false
jq -e '.SupportFailures | length > 0' "$VULKAN_REPORT"
jq -e '.SupportFailures | any(test("timelineSemaphore"))' "$VULKAN_REPORT"
# The refusal must be actionable: the operator is told where the full
# report is, not merely that something was unsupported.
grep -q 'graphical-capabilities-vulkan.json' artifacts/vulkan-forced.log
# (d) The committed .spv are the only shaders the Vulkan backend ever
# loads. An App test already re-hashes the GLSL sources against the
# manifest, which catches "edited a shader, forgot to recompile". Nothing
# until now tied the committed BINARIES to those sources, so a stale or
# hand-edited .spv would have shipped silently. Recompiling here closes
# that, and does it on a second operating system.
- name: Verify the committed SPIR-V is fresh
shell: bash
run: |
set -euo pipefail
committed=src/AcDream.App/Rendering/Shaders/spv
fresh="$PWD/artifacts/spv-fresh"
rm -rf "$fresh"
mkdir -p "$fresh"
pwsh tools/compile-shaders.ps1 -OutputDirectory "$fresh"
# The file SET first: a .spv present in one tree and not the other is
# drift the per-file compare would never visit.
diff <(cd "$committed" && ls -1 | sort) <(cd "$fresh" && ls -1 | sort)
drift=0
for path in "$committed"/*.spv; do
name=$(basename "$path")
if ! cmp -s "$path" "$fresh/$name"; then
echo "DRIFT: $name differs from a fresh compile"
echo " committed $(sha256sum "$path" | cut -d' ' -f1) $(stat -c%s "$path") bytes"
echo " fresh $(sha256sum "$fresh/$name" | cut -d' ' -f1) $(stat -c%s "$fresh/$name") bytes"
drift=$((drift + 1))
fi
done
# The manifest is compared as JSON rather than as bytes: it is written
# with Environment.NewLine, so a byte compare would report drift for
# the operating system rather than for the shaders.
diff <(jq -S . "$committed/shaders.manifest.json") \
<(jq -S . "$fresh/shaders.manifest.json")
if [ "$drift" -ne 0 ]; then
echo "$drift .spv artifact(s) are stale."
echo "Run tools/compile-shaders.ps1 and commit the result."
exit 1
fi
echo "all committed .spv match a fresh compile"
- name: Upload Vulkan evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: linux-vulkan-evidence
if-no-files-found: warn
path: |
artifacts/vulkaninfo-summary.txt
artifacts/vulkan-probe.log
artifacts/vulkan-forced.log
artifacts/vulkan-capabilities-pass.json
artifacts/vulkan-capabilities-forced.json
artifacts/vulkan-probe/*.png

File diff suppressed because it is too large Load diff

View file

@ -1,146 +0,0 @@
---
description: Daily hygiene assessment of acdream's main branch — flag workarounds,
ungrounded code, Phase/roadmap drift, and architecture violations.
on:
schedule: daily
workflow_dispatch: {}
permissions: read-all
network:
allowed:
- defaults
- dotnet
tools:
github:
toolsets: [default]
safe-outputs:
create-issue:
max: 1
close-older-issues: true
labels:
- ai
- hygiene
engine:
id: copilot
model: gpt-5.3-codex
---
# acdream Hygiene Assessment
You are **DereLint**, a focused AI auditor for the acdream Asheron's Call client.
Your job: scan `main` once a day and produce a single rolling report on hygiene
drift. Engineer-grade tone. No persona slang. The audience is a senior C# /
systems engineer who already operates under a strict retail-faithfulness rule.
## Mission
acdream's core rule (from `CLAUDE.md`): **"The code is modern. The behavior is
retail."** Every AC-specific algorithm must be ported from
`docs/research/named-retail/` (the Sept 2013 EoR PDB) and never guessed. The
roadmap drives one phase at a time. Workarounds are forbidden unless the user
has explicitly approved them. Drift from any of that is what you flag.
Before you start your analysis: `git fetch origin main && git checkout main`.
Then read these to ground yourself:
- `CLAUDE.md` — the project's operating instructions (most important)
- `docs/plans/2026-04-11-roadmap.md` — current phase, agreed order
- `docs/plans/2026-05-12-milestones.md` — current milestone
- `docs/ISSUES.md` — open issues you must NOT re-file
- `docs/architecture/acdream-architecture.md` — architecture source of truth
## What to look for
Five categories. For each finding, cite `file:line`.
### 1. Workaround patterns (CLAUDE.md forbids these unless user-approved)
- `// WORKAROUND` / `// HACK` / `// FIXME` / `// XXX` comments
- Guard early-returns at symptom sites (`if (badState) return;`) that look like
band-aids rather than root-cause fixes
- `try/catch` blocks swallowing exceptions silently
- "grace period" timers / "settle delay" sleeps
- Flags named like `_suppressXDuringY` that mask wire-level mistakes
### 2. Ungrounded retail-port code
- AC-specific algorithm code (collision, animation, motion, dat-decode,
rendering math) that has **no decomp citation** in comments. Every
retail-faithful port should reference a symbol from
`docs/research/named-retail/symbols.json` or a function address from
`docs/research/decompiled/`.
- Magic numbers in physics / motion / wire-format paths that aren't cited
against a retail source.
### 3. Roadmap drift
- Phase markers in code (`// Phase L.5:`, `// Phase N.4:`) that reference
phases no longer matching the roadmap.
- Sections of `docs/plans/2026-04-11-roadmap.md` flagged "ahead" / "active"
that don't match what the last 20 commits actually touched.
- The "Currently working toward" line in `CLAUDE.md` vs. what the last 20
commit subjects actually touched. If they disagree, flag it.
### 4. Test / build hygiene
- `dotnet build` warnings (the project should build with zero warnings).
- Tests in failing state (`dotnet test`).
- Test count regression below the baseline documented in `CLAUDE.md`.
- Build / launch needing `--no-build` workarounds anywhere.
### 5. Architecture drift
- `using WorldBuilder.*` outside `src/AcDream.App/Rendering/Wb/` and
`src/AcDream.Core/Rendering/Wb/` (Phase O extracted WB code into those
directories — references outside are a regression).
- `Environment.GetEnvironmentVariable("ACDREAM_*")` calls outside diagnostic
owner classes (per `CLAUDE.md` "Code Structure Rules" item 5).
- `IDatReaderWriter` consumers that should be using `DatCollection`
(post-Phase O: `DatCollection` is the only dat reader).
- Code in `AcDream.Core` that references `AcDream.App` or GL types directly
(layer separation violation per `CLAUDE.md` Code Structure Rules item 2).
## Accepted exceptions
If `docs/ISSUES.md` already has an OPEN entry for a finding, **don't re-file
it**. Mention it under "Known accepted exceptions" instead. Same for items
explicitly listed as deferred in the roadmap.
## Output
Create one GitHub Issue titled `acdream Hygiene Report YYYY-MM-DD`. The
framework will close any prior `ai+hygiene`-labeled issues automatically.
Body structure:
### Executive Summary
Two sentences on overall hygiene. Concrete; no fluff.
### Findings
For each: **Location** (file:line, linked to the source), **Category** (1-5),
**Problem** (one sentence), **Recommendation** (one sentence),
**Decomp/Doc reference** (where applicable — cite the named symbol or doc).
### Roadmap reality check
Currently-working-toward line vs. recent commit subjects. State whether they
match or where they diverge.
### Known accepted exceptions
Issues already filed in `docs/ISSUES.md` that you observed during the scan.
Name them by ID, don't re-file.
### Suggested next step
ONE concrete action the team should take. If everything is clean, call the
`noop` safe-output with "All clear — no hygiene drift found." instead of
creating an issue.
## Style
- Engineer tone. No slang.
- Be specific. "Workaround in PhysicsEngine.cs:142" beats "physics has issues."
- Be conservative. If you're unsure something is a workaround vs. an
intentional retail-faithful port, say so — don't assert.
- Keep the report under 1500 words. The team wants signal, not a wall of text.

View file

@ -1,35 +0,0 @@
name: Complete Release gate
on:
workflow_dispatch:
permissions:
contents: read
jobs:
complete-release:
name: Complete Release suite (Windows)
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pinned .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Run complete bounded Release gate
shell: pwsh
run: ./tools/run-release-gate.ps1
- name: Upload Release gate evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: release-gate-${{ github.run_id }}-${{ github.run_attempt }}
if-no-files-found: error
retention-days: 14
path: artifacts/release-gate/

56
.gitignore vendored
View file

@ -2,11 +2,6 @@
bin/
obj/
out/
# NOTE: the repo-root /bin folder holds the alpha distribution feed written by
# tools/publish-bin.ps1. It stays IGNORED here on purpose so a stray `git add`
# can never put ~150 MB of payloads on main (GitHub also hard-rejects any file
# over 100 MB). tools/publish-dist.ps1 force-adds it onto the Gitea-only `dist`
# branch instead, which is what the launcher's update feed reads.
# Rider / VS
.idea/
@ -23,31 +18,13 @@ packages/
Thumbs.db
# Reference repos and retail client (large, not our code, separate licenses)
# WorldBuilder is exempt — it's a load-bearing dependency tracked as a git
# submodule pointing at our fork (Phase N, see docs/architecture/worldbuilder-inventory.md).
references/*
!references/WorldBuilder
!references/WorldBuilder/
references/
# Claude Code session state
.claude/
# Superpowers brainstorm visual-companion scratch (mockups regenerate; not source)
/.superpowers/
launch.log
launch-*.log
proveout*.log
launch.utf8.log
n4-verify*.log
# A6.P5 (2026-05-25) — door-stuck reproduction captures (multi-MB);
# the 3-record fixture extracted from these lives at
# tests/AcDream.Core.Tests/Fixtures/door-bug/over-penetration-capture.jsonl
door-stuck-capture.jsonl
door-stuck-*.launch.log
door-stuck-*.launch.utf8.log
door-fix-*.launch.log
door-fix-*.jsonl
door-walkthrough.*
# ImGui auto-saved window/docking state (per-user, not source)
imgui.ini
@ -62,12 +39,6 @@ __pycache__/
# Per-session scratch (Claude commit message drafts, ad-hoc temp files)
tmp/
# Disposable dotnet test/build output redirected by local validation runs
.test-out/
# Connected-gate, benchmark, and visual-capture artifacts are machine-local
logs/
# Git worktrees for isolated feature work
.worktrees/
@ -75,8 +46,6 @@ logs/
# The committed reference workflow lives in CLAUDE.md "Retail debugger toolchain";
# session-specific traces should not pollute the repo.
*.cdb
# tools/cdb/ holds committed reference scripts — exempt them from the blanket rule above.
!tools/cdb/*.cdb
launch_*.log
launch_*.err
launch_*.ps1
@ -95,26 +64,3 @@ substep_trace*
sg_built.txt
# Stray bash-mangled path artifacts from PowerShell-via-bash escaping
C[€-￿]*
# Obsidian vault config (personal, not project-wide)
.obsidian/
# Junction to Claude Code per-project memory (Obsidian vault visibility)
claude-memory
studio-shots/
# MP1b acdream-bake output — user-machine artifact, never committed
# (docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md, Task 5).
*.pak
# session-local physics capture artifacts (worktree root)
/resolve-*.jsonl
/launch-*.log
# Campaign V capture/evidence output - session-local, never tracked (423 MB lesson, 2026-07-29)
artifacts/
341-slope-capture.jsonl
# IconForge DAT extraction scratch (geometry + textures dumped from the
# installed client dats; regenerate with tools/MosswartArt, never commit).
tools/IconForge/work/

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "references/WorldBuilder"]
path = references/WorldBuilder
url = git@github.com:eriknihlen/WorldBuilder.git
branch = acdream

View file

@ -1,5 +0,0 @@
{
"github.copilot.enable": {
"markdown": true
}
}

1782
AGENTS.md

File diff suppressed because it is too large Load diff

View file

@ -1,67 +1,21 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/AcDream.App/AcDream.App.csproj" />
<Project Path="src/AcDream.Bake/AcDream.Bake.csproj" />
<Project Path="src/AcDream.Cli/AcDream.Cli.csproj" />
<Project Path="src/AcDream.Content/AcDream.Content.csproj" />
<Project Path="src/AcDream.Core/AcDream.Core.csproj" />
<Project Path="src/AcDream.Core.Net/AcDream.Core.Net.csproj" />
<Project Path="src/AcDream.Headless/AcDream.Headless.csproj" />
<Project Path="src/AcDream.Launcher/AcDream.Launcher.csproj" />
<Project Path="src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj" />
<Project Path="src/AcDream.Platform/AcDream.Platform.csproj" />
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
<Project Path="src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj" />
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
<Project Path="src/AcDream.Runtime/AcDream.Runtime.csproj" />
<Project Path="src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples/AcDream.RenderPacks.AtmosphericTier2/AcDream.RenderPacks.AtmosphericTier2.csproj" />
<Project Path="samples/AcDream.RenderPacks.NoOp/AcDream.RenderPacks.NoOp.csproj" />
<Project Path="samples/AcDream.RenderPacks.ShadowsOnlyTier2/AcDream.RenderPacks.ShadowsOnlyTier2.csproj" />
<Project Path="src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/A8CellAudit/A8CellAudit.csproj" />
<Project Path="tools/AnimHookScan/AnimHookScan.csproj" />
<Project Path="tools/dump-keymap/dump-keymap.csproj" />
<Project Path="tools/LayoutDump/LayoutDump.csproj" />
<Project Path="tools/MosswartArt/MosswartArt.csproj" />
<Project Path="tools/PesChainAudit/PesChainAudit.csproj" />
<Project Path="tools/ProjectileVfxAudit/ProjectileVfxAudit.csproj" />
<Project Path="tools/RainMeshProbe/RainMeshProbe.csproj" />
<Project Path="tools/RenderPackValidator/AcDream.Tools.RenderPackValidator.csproj" />
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
<Project Path="tools/SetupInspect/SetupInspect.csproj" />
<Project Path="tools/ShaderCompiler/ShaderCompiler.csproj" />
<Project Path="tools/SkyObjectInspect/SkyObjectInspect.csproj" />
<Project Path="tools/SpellDump/SpellDump.csproj" />
<Project Path="tools/StarsProbe/StarsProbe.csproj" />
<Project Path="tools/TextureDump/TextureDump.csproj" />
<Project Path="tools/WeatherEnumerator/WeatherEnumerator.csproj" />
<Project Path="tools/WeatherSetupProbe/WeatherSetupProbe.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/AcDream.App.Tests/AcDream.App.Tests.csproj" />
<Project Path="tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj" />
<Project Path="tests/AcDream.Cli.Tests/AcDream.Cli.Tests.csproj" />
<Project Path="tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj" />
<Project Path="tests/AcDream.Core.Tests.Fixtures.HelloPlugin/AcDream.Core.Tests.Fixtures.HelloPlugin.csproj" />
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
<Project Path="tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj" />
<Project Path="tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj" />
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder/AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder.csproj" />
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.ConsoleSignalChild/AcDream.Launcher.Core.Tests.Fixtures.ConsoleSignalChild.csproj" />
<Project Path="tests/AcDream.Launcher.Core.Tests.Fixtures.ConsolelessSupervisorParent/AcDream.Launcher.Core.Tests.Fixtures.ConsolelessSupervisorParent.csproj" />
<Project Path="tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj" />
<Project Path="tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj" />
<Project Path="tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj" />
<Project Path="tests/AcDream.Plugins.MossTank.Tests/AcDream.Plugins.MossTank.Tests.csproj" />
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.HostPlugin/AcDream.Plugin.Tests.Fixtures.HostPlugin.csproj" />
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackInternal/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackInternal.csproj" />
<Project Path="tests/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackMultiple/AcDream.Plugin.Tests.Fixtures.InvalidRenderPackMultiple.csproj" />
<Project Path="tests/AcDream.RenderPackValidator.Tests/AcDream.RenderPackValidator.Tests.csproj" />
<Project Path="tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj" />
<Project Path="tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj" />
</Folder>
</Solution>

1280
CLAUDE.md

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
<Project>
<PropertyGroup>
<!-- Repository-wide language and warning policy. Project files only override
these values when a target has a documented, target-specific need. -->
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<!-- Use custom names for every graph. A conventional packages.lock.json
always overrides NuGetLockFilePath, which makes neutral and RID locks
impossible to keep side by side. -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NuGetLockFilePath Condition="'$(RuntimeIdentifier)' == ''">$(MSBuildProjectDirectory)/packages.neutral.lock.json</NuGetLockFilePath>
<NuGetLockFilePath Condition="'$(RuntimeIdentifier)' != ''">$(MSBuildProjectDirectory)/packages.$(RuntimeIdentifier).lock.json</NuGetLockFilePath>
</PropertyGroup>
</Project>

View file

@ -1,38 +0,0 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Arch" Version="2.1.0" />
<PackageVersion Include="Avalonia" Version="12.1.1" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<PackageVersion Include="Avalonia.Headless.XUnit" Version="12.1.1" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" />
<PackageVersion Include="BCnEncoder.Net" Version="2.2.1" />
<PackageVersion Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
<PackageVersion Include="Chorizite.Core" Version="0.0.18" />
<PackageVersion Include="Chorizite.DatReaderWriter" Version="2.1.7" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Serilog" Version="4.0.2" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Silk.NET.Input" Version="2.23.0" />
<PackageVersion Include="Silk.NET.OpenAL" Version="2.23.0" />
<PackageVersion Include="Silk.NET.OpenAL.Extensions.Creative" Version="2.23.0" />
<PackageVersion Include="Silk.NET.OpenAL.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.OpenAL.Soft.Native" Version="1.23.1" />
<PackageVersion Include="Silk.NET.Shaderc" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="StbImageSharp" Version="2.30.16" />
<PackageVersion Include="StbTrueTypeSharp" Version="1.26.12" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>

View file

@ -1,45 +0,0 @@
# Third-Party Notices
This file lists third-party software used by acdream, along with their
license terms and copyright notices.
---
## WorldBuilder
Portions of acdream's rendering and dat-handling code are copied from
WorldBuilder (https://github.com/Chorizite/WorldBuilder), MIT-licensed.
The extracted code lives under:
- `src/AcDream.Core/Rendering/Wb/` — pure helpers (texture decode,
scenery transforms, terrain math).
- `src/AcDream.App/Rendering/Wb/` — GL infrastructure and mesh pipeline.
Original copyright holders: Chorizite contributors (see WorldBuilder's
LICENSE file). Adapted by acdream maintainers to consume our
`DatCollection` directly (replacing WB's `DefaultDatReaderWriter`) and
to remove editor-only code paths.
Original MIT license text:
MIT License
Copyright (c) Chorizite contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<fallbackPackageFolders>
<clear />
</fallbackPackageFolders>
</configuration>

370
README.md
View file

@ -2,294 +2,174 @@
A modern open-source C# / .NET 10 Asheron's Call client.
acdream ports the observable behaviour of the September 2013 retail client to
Silk.NET and a modern, plugin-friendly architecture. The code is modern; the
behaviour is retail.
Faithful port of the retail client's behaviour to Silk.NET with a modern,
plugin-friendly architecture. The code is modern; the behaviour is retail.
**Status:** playable pre-alpha. M3, “Cast a spell,” landed on 2026-07-21 and
M4, “Live in the world,” is active. The graphical client supports the connected
combat, magic, movement, portal, inventory, loot, and retained-UI loops used by
the current test characters. The presentation-independent `GameRuntime` and
the Linux/Windows multi-session headless host are complete. Native Linux
graphics are intentionally parked at the L1 capability checkpoint; Windows is
the currently validated graphical platform.
**Status:** playable pre-alpha. You can log in to an ACE server, walk and
run through Dereth, see other players animate correctly, watch the
day-night cycle, hear ambient audio, and take weapons out. Many systems
are still stubbed or in-progress — see roadmap.
The [documentation map](docs/README.md) is the entry point for current
milestones, roadmap state, architecture, issues, retail divergences, research,
and durable project memory.
## Stack
## Technology
- **Runtime:** C# and .NET 10
- **Graphics:** [Silk.NET](https://github.com/dotnet/Silk.NET), OpenGL 4.3
core, bindless textures, shader draw parameters, SSBOs, and multi-draw
indirect
- **Audio:** OpenAL through Silk.NET
- **Content:** retail DAT files plus a machine-local, memory-mapped
`acdream.pak` produced by `AcDream.Bake`
- **Networking:** custom UDP, ISAAC cipher, and game-message layers compatible
with ACEmulator
- **UI:** retained retail gameplay UI plus opt-in ImGui developer tools
- **Automation:** the same presentation-independent `GameRuntime` is hosted by
both the graphical client and `AcDream.Headless`
The modern renderer is mandatory. There is no legacy renderer fallback.
Startup reports an actionable error if the required OpenGL capabilities are
missing.
- **Language:** C# .NET 10
- **Graphics:** [Silk.NET](https://github.com/dotnet/Silk.NET) (OpenGL 4.3)
- **Audio:** OpenAL via Silk.NET
- **Dat parsing:** [Chorizite.DatReaderWriter](https://github.com/Chorizite/DatReaderWriter)
- **Networking:** Custom UDP + ISAAC cipher + game-message layer, wire-compatible
with ACEmulator server
## What works
- ACE login, character selection, world entry, chat, client commands,
reconnect, and graceful logout.
- Outdoor, building, cellar, and dungeon streaming with prepared terrain,
scenery, buildings, EnvCells, collision, portal visibility, sky, fog,
lighting, audio, and day/night presentation.
- Local and observed movement, animation, jumping, selection, radar, combat
stances, melee, bows, crossbows, spell projectiles, death, corpses, chests,
and looting.
- Inventory bags, stable server ordering, stack splitting, ground drops,
paperdoll equipment, weapon switching, quick bars, item use, cooldowns, and
giving items to NPCs.
- Retail-style retained UI for vitals, chat, toolbar, inventory, character,
attributes, skills, spellbook, components, effects, combat/spell/jump bars,
radar/compass, dialogs, external containers, and assessment.
- Complete end-of-retail spell catalog, learned and favorite spells,
component preflight, connected casts, enchantments, DAT-driven projectiles
and effects, recall, portal-space travel, Hidden/UnHide, and remote
materialization.
- One presentation-independent runtime owner for session, entities, objects,
inventory, character state, selection, interactions, combat, magic,
movement, physics, projectiles, world environment, and portal transit.
- A no-window Windows/Linux host with deterministic bot commands/events,
shared immutable content, multi-session scheduling, isolation, reconnect,
resource telemetry, and tested 1/5/10/30-session ownership.
- Plugin loading, shared command/input abstractions, retained markup panels,
and permanent ImGui developer tools behind `ACDREAM_DEVTOOLS=1`.
- Connecting to a local ACEmulator (ACE) server on `127.0.0.1:9000`
- Character selection and login
- Rendering Dereth terrain with retail-correct texture blending,
per-vertex lighting, and road overlays
- Static scenery (buildings, trees, scenery objects) via EnvCell walker
- Animated characters (own + remote) with walk / run / strafe / jump /
turn / attack motions sourced from the retail motion tables
- Network sync with remote players — you can watch other characters
animate correctly, including speeds and directional motion
- Day-night cycle driven from the retail Region dat (0x13000000) —
correct DayGroup picking via the retail LCG, correct keyframe
interpolation, correct per-keyframe sky-object replace
- Weather (rain/snow particles synced from the server via the retail
DayGroup name)
- Sky dome, stars, moon, clouds, sun — each rendered from the retail
Region's SkyObjects with texture scrolling and alpha fade
- Plugin host with live event replay-on-subscribe
## Current boundaries
## What's stubbed or in-progress
- The active M4 prelude is
[world interaction completion](docs/plans/2026-07-23-world-interaction-completion.md).
Slices 13, including assessment and its final formula/icon/layout
correction, are user-accepted. Equipped-child picking and vendor
browse/buy/sell are the next uncompleted slices.
- Issue `#225` retains the lifestone/particle shared-alpha visual comparison.
Its connected lifetime and performance routes already pass.
- Narrow carried behaviour debt includes issue `#153` (an unstreamed
far-teleport edge), issue `#116` (slide feel), issue `#235` (30 Hz capped/RDP
jump presentation), and the live temporary-stopgap rows in the
[retail divergence register](docs/architecture/retail-divergence-register.md).
- Native Linux graphics are deferred. L0 portability and L1 backend/capability
reporting are implemented; WSLg reaches the GPU through Mesa D3D12 but does
not expose mandatory `GL_ARB_bindless_texture`. Resume with a supported
physical Linux AMD/NVIDIA driver before beginning later Slice L work.
- Advanced vendor/trade/crafting/social surfaces and larger M4 quest,
character-creation, and emote bodies remain roadmap work.
- Indoor transitions (building interiors) — disabled, Phase B.3 pending
- Combat — animation works, damage math not wired
- Lightning visual — the retail PhysicsScript-driven flash is researched
but not wired (see `docs/research/2026-04-23-lightning-real.md`)
- TimeSync drift — we only sync calendar on login, not periodically,
so acdream's in-game clock gradually drifts from retail's
- Landscape draw distance — currently `ACDREAM_STREAM_RADIUS=2` (~400m)
vs retail's several kilometres
## Prerequisites
See `docs/plans/2026-04-11-roadmap.md` for the ordered phase list.
See `docs/ISSUES.md` for the rolling list of known bugs + small deferred
features (tactical, bug-level; the roadmap is strategic, phase-level).
## Building + running
**Requires:**
- .NET 10 SDK
- Your own retail Asheron's Call DAT directory containing:
- `client_portal.dat`
- `client_cell_1.dat`
- `client_highres.dat`
- `client_local_English.dat`
- A machine-local `acdream.pak` built from those DATs
- A running ACE server for connected play; the examples use
`127.0.0.1:9000`
- For the graphical client, a driver exposing the mandatory modern OpenGL
capabilities
- A retail Asheron's Call dat directory (Turbine/Microsoft property —
supply your own). Contains `client_portal.dat`, `client_cell_1.dat`,
`client_highres.dat`, `client_local_English.dat`.
- A running ACE (ACEmulator) server on `127.0.0.1:9000` (or override
via env var)
The project does not distribute Microsoft/Turbine DAT files or derived
prepared packages.
## Build and test
**Launch (PowerShell on Windows — bash has trouble with the apostrophe
in "Asheron's Call"):**
```powershell
dotnet restore AcDream.slnx
dotnet build AcDream.slnx -c Release
dotnet test AcDream.slnx -c Release --no-build
```
The current baseline is a successful Release build with **8,826 passing tests
and 5 intentional skips**. The build currently reports 17 test-project
warnings tracked by [`#228`](docs/ISSUES.md#228--clean-release-build-emits-17-test-project-warnings);
production compilation has zero errors.
## Prepare content
Production rendering and collision use the validated prepared package rather
than decoding world meshes on the frame path:
```powershell
dotnet run --project src\AcDream.Bake\AcDream.Bake.csproj -c Release -- `
--dat-dir "C:\Games\Asheron's Call" `
--out "C:\Games\Asheron's Call\acdream.pak"
```
A complete package is approximately 30 GB. It is machine-local and must not be
committed. `ACDREAM_PAK_PATH` overrides the default
`<DAT directory>\acdream.pak`.
## Run the graphical client
```powershell
$env:ACDREAM_DAT_DIR = "C:\Games\Asheron's Call"
$env:ACDREAM_PAK_PATH = "C:\Games\Asheron's Call\acdream.pak"
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_RETAIL_UI = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Release
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug
```
The DAT directory can instead be supplied as the first positional argument:
Offline CLI dat inspector (no server needed):
```powershell
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Release -- `
"C:\Games\Asheron's Call"
```
dotnet run --project src/AcDream.Cli -- "C:\path\to\Asheron's Call"
```
## Run a headless session
`AcDream.Headless` loads no App, UI, OpenGL, native-window, or audio assembly.
Create a version-1 configuration such as `bot.json`:
```json
{
"version": 1,
"process": {
"content": {
"datDirectory": "/opt/ac",
"preparedAssetPath": "/opt/ac/acdream.pak"
}
},
"sessions": [
{
"id": "bot-1",
"endpoint": { "host": "127.0.0.1", "port": 9000 },
"account": "testaccount",
"character": { "index": 0 },
"policy": { "id": "idle" },
"credential": {
"provider": "environment",
"reference": "ACDREAM_BOT_PASSWORD"
}
}
]
}
```
Then validate and run it:
```bash
export ACDREAM_BOT_PASSWORD='testpassword'
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- \
validate --config bot.json
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- \
run --config bot.json
```
For a single local session, `run` also accepts
`--user <account> --password <password>`. Add uniquely identified session
entries and credential references for a multi-session process. Available
built-in policies are `idle`, `lifecycle-smoke`, `observer-movement`, and
`portal-route-smoke`.
## Useful startup options
## Diagnostic env vars
| Variable | Effect |
|---|---|
| `ACDREAM_DAT_DIR` | Retail DAT directory |
| `ACDREAM_PAK_PATH` | Prepared package path; defaults to `<DAT dir>/acdream.pak` |
| `ACDREAM_LIVE=1` | Enable connected mode |
| `ACDREAM_TEST_HOST` / `ACDREAM_TEST_PORT` | ACE endpoint |
| `ACDREAM_TEST_USER` / `ACDREAM_TEST_PASS` | Graphical-client credentials |
| `ACDREAM_RETAIL_UI=1` | Enable the retained retail gameplay UI |
| `ACDREAM_DEVTOOLS=1` | Enable ImGui developer tools |
| `ACDREAM_NO_AUDIO=1` | Suppress OpenAL initialization |
| `ACDREAM_UNCAPPED_RENDER=1` | Disable normal frame pacing for diagnostics |
| `ACDREAM_DISPLAY_PROTOCOL=auto\|x11\|wayland` | Select the Linux GLFW backend |
| `ACDREAM_DAY_GROUP=N` | Force a day-group index for weather/lighting comparisons |
| `ACDREAM_STREAM_RADIUS=N` | Legacy override over configured streaming radii |
| `ACDREAM_DUMP_SKY=1` | Dump sky interpolation and draw diagnostics |
| `ACDREAM_DUMP_MOTION=1` | Dump inbound movement and motion-cycle decisions |
| `ACDREAM_DUMP_SKY=1` | Per-second dump of the interpolated `SkyKeyframe` values + per-SkyObject draw info + texture alpha histograms |
| `ACDREAM_DUMP_MOTION=1` | Dump every inbound `UpdateMotion` + resulting `SetCycle` |
| `ACDREAM_STREAM_RADIUS=N` | Tune landblock visible-window radius (default 2 = 5×5) |
| `ACDREAM_NO_AUDIO=1` | Suppress OpenAL init |
| `ACDREAM_DAY_GROUP=N` | Force a specific DayGroup index for A/B-testing weather presets |
| `ACDREAM_RUN_SKILL=N` / `ACDREAM_JUMP_SKILL=N` | Client-side run/jump skill (default 200) |
Additional diagnostic and budget controls are documented beside their typed
owners and in the linked research plans; they are not stable user settings.
## Layout
## Repository layout
```text
```
src/
AcDream.Runtime/ presentation-independent GameRuntime
AcDream.App/ graphical host, retained UI, renderer, audio
AcDream.Headless/ Windows/Linux no-window multi-session host
AcDream.Core/ retail gameplay, movement, physics, world logic
AcDream.Core.Net/ UDP, ISAAC, protocol and message routing
AcDream.Content/ GL-free DAT and prepared-package content
AcDream.Bake/ offline acdream.pak builder
AcDream.Cli/ offline DAT inspector
AcDream.UI.Abstractions/ shared UI/input models and contracts
AcDream.UI.ImGui/ developer-tool presentation
AcDream.Plugin.Abstractions/ BCL-only plugin contracts
AcDream.Plugins.Smoke/ example plugin
AcDream.App/ rendering + audio + main loop (Silk.NET)
AcDream.Core/ game state, meshing, physics, sky, weather, lighting
AcDream.Core.Net/ UDP + ISAAC + game-message layer
AcDream.Cli/ offline dat-inspector console app
AcDream.Plugin.Abstractions/ plugin host interfaces
AcDream.Plugins.Smoke/ example plugin
tests/
AcDream.*.Tests/ layer-matched xUnit projects
AcDream.Core.Tests/ xUnit tests (742 passing)
AcDream.Core.Net.Tests/ network-layer tests
tools/
RetailTimeProbe/ Win32 P/Invoke ReadProcessMemory probe of
the live retail acclient.exe — dumps
TimeOfDay + sky-lighting globals so we
can compare against acdream's state
SkyObjectInspect/ dat-inspector for Region sky objects
references/ vendored read-only reference code — ACE,
ACViewer, WorldBuilder, holtburger,
AC2D, Chorizite, DatReaderWriter.
Gitignored.
docs/
README.md documentation authority and current map
architecture/ ownership, structure, divergence, WB inventory
plans/ milestone, roadmap, and execution plans
research/ retail pseudocode, traces, fixtures, evidence
audit/ completion and conformance audits
memory/ durable engineering references
references/ gitignored external reference repositories
architecture/ single-source-of-truth architecture doc
plans/ phase roadmaps + per-phase specs
research/ decompile-derived research, per-phase
findings, deep-dive agent reports
audit/ phase-completion audits
```
## Development workflow
All AC-specific behaviour starts from the named retail oracle in
`docs/research/named-retail/`:
All AC-specific behaviour is ported from the decompiled retail client
(`docs/research/decompiled/`). The workflow is:
1. Search the named retail pseudo-C and headers by `class::method`.
2. Use the older Ghidra chunks only when the named oracle is insufficient.
3. Cross-reference ACE and the relevant client/viewer implementation.
4. Record readable pseudocode and exact constants/order.
5. Port the retail mechanism into the correct modern owner.
6. Add conformance, lifecycle, and failure-boundary tests.
7. Run the automated gate and the appropriate connected or visual gate.
8. Update architecture, roadmap, divergences, and durable memory with the
same change.
1. **Decompile first.** Find the matching function in the decompiled
client.
2. **Cross-reference.** Check against ACE's C# port and ACViewer /
WorldBuilder.
3. **Write pseudocode.** Translate C to readable pseudocode first.
4. **Port faithfully.** Translate line-by-line, preserving variable
names and control flow.
5. **Conformance test.** Add tests using golden values from retail.
6. **Integrate surgically.** Minimise churn in the surrounding pipeline.
Guessing at AC-specific algorithms is forbidden. See
[AGENTS.md](AGENTS.md), [CLAUDE.md](CLAUDE.md), and the
[architecture guide](docs/architecture/acdream-architecture.md) for the full
rules.
Guessing at AC-specific algorithms is explicitly forbidden — see
`CLAUDE.md` for the full workflow rationale and the list of failure
modes we've paid for in the past.
## Reference projects
## Reference repos
- **ACE / ACEmulator:** authoritative server and protocol behaviour
- **ACViewer:** character appearance and DAT presentation cross-check
- **WorldBuilder:** extracted Silk.NET DAT/rendering foundation
- **Chorizite.ACProtocol:** clean-room protocol reference
- **holtburger:** broad non-retail client behaviour reference
- **AC2D:** terrain and movement-packet cross-checks
We cross-reference five external projects for every retail behaviour:
The retail binary/decomp remains the behavioural oracle when references
disagree.
- **ACE** (ACEmulator) — authoritative server-side protocol
- **ACViewer** — MonoGame dat viewer; good for character appearance
- **WorldBuilder** — Silk.NET dat editor; matches our stack
- **Chorizite.ACProtocol** — clean-room C# protocol library
- **holtburger** — most complete non-retail client; Rust TUI, full
client-side behaviour
- **AC2D** — C++ AC-client emulator; has the real terrain split
formula and 0xF61C movement packet format
## Licence and game assets
See `CLAUDE.md` for which reference is authoritative for which domain.
The acdream source has not yet been assigned a top-level licence and is not
ready for public redistribution. External reference code retains its own
licence.
## Licence
Asheron's Call DAT files, art, names, and other game assets remain the property
of Microsoft/Turbine. This repository does not distribute them; users must
supply their own retail installation.
Not yet chosen. All external reference code is vendored under its own
licence; see `references/*/LICENSE`. The acdream source code itself is
unreleased — not yet distributed to the public. Once the licence
choice is made it will go in a top-level `LICENSE` file.
The AC dat files and the game's intellectual property remain the
property of Microsoft / Turbine. This project does not distribute any
of those files or assets — you must supply your own retail install.

View file

@ -1,44 +0,0 @@
import sys, re, math
from collections import Counter
pat = re.compile(
r'outRoot=(\w) flood=(\d+) eye=\(([^)]+)\) player=\(([^)]+)\) '
r'rawPlayer=\(([^)]+)\) yaw=([-\d.]+)')
rows = []
for l in sys.stdin:
m = pat.search(l)
if not m:
continue
rows.append((
m.group(1), int(m.group(2)),
tuple(float(x) for x in m.group(3).split(',')), # eye
tuple(float(x) for x in m.group(4).split(',')), # player (RenderPosition)
tuple(float(x) for x in m.group(5).split(',')), # rawPlayer (physics body)
float(m.group(6)))) # yaw
print("parsed pv-input rows:", len(rows))
if not rows:
raise SystemExit
print("flood histogram (outRoot,flood)->count:", dict(Counter((r[0], r[1]) for r in rows)))
def rng(idx):
return [max(r[idx][k] for r in rows) - min(r[idx][k] for r in rows) for k in range(3)]
print(f"eye range over window (m): {[round(v,6) for v in rng(2)]}")
print(f"render-pos range over window (m): {[round(v,6) for v in rng(3)]}")
print(f"raw-phys range over window (m): {[round(v,6) for v in rng(4)]}")
print(f"yaw range over window (rad): {round(max(r[5] for r in rows)-min(r[5] for r in rows),6)}")
flips = 0
samples = []
for i in range(1, len(rows)):
a, b = rows[i-1], rows[i]
if a[1] == b[1]:
continue
flips += 1
ed = math.dist(a[2], b[2]); pd = math.dist(a[3], b[3])
rd = math.dist(a[4], b[4]); yd = abs(b[5]-a[5])
if len(samples) < 18:
samples.append(f"{b[0]} {a[1]}->{b[1]:<2} eye={ed*1000:7.3f}mm rend={pd*1e6:8.1f}um raw={rd*1e6:8.1f}um yaw={yd*1000:8.4f}mrad")
print(f"flood flips in window: {flips}")
for s in samples:
print(" ", s)

View file

@ -1,93 +0,0 @@
# acdream application icons
Two marks, one family.
| Mark | Files | Used by |
|---|---|---|
| **Client** — the mosswart head | `acdream-client-*.png`, `acdream-client.ico` | `AcDream.App` (PE icon + runtime window icon) |
| **Launcher** — the ring and crescent | `acdream-launcher-*.png`, `acdream-launcher.ico` | `AcDream.Launcher` (PE icon + Avalonia `Window.Icon`) |
Each ships PNGs at 16/24/32/48/64/128/256/512/1024 plus a multi-size `.ico`
carrying 16 through 256.
## Where the art comes from
**The client mark is the retail mosswart**, not a drawing of one. It is the
actual creature head — `Setup 0x02000B4F` part 14, skin atlas `0x05001E11`,
`ClothingBase 0x10000344` — pulled from `client_portal.dat`, smoothed, lit and
graded. Palette values throughout both marks are sampled from that texture:
| | |
|---|---|
| `#ACB820` | chartreuse upper skin |
| `#A09800` | mustard belly — the "foul yellow" the lore names |
| `#485010` | deep olive shadow |
| `#F2ECD2` | tusk bone |
| `#AC7438` | ear membrane / hide |
**The launcher mark is inspired by the Asheron's Call sigil** — a forged ring
enclosing a hooked crescent — rebuilt from measurements of the retail wordmark
and the `acclient.exe` icon resource. It is an original construction in the
same visual language, not a copy of the logo. Its warm field matches the retail
client icon's dark-to-gold interior.
> **Note on rights.** "Asheron's Call" and its logo are trademarks of their
> owners, and the client mark is rendered from copyrighted game art. Unlike DAT
> content — which stays on the user's own disk — these icons are compiled into
> the shipped binaries. If acdream is ever distributed broadly, both marks
> should be reviewed, and the client mark is the one most likely to want an
> original redraw using these renders as reference.
## Regenerating
The launcher mark is fully procedural and rebuilds anywhere:
```bash
py tools/IconForge/forge.py launcher
```
That is byte-for-byte deterministic — it reproduces the committed PNGs exactly,
so an accidental edit is visible as a diff.
The client mark renders real game geometry, so it needs the installed DATs.
One command extracts both halves — the posed geometry and the surfaces it
references — into `tools/IconForge/work/`:
```bash
dotnet run --project tools/MosswartArt -- 0x02000B4F 0x10000344 tools/IconForge/work/mosswart_mesh.json 0x09000009
```
The trailing MotionTable id is required. Creatures do not define an upright pose
in `Setup.PlacementFrames`; without it every part stacks on the origin.
Then:
```bash
py tools/IconForge/forge.py client
```
This is deterministic too — given the same DATs it reproduces the committed
PNGs byte-for-byte.
Requires Python with `numpy`, `pillow` and `scipy`.
## How they are wired in
Neither icon is loaded from disk at runtime.
- **PE icon**`<ApplicationIcon>` in each `.csproj`, pointing at the `.ico`
here. This is what Explorer and the taskbar shortcut show.
- **Client window icon**`AcDream.App.Rendering.WindowIconLoader` hands GLFW
four sizes **from the `Load` callback**. That timing is load-bearing: Silk's
`Window.Create` only builds the managed object, and `IWindow.Initialize` is
what creates the native window, so applying an icon any earlier throws
"Window should be initialized". The failure is quiet and misleading — GLFW
falls back to the stock Windows application icon rather than the
executable's, so Explorer shows the mark and the running window does not.
The PNGs are *embedded resources* linked from this directory, so there is one
source of truth for the art and no missing-file case at runtime.
`WindowIconLoaderTests` guards both the resource names, which are otherwise
coupled to `LogicalName` in the csproj by string only, and the call-site
ordering.
- **Launcher window icon**`AvaloniaResource` linked from here, referenced as
`avares://acdream-launcher/Assets/acdream-launcher.png`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,132 +0,0 @@
# acdream documentation map
This page is the entry point for project documentation. It distinguishes
current sources of truth from implementation history so an old plan or issue
banner cannot silently override the current program state.
## Current snapshot — 2026-07-27
- **Milestone state:** M3, “Cast a spell,” landed 2026-07-21. M4, “Live in the
world,” is active.
- **M4 gameplay program:** resume the pre-M4
[world-interaction completion program](plans/2026-07-23-world-interaction-completion.md).
Favorite-spell overflow, status Use/Assess, and the complete assessment
surface are user-accepted. Equipped-child picking and vendor
browse/transactions remain Slices 46.
- **Structural/runtime state:** all eight `GameWindow` decomposition slices,
Modern Runtime Slices AJ, and the connected visual/lifecycle gates are
complete. `GameWindow` is a 1,622-line composition/callback shell.
`AcDream.Runtime.GameRuntime` owns canonical session, entity/object,
gameplay, movement, physics, projectile, environment, and portal state;
graphical and no-window hosts borrow the same owner graph.
- **Headless state:** Slice K is complete. `AcDream.Headless` is a
presentation-free Windows/Linux host with deterministic commands/events,
shared immutable content, multi-session isolation, reconnect, resource
telemetry, and 1/5/10/30-session gates. The final two-account native-Linux
soak completed ten minutes, logged out through ACE, and converged every
ownership ledger.
- **Linux graphical state:** Slice L0 and the L1 implementation checkpoint are
complete at `66f114b2` and `11501d52`. Native Windows passes the active
modern-GL/audio/window smoke. WSLg X11/Wayland correctly reject their
missing `GL_ARB_bindless_texture`. Physical-Linux validation and L2L6 are
explicitly deferred; resume at the supported AMD/NVIDIA L1 gate.
- **Completed gameplay gates:** R6 locomotion/collision/projectile/teleport/
radar, two-client portal-out/materialization, indoor prepared collision,
loot ordering, local/remote ground drops, and selection-marker lifetime.
- **Separate visual verification:** issue `#225`, the shared-alpha
lifestone/particle result; its connected resource-lifetime and performance
routes pass.
- **Carried behaviour debt:** issue `#153` (far teleport onto an unstreamed
edge), `#116` (narrowed slide response), `#235` (capped/RDP jump cadence),
and the active temporary-stopgap rows in the divergence register.
- **Divergence audit:** 189 active rows — IA 18, AD 38, AP 91, TS 38, and
UN 4 — plus retained struck/retired historical rows such as TS-37.
- **Latest automated baseline:** the Release build succeeds with the 17
test-project warnings tracked by issue `#228`; 8,826 tests pass and five are
intentionally skipped. App passes 3,763 / 3 skips. The L1 Windows supported
smoke and WSLg X11/Wayland negative-capability reports all end with zero
window/GL/input/audio ownership.
## Sources of truth
Read these in this order when deciding what to do next:
1. [`plans/2026-05-12-milestones.md`](plans/2026-05-12-milestones.md) — the
active playable outcome, freeze boundaries, and visual gates.
2. [`plans/2026-04-11-roadmap.md`](plans/2026-04-11-roadmap.md) — strategic
phase ledger: shipped, active, deferred, and future work.
3. [`ISSUES.md`](ISSUES.md) — tactical defects and small follow-ups. The status
inside an issue is authoritative; physical order is not.
4. [`architecture/retail-divergence-register.md`](architecture/retail-divergence-register.md)
— every known place runtime behavior can differ from retail.
5. [`architecture/acdream-architecture.md`](architecture/acdream-architecture.md)
and [`architecture/code-structure.md`](architecture/code-structure.md) —
ownership, dependency, update-thread, and extraction rules.
6. [`architecture/worldbuilder-inventory.md`](architecture/worldbuilder-inventory.md)
— rendering/DAT code already owned in-tree versus mechanisms still ours to
port.
If these disagree, milestones control the current outcome, the roadmap controls
work ordering, the issue status controls the individual defect, and the
architecture documents control implementation shape. Reconcile the stale
document in the same change; do not leave both claims standing.
## Research and implementation records
- [`research/named-retail/`](research/named-retail/) is the primary retail
oracle: named pseudo-C, headers, symbols, and types from the Sept 2013 build.
- [`research/decompiled/`](research/decompiled/) is the older Ghidra fallback.
- [`research/`](research/) contains focused pseudocode, traces, fixtures, and
gate reports. A dated research note records evidence; it does not become a
new roadmap.
- [`superpowers/specs/`](superpowers/specs/) and
[`superpowers/plans/`](superpowers/plans/) are per-slice design and execution
records. Completed plans remain historical.
- [`ci-and-releases.md`](ci-and-releases.md) is the SSOT for the Gitea CI
pipeline, the self-hosted runners, and how alpha releases are published.
Load-sensitive tests live in `Lane=Timing`; see
[`release-gate.md`](release-gate.md) before adding to it.
- [`launch-options.md`](launch-options.md) is the SSOT for every environment
variable and command-line argument the client reads, including what each one
changes about the run beyond its obvious effect. Read the side-effects column
before trusting any measurement. Enforced by
`LaunchOptionsDocumentationTests`: a flag without a row fails the build, and
so does a row whose read site was deleted.
- [`audit/`](audit/) contains completion and conformance audits.
- [`reference/ace-commands.md`](reference/ace-commands.md) preserves the local
ACE server's complete in-game command catalog and points to the authoritative
per-command help surface.
## Durable memory
- [`../claude-memory/MEMORY.md`](../claude-memory/MEMORY.md) indexes the live
subsystem memories and the render/physics digests. Read a domain digest
before changing that subsystem, especially its DO-NOT-RETRY table.
- [`../memory/`](../memory/) contains stable engineering references such as the
modern rendering pipeline, two-tier streaming, and toolchain notes.
Memory accelerates recall; it does not outrank the canonical documents above.
When current truth changes, update the relevant canonical document and distill
only the durable lesson into memory.
## Historical and deprecated documents
- [`bugs.md`](bugs.md) is the April 2026 bug snapshot. It is preserved for
archaeology and is not an active ledger.
- Dated plans and specs describe the decision at that time. Their completion
wording is historical unless the current milestone/roadmap explicitly links
the item as active.
- Old `R1→R8` architecture sequencing is superseded. Current execution comes
from the milestones and strategic roadmap.
## Documentation maintenance rules
- Update milestone, roadmap, issue, divergence, architecture, and memory claims
in the same commit when a shipped change affects them.
- Keep one issue ID per defect. Narrow an issue in place; do not reuse another
issue's number as a shorthand.
- Mark automated, connected, and visual gates separately. An automated pass is
not a visual acceptance, and an RDP throughput sample is not a local-display
visual comparison.
- Preserve research history, but remove stale “current/next” claims from living
documents once the state advances.

File diff suppressed because it is too large Load diff

View file

@ -1,854 +0,0 @@
# acdream — code structure & extraction sequence
**Status:** Living document. Created 2026-05-16; implementation reconciliation
completed 2026-07-21; Slices 18 and their automated closeout landed by
2026-07-22. The connected visual matrix passed 2026-07-23; the campaign is
complete and new M4 subsystems may enter through the extracted owners.
**Purpose:** Describe the desired structural state of the App layer,
explain the rules we've adopted, and lay out the safe extraction
sequence from today's reality (one 15,723-line `GameWindow.cs` at the
2026-07-21 audit) to the
target (thin `GameWindow`, small focused collaborators).
**Companion to:** [`acdream-architecture.md`](acdream-architecture.md)
(the layered architecture) and
[`worldbuilder-inventory.md`](worldbuilder-inventory.md) (what we take
from WB vs port ourselves).
---
## 1. The structural problem we're solving
The layered architecture works: `AcDream.Core` is backend-free, the network
layer is wire-compatible, the UI has a stable contract, plugins load.
The structural debt is concentrated in **one file**:
```
baseline cf50ee3d 15,723 lines / 278 fields / 205 methods
after Slice 1 14,912 lines / 278 fields / 191 methods
after Slice 2 14,546 lines / 277 fields / 190 methods
after Slice 3 14,310 lines / 274 fields / 190 methods
after Slice 4 10,301 lines / 267 fields / 163 methods
after Slice 5 closeout 8,811 lines / 247 fields / 153 methods
after Slice 6 closeout 7,026 lines / 241 fields / 108 methods
after Slice 7 draw-frame cutover 4,666 lines / 196 fields / 70 methods
after Slice 8 checkpoint D 4,330 lines / 192 fields / 67 methods
after Slice 8 checkpoint E 4,266 lines / 194 fields / 65 methods
after Slice 8 checkpoint F 4,057 lines / 198 fields / 54 methods
after Slice 8 checkpoint G 3,663 lines / 162 fields / 37 methods
after Slice 8 checkpoint I 1,945 lines / startup composition shell
after Slice 8 checkpoint J 1,625 lines / focused lifetime shell
after Slice 8 checkpoint K 1,622 lines / canonical soak shell
```
At the campaign baseline, `GameWindow` was the single object that:
- Owns the Vulkan device, the window, input, and shaders.
- Reads ~40 different environment variables across its lifetime.
- Composes the shipped `LiveSessionController`/host/router boundary; it no
longer owns a parallel session, command bus, subscription lifetime, connect,
character-entry, reconnect, or graceful-close body.
- Owns the adapters that hydrate canonical Runtime entity records into
exact-key App projection sidecars and their animation, collision, rendering,
and DAT-backed appearance resources.
- Composes the shipped `WorldSelectionQuery` and
`SelectionInteractionController`; it no longer owns world-picking,
selection intent, Use/PickUp, or auto-walk deferral algorithms.
- Drives per-frame render orchestration (sky → terrain → opaque mesh →
transparent mesh → particles → debug lines → UI).
- Builds and applies streamed landblock presentation (DAT decode, scenery,
EnvCells, mesh publication, collision, and retirement) around the shipped
`StreamingController` and `GpuWorldState` owners.
- Wires up every plugin hook sink, every diagnostic, every panel.
The completed slices moved those bodies into focused, tested owners. The rule
that governed the campaign still applies: a collaborator is a completed
extraction only when it owns the state and behavior body; wrapping a
`GameWindow` method in a delegate preserves ordering but remains a partial
extraction.
The fix is **not** "rewrite `GameWindow` in one pass" — that is a
high-risk change. The fix is to **extract one
collaborator at a time**, verify behavior is unchanged, ship, and
move on. This document defines that sequence.
---
## 2. Code Structure Rules — the discipline
Recap of the rules from `CLAUDE.md` with the rationale:
### Rule 1: No new substantial feature bodies in `GameWindow.cs`
**Why:** New feature bodies in the native shell would reverse the ownership
campaign. Runtime work belongs in focused App owners, with only construction
and narrow callback handoffs retained here.
**How to apply:** A new feature gets its own class under
`src/AcDream.App/<Subsystem>/` (or deeper in `AcDream.Core` if it's pure
logic). `GameWindow` owns a field and a wiring call, nothing more. If
you find yourself adding a 200-line method to `GameWindow`, stop and
extract.
**Exemption:** Trivial wiring that *must* stay in `GameWindow` because
it touches device state during `OnLoad` is acceptable, but should still
delegate to a collaborator for the substance.
### Rule 2: `AcDream.Core` must not depend on window / backend projects
**Why:** Core is the backend-free, testable layer. The moment Core imports
a graphics or windowing namespace, we've lost the ability to test it without
a device, and the layer split becomes fiction.
**How to apply:** Phase O removed both external WorldBuilder/backend project
references. The only currently allowed seams are the backend-free helpers owned in
our tree under `src/AcDream.Core/Rendering/Wb/`: `TerrainUtils`,
`TerrainEntry`, `RegionInfo`, `SceneryHelpers`, and `TextureHelpers`.
`ObjectMeshManager` and every GPU resource owner remain in App. If Core needs a
new capability, define a narrow Core interface and implement it in App; adding
a new project reference requires an inventory-doc update explaining why.
### Rule 3: UI panels target `AcDream.UI.Abstractions` only
**Why:** The rule was written to keep D.2b (the retail-look backend)
viable while ImGui was still the developer stack, and **it paid off**: when
Campaign V slice V11 deleted ImGui on 2026-07-29, not one panel contract,
ViewModel or command had to change, because none of them had ever imported
`ImGuiNET`. `AcDream.UI.Abstractions` survived a backend deletion intact.
**How to apply:** A panel's `using` block must mention
`AcDream.UI.Abstractions.*` and nothing from any backend assembly. The panel
writes against `IPanelRenderer`; a renderer implementation translates those
calls at runtime. Plugin-facing UI follows the same rule.
The shared chat parser/router/catalog and its four command intents live in
`AcDream.Runtime/Chat`, not in a panel or App. `AcDream.UI.Abstractions`
references Runtime so retained `ChatVM` can implement the narrow
`IChatCommandFeedback` seam and its existing panel input can call the shared
router. That dependency does not permit panels to import App, windowing,
rendering, audio, or another presentation backend; Runtime itself remains
presentation-independent and its dependency guards enforce that boundary.
**Status:** there is currently no `IPanelRenderer` implementation in the tree —
the ImGui one went with V11 and the replacement is issue **#258**. The contract
is kept rather than deleted precisely because this rule proved its worth; a new
host binds to it without touching a single panel.
### Rule 4: Startup env vars enter through `RuntimeOptions`
**Why:** Environment variables are global mutable state. Reading them
at random call sites means (a) duplicated `Environment.GetEnvironmentVariable`
boilerplate, (b) no single place to see "what flags does the client
respond to?", (c) impossible to unit-test parsing.
**How to apply:** `src/AcDream.App/RuntimeOptions.cs` is the typed
options object. `Program.cs` builds it once from args + env and passes
it to `GameWindow`. New startup flags add a field to `RuntimeOptions`
and a parser in `RuntimeOptions.FromEnvironment`. They don't add
`Environment.GetEnvironmentVariable` reads.
**Scope:** `RuntimeOptions` is for **startup-time** configuration —
things that don't change once the window is up. Runtime diagnostic
toggles are Rule 5's domain.
### Rule 5: Runtime diagnostic toggles live in diagnostic owner classes
**Why:** Diagnostic flags (`ACDREAM_DUMP_MOTION`, `ACDREAM_PROBE_*`,
etc.) need to be both env-readable at startup *and* runtime-toggleable
from the DebugPanel. Per-call-site env reads can't be runtime-toggled.
**How to apply:** Today's template is
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs` — one static class with
typed `Probe*` properties read from env vars once at startup, plus
runtime setters that the DebugPanel binds. New diagnostic flags follow
this shape, not the per-call-site pattern that dominates `GameWindow.cs`.
**Cleanup direction:** The dozens of existing `ACDREAM_DUMP_*` reads
inside `GameWindow.cs` are tech debt. We do NOT bulk-migrate them as
part of this refactor — they're working, they're scattered, and
moving them carries risk without a current acceptor. We migrate them
opportunistically: when a `GameWindow` extraction lands and a diagnostic
moves with it, route it through the new owner's diagnostic class.
### Rule 6: Tests live in the project matching the layer
**Why:** Test discoverability + dependency hygiene. A test for a Core
class belongs next to other Core tests; a test for an App class belongs
in an App test project. Co-locating tests across layers makes the
dependency graph dishonest.
**How to apply:** One test project per source project that has tests.
Today:
- `tests/AcDream.Core.Tests/``src/AcDream.Core/`
- `tests/AcDream.Core.Net.Tests/``src/AcDream.Core.Net/`
- `tests/AcDream.Runtime.Tests/``src/AcDream.Runtime/`
- `tests/AcDream.UI.Abstractions.Tests/``src/AcDream.UI.Abstractions/`
- `tests/AcDream.App.Tests/``src/AcDream.App/`
`tests/AcDream.App.Tests/` now exists and owns App-layer controller, streaming,
render-resource lifetime, retained-UI, and `RuntimeOptions` tests. New App tests
belong there; do not place backend-free Core behavior in that project merely because
App currently wires it.
---
### Render-frame ownership versus recursive reachability
The typed `RenderFrameOrchestrator` graph owns frame sequencing and borrows
long-lived runtime/presentation owners. The enforceable ownership invariant is
that the orchestrator and its immediate phase owners retain neither
`GameWindow`, an anonymous callback bag, nor direct window delegates. It is
**not** an invariant that recursively walking every borrowed canonical owner
can never reach an event subscriber or callback that was composed by
`GameWindow`; that claim would be false and would confuse object-graph
reachability with frame ownership.
Known recursive callback-bearing paths include (this is evidence and design
documentation, not an exhaustive allowlist):
- `RetainedGameplayUiFrame -> RetailUiRuntime -> RetailUiRuntimeBindings` for
retained gameplay UI state and commands.
- `PrivatePresentationRenderer -> PaperdollFramePresenter ->
RetailPaperdollFrameView/PaperdollInventoryVisibility -> UiElement tree` for
private paperdoll visibility and texture publication. This is a second,
independent retained-UI entry because `UiElement.Parent` links back to the
shared root and sibling controls.
- `DevToolsFramePresenter -> DevToolsPanelSet -> panel/ViewModel bindings` for
the optional developer UI.
- `WorldRenderFrameBuilder -> RuntimeWorldFrameSettingsPreview ->
IRuntimeSettingsPreviewSource -> RuntimeSettingsController` for the settings
snapshot read before world drawing. (The `SettingsVM` draft-preview tail of
this seam was retired at Campaign OP slice OP9 — the preview source now
mirrors the committed Display/Audio snapshot directly; the retail Options
panel applies its edits live through `SaveDisplay`/`SaveAudio` instead of a
draft layer.)
- `LocalPlayerPortalViewport -> LocalPlayerTeleportController ->
GameplayInputFrameController -> InputDispatcher.Fired -> GameWindow` for the
canonical portal/input lifetime and the host's input-action subscription.
These are presentation state/command seams, not alternative owners of the
window or frame transaction. The screenshot path is deliberately not on this
list: viewport width/height now travel in `RenderFrameInput`, so
`FrameScreenshotController` no longer stores a window-size callback. New
immediate phase owners must still satisfy the direct ownership invariant;
recursive shared-owner paths are reviewed against their canonical subsystem
lifetime rather than an impossible global no-callback assertion.
---
## 3. Target structure of the App layer
The end state — not what we're shipping in one pass, but the shape
we're aiming at.
```
src/AcDream.App/
├── Program.cs # parse args + env → RuntimeOptions, build GameWindow
├── RuntimeOptions.cs # typed startup options (Rule 4)
├── Rendering/
│ ├── GameWindow.cs # thin: device/window lifecycle + delegates per-frame to RenderFrameOrchestrator
│ ├── RenderFrameOrchestrator.cs # GPU-flight boundary + typed world/private/UI render phases
│ ├── LiveEntityAnimationScheduler.cs # shipped: ordinary live-object update workset
│ ├── LiveEntityAnimationPresenter.cs # final part-pose/mesh/effect composition after scheduler output
│ ├── RetailStaticAnimatingObjectScheduler.cs # shipped: separate static-animation workset
│ ├── StaticLiveRootCommitter.cs # live static root → pose + collision boundary
│ ├── TerrainModernRenderer.cs # (already exists)
│ ├── TextureCache.cs # (already exists)
│ ├── ParticleRenderer.cs # (already exists)
│ ├── Sky/ # (already exists)
│ ├── Wb/ # WB seam + EnvCellLandblockBuild transaction
│ └── Vfx/ # (already exists)
├── Net/
│ ├── LiveSessionController.cs # owns complete WorldSession connect/enter/logout/reconnect lifecycle
│ ├── LiveSessionHost.cs # App reset/selection/entry/route-factory composition over the canonical controller
│ ├── LiveSessionLifecycleHost.cs # exact borrowed-session attachment guard
│ ├── RetailSkillFormula.cs # named-retail unsigned skill-credit calculation + DAT lookup
│ └── LiveSessionEventRouter.cs # typed subscriptions → focused domain handlers
├── Physics/
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
│ ├── RemotePhysicsUpdater.cs # ordinary/Hidden remote narrow-tick integration
│ ├── LiveEntityOrdinaryPhysicsUpdater.cs # manager-less canonical body Transition path
│ ├── LiveEntityNetworkUpdateController.cs # Position/Vector/State/Motion App integration
│ ├── LiveEntityInboundAuthorityGate.cs # exact per-channel timestamp/version admission
│ ├── LiveEntityMotionRuntimeController.cs # shared host/setup/MoveTo/Sticky runtime policy
│ ├── DeferredLiveEntityMotionRuntimeBindings.cs # fail-fast construction-order bridge
│ ├── LiveEntityShadowPublisher.cs # authoritative exact-owner/residency collision gate
│ ├── RemoteInboundMotionDispatcher.cs # shared animated/headless UpdateMotion funnel
│ └── RemoteTeleportHook.cs # ordered retail teleport teardown actions (teleport_hook port; C4 4b-3 deleted RemoteTeleportController/RemoteTeleportPlacement/RemoteShadowPlacementSynchronizer — the teleport arm now routes through RuntimeRemotePlacementDriveController, the same canonical placement owner the far snap uses)
├── World/
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
│ ├── LiveEntityHydrationController.cs # spawn/appearance/parent/delete resource integration
│ ├── RetailInboundEventDispatcher.cs # update-thread packet/frame FIFO barrier
│ ├── RetailLiveFrameCoordinator.cs # shipped: object/network/command/reconcile phase order
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
│ ├── WorldEnvironmentController.cs # one-shot clock/DAT-sky/day/weather/AdminEnvirons owner
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
├── Interaction/
│ ├── WorldSelectionQuery.cs # read-only picking/classification/description queries
│ └── SelectionInteractionController.cs # owns selection intents, Use/PickUp, auto-walk deferral
├── Streaming/
│ ├── LandblockPresentationPipeline.cs # DAT build/apply/retire transaction and resource publication
│ ├── StreamingController.cs # typed frame scheduler + reveal destination reservation
│ ├── WorldRevealCoordinator.cs # graphical reveal projection and exact host receipts
│ └── ... # shipped streamer/world-state owners
├── Input/ # (already exists)
├── Audio/ # (already exists)
└── Plugins/ # (already exists)
```
What `GameWindow` keeps:
- `IWindow` / device / `IInputContext` lifecycle (constructor + `OnLoad` +
`Run` + `OnClosing`).
- `RuntimeOptions` reference (the typed startup config).
- GPU resource construction and top-level collaborator composition. Construction
is allowed here; feature algorithms and mutable subsystem state are not.
- One field per top-level collaborator (`_liveSessionController`,
`_liveEntityRuntime`, `_selectionInteraction`, `_streamingPresentation`,
`_liveObjectFrame`, `_renderFrameOrchestrator`).
- The Silk.NET event-handler stubs that delegate to collaborators.
What `GameWindow` loses:
- Live connect/enter/logout and event-subscription bodies → completed inside
`LiveSessionController` / `LiveSessionEventRouter`.
- Live-object hydration adapters and final animated-part presentation → focused
world/render owners over exact-key App projection sidecars backed by
canonical Runtime records.
- `WorldPicker`, target queries, and selection-driven Use/PickUp/auto-walk →
`WorldSelectionQuery` + `SelectionInteractionController`. Core
`SelectionState` remains the injected session owner.
- Landblock DAT build/apply/retirement presentation →
`LandblockPresentationPipeline`; `StreamingController` remains the residency
scheduler and `GpuWorldState` remains the spatial registry.
- Per-frame draw orchestration and its frame-local scratch state →
`RenderFrameOrchestrator`.
The eventual `GameEntity` aggregation (target state described in
`acdream-architecture.md` §"GameEntity: The Unified Entity") happens
**after** `RuntimeEntityDirectory` is the single owner of canonical entity
state. That boundary is now shipped. App's former parallel presentation
dictionaries are bounded by one exact-key projection store rather than spread
across `GameWindow`.
`RuntimeEntityDirectory` owns the only active server-GUID/incarnation map,
Runtime local-ID allocation/reverse lookup, accepted immutable CreateObject
snapshot and nine-channel timestamp gates, parent relations, operation
versions, and exact teardown tombstones. `RuntimeEntityRecord` is
presentation-free. `LiveEntityRuntime` is the App projection/lifecycle host:
it creates a `LiveEntityRecord` sidecar only after Runtime local-ID claim and
stores it in `LiveEntityProjectionStore` by exact `RuntimeEntityKey`.
Hydration, animation, effects, lights, equipped children, render identity,
movement observations, liveness, teardown, and spatial worksets borrow that
same exact identity. `LiveEntityPresentationController` projects Runtime state
transitions into draw, collision, effects, child-NoDraw, and target visibility
without becoming a second canonical owner.
`Rendering/Vfx/EntityEffectController` owns the focused mixed F754/F755 pending
FIFO, effect profiles, typed-table resolution, and readiness markers. A GUID
is retained there only as pending wire-delivery metadata; current-incarnation
resolution always asks `RuntimeEntityDirectory` and then the exact projection
store. `EntityScriptActivator` uses the same Runtime-issued `WorldEntity.Id` as
rendering and physics; disjoint static ID allocators fail fast instead of
wrapping into another landblock's namespace. All other non-Parent pre-create
packet families still need the future general queue tracked by divergence
AD-32.
The per-frame object scheduler is extracted, but final animated-part
presentation is not. `LiveEntityAnimationScheduler` snapshots canonical spatial
root records and advances the incarnation-stable object clock, PartArray,
hooks, one selected movement owner, and manager tail in retail order. The
remaining `GameWindow.TickAnimations` loop still composes part transforms,
drawable `MeshRef`s, and effect poses from the scheduler output; moving that
body into `LiveEntityAnimationPresenter` is an explicit pending slice.
Manager-less
bodies delegate their candidate/Transition/cell/shadow commit to
`LiveEntityOrdinaryPhysicsUpdater`; retained projectile bodies and remote
MovementManagers remain mutually exclusive movement owners. Static animation
is deliberately separate: `RetailStaticAnimatingObjectScheduler` owns the
`CPhysics::static_animating_objects` workset for DAT and live PhysicsState-
Static owners with Setup DefaultAnimation. Both schedulers are wired by
`GameWindow`, but neither owns GUID identity or logical resources.
The typed animation view fills its reusable snapshot and render-ID set from
the exact-key projection workset, avoiding interface-enumerator boxing on both
update and render hot paths.
`RemoteInboundMotionDispatcher` similarly keeps UpdateMotion protocol behavior
outside `GameWindow`: GameWindow resolves the canonical record/body and the
optional PartArray sink, while one dispatcher owns retail's interrupt, style,
MoveTo/type-0, sticky, and standing-long-jump order. Static root projection is
bounded by `StaticLiveRootCommitter`, which synchronizes changed roots to
effects and collision without rebuilding zero-omega shadows or resurrecting a
Hidden/withdrawn registration.
Synchronous network and lifecycle callbacks are bounded by
`RetailInboundEventDispatcher`. It owns no wire state and no identity; it only
serializes nested live-object operations until the current packet or full
object-frame tail completes. State-bearing direct dispatch is allocation-free;
only a genuinely nested operation allocates its retained queue wrapper.
`LiveEntityRecord` then supplies exact-incarnation
and per-channel authority versions at callback boundaries. Position, State,
Vector, and Movement remain independent, while a separate velocity version
invalidates only an older operation that would overwrite a newer velocity
installed by Position, Vector, or Movement. This prevents re-entrant App
observers from creating call-stack ordering that retail's update-thread packet
FIFO cannot produce.
Pose-dependent hook deferral is similarly incarnation-scoped rather than GUID-
or local-ID-scoped. `EntityEffectPoseRegistry` publishes a monotonic pose-owner
lifetime, and `AnimationHookFrameQueue` captures it before semantic callbacks
and rechecks it before each semantic AnimationDone and each routed hook. Static animation retains `process_hooks`
until its root, live parts, and children are published; withdrawal invalidates
both its prepared pose and pending hook tail.
Resolved ordinary motion commits its body/root/contact state before the
canonical full-cell setter enters `LiveEntityRuntime.RebucketLiveEntity`.
Because that setter may synchronously move the projection to pending or replace
the GUID, both `RemotePhysicsUpdater` and `LiveEntityOrdinaryPhysicsUpdater`
revalidate the exact incarnation before collision or manager-tail publication.
Collision residency itself is projection-owned, not updater-owned:
`LiveEntityPresentationController` suspends retained non-projectile shadows on
every unavailable-projection edge, restores them on hydration, and reconciles
the pending-first case at `OnLiveEntityReady` after collision registration.
Local projection and both authoritative remote UpdatePosition tails commit the
complete root before rebucketing, then publish collision only through an
exact-record/spatial-residency gate. Remote reflood tracks translation,
sign-invariant complete orientation, and cell changes so an in-place turn or a
same-pose EnvCell crossing cannot leave offset Setup shapes stale.
The projectile controller retains its separate body/InWorld/shadow edge owner.
During login/portal quiescence, loaded projection residency and gameplay
visibility are separate. `LiveEntityRuntime` validates spatial ownership
through `GpuWorldState.IsLiveEntityProjectionResident`, allowing destination
renderer preparation to converge offscreen. World consumers continue through
availability-gated `IsLiveEntityVisible`, so drawing, collision, picking,
radar/status targeting, effects, and audio remain closed until reveal. This
distinction is part of the Slice E connected gate, not an alternate live-entity
lifetime.
**C4 route 4b-3 (2026-08-04) deleted `Physics/RemoteTeleportController` and
`RemoteTeleportPlacement` outright** (605 + 85 lines, plus
`RemoteShadowPlacementSynchronizer`, 49 lines). Remote teleport placement is
no longer a separate App-layer incarnation-scoped machine — it is the SAME
canonical `RuntimeRemotePlacementDriveController` route 4b-2's far snap
already uses, dispatched via `ApplyAcceptedRemoteTeleport`, which shares
`StoresAcceptedDestination`/`StoreAcceptedDestinationPose` unchanged.
`teleport_hook` (@0x00514ED0) still runs first — `RemoteTeleportHook`,
invoked from `LiveEntityNetworkUpdateController`'s teleport-arm dispatch —
and its `report_collision_end(this, 1)` action now routes through
`RuntimeCollisionReportingState.LeaveWorld` (the existing, exact port of
that retail call) rather than `ShadowObjectRegistry.Suspend`, which ports a
DIFFERENT retail function `teleport_hook` never calls. `GpuWorldState`
performs remove+place as one spatial rebucket,
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
delayed duplicates. A rollback inside an observer cannot race the outer
destination-visible notification or expose an intermediate false pulse.
`LiveEntityTeardown` executes those independent owner callbacks to completion
and aggregates failures afterwards, so a throwing effect/plugin sink cannot
strand teleport, movement, shadow, light, or GUID-scoped state.
---
## 4. Reconciled ownership ledger — 2026-07-21
This section replaces the original six-step sketch. That sketch correctly
identified the target but overstated some extractions and omitted large bodies
added during M2/M3. The current audit is based on implementation ownership, not
whether a class with the planned name exists.
### 4.1 What counts as extracted
An extraction is **complete** only when:
1. the collaborator owns the mutable state and behavior body;
2. `GameWindow` constructs it and delegates through a narrow method;
3. the collaborator does not call back into arbitrary `GameWindow` methods;
4. focused tests exercise the collaborator without constructing a Silk window;
5. the accepted connected/visual behavior is unchanged.
A class that stores delegates back to substantial `GameWindow` methods is a
useful ordering seam, but its ownership status is **partial**.
### 4.2 Current implementation truth
| Area | Status | Current truth |
|---|---|---|
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
| Network session | **Complete Runtime ownership** | `LiveSessionController` owns the sole `WorldSession` generation and resolve/create/Connect/pre-world selection/EnterWorld/Tick/stop/reconnect/disposal transaction. Its `RuntimeCharacterSelectionState` owns the full active roster (including greyed entries and retained wire slots), highlight, delete confirmation, restore/delete/error state, generation/lifecycle, borrowed view, ordered deltas, and typed commands. A selector-free graphical launch pauses on that owner; explicit and headless selection retain the established fallback. Runtime route owners preserve exact inbound/outbound ordering and retryable teardown. App supplies immutable options, graphical/domain callbacks, and borrowed projections—no mirrored session, selection state, or reset plan. |
| World environment | **J6.1 complete Runtime ownership** | `RuntimeWorldEnvironmentState` owns the instance-scoped Dereth calendar, synchronized clock, weather progression/state, selected day group, AdminEnvirons state, and typed debug overrides. App converts immutable DAT sky definitions once and projects the borrowed Runtime snapshot into rendering; no process-global Region origin or second App clock/weather owner remains (`902076c0`). TS-54/TS-55 register the remaining centered UI sound and full fog/ambient/radar behavior gaps. |
| Live identity/lifetime | **J3 complete** | `RuntimeEntityObjectLifetime` owns the sole `RuntimeEntityDirectory`, live `ClientObjectTable`, direct views, and ordered entity/object stream. The directory owns canonical GUID/incarnation/local-ID identity, accepted snapshots/timestamps, parent state, operation versions, and tombstones. `LiveEntityProjectionStore` owns App graphical sidecars by exact `RuntimeEntityKey`; hydration, presentation components, `GpuWorldState` residence/visibility, and retryable teardown preserve that key without another authority. Exact receipts precede fallible callbacks, re-entrant commits drain synchronously in sequence, and stable reset/disposal must converge the complete ledger to zero (`f46ddb5c`, `420e5eea`, `e937cc36`, `5ef8b537`, `ce3ac310`, `119b7c11`). |
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
| World reveal/transit | **J6 complete Runtime ownership** | `RuntimeWorldTransitState` owns the sole monotonic login/portal reveal generation, wrap-safe F751 history, pending/active teleport sequence, both accepted F751/Position packet orders, exact destination, typed atomic readiness latch, generation/sequence/cell placement and materialization validation, simulation edge, viewport observation, completion, cancellation, retail wait cue, portal count, and exact typed graphical-host acknowledgement suffix. `WorldRevealCoordinator` and `LocalPlayerTeleportController` are graphical adapters over App readiness, generation-scoped resource receipts, render-space placement, tunnel, and UI; failure/re-entrancy retains the exact unacknowledged suffix. `WorldGenerationAvailabilityState` borrows Runtime truth and owns no mutable generation. The App transit coordinator and accepted-destination mirror are deleted (`a6860d55`, `acb845d8`, `6a063a27`, `18d17d8b`). |
| Retained gameplay UI | **Mostly complete feature ownership** | `RetailUiRuntime` and focused panel/controllers own layout and behavior. `GameWindow.OnLoad` still performs substantial service composition, which is allowed until the final composition cleanup. |
| Action/combat/magic/movement | **J5 complete** | Runtime `RuntimeActionState` owns the exact selection, temporary target mode, interaction transactions, combat attack/target/mode, and spell-cast intent children. Interaction owns use/appraisal/pickup identity, ordered FIFO, and exact post-arrival token while borrowing J4's sole busy gate. `RuntimeLocalPlayerMovementState` owns the exact local controller, construction seam, autorun latch, typed view, and outbound MTS/jump/AP cadence. Retained bars, physical input, world/content queries, transport, lighting, toasts, drag/drop, effects, and animation remain App adapters/presentation. One combined Runtime ledger spans entity/object, gameplay, physics, remote, and projectile ownership (`b298f99f`, `f5f7b417`, `20df9d15`, `aa3f4a60`, `cdee7a4b`). |
| Physics/remote simulation | **J5.5 complete** | `RuntimePhysicsState` owns one per-session engine, production cache/cell graph, transition scratch, shadow registry, typed collision admissions, canonical bodies/hosts/remotes, keyed ordinary/remote worksets, simulation, and cell commits. App streaming supplies immutable prepared collision; animation/shape adapters supply graphical inputs and project committed snapshots only (`7e6033d0`). |
| Projectile simulation | **J5.6 complete** | `RuntimeEntityRecord`/`RuntimePhysicsState` own the canonical projectile component, exact-key workset, prediction/correction state, unchanged retail quantum/integration/sweep/collision loop, and cell commits. App resolves immutable Setup/DAT shape input and projects committed render, shadow, and effect-pose results only (`2aee3356`). |
| Landblock presentation | **Complete** | `LandblockBuildFactory` owns the captured-origin DAT transaction; concrete render/physics/DAT-static publishers and `LandblockPresentationPipeline` own typed-meter publication and exact retryable retirement. `StreamingController` owns stable destination/control/unload/Near/Far queues and destination reservation. CPU mesh-cache restaging requires an exact live owner. `StreamingOriginRecenterCoordinator` serializes old-window retirement with teleport/session origin lifetimes. `GameWindow` retains construction and one pipeline field only (`c79d0a49`, closeout `4a205a3e`; Slice E closeout `91e82c3c`). |
| Render-frame orchestration | **Complete** | `RenderFrameOrchestrator` owns the GPU-flight, resource, world/PView/shared-alpha, private-presentation, diagnostics, screenshot, and recovery graph. `GameWindow.OnRender` takes one logical window-size snapshot and performs one immutable handoff (`9d7df1bf`). |
| Unified `GameEntity` | **Slice J complete** | Canonical identity, retained-object lifetime, direct views, ordered entity/object deltas, gameplay state, action/combat/magic/movement/physics/remote/projectile simulation, world environment, reveal/transit truth, and host acknowledgement share failure-safe Runtime owners while exact graphical sidecars stay in App. One `GameRuntime` composes the graph. `RuntimeGenerationReset` is the sole retryable canonical-generation reset for graphical and no-window hosts; the deterministic direct host proves lifecycle, commands, portal, reconnect, GUID reuse, fault recovery, isolation, and terminal convergence without presentation assemblies (`a9a822f2`). |
| Headless host | **Slice K complete** | `AcDream.Headless` references only Runtime and loads no App/UI/graphics/window/audio assembly. Portable paths/config/credentials, deterministic command/event scheduling, shared immutable content, exact per-session identity, failure quarantine, reconnect, resource telemetry, 1/5/10/30-session isolation, two-hour simulated endurance, and the exact two-account native-Linux connected soak pass through `776482da`. |
### 4.3 Revised extraction sequence
Every numbered slice is behavior-preserving and independently committed. A
slice does not include feature work or opportunistic gameplay fixes.
#### Gate 0 — deterministic baseline — COMPLETE
The Release suite, connected R6 soak, world-lifecycle screenshots/checkpoints,
graceful reconnect, local locomotion/collision/projectile/teleport gate, and
two-client portal observer gate form the pre-refactor baseline. Each later
slice runs the subset capable of detecting its risk; render/session slices run
the complete connected lifecycle gate.
#### Slice 1 — finish selection/interaction ownership — COMPLETE 2026-07-21
Detailed execution plan:
[`docs/plans/2026-07-21-gamewindow-slice-1-selection-interaction.md`](../plans/2026-07-21-gamewindow-slice-1-selection-interaction.md).
Split the old Step 4 into three reviewable commits:
1. `WorldSelectionQuery` receives camera/scene/live-object read seams and owns
picking, target classification, selection bounds, closest-target lookup,
names, and descriptions. It cannot mutate selection or send packets.
2. `SelectionInteractionController` owns selection intents, double-click Use,
Use/PickUp packet requests, range decisions, speculative facing, and the
pending auto-walk action. It composes the existing item/combat controllers
rather than duplicating their rules.
3. Selection/Use/PickUp/combat-target `InputAction` cases delegate to the new
controller; the old methods and fields are deleted from `GameWindow`.
Tests cover read-only query classification separately from stateful intents,
including direct use, distant auto-walk completion, corpse/container opening,
pickup placement, hostile-only targeting, Hidden objects, and same-GUID reuse.
The cutover landed in `047a4c83`, `52dbb574`, `e74f2ca9`, `fa8d5232`,
`d2bb5af4`, and review-correction commit `5acc3f01`. The final review also
ported the shared `ACCWeenieObject::prevRequest` transaction shape across
inventory, toolbar, paperdoll, external-container, give, split, merge, and
drop routes. Queued actions, pending placements, and responses are bound to
exact object incarnations/tokens; optimistic projection, rollback projection,
and authoritative response notices are distinct.
Measured against `cf50ee3d`, `GameWindow` fell from 15,723 to 14,912 lines and
from 205 to 191 methods; field count stayed at 278 because the slice exchanged
legacy state for composed owners. Three independent retail, architecture, and
adversarial review loops finished clean. Release build passed; 6,558 tests
passed and five fixture/conformance tests skipped intentionally.
#### Slice 2 — finish live animation presentation — COMPLETE 2026-07-21
Move `TickAnimations`, final rigid/visual part composition, effect-pose
publication, and motion-done binding into `LiveEntityAnimationPresenter`.
`LiveEntityAnimationScheduler` remains the time/movement owner; the presenter
only consumes one scheduler result and publishes the final draw/effect pose.
Move the surviving motion diagnostics with this owner instead of adding more
environment reads to `GameWindow`.
Tests pin legacy and sequencer paths, Hidden/static eligibility, ObjScale versus
rigid effect poses, part availability, same-frame hook pose publication, and
incarnation replacement. Run the R6 object-frame and projectile/effect suites
plus the connected locomotion/projectile gate.
Result: `LiveEntityAnimationPresenter` owns final visual/rigid part
composition, effect-pose publication, MotionDone binding, and typed diagnostics.
Schedules carry exact record/entity/component/epoch/projection/presentation
identity and own their authored frame prefix until the next scheduler tick.
Appearance and static-owner rebinding invalidate stale work. Named-retail
`CPartArray::UpdateParts` short-frame retention now preserves both visual and
rigid trailing poses. `GameWindow` lost 366 lines, two methods, and one net
field. Three independent retail, architecture, and adversarial review loops
finished clean; focused App/Core tests and the complete App suite pass.
#### Slice 3 — complete live-session ownership — COMPLETE 2026-07-21
Detailed execution plan:
[`docs/plans/2026-07-21-gamewindow-slice-3-live-session.md`](../plans/2026-07-21-gamewindow-slice-3-live-session.md).
`LiveSessionController` now owns the complete resolve/create/bind/Connect/
CharacterList/select/EnterWorld/Tick/stop/reconnect/dispose transaction. Exact
generation and operation-depth gates prevent synchronous lifecycle callbacks
from resurrecting a superseded session. Staged teardown makes commands inert,
detaches events, gracefully disposes the exact `WorldSession`, detaches the
host, and runs the full reset manifest to convergence before reuse.
`LiveSessionLifecycleHost` is the narrow App adapter; the router and command
owners are transactional and session-bound. `GameWindow` lost `_liveSession`,
the router/command fields, and the old startup/reset/wiring/disposal methods.
Both UI stacks resolve `controller.Commands`; direct feature sends borrow
`CurrentSession` at call time. Forty-seven controller cases plus host,
transport-allocation, and structural gates cover failure/reentrancy/reuse.
Three independent retail, architecture, and adversarial reviews finished
clean. The Release suite passes 6,714 tests with five intentional skips. The
306-second connected lifecycle gate passed capped login, five travel/revisit
checkpoints, exact graceful close, and uncapped fresh-process reconnect; both
client processes exited normally.
`GameWindow` fell another 236 lines and three fields to 14,310 lines / 274
fields / 190 methods. Slice 4 is next.
#### Slice 4 — extract live-entity App integration — COMPLETE 2026-07-21
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-4-live-entity-integration.md`](../plans/2026-07-21-gamewindow-slice-4-live-entity-integration.md).
This is two owners, not one replacement god object:
- `LiveEntityHydrationController` owns CreateObject/ObjDesc/Parent/Pickup/Delete
integration, DAT-backed appearance hydration, resource registration, and
exact teardown callbacks over `LiveEntityRuntime`.
- `LiveEntityNetworkUpdateController` owns App routing for accepted
Position/Vector/State/Movement updates into the existing remote physics,
motion, projectile, presentation, and teleport controllers.
Neither may own a GUID dictionary. Both resolve the current incarnation only
through `LiveEntityRuntime` and must preserve the inbound FIFO/authority-token
rules. Run the complete live-entity stress suite, R6 connected route, inventory
equip/parenting, death/corpse, and portal gates.
Checkpoints A and B are complete. `d68c83d1`, `5882b308`, and `fcb66198`
established the shared origin, lifecycle, remote-motion, and exact-record
physics-host seams. `69a2ca0c` extracted appearance/collision/default-pose and
projection-withdrawal mechanics, including exact recursive `leave_world`
subtree handling and retained retry ownership. Three independent reviews were
clean and the Release suite passes 6,799 tests with five intentional skips.
Checkpoints C and D are complete. `d10c5f2d` moved CreateObject, first
materialization, origin bootstrap, and landblock recovery into
`LiveEntityHydrationController`; `fe551496` moved ObjDesc, Parent, Pickup,
child-ready, and exact leave-world/re-entry routing. Appearance and attached
subtree recovery now use independent authority transactions and preserve the
same logical/runtime identities. Checkpoint E is complete in `f38822c4`:
Delete, visibility prune, retryable exact-incarnation component cleanup, and
derived remote stop-observation lifetime now have focused owners. Checkpoint F
completed in `aa90c646`: accepted Position/Vector/State/Movement and the
equal-generation CreateObject tail now route through focused owners.
Independent authority/version captures survive reentrant callbacks and GUID
reuse; shared motion-runtime policy is one-way bound and no extracted owner
keeps a second identity dictionary. ForcePosition serializes and stamps one
exact canonical outbound Position. Three corrected-diff reviews, the complete
6,940-test Release suite, and the 310-second seven-destination connected R6
route all passed. `GameWindow` is 10,301 lines / 267 fields / 163 methods.
Slice 5 checkpoints AG are complete. The pipeline owns the serialized worker
build, concrete render/physics/static publication, and retryable Near/full
retirement. The direct cutover deleted the legacy apply/facade/scratch body and
added one retained shared-origin lifetime transaction for teleport, logout,
and fast relogin. `GameWindow.cs` was 8,793 raw lines at checkpoint G and no
longer owns landblock build/apply/retirement behavior. H then corrected native
shutdown ordering so live-session reset converges before the streamer and
other reset dependencies are disposed. The final measurement is 8,811 raw
lines / 247 fields / 153 methods. The complete Release suite (7,054 pass / 5
skip), the 311-second capped/reconnect lifecycle gate, and the synchronized
394-second nine-stop resource soak all pass (`4a205a3e`).
#### Slice 5 — extract landblock presentation — COMPLETE
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-5-landblock-presentation.md`](../plans/2026-07-21-gamewindow-slice-5-landblock-presentation.md)
(complete).
Create `LandblockPresentationPipeline` around the existing immutable
`LandblockBuild` transaction. It owns DAT build context, scenery/EnvCell
construction, mesh/collision/light publication, demotion/full-retirement, and
the single-reader DAT lock contract. `StreamingController` continues deciding
what is resident; `GpuWorldState` continues owning spatial buckets.
Tests pin loaded/pending/demoted/unloaded symmetry, stale build generations,
resource pin/release balance, collision footprints across landblock seams, and
first-login bootstrap replacement. The deterministic world-lifecycle gate and
nine-destination resource soak pass.
#### Slice 6 — extract update-frame orchestration — COMPLETE 2026-07-22
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`](../plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md)
(complete).
After the stateful bodies above have owners, make the update path a real
orchestrator instead of delegates back into the window. It preserves the
accepted production order—not an overclaimed retail host order—covering
teardown/clock/streaming convergence, input, the retail-shaped object → inbound
network → command barrier, conditional spatial reconciles, liveness, local
teleport, player-mode entry, and camera presentation. The exact twelve-phase
graph and registered TS-53 host-order adaptation live in the detailed ledger.
`GameWindow.OnUpdate` becomes a short time/input handoff.
Frame-order tests and the existing R6 gate must produce the same lifecycle and
movement traces before and after extraction.
The production cutover is complete in `e91f3102`. `GameWindow.OnUpdate` now
contains the profiler scope and one `UpdateFrameOrchestrator.Tick` handoff;
streaming, input, the object/network barrier, liveness, teleport, player-mode,
and camera phases have typed owners with no substantial callback facade into
the window. The complete Release suite passes 7,182 tests with five intentional
skips. The 314-second lifecycle/reconnect gate and 394-second synchronized
nine-stop resource soak both pass with graceful exits, stable live-owner
counts, and a 0.8 ms update-frame p95 at the final Caul checkpoint.
`GameWindow` is 7,026 raw lines / 241 fields / 108 methods: 8,697 lines (55.3%)
smaller than the campaign baseline. TS-50, TS-51, and TS-53 remain accurately
registered; this behavior-preserving extraction introduced no new divergence.
#### Slice 7 — extract `RenderFrameOrchestrator` — COMPLETE 2026-07-22
Detailed execution ledger:
[`docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md`](../plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md)
(complete).
Move the complete draw graph and its reusable frame-local scratch state into a
GPU-owning App collaborator. Preserve the exact modern pipeline order, clip
routing, PView flood, landscape/opaque/shared-alpha flush boundaries,
particles, debug draw, paperdoll, retained UI, and frame fences. Do not pass a
hundred individual delegates or let the orchestrator reach back into
`GameWindow`; inject a small immutable service set plus explicit per-frame
input.
Automated acceptance uses framebuffer artifacts and render/resource
checkpoints. Visual acceptance compares outdoor, building, dungeon, portal
exit, translucent lifestone/particles, and UI at the same camera positions.
The production draw cutover is complete. `GameWindow.OnRender` now performs one
value-only handoff to `RenderFrameOrchestrator`; focused resource, world,
private-presentation, and diagnostic owners contain the former frame body and
failure recovery. The Release suite passes 7,341 tests with five intentional
skips. `GameWindow` is 4,666 raw lines / 196 fields / 70 methods: 11,057 lines
(70.3%) smaller than the campaign baseline. The 315.6-second lifecycle and
395.2-second synchronized nine-stop soak pass with code-zero graceful exits;
the six stable framebuffer checkpoints preserve the Slice 6 presentation.
Issue #232 records the coarse process-residency gate's run-to-run variance so
future diagnostics can distinguish canonical owner growth from OS/driver/GC
residency without weakening leak detection.
#### Slice 8 — composition and shutdown cleanup — AUTOMATED COMPLETE
Detailed execution ledger:
[`docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](../plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md).
Keep device/window construction in `GameWindow.OnLoad`, but group creation into
small composition functions and delete feature state left behind by prior
slices. `OnClosing` delegates to the existing retryable shutdown transaction.
Silk callbacks become narrow calls into the input, update, render, resize,
focus, and shutdown owners.
Checkpoints E, F, G, and H are complete. `CameraPointerInputController` owns raw pointer,
camera cursor/focus/scroll/sensitivity behavior; `FramebufferResizeController`
alone publishes physical framebuffer changes. All Silk, dispatcher, retained
UI, and optional devtools device edges are reversible, transactional, and
terminal after disposal. Logical cutoff precedes live-session retirement;
physical detach follows it, so neither copied callbacks nor a failed event
remove can interfere with graceful transport teardown.
`GameplayInputActionRouter` is now the sole `InputDispatcher.Fired`
subscriber and preserves the accepted input-priority graph. Focused combat,
diagnostic, and general command owners replace the remaining window command
bodies. The retained-root item-drop edge has one reversible owner, while its
deferred toolbar targets become inert before session retirement. `GameWindow`
now has no settings-store construction, duplicate settings loads, persisted
settings mirrors, or display/quality feature bodies. `RuntimeSettingsController`
is constructed before `Window.Create`, owns the one immutable startup snapshot
and current/toon settings state, applies startup display/pacing/FOV/audio once,
then borrows late-bound runtime targets without replay. Saved FOV now applies at
startup even when devtools are disabled; this is a deliberate correction of the
old accidental SettingsVM gate. The controller also preserves unrelated unsaved
Gameplay draft fields when combat preferences change. `GameWindow` is now 3,663
raw lines / 162 fields / 37 methods at G. H adds sole lifetime roots for the
terrain atlas and dedicated sky shader, one retained Host/runtime lease, an
atomic update/render frame-root slot, and a prepare-aware portal fallback and
transfer slot. GPU construction and state mutations now use checked commit
boundaries with exact retry ownership for failed names, bindless residency,
and texture-binding restoration. `GameWindow` is 3,689 raw lines / 162 fields /
37 methods at H. Checkpoint I.1I.5 now provide the executable nine-phase
oracle, platform/host/content/settings phases, and the production world/render
phase. `WorldRenderCompositionPhase` owns Region/environment, the mandatory
modern-renderer foundation, immutable terrain-worker inputs, and WB/texture/
sampler construction; every Phase-4 GPU constructor has retryable prefix
ownership. `GameWindow` is 3,522 raw lines after I.5. I.6 moves interaction,
retained UI, live presentation, and landblock publication into ordered
composition phases. Typed exact-owner sources bridge later session,
selection, radar, view-plane, diagnostics, inventory, hydration, and automation
owners without callbacks to the window. The old item-use and selection wrapper
methods are deleted. I.7 now owns the complete streaming/session/hydration/
local-player/teleport construction phase and every late edge through named
exact-owner tokens; the former 432-line inline body and spawn-claim memo are
gone. I.8a moves the exact live-session reset/router graph, combat and
diagnostic command targets, and sole gameplay input subscriber into Phase 7
before frame publication. I.8b moves the complete update/render construction
body into `FrameRootCompositionPhase`, publishes the pair through an exact
lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
1,945 raw lines after I.9 corrects production to invoke the same executable
pipeline as the failure oracle, carries exact platform/settings results through
the typed contract, and keeps live-session start terminal. Checkpoint J then
moves the complete shutdown manifest into `GameWindowLifetime`: typed root
groups, retry/no-replay progress, structured reportable physical-detach
failures, hard session/GPU barriers, and native-window-last release now live
outside the host. The shell captures roots once and makes one lifetime call
from Closing/Dispose; no shutdown stage remains in it. `GameWindow` is 1,625
raw lines after J. Checkpoint K adds acknowledged same-frame canonical resource
checkpoints and leaves the shell at 1,622 lines. The App gate passes 3,451 tests
/ 3 intentional skips and the complete Release suite passes 7,823 / 5. Two
fresh-process nine-stop soaks and the connected lifecycle/reconnect route pass
with graceful exits; deterministic framebuffer captures preserve accepted
Slice 7 geometry, UI/paperdoll layering, private viewports, depth/alpha, and
reveal. The clean solution build retains only the 17 test-project warnings
tracked by #228, and all three final corrected-diff reviews are clean. The
user's connected visual matrix passed 2026-07-23, including first-login radar,
movement/combat, shared retained panels, recall/portal presentation, and
graceful reconnect.
### 4.4 Exit criteria
The campaign is complete when:
- `GameWindow` contains no AC gameplay algorithm, entity scan, packet builder,
DAT landblock builder, animation-part composer, or draw-graph body;
- `OnUpdate`, `OnRender`, `OnInputAction`, and live-session callbacks are short
delegation methods;
- every extracted owner has App-layer tests and symmetric teardown;
- the Release suite and connected lifecycle/resource gates stay green after
every slice;
- the final local visual matrix passes unchanged.
Line count is a progress signal, not the acceptance test. The expected result
is below roughly 5,000 lines, but ownership and dependency direction decide
completion. Full `GameEntity` type aggregation is evaluated only afterwards as
a separate migration; it is not folded into this campaign.
---
## 5. Rules of the road during the extraction
1. **One slice at a time.** Each commit ships one ownership boundary or
mechanical call-site cutover. Bundling slices makes failures hard to
isolate.
2. **Behavior preservation is the acceptance criterion.** Every slice
must build clean, all tests pass, and visual verification at the
appropriate accepted milestone scenarios must succeed. We're moving code, not
changing it.
3. **No new features during an extraction step.** If you spot a real
bug while extracting, file it in `docs/ISSUES.md` and address it in
a separate commit (before or after the extraction, not folded into
it).
4. **Diagnostic toggle migrations are opportunistic.** When a method
moves to a new owner, the diagnostic flag inside it can move to a
diagnostic class as part of the same commit. We do not do a bulk
diagnostic-cleanup pass.
5. **Update this document when the plan changes.** If a slice turns out
to need a different ownership shape than described above, update §4 in the
same session you discover the divergence.
6. **No façade-only completion claims.** A delegate from a new class back to a
substantial `GameWindow` method is a useful intermediate seam, not a
completed extraction.
7. **No duplicate ownership.** New collaborators query `LiveEntityRuntime`,
`GpuWorldState`, `SelectionState`, or the relevant existing owner. They do
not create replacement GUID, visibility, session, or resource maps.
---
## 6. What this document is **not**
- **Not a full rewrite plan.** The point is the *opposite* — small
steps, verified at each boundary.
- **Not a feature phase.** Following the 2026-07-21 user decision, this
behavior-preserving campaign is the structural prerequisite before new M4
subsystem bodies are added. Severe regressions remain fixable in separate
commits; ordinary feature work waits.
- **Not a substitute for the milestones / roadmap.** Those drive the
feature work. This drives the structural work that runs underneath.

File diff suppressed because one or more lines are too long

View file

@ -1,616 +0,0 @@
# WorldBuilder Inventory — what we extracted, adapted, or left behind
> **Phase O shipped 2026-05-21.** The ~33 WB files we actually use have
> been extracted into our tree. `references/WorldBuilder/` stays as a
> **read-reference only** — nothing in `src/AcDream.*` references it as a
> project dependency. `DatCollection` is now the only dat reader in process.
>
> Use this document to:
> 1. Know **where our extracted code lives** (look for the "Extracted to"
> column / notes in each section below).
> 2. Know **what WB still has** that we haven't needed yet — grep
> `references/WorldBuilder/` if you ever need to add something.
> 3. Know **what WB never had** (the 🔴 list) — those are always ours.
**Pre-O status (archived for context):** As of Phase N.4 (2026-05-08)
acdream relied heavily on WorldBuilder as a project reference for rendering
and dat-handling. WorldBuilder is MIT-licensed, verified by visual inspection
to render the AC world correctly (terrain, scenery, slabs, dungeons, slopes,
particles), and uses the same Silk.NET + .NET stack we target.
**Post-O integration model:** Extracted WB code lives in two locations in
our tree (see CLAUDE.md for the full breakdown):
- `src/AcDream.Core/Rendering/Wb/` — pure helpers (no GL): `TerrainUtils`,
`TerrainEntry`, `RegionInfo`, `SceneryHelpers`, `TextureHelpers`.
- `src/AcDream.App/Rendering/Wb/` — GL infrastructure + mesh pipeline:
`ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`, texture cache,
shader infra, EnvCell/portal/scenery/terrain-blending pipeline classes.
**EnvCell streaming seam:** `EnvCellLandblockBuildBuilder` is an acdream-owned
adapter around the extracted WB rendering path. One streaming job privately builds
the complete portal-cell + shell-placement payload, and `EnvCellRenderer.CommitLandblock`
publishes that completed snapshot on the render thread. WB's geometry-id arithmetic
and mesh preparation remain unchanged; the transaction wrapper exists because
acdream streams asynchronously while the WB editor's manager owned its own loading
loop. Do not reintroduce worker-side `RegisterCell` calls or shared pending cell
collections.
`DatCollectionAdapter` bridges the sole `DatCollection` to Content's
`IDatReaderWriter`. Since MP1c, production `ObjectMeshManager` no longer reads
DAT; the adapter remains the bounded typed-object access seam for runtime
non-render content plus explicit bake/equivalence tooling.
**MP1a (2026-07-05): CPU mesh-extraction half moved to `AcDream.Content`.**
The GL-free portion of the former `ObjectMeshManager` — dat read → polygon
walk → vertex/index build → inline BCn/palette texture decode →
`ObjectMeshData` — is now `MeshExtractor` in a new `src/AcDream.Content/`
assembly (no Silk.NET dependency), so the MP1b bake tool can run the exact
same extraction code offline without an OpenGL context. This was a
mechanical, verbatim move (namespace + visibility only) per
`docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md` — no
behavior change, no divergence-register row.
- `src/AcDream.Content/MeshExtractor.cs` — the `Prepare*` family
(`PrepareMeshData`, `PrepareSetupMeshData`, `PrepareGfxObjMeshData`,
`PrepareEnvCellMeshData`, `PrepareCellStructMeshData`,
`PrepareCellStructEdgeLineData`) + private helpers (`CollectParts`,
`CollectEmittersFromScript`, `ComputeBounds`, `BuildPolygonIndices`,
`BuildCellStructPolygonIndices`) and the decoded-texture cache /
`ThreadLocal<BcDecoder>` that back them.
- `src/AcDream.Content/ObjectMeshData.cs` — the CPU-side boundary records:
`VertexPositionNormalTexture`, `StagedEmitter`, `ObjectMeshData`,
`MeshBatchData`, `TextureBatchData`.
- `src/AcDream.Content/TextureKey.cs` — the atlas dedup key, lifted out of
the GL-owning `TextureAtlasManager` (which stays in App and now
references the lifted struct).
- `src/AcDream.Content/UploadFormats.cs` — Content-owned
`UploadPixelFormat`/`UploadPixelType` enums carried by
`MeshBatchData`/`TextureBatchData` instead of
`Silk.NET.OpenGL.PixelFormat`/`PixelType` (Content must stay
Silk.NET-free — the bake tool must not ship GL binaries). Underlying
values are the GL ABI constants, numerically identical to the Silk.NET
members; App casts at its single upload boundary (the `AddTexture` call
in `UploadGfxObjMeshData`) via a lifted nullable enum conversion —
value- and null-preserving.
- `src/AcDream.Content/IDatReaderWriter.cs`, `EdgeLineBuilder.cs` — GL-free
dependencies of the extractor, moved (namespace-only) alongside it.
- **Side-stage sink seam:** `CollectEmittersFromScript` pre-loads particle
GfxObj meshes mid-extraction and, pre-MP1a, enqueued them directly onto
`ObjectMeshManager._stagedMeshData`. The extractor now takes an
`Action<ObjectMeshData>? sideStagedSink` constructor parameter; App wires
it to `_stagedMeshData.Enqueue`, preserving the original
immediate-enqueue semantics exactly — including on a mid-`Prepare*`
throw (preloads staged before a malformed-dat texture-decode exception
survive, as they always did). The MP1b bake tool passes its own
collector.
- **Stays in `src/AcDream.App/Rendering/Wb/`:** `ObjectMeshManager` (the
staged-queue/worker-pool/Dispose-quiesce lifecycle and all GL upload;
production workers now consume `IPreparedAssetSource`),
`ObjectRenderData`/`ObjectRenderBatch`
(hold a GL `TextureAtlasManager` field), `TextureAtlasManager`,
`GeometryUtils` (used only by App-side raycasting, not by extraction),
`AcSurfaceMetadata`/
`AcSurfaceMetadataTable` (not on the extraction path), `Building.cs`
(explicitly out of scope).
- `AcDream.Core` is untouched; `AcDream.Content` references `AcDream.Core`
(for `TextureHelpers`, `Sphere`/`BoundingBox` via `Chorizite.Core.Lib`);
`AcDream.App` references `AcDream.Content`. `AcDream.Core` does NOT
reference `AcDream.Content` (one-way dependency, per Code Structure Rule 2).
- Reason: MP1 (`docs/superpowers/specs/2026-07-05-modern-pipeline-design.md`
§6.1) — the bake tool needs the identical mesh/texture extraction code
running with no GL context, so baked pak output and live-client output stay
byte-identical.
**MP1b EnvCell content identity correction (2026-07-24).** The extracted
WorldBuilder `EnvCellRenderManager.GetEnvCellGeomId` 31× polynomial is not a
safe unique resource key. A guarded full retail-DAT catalog found the concrete
collision `0x00030175` versus `0x01BC0105`: different environment/surface
tuples both map to `0x00000002020E8C13` and contain different polygons.
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity` now owns one namespaced
FNV-1a identity shared by the App streaming build and `acdream-bake`; the
legacy calculation remains executable only for the conformance test that proves
the collision. The bake additionally compares the complete source tuple before
aliasing and fails on any collision. This is an acdream resource-ownership seam,
not a second DAT interpreter; `DatCollection` and `MeshExtractor` remain the
only reader/extractor path.
**MP1c production prepared-asset cutover (2026-07-24).** Production
world-mesh workers no longer invoke `MeshExtractor` or rebuild Setup, GfxObj,
EnvCell, Surface, palette, and texture graphs during portals. The validated
machine-local `acdream.pak` is opened through Content's
`IPreparedAssetSource`; typed GfxObj and EnvCell requests deserialize immutable
`ObjectMeshData` while retaining the existing App worker, staging, render-thread
upload, cache, ownership, and shutdown contracts. The original
format-1/bake-tool-3 render payload persists exact batch translucency so App
does not reconstruct a
`GfxObjMesh` for metadata. Setup activation uses the package TOC as an explicit
type-presence index before reading valid Setup records through the bounded DAT
cache. `DatPreparedAssetSource` and `MeshExtractor` remain explicit
bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes
→ Language → Cell lookup precedence is encoded directly in
`DatCollectionAdapter.TryResolvePreferred`. The connected physical and
installed-DAT gates are recorded in
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`.
**Launcher cumulative-overlay extension (2026-08-25).** Production still has
no live-DAT fallback and consumes the same prepared-payload contracts. For a
bounded recipe migration, App and Headless may receive one complete base pak
plus one cumulative filtered pak through `LayeredPreparedAssetSource`. The
overlay is probed first: Missing falls through to the base, while a present but
corrupt render or collision payload remains authoritative corruption. Both
mapped owners share one composite lifetime and there is never an overlay
chain. The launcher binds the overlay to the base digest in the optional
`pak/content.current.json` sidecar; format/global extraction migrations retain
the explicit full-rebuild path. A tiny `pak/content.client-pending` marker
keeps either result non-launchable until the matching client is confirmed,
including across a crash/restart. Design and gates:
`docs/plans/2026-08-25-launcher-content-stabilization.md`.
**Slice I3 prepared collision extension (2026-07-25).** The package remains
format 1 and retains mesh type values 13; bake-tool 4 appends typed GfxObj,
Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the
immutable flat records and deterministic raw-DAT flattener. Content owns the
strict little-endian codec and `IPreparedCollisionSource`.
`PakPreparedAssetSource` implements the render and collision interfaces over
one mmap; it does not create a second DAT reader or mapping. CellStruct
payloads alias only after exact serialized-byte comparison, while each
EnvCell topology remains independently keyed. Production traversal remained
on the parsed graph oracle until the later Slice-I cutover. Full-catalog evidence:
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
**Slice I5 dual-publication seam (2026-07-25).** Near-tier
`LandblockBuild` payloads now carry one immutable prepared-collision closure
outside the DAT lock. `LandblockPhysicsPublisher` installs the parsed oracle
and flat view under the same retained receipt; live objects use the strict
`LiveCollisionAssetPublisher`. Cell/topology ownership is landblock-scoped and
withdrawn on replacement, demotion, removal, and reset. The connected
graph-authoritative referee completed 14,064 exact samples with no mismatch or
fault. This is still one `DatCollection` and one prepared-package mmap, not a
second reader or a WorldBuilder runtime dependency. Evidence:
`docs/research/2026-07-25-slice-i5-dual-collision-shadow.md`.
**Slice I6/I7 flat-authoritative closeout (2026-07-25).** Production
`PhysicsDataCache` now publishes only immutable flat GfxObj, Setup,
CellStruct, and EnvCell-topology records from the validated package. Stable
world state strips temporary `PhysicsDatBundle` source material, and near
landblock builds discard raw Environment/GfxObj collision graphs once the
flat closure exists. Parsed graph constructors remain explicit
test/bake/equivalence oracles only; gameplay has no graph fallback or referee.
Both exact-binary connected routes report `0/0/0` retained parsed collision
graphs at every stable checkpoint while flat residency remains populated.
This remains one `DatCollection`, one preparation algorithm, and one package
mmap. Evidence:
`docs/research/2026-07-25-slice-i7-closeout.md`.
**Prepared indoor-transit consumer correction (2026-07-26).** The package
already retained exact portal planes through
`FlatEnvCellTopology.PolygonIndex` plus the aliased CellStruct portal-polygon
table. The production `CellTransit` consumer now reads that prepared
relationship directly; it no longer treats the intentionally absent parsed
`CellPhysics.PortalPolygons` dictionary as “this cell has no portals.” No
package schema, bake, DAT reader, collision formula, or render portal graph
changed. Evidence:
`docs/research/2026-07-26-prepared-indoor-transit-regression.md`.
**Cell availability semantics (2026-07-31, corrected after full-catalog
audit).** Raw and prepared CellStruct publication retains a `CellPhysics`
record when the physics root is empty but requires a valid containment root.
The installed 729,888-record raw and prepared catalogs contain zero rootless
containment payloads. A malformed null/-1 root is quarantined atomically; the
recursive inside base case applies only to a missing positive child below a
valid root. Registration-side outdoor floods still add outside cells but skip
transit when the active CLandCell is unavailable, and every later outdoor
candidate independently requires its own visible landcell before building
transit. The existing reflood retries after terrain/cell hydration. Both raw
and prepared point-in-cell paths preserve retail's zero-portals guard. No
package schema or DAT reader changed.
Evidence: `docs/research/2026-07-31-cell-availability-semantics.md`.
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
`src/AcDream.Content/Vfx/` read raw bytes through the existing `DatCollection`
database, delegate every ordinary hook to the package, and substitute only the
retail blocking-particle shape. Both live animation playback and PhysicsScript
loading use those cached readers; `MeshExtractor` uses the same PhysicsScript
loader when preloading emitter meshes. This is not a second DAT reader:
`DatCollection` remains the sole database owner and access path. Retail anchors:
`CreateBlockingParticleHook::Execute` `0x00526EF0` and
`ParticleManager::CreateBlockingParticleEmitter` `0x0051B8A0`.
**Retail particle visibility/degradation seam (2026-07-17).** WorldBuilder's
particle simulator remains a useful DAT-integrator and batching reference, but
it does not carry the live retail client's `CObjCell::IsInView` degradation
path. `EmitterDescRegistry` now resolves the hardware particle GfxObj and its
ordered `GfxObjDegradeInfo` entries through the same sole `DatCollection`;
`ParticleSystem` ports the retail finite/infinite degraded branches; App feeds
the unified PView interior set plus the landscape renderer's independently
computed outdoor landcell set through a focused
`ParticleVisibilityController`. Examination and dedicated-pass emitters carry
an explicit bypass policy; missing/portal world views are empty rather than
fail-open.
No WorldBuilder dependency or second DAT access layer was introduced. Retail
anchors: `CPhysicsPart::GetMaxDegradeDistance` `0x0050D510`,
`GfxObjDegradeInfo::get_max_degrade_distance` `0x0051E2D0`,
`CPhysicsObj::ShouldDrawParticles` `0x0050FE60`, and
`ParticleEmitter::UpdateParticles` `0x0051D180`.
Hardwareless ParticleEmitterInfo records are also retained exactly. Retail
`ParticleEmitter::SetInfo @ 0x0051CE90` returns false when
`hw_gfxobj_id == INVALID_DID`; it does not substitute the software GfxObj.
`EmitterDescRegistry` negative-caches that authored outcome and the hook sink
reports it once per DAT ID rather than once per owner. This is classification
and bounded diagnostics around the existing `DatCollection`, not a fallback
reader or invented VFX. Evidence:
`docs/research/2026-07-26-retail-hardwareless-particle-emitter-diagnostics.md`.
**Retail shared world-alpha seam (2026-07-18).** WorldBuilder's editor
renderers classify translucent mesh batches correctly, but they have no live
retail `CPartCell`/`CShadowPart` list and render particles in a separate
batcher. `src/AcDream.App/Rendering/RetailAlphaQueue.cs` is therefore an
acdream-owned runtime seam above the extracted mesh pipeline. During the main
world frame, `WbDrawDispatcher` and `ParticleRenderer` submit transparent
GfxObj subsets and scene particles into one stable far-to-near stream keyed by
the transformed DAT `SortCenter`; only adjacent compatible entries may batch.
Billboard particle textures are resident bindless `sampler2DArray` handles in
the per-instance vertex ABI, so different textures preserve that sorted order
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
keeps dense particle fields from becoming one GL draw per alternating texture.
`RetailPViewRenderer` drains the landscape scope before the optional depth
clear, and `GameWindow` drains the final scope before private viewports/UI.
Sky and sealed off-screen render targets remain independent. No DAT reader,
mesh decoder, or second scene graph was introduced. Retail anchors:
`CPhysicsPart::UpdateViewerDistance` `0x0050E030`,
`RenderDeviceD3D::DrawObjCellForDummies` `0x005A0760`,
`CShadowPart::insertion_sort` `0x006B5130`,
`D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and
`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. The modern per-cell-order and
EnvCell-shell residual is tracked explicitly as AP-34.
**Retail portal-space viewport adapter (2026-07-15).**
`src/AcDream.App/Rendering/PortalTunnelPresentation.cs` uses the extracted
Setup/GfxObj mesh pipeline and mandatory `WbDrawDispatcher` for retail's
synthetic CreatureMode tunnel object. The adapter does not duplicate mesh or
DAT decoding: it resolves the two client-enum assets through `DatCollection`,
uses the shared `RetailAnimationLoader`, registers Setup part refs through
`WbMeshAdapter`, and submits the animated `SetupMesh` through the existing
dispatcher. It clears world clip routing and point lights for the private scene
before installing retail's distant light. The lifecycle, camera, animation,
and draw ordering are acdream-owned ports of `gmSmartBoxUI`; WorldBuilder never
implemented this UI viewport.
**Portal destination render-readiness seam (2026-07-16).**
`GpuWorldState.IsRenderReady` does not treat dictionary publication as a draw
barrier. `LandblockSpawnAdapter` retains the complete required-id set for each
landblock: atlas-tier GfxObjs plus the synthetic geometry ids prepared by the
independent EnvCell shell pipeline. `WbMeshAdapter.IsRenderDataReady` opens the
gate only after `ObjectMeshManager` has real GPU render data. Its bounded CPU
cache retains texture payloads and stages a missing GPU object through a
deduplicated upload queue on cache hit only when the exact renderer owner is
still live, covering eviction and revisit churn without letting an unowned
cache hit recreate stale staged work. Reacquiring an exact owner stages once.
GPU upload is deliberately not an ownership acquire: atlas GfxObjs use their
landblock/entity pins, while synthetic EnvCell geometry uses a no-generic-decode
pin balanced from each landblock snapshot. A late upload after all owners have
released enters the evictable LRU instead of resurrecting a reference. After
publication pins the synthetic ids, the controller replays their immutable
environment/cell-structure/surface preparation descriptors; if a formerly
unowned mesh was evicted between worker scheduling and publication, this
schema-aware replay re-stages it without a generic GfxObj lookup.
Near-to-Far demotion is a separate App transaction: it releases EnvCell
rendering and landblock mesh pins while retaining the terrain slot. Core's
matching physics demotion preserves the terrain surface but removes indoor
cells, portals, buildings, and static shadow registrations.
**Cost-budgeted publication seam (2026-07-24).** WorldBuilder's editor path
publishes a complete manager-owned scene; acdream's live streamer instead
advances prepared render, physics, static, building, EnvCell, and spatial
receipts under one typed frame meter. Stable cursor work may span frames, while
building/EnvCell replacement and the final `GpuWorldState` spatial identity
swap remain observer-atomic. Destination live-object render ownership prepares
while the world is quiesced. `GpuWorldState.IsLiveEntityProjectionResident`
answers that spatial ownership question; availability-gated drawing,
collision, picking, radar/status targeting, effects, and audio continue to use
`IsLiveEntityVisible`. This is an acdream async-integration seam around the
extracted WB pipeline, not a second mesh or DAT implementation. Connected
evidence:
`docs/research/2026-07-24-slice-e-cost-budgeted-streaming-report.md`.
**Bounded residency and GPU retirement seam (2026-07-18).** Runtime DAT access
keeps raw file payload caching disabled and layers bounded typed-object and
decoded-pixel LRUs above the single `DatCollection`. `ObjectMeshManager`, the
standalone bindless texture cache, and the owner-scoped composite texture-array
cache all distinguish an active owner from an evictable unowned entry. Appearance
changes and landblock demotion acquire-before-publish and withdraw-before-release;
rebucketing never creates a second owner. `GlobalMeshBuffer` uses reclaimable,
coalescing vertex/index ranges and migrates incrementally within explicit physical
ceilings. Texture layers, terrain slots, mesh ranges, and old backing stores are
returned only after `GpuFrameFlightController` observes the frame fence that can
no longer reference them. This lifetime machinery is acdream-owned integration
around the extracted WB mesh pipeline; it does not add a second DAT decoder or a
reduced-distance rendering path.
**Unified residency policy seam (2026-07-24).** The extracted WB caches remain
the physical owners of mesh, arena, atlas, and texture resources.
`AcDream.App.Rendering.Residency.ResidencyManager` adds an acdream-owned typed
policy/diagnostic layer over them: generation-safe asset handles, independent
owner tokens and leases, immutable startup budgets, aggregate accounting, and
bounded trim requests. It never stores or deletes a GL name. Existing owners
perform logical eviction and fence-delayed physical release on the render
thread. The same ledger observes the prepared-package mapping, prepared/staged
CPU mesh data, decoded animation/audio data, and retained shared-alpha scratch
without double-counting the package's clean memory-mapped pages as committed
heap. Deterministic pressure tests exceed every configured ceiling and prove
zero-charge teardown. Connected evidence:
`docs/research/2026-07-24-slice-d-unified-residency-report.md`.
**Workflow:** Before re-implementing any AC-specific rendering or dat-handling
algorithm, **check this inventory first**. If we already extracted it (🟢
sections), it's in `src/AcDream.App/Rendering/Wb/` — use our copy. If WB has
it but we haven't extracted it yet, grep `references/WorldBuilder/` and extract
as needed. Retail decomp remains the oracle for things WB never had (🔴 list).
Attribution: WorldBuilder is MIT-licensed. `NOTICE.md` includes WB attribution.
---
## Read-reference layout (under `references/WorldBuilder/`, not project-referenced)
- **`Chorizite.OpenGLSDLBackend/`** — full OpenGL renderer (Silk.NET). The
components we use are extracted into `src/AcDream.App/Rendering/Wb/`.
- **`WorldBuilder.Shared/`** — data models, dat parsers, landscape module.
The helpers we use are extracted into `src/AcDream.Core/Rendering/Wb/`.
- **`WorldBuilder/`** — Avalonia desktop app shell (not taken).
- **`WorldBuilder.{Windows,Linux,Mac}/`** — platform entry points (not taken).
- **`WorldBuilder.Server/`** — collab editing backend (not taken).
- **`Tests/` + `WorldBuilder.Shared.Benchmarks/`** — test harness (study only).
**Upstream NuGet dependencies** (these stay as NuGet packages, we don't
vendor them):
| Package | Version | Purpose |
|---|---|---|
| `Chorizite.Core` | 0.0.18 | Plugin framework — contains `Chorizite.Core.Lib.BoundingBox`, `Chorizite.Core.Render.*` interfaces used by every render manager |
| `Chorizite.DatReaderWriter` | 2.1.x | dat parsing (we already use 2.1.7) |
| `Chorizite.DatReaderWriter.Extensions` | 1.1.x | extra dat helpers |
| `BCnEncoder.Net` | 2.2.x | DXT decode (we already use) |
| `SixLabors.ImageSharp` | 3.1.x | image loading |
| `Silk.NET.OpenGL` + `Silk.NET.SDL` | 2.23.x | GL + windowing (we use Silk's own windowing, they use SDL) |
| `MP3Sharp` | 1.0.5 | MP3 decode |
---
## 🟢 RENDERING — take wholesale or adapt
These are what makes WB "perfect". Anything in this section, we should
use from WB rather than re-implement.
### Terrain
| Component | What it does |
|---|---|
| `TerrainRenderManager` | Full pipeline (per-chunk GPU buffers, draw orchestration) |
| `LandSurfaceManager` | Texture blending atlas (palCode, alpha masks, road overlays) |
| `TerrainGeometryGenerator` | Heightmap → mesh, normals, OnRoad, GetHeight, GetNormal |
| `TerrainChunk` | 16×16 landblock chunk geometry |
| `TextureAtlasManager` | Texture atlas builder |
| `VertexLandscape` | Terrain vertex format |
**Modern terrain adapter:** acdream's bindless path uses `TerrainAtlas` plus
`TerrainModernRenderer` rather than WB's draw manager, but retains
`LandSurfaceManager`'s layer-indexed `TerrainTex.TexTiling` contract. The
36-entry table is uploaded to `terrain_modern.frag`; base, overlay, and road
layers each use their owning repeat count while alpha masks stay at cell scale.
Retail `TexMerge::CopyAndTile` (`0x00503580`) and `TexMerge::Merge`
(`0x005038C0`) are the behavior oracle; see
`docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md`.
### Scenery (procedural placement: trees, bushes, rocks, fences)
| Component | What it does |
|---|---|
| `SceneryRenderManager` | Generate + render per-vertex scenery |
| `SceneryHelpers` | Displace / RotateObj / ScaleObj / ObjAlign / CheckSlope |
| `SceneryInstance` | Per-spawn instance data |
acdream's streamed projection assigns generated instances local runtime IDs
through `AcDream.Core.World.ProceduralSceneryIdAllocator`. The namespace is
`0x8XXYYIII` (full X/Y bytes plus a 12-bit counter); bit 31 remains the stable
scenery classification seam. This is projection identity, not placement
behavior. The former 8-bit counter rejected dense retail-DAT landblocks before
their render transaction could publish (#218).
### Static objects (buildings, slabs, props — Setup + GfxObj + ObjDesc)
| Component | What it does |
|---|---|
| `StaticObjectRenderManager` | Master pipeline for static objects |
| `ObjectRenderManagerBase` + `BaseObjectRenderManager` | Common render base |
| `ObjectMeshManager` | Mesh extraction from Setup/GfxObj, ObjDesc application |
### Dungeons / interiors
| Component | What it does |
|---|---|
| `EnvCellRenderManager` | Dungeon interior cell geometry |
| `PortalRenderManager` | Portal traversal / visibility |
### Sky + atmosphere
| Component | What it does |
|---|---|
| `SkyboxRenderManager` | Skybox rendering |
| `ParticleEmitterRenderer` + `ParticleBatcher` + `ActiveParticleEmitter` | Particle systems (sky particles, weather, magic) |
### Visibility / culling
| Component | What it does |
|---|---|
| `VisibilityManager` + `VisibilitySnapshot` | Frustum + cell visibility |
| `Frustum` | Frustum-cull math |
### Other rendering helpers
| Component | What it does |
|---|---|
| `MinimapRenderer` | Top-down minimap |
| `GlobalMeshBuffer` | Shared GPU mesh buffer; one reclaimable vertex/index allocation per mesh, released by `ObjectMeshManager` eviction |
| `GpuResourceManager` | GPU resource lifecycle |
| `InstanceData` | Instanced draw data |
| `TextureHelpers` | INDEX16, P8, BGRA, DXT decode + alpha (canonical port) |
| `DebugRenderer` + `DebugRendererLineDrawer` + `EdgeLineBuilder` | Debug primitives |
### Shaders (22 total)
Located at `Chorizite.OpenGLSDLBackend/Shaders/`:
`Landscape.{vert,frag}` · `StaticObject.{vert,frag}` · `StaticObjectModern.{vert,frag}` · `Particle.{vert,frag}` · `PortalStencil.{vert,frag}` · `Outline.{vert,frag}` · `Simple3D.{vert,frag}` · `InstancedLine.{vert,frag}` · `Text.{vert,frag}` · `UI.{vert,frag}` · `Gizmo.{vert,frag}` (editor-only)
---
## 🟢 LOW-LEVEL GL / FRAMEWORK — take or replace with our own
Either take WB's wrappers wholesale, or keep our own and adapt the
render managers to use ours. These wrappers are stateless or
near-stateless and are the easiest to swap.
| Component | What it does |
|---|---|
| `OpenGLGraphicsDevice` | Silk.NET.OpenGL wrapper |
| `OpenGLRenderer` | Render orchestration |
| `GLSLShader` | Shader compile/link/uniforms |
| `GLHelpers` + `GLStateScope` | GL state utility |
| `ManagedGLFrameBuffer` / `ManagedGLIndexBuffer` / `ManagedGLTexture` / `ManagedGLTextureArray` / `ManagedGLUniformBuffer` / `ManagedGLVertexArray` / `ManagedGLVertexBuffer` | GL resource wrappers |
| `TextureParameters` | Sampler config |
| `GpuMemoryTracker` | Memory tracking |
| `Camera2D` / `Camera3D` / `CameraBase` / `ICamera` / `CameraController` | Camera primitives |
| `GameScene` + `SingleObjectScene` + `SceneData` + `ModernRenderData` + `RenderPass` | Scene / pass structures |
---
## 🟢 GEOMETRY / MATH UTILS — take wholesale
| Component | File |
|---|---|
| `TerrainUtils` (OnRoad, GetNormal, GetHeight, GetRoad, palCode) | `WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs` |
| `TerrainCacheManager` | `…/Lib/TerrainCacheManager.cs` |
| `TerrainRaycast` | `…/Lib/TerrainRaycast.cs` |
| `GeometryUtils` | `WorldBuilder.Shared/Lib/GeometryUtils.cs` |
| `RaycastingUtils` (ray-vs-sphere/AABB/triangle) | `WorldBuilder.Shared/Lib/RaycastingUtils.cs` |
| `DoubleNumerics` (double-precision Vector/Matrix) | `WorldBuilder.Shared/Lib/DoubleNumerics.cs` |
| `DatUtils` | `WorldBuilder.Shared/Lib/DatUtils.cs` |
| `BoundingBoxExtensions` | `Chorizite.OpenGLSDLBackend/Lib/BoundingBoxExtensions.cs` |
---
## 🟢 DATA MODELS — take selectively
| Component | What it does |
|---|---|
| `RegionInfo` | Landblock metadata wrapper (LandblockSizeInUnits, CellSizeInUnits, etc.) |
| `TerrainEntry` | Per-vertex terrain (Type/Scenery/Road/Height) |
| `MergedLandblock` | Merged dat data |
| `CellSplitDirection` | SW-NE vs NE-SW |
| `Cell` | Generic cell wrapper |
| `ObjectId` | Object identifier |
| `Position` | World position |
| `ACEnums` | AC-specific enums |
| `WbBuildingPortal` / `WbCellPortal` | Portal structures |
| `BuildingObject` | Building data |
---
## 🟡 EDITOR-ONLY — leave behind / delete in fork
These exist for the editor experience and have no place in a game
client. Delete in fork.
- **`Modules/Landscape/Tools/*`** — `BrushTool`, `BucketFillTool`,
`RoadLineTool`, `RoadVertexTool`, `InspectorTool`,
`ObjectManipulationTool`, `Gizmo*` (DragHandler, HitTester, Renderer,
State), `TexturePainting*`, `SceneRaycaster`,
`LandscapeBrush`, `LandscapeToolBase`, `LandscapeToolContext`,
`IToolSettingsProvider`, `ILandscapeBrush`, `ILandscapeEditorService`,
`ILandscapeRaycastService`, `ILandscapeTool`, `ITexturePaintingTool`
- **`Modules/Landscape/Commands/*`** — undo/redo command pattern for
editor (Add/Delete/Move/Rename/Reorder/etc.)
- **`LandscapeDocument` + `LandscapeLayer` + `LandscapeLayerGroup` + `LandscapeChunk` + `LandscapeLayerChunk` + `LandscapeLayerBase`** — editor document model
- **`Modules/Landscape/Models/TerrainPatch*` + `LandblockChangedEventArgs`** — editor mutation events
- **`Modules/Landscape/Services/ILandscapeCacheService` + `ILandscapeDataProvider` + `ILandscapeObjectService` + impls** — editor data flow
- **All `Migrations/*`** — SQLite schema migrations (project file format)
- **`Repositories/*`** + **`Services/*`** — project storage, dat repository, AceDb, SignalR sync, document manager, undo stack, world coordinates, keyword DB, project migration, semantic kernel AI helpers
- **`Hubs/*`** — collaborative editing via SignalR
- **`StaticObject` (editor model)** — replace with our own scene-state data model fed from network
- **`BackendGizmoDrawer` + `GizmoRenderer`** — editor gizmos
- **`ProjectStructures, IProject, Project`** — editor project files
- **`KeyBinding`** — editor input binding
- **`ViewportInputEvent[Extensions]`** — editor viewport input
- **`EditorState`** — editor state container
---
## 🟡 AUDIO / FONT — we already have alternatives
Keep ours; don't take theirs.
- **`AudioPlaybackEngine`** — uses MP3Sharp. We have OpenAL.
- **`FontRenderer`** — uses ImageSharp. We have BitmapFont/StbTrueTypeSharp + ImGui.
---
## 🔴 NOT IN WORLDBUILDER — port from retail decomp ourselves
WorldBuilder is a dat editor; it does not have:
- **Network protocol** — UDP framing, ISAAC, packet codec, ACE message
layer (we have this; oracle is `references/holtburger`)
- **Physics** — collision (CPhysicsObj transitions, BSP queries, sphere
sweeps), step-up, walkable validation (we have partial; oracle is the
retail decomp at `docs/research/named-retail/`)
- **Animation** — motion sequencer, cycle/non-cycle parts, animation
frame interpolation (we have this; oracle is retail decomp)
- **Movement** — local player WASD → MoveToState wire, remote-entity
motion via UpdateMotion + dead-reckoning (we have this; oracle is
`references/holtburger` + retail decomp)
- **Game UI** — chat, vitals, inventory, spell book, allegiance, options
(we have this; ImGui-based today, custom-toolkit later)
- **Plugin API**`IGameState`, `IEvents`, `IActions`, `IPacketPipeline`,
`IOverlay` (we have this — acdream-unique)
- **Game events** — combat, allegiance, spell casting, quest events
(we have this; oracle is ACE for opcodes + retail for client behavior)
- **Audio** — OpenAL pipeline, sound triggers (we have this)
- **TurbineChat** + **slash commands** (we have this)
- **Login + character selection flow** (we have this)
- **World-object mouse selection** — WorldBuilder supplies mesh/DAT access but
no retail client picker. Our narrow `RetailSelectionGeometryCache` reuses
`DatCollection` to expose each GfxObj drawing-BSP root sphere and visual
polygons; `WbDrawDispatcher` supplies the normal draw's current part
transforms to the named-retail selection accumulator.
---
## What this means for the workflow (post-Phase O)
The CLAUDE.md "grep named → decompile → verify → port" workflow stays
the rule for everything in the 🔴 list (network, physics, animation,
movement, UI, plugin, audio, chat).
For anything in 🟢 that we've already extracted: **the code is in our
tree at `src/AcDream.{Core,App}/Rendering/Wb/`**. Read it there — don't
grep `references/WorldBuilder/` unless you want to compare against the
original. Re-porting from retail decomp when we already have a tested
port is still how we'd get the scenery edge-vertex bug back.
For anything in 🟢 that we have NOT yet extracted: grep
`references/WorldBuilder/` to find the source, then extract it using the
Phase O pattern (verbatim copy → adapt constructor to accept
`IDatCollection` via `DatCollectionAdapter` where needed → add to
`src/AcDream.App/Rendering/Wb/`). Do NOT add a new project reference back
to `WorldBuilder.Shared` or `Chorizite.OpenGLSDLBackend` — Phase O
permanently removed those.
When we discover a behavior mismatch with retail (rare — the extracted
code is the same as the original), the resolution is: reconcile extracted
code ↔ retail decomp ↔ holtburger ↔ ACE ↔ ACViewer (the existing
reference hierarchy in CLAUDE.md). Our extracted code ranks at the top
of that hierarchy for anything 🟢.

View file

@ -1,18 +1,11 @@
# acdream — historical bug snapshot (deprecated)
# acdream — known bugs
> **Not an active issue tracker.** This file preserves the 2026-04-14 snapshot
> for archaeology. Several “open” entries below were superseded, narrowed, or
> fixed by later collision/rendering work. Do not update status here and do not
> use these BUG numbers for new work. The canonical tactical ledger is
> [`ISSUES.md`](ISSUES.md); current program navigation is in
> [`README.md`](README.md).
The text below is intentionally retained as historical evidence of what was
believed at the time.
Track visual, gameplay, and protocol bugs here. Close by moving to
the "Fixed" section with the commit hash that resolved it.
---
## Historical open snapshot (not current)
## Open
### BUG-001: Wrong cloth textures on characters
- **Observed:** 2026-04-13
@ -60,7 +53,7 @@ believed at the time.
---
## Historical fixed snapshot
## Fixed
### BUG-002: Jump not visible from retail client
- **Fixed:** 5634e71 — Jump packet (opcode 0xF61B) now sent to server.

View file

@ -1,150 +0,0 @@
# Continuous integration and alpha releases (Gitea)
Single source of truth for how acdream builds, gates, and ships alpha builds.
Landed 2026-08-19. Companion to [`release-gate.md`](release-gate.md), which
owns the *local* bounded gate.
## What happens on a push to main
```
git push origin main
├─ windows-gate (RARE-win) build + full lane-filtered suite
├─ linux-portable (eriktestLinux) portable closure, Linux lanes
└─ release (needs BOTH green) publish a Gitea Release
+ republish the `latest` pointer
```
Workflow: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml). Docs-only
pushes (docs/, the memory trees, markdown) skip the pipeline entirely — no
test can fail on them and a run costs ~7 minutes plus a 121 MB release. A red gate
cannot publish: `release` uses `needs:`, not a `workflow_run` trigger, whose
Forgejo support is unreliable.
## Why Gitea and not GitHub
GitHub Actions is **billing-blocked** on this account ("recent account payments
have failed"), and the repo is private, so hosted runners consume paid minutes.
Forgejo ships **no hosted runners at all**, so Actions there requires
self-hosted ones — which are free on both platforms. The same two machines can
serve GitHub later by registering a second agent; only the workflow's
`runs-on` labels change.
## The runners
| | Windows | Linux |
|---|---|---|
| Host | `RARE` (10.6.0.3) | `eriktestLinux` (10.0.0.202) |
| Agent | `act_runner` 0.2.13 | `forgejo-runner` 13.0.0 |
| Persistence | Scheduled task `ForgejoRunner`, at logon of `acbot` | systemd `forgejo-runner`, `Restart=always` |
| Labels | `windows`, `windows-latest`, `windows-x64` | `ubuntu-latest`, `ubuntu`, `linux`, `ubuntu-slim` |
| Execution | host mode (`:host`) — no Docker on either box | host mode |
Both **poll outbound** over HTTPS. Gitea never connects to them, so no inbound
ports, no port forwarding, and no static IP; they work behind NAT. The runner
does not have to live next to the Gitea container (which runs on `bluesnake`,
a host we have no shell on).
`forgejo-runner` publishes **no Windows binary in any release**, which is why
Windows uses Gitea's `act_runner`. Forgejo speaks the same Actions protocol.
### Prerequisites on a runner
- **.NET SDK in the `global.json` band** — currently `10.0.3xx`. `10.0.400` is a
different feature band and `rollForward: latestPatch` rejects it.
- **Node.js**`actions/checkout` and `actions/upload-artifact` are JavaScript
actions. Docker images normally supply Node; in host mode the machine must.
- **Git**, and outbound HTTPS to `git.snakedesert.se`.
- **PowerShell 7** on Windows (`pwsh`); `tools/*.ps1` require it.
## Releases
Everything about distribution lives under **Releases** — nothing in git. A build
is ~120 MB, so payloads are release attachments; and the pointer the launcher
polls is itself a release asset, so there is no payload branch, no bot commit on
`main`, and no push that could retrigger the pipeline.
```
Release 0.1.0-build.<yyyyMMddHHmm> <- the actual build
client-win-x64.zip AcDream.App.exe + acdream-headless.exe
launcher-win-x64.zip acdream-launcher.exe + acdream-bake.exe
manifest.json
Release latest <- pointer, replaced every publish
manifest.json names the version above and its asset URLs
```
The launcher polls the pointer at a URL that never changes
(`ReleaseManifestClient.ProductionManifestUri`):
```
https://git.snakedesert.se/erik/acdream/releases/download/latest/manifest.json
```
A pointer is needed because **Forgejo has no `/releases/latest/download/`
route** (verified: 404) — unlike GitHub, there is no built-in stable URL for
"the newest release". Publishing it recreates the `latest` tag each time, which
means deleting the old release *and* its tag; the tag outlives its release and
would otherwise block recreation.
The newest **5** versioned releases are kept and older ones are pruned with
their tags. Each build is ~121 MB of attachments, so retaining every one grew
the server by that much per push — 5 builds had already reached 606 MB. Five is
enough to grab a previous build or bisect a regression while staying bounded.
The `latest` pointer is never pruned; it is the feed, not a build.
`tools/publish-bin.ps1 -BaseUrl <release asset base>` builds the payloads; CI
passes the tag's asset base. Running it locally is for inspection only —
publishing is CI's job.
### Verifying a release
```powershell
dotnet test tests/AcDream.Launcher.Core.Tests --filter Lane=Live
```
`LiveGiteaReleaseInstallTests` installs the advertised client from the real feed
through the production updater — real SHA-256/size verification, extraction, and
atomic activation — then asserts both hosts resolve out of the activated
directory and `current.json` names the installed version.
## Landmines
Each of these cost a red pipeline; none was a config typo. Two rows record a
fix that was tried and **disproved** — read those before repeating it.
| Symptom | Cause |
|---|---|
| `Cannot find: node in PATH` | JS actions need Node on the host in `:host` mode |
| `actions/setup-dotnet` never resolves | `data.forgejo.org` does not mirror it (404). `checkout` and `upload-artifact` **are** mirrored. Self-hosted runners carry the SDK anyway |
| Job "failed" while dotnet processes still run | `run-release-gate.ps1` redirects children to log files, so the step goes silent; Forgejo fails a non-reporting task as a zombie. CI runs `dotnet test` directly so output streams |
| ~40 tests fail on formatted numbers | Runner's `HKCU` locale was `en-SE` (comma decimal): expected `"update:0.25"`, got `"update:0,25"`. `Set-Culture` does **not** reach a scheduled task without a loaded profile — set the registry directly |
| `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` as the locale fix | Too blunt — it breaks tests that legitimately construct a culture. Fix the machine locale instead |
| `FileNotFoundException: client_cell_1.dat` | DAT-dependent tests missing `[Trait("Lane", "InstalledDat")]`. Build machines have no DATs |
| Timing-sensitive test fails only under load | It belongs in `Lane=Timing` (see [`release-gate.md`](release-gate.md)). Do **not** chase these individually: four separate fixes each surfaced a different member of the same family, and serializing `Core.Net` to fix Linux regressed Windows from 1000 passed in 7 s to 999/1000 in 17 s |
| Avalonia "calling thread cannot access this object" in cleanup | `MainWindowViewTests` needs a real desktop session and is `Lane=Manual`. Measured: PASSES on a dev desktop and on the CI Windows box over SSH; FAILS under `act_runner` and on Linux. Serializing the assembly does **not** fix it (tried via `xunit.runner.json` and a compiled-in `CollectionBehavior` attribute), and de-async-ing the test actively causes the failure. The stack shows a compositor being **constructed** during teardown — it is the headless session lifecycle, not parallelism |
## Do not leave load on a runner
A stress/diagnostic run left going on a runner competes with CI for the same
machine and makes every job slower and more likely to trip a load-sensitive
test — the exact failures you would then be trying to diagnose. Kill background
work before trusting a timing result:
```powershell
Get-Process dotnet -ErrorAction SilentlyContinue | Stop-Process -Force # Windows
pkill -9 dotnet # Linux
```
Leave `act_runner` / `forgejo-runner` itself alone; killing those unregisters
nothing but stops the machine picking up jobs until it restarts.
## Culture note
The `en-SE` discovery is worth remembering beyond CI: config files, numeric
parsing, and the wire are all culture-safe (`System.Text.Json` is invariant by
spec, every `float/double.TryParse` passes `CultureInfo.InvariantCulture`, and
the protocol is binary). Only **diagnostic strings** format with the current
culture, so a European player sees `local=(8,00; 191,00)` in an F3 dump. The
client installs and runs correctly in both the US and Europe.

View file

@ -1,382 +0,0 @@
# acdream launch options — operator reference
Every environment variable and command-line argument the acdream client
reads, what it does, and **what else it changes about the run**.
**This is an operator's reference, not user documentation.** Players never
set these: the launcher owns installation and login, and the in-client
Options panel (F11) owns settings. If a flag here looks like something a
player would want, that is a signal it belongs in the Options panel, not a
signal to document it better.
## How to use this document
- **Running the client for yourself?** Read *Production launch* and stop.
- **Taking a measurement?** Read *Production launch*, then read the
*Side effects* column of every flag you are about to set. A flag that
changes what you are measuring is the normal case, not the exception.
- **Adding a flag?** Add its row in the same commit. `LaunchOptionsDocumentationTests`
fails the build otherwise — in both directions, so deleting a read site
without deleting its row fails too.
### Why the side-effects column exists
Two flags in this list were believed to be inert and were not:
- `ACDREAM_AUTOMATION_ARTIFACT_DIR` reads like an output path. It also
constructs a per-frame diagnostics referee that re-enabled a retired
render pass, costing ~6 MB and ~14 ms **every frame** — three days of
performance measurements were silently taxed before anyone noticed
([#432](ISSUES.md)).
- `ACDREAM_STREAM_RADIUS` reads like a radius knob. It forces the near
radius, only ever *raises* the far radius, and is then silently
discarded by any later quality apply — so a measurement taken with it
set is measuring a window production never uses.
Assume a flag has a side effect until its row says otherwise.
## Conventions
- **Everything diagnostic is OFF by default.** Every probe, dump, capture,
and measurement flag in this document is inert until its variable is
explicitly set — an unset environment runs zero diagnostics. Exactly
four flags default ON, and none is a diagnostic: `ACDREAM_RETAIL_CHASE`,
`ACDREAM_CAMERA_COLLIDE`, `ACDREAM_CAMERA_ALIGN_SLOPE`, and
`ACDREAM_RETAIL_CLOSE_DEGRADES` are retail *behaviors* wearing an A/B
off-switch (`=0` disables the behavior for a comparison run). That
four-flag set is frozen by `LaunchOptionsDocumentationTests` — a new
default-on flag fails the build.
- `=1` means the code tests for exactly the string `1`. Setting `true`,
`yes`, or `0` does **not** enable such a flag (and `0` does not disable
one whose test is "is the variable present").
- **Default** is the behavior when the variable is unset.
- **Kind** is one of:
| Kind | Meaning |
|---|---|
| `production` | Ordinary configuration; safe in a real run. |
| `measurement` | Profiling/instrumentation. Read the side effects before trusting numbers taken with it on. |
| `automation` | Drives scripted runs; usually implies extra machinery. |
| `permanent-probe` | A diagnostic toggle owned by a subsystem's diagnostics class. Expected to persist. |
| `temporary-probe` | Tied to an open investigation. Deleted with its issue — never build tooling on one. |
| `deprecated` | Superseded. Do not use for new work. |
---
## Production launch
The canonical connected launch against a local ACE server. PowerShell,
because the DAT path contains an apostrophe:
```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_RETAIL_UI = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Release
```
| Flag | Value | What it does | Side effects | Default | Read by |
|---|---|---|---|---|---|
| `ACDREAM_A2C` | `unset/""` keep preset; `"0"/"false"/"False"/"FALSE"` → off; any other non-empty → on | Overrides preset's `AlphaToCoverage` blend flag | Changes MSAA alpha-to-coverage blending mode for foliage/translucent draws — a visual-behavior change, not just perf | preset's `AlphaToCoverage` (High/Ultra=true, Low/Medium=false) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:52`) |
| `ACDREAM_AC_DIR` | `=<path>` | Points at a real retail AC install dir; loads `<dir>/controls/controls.ini` to source retail keybind display strings for the retained UI. | Only has any effect when `ACDREAM_RETAIL_UI=1` (retained UI composed). Unset → `ControlsIni.Parse(string.Empty)`, an empty (not error) controls table — silent, no fallback file is searched. | unset (null) → empty controls table | `RuntimeOptions.AcDir``InteractionRetainedUiComposition.cs:610` |
| `ACDREAM_ANISOTROPIC` | `=<int>` (`int.TryParse`, invariant) | Overrides preset's `AnisotropicLevel` texture filtering | Changes GPU texture sampling filter level (visual sharpness), not just perf | preset's `AnisotropicLevel` (Low=4, Medium=8, High/Ultra=16) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:49`) |
| `ACDREAM_CACHE_DIR` | `=<path>` | Overrides the resolved cache-root directory (used for `DiagnosticsDirectory`, etc.) | none beyond redirecting cache I/O | Windows: `%LOCALAPPDATA%\acdream\cache`; Linux: `$XDG_CACHE_HOME/acdream` or `~/.cache/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:83`), via `IApplicationPathEnvironment` seam |
| `ACDREAM_CAMERA_ALIGN_SLOPE` | `=0` disables (anything else/unset = on) | selects whether the chase camera basis tilts to the player's 5-frame averaged velocity vs staying flat/horizontal on slopes | alters camera orientation / rendered view every frame; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| true (on) | `AcDream.Core.Rendering.CameraDiagnostics.AlignToSlope` |
| `ACDREAM_CAMERA_COLLIDE` | `=0` disables (anything else/unset = on) | selects whether the chase camera sweeps a 0.3 m collision sphere from head-pivot to eye and stops at the first wall (retail spring-arm) | alters camera position every frame (camera can clip into geometry when disabled); startup-only | true (on) | `CameraDiagnostics.CollideCamera` |
| `ACDREAM_CONFIG_DIR` | `=<path>` | Overrides the resolved config-root directory (`settings.json`, `keybinds.json`) | none beyond redirecting config I/O | Windows: `%APPDATA%\acdream`; Linux: `$XDG_CONFIG_HOME/acdream` or `~/.config/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:79`), via `IApplicationPathEnvironment` seam |
| `ACDREAM_DATA_DIR` | `=<path>` | Overrides the resolved data-root directory (logs, screenshots, plugins) | none beyond redirecting data I/O | Windows: `%LOCALAPPDATA%\acdream`; Linux: `$XDG_DATA_HOME/acdream` or `~/.local/share/acdream` | `ApplicationPathSet.Resolve` (`ApplicationPathSet.cs:81`), via `IApplicationPathEnvironment` seam |
| `ACDREAM_DAT_DIR` | `=<path>` | Fallback dat-directory when no positional argument is given. App: single read at `Program.cs:58`. Cli: read independently per-subcommand (each subcommand does `args.ElementAtOrDefault(N) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")`) plus once more for the default (no-subcommand) asset-inventory mode at line 152. | Two of the four `Program.cs` line numbers in the raw grep (91, 135) are **not reads** — they're the literal string `ACDREAM_DAT_DIR` inside `Log.Error` usage-text messages, not `GetEnvironmentVariable` calls. Only line 58 is a real read in `AcDream.App`. | none — hard usage error (exit 2) if unset and no positional arg | `Program.cs:58` (App); `Cli/Program.cs:24,35,47,59,71,84,113,125,137,152` (every Cli subcommand) |
| `ACDREAM_DISPLAY_PROTOCOL` | `="auto"` / `"x11"` / `"wayland"` (case-insensitive, trimmed); any other value throws `InvalidOperationException` at startup | Linux-only: forces the GLFW 3.4 platform-init hint (X11 vs Wayland vs auto) before any window is created; ignored entirely on Windows (always `Windows` protocol) | An invalid value is fatal at startup (throws before any window exists), not a silent fallback | unset → auto-detected from `XDG_SESSION_TYPE`/`WAYLAND_DISPLAY`/`DISPLAY`, falling back to GLFW `Automatic` | `GraphicalWindowBackendSelection.Resolve` (`GraphicalWindowBackendSelection.cs:26-58`) |
| `ACDREAM_FAR_RADIUS` | `=<int>` | Overrides preset's `FarRadius` (outer streaming/reveal window, landblocks) | Enlarging changes streaming memory budget and what's resident/rendered — CLAUDE.md: leave unset for measurement/gate runs (same family as legacy `ACDREAM_STREAM_RADIUS`) | preset's `FarRadius` (Low=5, Medium=8, High=12, Ultra=15) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:47`) |
| `ACDREAM_LIVE` | `=1` (exactly the literal string `"1"`) | Core switch: connect to a live ACE server instead of running offline/no-connect. | The 4 non-`RuntimeOptions.cs` line numbers in the raw grep are **all comments or log-message text**, not reads — `SessionStartComposition.cs:39` is inside the string `"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"`; `Program.cs:126` is inside a `--session-config` override log line; `GameWindow.cs:614,627` are doc comments. The only actual parse is `RuntimeOptions.cs:141`. Requires `ACDREAM_TEST_USER`/`ACDREAM_TEST_PASS` too (`HasLiveCredentials`) or the session silently reports `MissingCredentials` and skips. Forced to effectively-on (LiveMode=true) unconditionally by `--session-config` launches regardless of this var. | `false` | `RuntimeOptions.LiveMode``SessionStartComposition.cs` (log text only), `Program.cs:126` (log text only), `GameWindow.cs:614,627` (comments only), consumed for real via `RuntimeOptions.HasLiveCredentials` and `WorldSession`/`GameRuntime` session-start gating |
| `ACDREAM_MAX_COMPLETIONS_PER_FRAME` | `=<int>` | Overrides preset's per-frame streaming-completion throughput cap | Directly changes the streaming admission budget measured by perf/completion gates — do not vary during a measurement run | preset's value (Low=2, Medium=3, High=4, Ultra=6) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:59`) |
| `ACDREAM_MSAA_SAMPLES` | `=<int>` (0/2/4/8) | Overrides preset's MSAA sample count | Changes GPU multisample anti-aliasing (visual + GPU-cost change) | preset's `MsaaSamples` (Low=0, Medium=2, High/Ultra=4) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:48`) |
| `ACDREAM_NEAR_RADIUS` | `=<int>` | Overrides preset's `NearRadius` (inner streaming ring radius) | Changes the streaming/render window size — CLAUDE.md: leave unset for measurement/gate runs | preset's `NearRadius` (Low=2, Medium=3, High=4, Ultra=5) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:46`) |
| `ACDREAM_NO_AUDIO` | `=1` | Suppresses OpenAL device init entirely (headless / driver-broken machines). | Fail-open design: even without this flag, a missing/broken OpenAL driver already makes `IsAvailable=false` and all `Play*` calls no-ops — this flag is the explicit, deliberate version of the same fallback. When set, `LiveSessionWorldRuntime.WorldAudio` is `null` and logout-audio reset/resume steps no-op. | `false` | `RuntimeOptions.NoAudio``GameWindow.cs:1430``ContentEffectsAudioCompositionPhase``OpenAlAudioEngine.cs` (fail-open doc), `LiveSessionRuntimeFactory.cs:71` (`WorldAudio` gate) |
| `ACDREAM_PAK_PATH` | `=<path>` | Overrides the path to the prepared machine-local asset bake (`acdream.pak`) that production world-mesh streaming reads through `IPreparedAssetSource`. | none beyond the obvious | unset → `<datDir>/acdream.pak` | `RuntimeOptions.PreparedAssetPath``ContentEffectsAudioComposition.cs:53,372,379`, `GameWindow.cs:1420` |
| `ACDREAM_RESIDENCY_ALPHA_SCRATCH_MIB` | `=<int MiB>` (`>0`, else default; overflow-checked) | Byte ceiling for the retail alpha (translucency) draw queue's scratch buffer | Shrinking below production working set changes translucency-queue eviction/reflow behavior — not comparable to a default-budget perf run | 16 MiB | `ResidencyBudgetOptions.Parse` (`ResidencyBudgetOptions.cs:88-89`), flows through `RuntimeOptions.ResidencyBudgets``AlphaScratchBudgetProfile.Create``RetailAlphaQueue` ctor (`GameWindow.cs:721-725`) |
| `ACDREAM_RESIDENCY_ANIMATION_ENTRIES` | `=<int>` (`>0`, else default) | Entry-count ceiling for the retained animation-data cache | Changes cache eviction cadence for animation data — a perf/memory measurement under a non-default value is not representative | 512 | `ResidencyBudgetOptions.Parse` (`:82-84`) |
| `ACDREAM_RESIDENCY_ANIMATION_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for the retained animation-data cache | Same cache-behavior-change caveat as the entries variant | 64 MiB | `ResidencyBudgetOptions.Parse` (`:79-81`) |
| `ACDREAM_RESIDENCY_AUDIO_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for the retained audio-buffer cache | Shrinking can force more frequent audio buffer re-decode/eviction | 32 MiB | `ResidencyBudgetOptions.Parse` (`:85-87`), consumed by `ContentEffectsAudioComposition.cs` |
| `ACDREAM_RESIDENCY_COMPOSITE_PHYSICAL_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for physically-resident composite (character palette/texture) GPU memory | Changes composite-texture eviction pressure — not representative of production if varied during a measurement run | 128 MiB | `ResidencyBudgetOptions.Parse` (`:67-69`), consumed by `TextureCache.cs` |
| `ACDREAM_RESIDENCY_COMPOSITE_UNOWNED_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for unowned/retained (not currently referenced) composite textures kept for reuse | Same eviction-pressure caveat | 64 MiB | `ResidencyBudgetOptions.Parse` (`:70-72`), consumed by `TextureCache.cs` |
| `ACDREAM_RESIDENCY_MESH_GPU_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for GPU-resident object mesh data | The single largest residency budget (1024 MiB default) — shrinking it directly forces more mesh re-upload/eviction; do not vary during an FPS/GPU-memory measurement run | 1024 MiB | `ResidencyBudgetOptions.Parse` (`:49-51`), consumed by `ObjectMeshManager.cs`/`WbDrawDispatcher.cs` |
| `ACDREAM_RESIDENCY_MESH_STAGING_ENTRIES` | `=<int>` (`>0`, else default) | Entry-count ceiling for the mesh upload staging cache | Changes staging-buffer churn/eviction cadence | 256 | `ResidencyBudgetOptions.Parse` (`:64-66`) |
| `ACDREAM_RESIDENCY_MESH_STAGING_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for the mesh upload staging cache | Same staging-churn caveat | 128 MiB | `ResidencyBudgetOptions.Parse` (`:61-63`) |
| `ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES` | `=<int>` (`>0`, else default) | Entry-count ceiling for unowned (retained-for-reuse) object mesh entries | Changes mesh-cache eviction cadence | 50 | `ResidencyBudgetOptions.Parse` (`:52-54`) |
| `ACDREAM_RESIDENCY_PREPARED_MESH_ENTRIES` | `=<int>` (`>0`, else default) | Entry-count ceiling for the CPU-side "prepared mesh" cache (post-classification, pre-upload) | Changes eviction cadence for prepared-mesh CPU memory | 100 | `ResidencyBudgetOptions.Parse` (`:58-60`) |
| `ACDREAM_RESIDENCY_PREPARED_MESH_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for the CPU-side prepared-mesh cache | Same eviction-cadence caveat | 128 MiB | `ResidencyBudgetOptions.Parse` (`:55-57`) |
| `ACDREAM_RESIDENCY_STANDALONE_UNOWNED_ENTRIES` | `=<int>` (`>0`, else default) | Entry-count ceiling for unowned standalone (non-composite) texture entries | Changes standalone-texture eviction cadence | 256 | `ResidencyBudgetOptions.Parse` (`:76-78`), consumed by `TextureCache.cs` |
| `ACDREAM_RESIDENCY_STANDALONE_UNOWNED_MIB` | `=<int MiB>` (`>0`, else default) | Byte ceiling for unowned standalone texture memory | Same eviction-cadence caveat | 32 MiB | `ResidencyBudgetOptions.Parse` (`:73-75`) |
| `ACDREAM_RETAIL_CHASE` | `=0` disables (anything else/unset = on) | selects the retail-faithful `RetailChaseCamera` vs. the legacy rigid-follow `ChaseCamera` | swaps the entire active camera implementation — changes camera motion/feel; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| true (retail camera on) | `CameraDiagnostics.UseRetailChaseCamera` |
| `ACDREAM_RETAIL_CLOSE_DEGRADES` | inverted: `="0"` disables; any other value (incl. unset) enables | Default-**on** real gameplay behavior: applies retail's close-range LOD mesh-part swap (`GfxObjDegradeResolver`) to humanoid setups (issue #47), matching retail's close-detail degrade. | Inverted default (opposite of every other boolean flag in this table — presence of the literal string `"0"` is what disables it, not presence of `"1"` enabling it). Documented explicitly as "set only for before/after diagnostic comparisons" — so although default-on production behavior, its *disable* path exists purely for A/B measurement. | `true` (enabled) unless value is exactly `"0"` | `RuntimeOptions.RetailCloseDegrades``DatLiveEntityProjectionMaterializer.cs:275-276,480-498` |
| `ACDREAM_RETAIL_UI` | `=1` | Switches on the retained retail UI host tree (`UiHost`/`UiRoot`, D.2b). Without it, no retained UI is composed at all — e.g. no chargen Appearance page, no Summary page. | **Forced to `true` unconditionally** for every `--session-config` / launcher launch (`RuntimeOptions.cs:282`, "a session-config launch IS a product launch — the retail UI is the shipped UI, not a dev option"), regardless of this env var's value — the env var only matters for the bare env-var dev-flow launch path. | `false` for the env-var dev flow; `true` always for `--session-config` launches | `RuntimeOptions.RetailUi``LivePresentationComposition.cs:1108-1131` (gates retained-UI mount via `InteractionRetainedUiComposition`), `GameWindow.cs:455,566` (comments), `RuntimeOptions.cs:277` |
| `ACDREAM_TEST_HOST` | `=<host>` | ACE server hostname for live-mode connect. | none | `"127.0.0.1"` | `RuntimeOptions.LiveHost` (`RuntimeOptions.cs:142`) |
| `ACDREAM_TEST_PASS` | `=<string>` | ACE account password for live-mode connect. | Redacted in `RuntimeOptions.ToString()`/diagnostic printing by design (`PrintMembers` override, `RuntimeOptions.cs:326-342`) — defense-in-depth so it can never leak into a log/exception via the record's default printing. | `null` (empty → `HasLiveCredentials` false) | `RuntimeOptions.LivePass` (`RuntimeOptions.cs:145`) |
| `ACDREAM_TEST_PORT` | `=<int>` | ACE server port for live-mode connect. | none | `9000` | `RuntimeOptions.LivePort` (`RuntimeOptions.cs:143`) |
| `ACDREAM_TEST_USER` | `=<string>` | ACE account name for live-mode connect. | none | `null` (empty → `HasLiveCredentials` false) | `RuntimeOptions.LiveUser` (`RuntimeOptions.cs:144`) |
| `ACDREAM_VULKAN_DEVICE` | `=<int>` (decimal index) or `=<substring>` (case-insensitive device-name match) | Overrides automatic Vulkan physical-device selection (normally: discrete > integrated > virtual > CPU, tie-broken by device-local heap size) — for multi-GPU machines. | A bare-digits value is matched as an index ONLY (never falls through to substring match) specifically because digits like `"7"` are substrings of real device names ("AMD Radeon RX 9070 XT") — a fallback would silently select the wrong device by coincidence. An override matching nothing falls back to the automatic choice (does not fail startup) and records why in the capability report. | `null` → automatic ranked choice | `RuntimeOptions.VulkanDeviceOverride``VulkanPhysicalDeviceSelection.Choose` (`VulkanPhysicalDeviceSelection.cs:54-100`), consumed at `VulkanGraphicsContext.cs:207,322` |
## Command-line arguments
### `AcDream.App`
| Arg | What it does | Side effects |
|---|---|---|
| `<dat-directory>` (positional) | Dat directory; outranks `ACDREAM_DAT_DIR`. | Not read at all once `--session-config` is present. |
| `--session-config <path>` | The launcher's launch path: endpoint, account, credential reference, character selector, status file, plugins, login commands. | **Overrides `ACDREAM_LIVE` and every `ACDREAM_TEST_*`** (logged at startup). Diagnostic flags stay env-controlled. Missing value is a startup error. |
### `AcDream.Headless`
Its usage banner matches the parser exactly. `validate` loads and checks a
config without connecting; `run` connects.
| Arg | What it does | Side effects |
|---|---|---|
| `validate` \| `run` (positional) | Selects the mode; must be the first argument. | Anything else is a parse error. |
| `--config <path>` | The versioned headless session-configuration document. Required. | — |
| `--config-dir` / `--data-dir` / `--cache-dir` `<path>` | Override each portable path root. | Merged over the config document's own `process.paths`; the command line wins. |
| `-user` / `--user`, `-password` / `--password` | Direct single-session credentials, bypassing the config's credential source. | Plaintext in the process command line — prefer the config's credential reference. |
| `--help` / `-h` (or no args) | Prints usage, exits 0. | — |
### `AcDream.Launcher`
| Arg | What it does | Side effects |
|---|---|---|
| `--verify-publish` | Packaging smoke probe: parses arguments and exits 0 without opening a display or resolving user paths. | — |
| `--config-dir` / `--data-dir` / `--cache-dir` `<absolute path>` | Override each path root. | **All three or none** — supplying a subset is an error. Must be absolute. |
| `--update-manifest-uri <uri>` | Points the self-updater at a different release manifest (test-feed seam). | Must be `https://` (or loopback `http://`). Changes where updates come from — do not point a real install at a test feed. |
| `--acdream-self-update-helper-v1`, `--acdream-self-update-confirm-v1` | Internal re-exec markers for the self-update handoff. | Not user-facing; never pass these by hand. |
### `AcDream.Cli`
A dat-dump and measurement tool dispatched by a positional subcommand
(`args[0]`); no `--flag` options. Most subcommands take a dat directory and
fall back to `ACDREAM_DAT_DIR`.
- **Measurement:** `summarize-frame-history <frames.csv> <checkpoints.jsonl> <markers.log> <out.json>`,
`compare-screenshots <expected.png> <actual.png> <out.json> [channelTolerance=2] [maxDifferentFraction=0.001] [mask.png]`,
`probe <in.png> <x0> <y0> <x1> <y1>`.
- **Dat inspection:** no subcommand (asset-type inventory), `dump-vitals-bars`,
`dump-vitals-layout [0xLayoutId]`, `list-ui-layouts [0xRootType]`,
`dump-sprite-sheet <0xId,...>`, `dump-font-atlas [0xFontId] [sample] [outBase]`,
`dump-edges <0xId>`, `export-ui-sprite <0xId> [out.png]`.
- **Mockup rendering:** `render-vitals-mockup [out.png]`, `mock-selbar [out.png]`,
`crop <in.png> <x> <y> <w> <h> <zoom> <out.png>`.
## Measurement and profiling
| Flag | Value | What it does | Side effects | Default | Read by |
|---|---|---|---|---|---|
| `ACDREAM_CAPTURE_RESOLVE` | `=<path>` | appends one JSON-Lines record (full before/after `PhysicsBody` snapshot) per player-side `ResolveWithTransition` call, filtered to `IsPlayer` movers | real per-tick allocation (snapshot object graph + `System.Text.Json` serialize) and buffered file I/O (`AutoFlush=false`) for the local player only; will skew any perf measurement of local-player physics while active; feeds `CellarUpTrajectoryReplayTests` fixtures | unset (off) | `AcDream.Core.Physics.PhysicsResolveCapture` (`CapturePath`) |
| `ACDREAM_COLLISION_SHADOW_DIR` | `=<dir>` | output directory for Slice I5 graph/flat collision-shadow mismatch artifacts | only takes effect when `ACDREAM_COLLISION_SHADOW_EVERY>0`; directory creation + file writes on mismatch | `<CurrentDirectory>/.test-out/collision-shadow` | `PhysicsDiagnostics.CollisionShadowArtifactDirectory` |
| `ACDREAM_COLLISION_SHADOW_EVERY` | `=<positive int>` | when >0 and the cache is constructed with `requirePreparedCollision:false`, arms a `CollisionShadowVerifier` that re-runs the graph-vs-flat collision referee every Nth traversal entry (`PhysicsDataCache` ctor) | extra CPU on sampled ticks + mismatch-artifact file I/O; graph path stays authoritative regardless of mismatch (doc-asserted, not independently verified here) — does not change production physics results, but does add work when active | `0` (disabled) | `PhysicsDiagnostics.CollisionShadowSampleEvery` (parsed via `ParsePositiveInt`, non-positive → 0) |
| `ACDREAM_DAY_GROUP` | `=<int>` | Forces Dereth's day-group (weather preset) selection instead of the retail hash-based pick, "useful for visually A/B-testing each weather preset against retail" (own doc comment). | **Dead second read**: the `SkyDescLoader.cs:252` raw read only feeds `SelectDayGroupIndex`, which is only called from `ActiveDayGroup(double)` and the `DefaultDayGroup` property — and grepping all of `src/` finds **zero production call sites** for either. That whole path is unreachable; only the typed `RuntimeOptions.ForcedDayGroupIndex` → Runtime path is live. Bounds differ too: the typed path only checks `>= 0` (`TryParseNonNegativeInt`) and Runtime clamps out-of-range to `null`; the dead Core-layer path checks `forced >= 0 && forced < DayGroups.Count` directly. `SkyState.cs:400,403` are doc-comment mentions only, not reads. | unset → normal server/date-driven hash selection | `RuntimeOptions.ForcedDayGroupIndex` (typed) → `GameWindow.cs:718``WorldEnvironmentController``RuntimeWorldEnvironmentState` (Runtime, live path); **also** raw `Environment.GetEnvironmentVariable` at `SkyDescLoader.cs:252` (Core layer, separate parse) |
| `ACDREAM_DISABLE_TIER1_CACHE` | `="1"` (ordinal exact match; anything else = enabled) | A/B diagnostic that forces **every** static (non-animated) entity through the slow per-entity classification path, bypassing the Tier-1 classification cache (`#53`) | Materially changes per-frame CPU cost for entity classification — a perf/FPS measurement taken with this set is NOT representative of production and must not be compared against a normal run | unset (cache enabled) | `WbDrawDispatcher` ctor field `_tier1CacheDisabled` (`WbDrawDispatcher.cs:473-474`) |
| `ACDREAM_FRAME_HISTORY` | `=<path>` | opts into a per-frame CSV history capture (frame idx, timestamps, per-stage CPU us, GPU us, alloc bytes) alongside the aggregated 5 s `[frame-prof]` report | allocates a `List<FrameHistoryRecord>` with ~131,072-record (~9 MiB) initial capacity, growing further for longer captures (~72 B/record, ~43 MB/hour at 165 fps) held in memory for the whole run; CSV write happens ONLY at `Dispose`/shutdown (no frame-thread I/O); only takes effect while `ACDREAM_FRAME_PROF` is ALSO on | unset (off) | `RenderingDiagnostics.FrameHistoryPath` / `AcDream.App.Diagnostics.FrameProfiler` |
| `ACDREAM_ORBIT_DISTANCE_METERS` | `=<float>`, must be finite and `>0` | Diagnostic-only initial distance for the offline orbit camera, so deterministic renderer acceptance captures land inside a finite shadow reach. | Own doc comment: "used by deterministic renderer acceptance captures." Rejects non-finite/non-positive values silently (parses to `null`, camera default used). | `null` (unset) → normal camera default | `RuntimeOptions.InitialOrbitDistanceMeters``GameWindow.cs:1412` → offline orbit-camera composition |
| `ACDREAM_ORBIT_PITCH_DEGREES` | `=<float>`, clamped `[-89, 89]` | Diagnostic-only initial orbit camera elevation. | Values outside `[-89,89]` or non-finite are silently rejected (→ `null`, default kept) rather than clamped. | `null` | `RuntimeOptions.InitialOrbitPitchDegrees``GameWindow.cs:1414` |
| `ACDREAM_ORBIT_YAW_DEGREES` | `=<float>`, must be finite | Diagnostic-only initial orbit camera heading. | Non-finite values silently rejected (→ `null`). | `null` | `RuntimeOptions.InitialOrbitYawDegrees``GameWindow.cs:1413` |
| `ACDREAM_PROBE_REVEAL_RADIUS` | `=<int>=1` (unparsable or `<1` → override absent; floor is 1, not 0) | #280 A/B measurement probe: forces the OUTDOOR reveal gate to use this landblock radius instead of the derived streaming window (near radius clamped to it), so a route can be measured with the pre-#280 behavior (`=1`, old `OutdoorNeighborhoodRadius`) vs. current | **Changes what gets revealed, not just measured** — genuinely resizes the reveal/visible window used by the live reveal gate. CLAUDE.md: "Leave it unset for any measurement or gate run — with it set you are measuring a different window than production." `=0` is rejected by the parser specifically because it would hang the very A/B route it exists to measure (`RequiredRenderRadius==0` fails `invalid-readiness-shape`). Not a user setting, not in Settings/RuntimeOptions, not persisted. | unset (derivation in charge, no override) | `StreamingDiagnostics.RevealRadiusOverride` (`StreamingDiagnostics.cs:25-27,76-80`), applied by `StreamingDiagnostics.ApplyRevealRadiusOverride` |
| `ACDREAM_PROBE_WORLD_FRAME` | `=1` | gates one `[world-frame] agree` line per projected conversion in `DatLiveEntityProjectionMaterializer`, recording the world-frame center both `LiveWorldOriginState` (App) and Runtime's physics-state owner used (issue #283, "measurement only; it never gates placement") | print-only | off | `PhysicsDiagnostics.ProbeWorldFrameEnabled` |
| `ACDREAM_SKY_PHASE_SECONDS` | `=<float>` (any finite value; negative accepted, taken mod 1 per axis) | Campaign V slice V7 instrument-determinism pin: freezes the sky's cloud-sheet UV scroll to a fixed elapsed-seconds value instead of wall-clock time, so two launches of a differential/offline gate agree about cloud position. | **Non-obvious dual effect**: this ONE var pins TWO independently-designed clocks that happen to share a name-adjacent purpose — the sky renderer's cloud scroll (`SkyRenderer.AnimationPhaseSecondsOverride`) AND, since Campaign VM slice VM6, the atmospheric post-process graph's foliage-wind clock (`_windClockSecondsOverride`). A gate that only knows about "sky clouds" and sets this to freeze them will *also* freeze foliage-wind evolution — deliberately snapped-to-target on the first advance per an A6 review fix, but still a second surface a naive reader wouldn't expect this var to touch. Distinct from `ACDREAM_DAY_GROUP`/`ACDREAM_WORLD_TIME`, which pin the OTHER sky clock (day group/sun angle) — retail's clouds drift independently of the calendar date by design. | `null` → wall-clock driven (every ordinary run) | `RuntimeOptions.SkyAnimationPhaseSeconds``SkyRenderer.cs:79,85` (cloud UV scroll) **and** `AtmosphericPostProcessGraph.cs:560,586,671` (foliage-wind clock) |
| `ACDREAM_STREAM_WORK_COMPLETIONS` | `=<int>` (`>0`, else default) | Per-frame ceiling on streaming completion admissions on the update thread | Class doc comment states explicitly: this whole `ACDREAM_STREAM_WORK_*` family "exists for A/B measurement only" — not a user/production setting. Directly changes streaming throughput per frame; do not compare a measurement taken with this set against a default run. | 64 | `StreamingWorkBudgetOptions.Parse` (`StreamingWorkBudgetOptions.cs:56-58`) |
| `ACDREAM_STREAM_WORK_CPU_MIB` | `=<int MiB>` (`>0`, else default) | Per-frame ceiling on adopted (newly resident) CPU bytes on the update thread | A/B-measurement-only family; changes per-frame CPU admission budget | 8 MiB | `StreamingWorkBudgetOptions.Parse` (`:59-61`) |
| `ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT` | `=<float percent>`, exclusive `0 < x < 100`, else default; stored as fraction (`percent/100`) | Fraction of the per-frame work budget reserved for the active reveal destination lane vs. background streaming | A/B-measurement-only family; reallocates frame budget between destination-lane and background streaming work, changing reveal-latency characteristics | 0.75 (75%) | `StreamingWorkBudgetOptions.Parse`/`ParseReservePercent` (`:71-73,154-169`) |
| `ACDREAM_STREAM_WORK_ENTITY_OPS` | `=<int>` (`>0`, else default) | Per-frame ceiling on entity-cursor operations (small ops, e.g. one dictionary/index write each) on the update thread | A/B-measurement-only family. Doc comment: elapsed-time ceiling (`ACDREAM_STREAM_WORK_MS`) remains the authoritative CPU guard — this is a secondary cap, deliberately loose (leaves >90% of the time budget unused at default) | 4,096 | `StreamingWorkBudgetOptions.Parse` (`:62-64`) |
| `ACDREAM_STREAM_WORK_GL_RETIRE_OPS` | `=<int>` (`>0`, else default) | Per-frame ceiling on GL/GPU resource-retirement operations on the update thread | A/B-measurement-only family; changes retirement cadence, which changes when GPU memory is actually reclaimed | 64 | `StreamingWorkBudgetOptions.Parse` (`:68-70`) |
| `ACDREAM_STREAM_WORK_GPU_MIB` | `=<int MiB>` (`>0`, else default) | Per-frame ceiling on GPU upload bytes on the update thread | A/B-measurement-only family; directly changes per-frame upload throughput | 8 MiB | `StreamingWorkBudgetOptions.Parse` (`:65-67`) |
| `ACDREAM_STREAM_WORK_HOLD_DEST_MS` | `=<double ms>` (`>0` and finite, else default `8.0`) | Absolute (not quality-scaled) time ceiling for destination-lane work during a portal/login hold; never shrinks a profile whose own ceiling is already ≥ this value | Explicitly documented as "NOT a user setting... exists for A/B measurement only, matching the rest of the `ACDREAM_STREAM_WORK_*` family" — do not set outside a deliberate hold-latency A/B comparison | 8.0 ms | `StreamingWorkBudgetOptions.Parse` (`:74-76`); `HoldDestinationCeilingMilliseconds` widens the frame meter via `StreamingWorkBudget.WidenForDestinationHold` while a destination reservation hides the world behind the authored tunnel (#418) |
| `ACDREAM_STREAM_WORK_MS` | `=<double ms>` (`>0` and finite, else default) | Per-frame elapsed-time ceiling for update-thread streaming work — "the authoritative CPU guard" per the entity-ops comment | A/B-measurement-only family; this is the primary per-frame time budget for streaming — changing it changes both perceived streaming latency and measured frame cost | 2.0 ms | `StreamingWorkBudgetOptions.Parse` (`:53-55`) |
| `ACDREAM_UNCAPPED_RENDER` | `=1` | Removes the normal VSync/refresh-rate software pacer, so the render loop runs as fast as the GPU/CPU allow. | Own doc comment (`RuntimeOptions.cs:147-150`): "Normal presentation is always bounded by VSync or a refresh-rate software pacer. This explicit diagnostic is the sole way to measure truly uncapped renderer throughput." Not representative of what a real player experiences — exists purely for throughput measurement. | `false` → VSync/pacer-bounded | `RuntimeOptions.UncappedRendering``GameWindow.cs:765``DisplayFramePacingController`; also `VulkanBringUpHost.cs:75` |
| `ACDREAM_WB_DIAG` | `=1` (raw `string.Equals` ordinal compare) | (a) `GameWindow`: gates the `[FRAME-DIAG]` render-thread entity-upload-distribution report; (b) `WbDrawDispatcher`: gates `BeginRhiTimer`/`SampleRhiTimers`, wrapping the opaque/detail/transparent draw passes in extra Vulkan GPU timer-scope queries and periodically logging a `[WB-DIAG]` CPU/GPU median/p95 report | adds extra per-pass GPU timestamp queries every frame while on — genuine measurement overhead; NOT read through `RenderingDiagnostics` or any diagnostics-owner class, unlike every other flag in this set — flag for whitelisting (see Notes #2); the flag's supposed interaction with `ACDREAM_FRAME_PROF`'s GPU query is stale documentation (see Notes #1) | unset (off) | read directly at `WbDrawDispatcher.cs:2061-2064` (every `Draw()`/`BeginEntityDispatch` call, i.e. effectively per frame, NOT cached) and cached once as a readonly field at `GameWindow.cs:153-156` |
| `ACDREAM_WORLD_TIME` | `=<float>`, accepted only in `[0, 1)` | Campaign V slice V7 instrument-determinism pin: freezes the Dereth day fraction (and therefore sun direction, sky keyframe, and every lit surface) instead of following the server clock. | Outranks BOTH the server `TimeSync` clock and the `/time` slash command's `SetDebugTime` (which is deliberately transient — the next `TimeSync` clears it); this pin does not clear. Distinct axis from `ACDREAM_DAY_GROUP` (day-group/weather-preset selection) and `ACDREAM_SKY_PHASE_SECONDS` (cloud scroll + foliage wind) — the calendar DATE still advances, only the intra-day fraction freezes. Anything outside `[0,1)` (including negative, unparseable, or unset) leaves the server clock alone entirely — no partial/clamped behavior. | `null` → server clock | `RuntimeOptions.PinnedWorldDayFraction``GameWindow.cs:720``WorldEnvironmentController``Runtime.WorldTime.PinnedDayFraction` |
## Automation
A scripted route run adds three things at once — a session config so the
client self-selects a character, a route script, and an artifact directory:
```powershell
$env:ACDREAM_UI_PROBE_SCRIPT = "$scratch\route.txt"
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = "$scratch\artifacts"
$env:ACDREAM_FRAME_PROF = "1"
$env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
& $exe --session-config "$scratch\session.json"
```
**Two traps this recipe exists to document:**
1. **Without `--session-config`, the client stops at character select** and
the route never runs. The session JSON supplies the endpoint, account,
and a character `index` for auto-selection.
2. **`ACDREAM_AUTOMATION_ARTIFACT_DIR` is not free.** It constructs the
render-scene oracle, which fingerprints every resident entity every
frame. The allocation cost was fixed in
[#432](ISSUES.md), but the CPU walk remains — automation-run frame
rates are diagnostics-loaded and must only be compared against other
automation runs, never against a plain run. Some route verbs
(`wait world-*`) additionally do nothing unless this is set.
| Flag | Value | What it does | Side effects | Default | Read by |
|---|---|---|---|---|---|
| `ACDREAM_AUTOMATION_ARTIFACT_DIR` | `=<path>` | Output directory for the retail-UI automation probe's checkpoint JSON + screenshot PNG artifacts; gates whether the full `WorldLifecycleAutomationController` (checkpoint/screenshot/render-pack-automation capable) is composed at all vs. the cheaper facts-only `WorldRevealFactsAutomationRuntime` fallback (`wait world-ready/visible` verbs work either way per issue #415's fix; checkpoint/screenshot verbs report "requires ACDREAM_AUTOMATION_ARTIFACT_DIR" without it). | **Known #432 surprise, confirmed still live**: `FrameRootComposition.cs:349-353``AutomationArtifactDirectory is not null` (together with `RetainedUi?.Screenshots is not null`) unconditionally constructs a `CurrentRenderSceneOracle` **and** a `RenderSceneShadowComparisonController` — a per-frame diagnostics referee — regardless of whether any checkpoint/screenshot is ever actually requested that session. Merely setting this var for its "just an output path" purpose pays the per-frame comparison cost for the whole run. | unset (null) → facts-only automation runtime, no per-frame referee constructed | `RuntimeOptions.AutomationArtifactDirectory``FrameRootComposition.cs:351,543-627`, `WorldLifecycleAutomationController.cs`, `RetailUiAutomationScriptRunner.cs:108` |
| `ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER` | `=1` | Forces the graphical host to use the persisted display resolution as the *initial* size of a **borderless** window at creation, so the OS window manager cannot clamp a decorated window to the desktop work area — needed for pixel-exact automated screenshot comparison. | Changes window chrome (borderless) at startup — a visible difference from an ordinary launch, not just an internal measurement knob. | `false` → normal decorated window | `RuntimeOptions.ExactAutomationFramebuffer``GameWindow.cs:852` (`CreateStartupWindowOptions`) |
| `ACDREAM_BAKE_PUBLISH_NONCE_V1` | `=<32-hex GUID "N" format>` | Launcher-to-bake-child authorization token: when present and valid, the bake child takes a cross-process publish file lock + writes an authorization file before atomic publication (serializes with launcher recovery) | If present but fails `IsValidNonce` (not a 32-char Guid "N"), throws `InvalidOperationException` and aborts the bake. When absent, bake runs unguarded (standalone mode). Never set this manually outside the launcher's own child-process spawn. | unset (standalone unguarded bake) | `BakePublicationGuardPaths.cs:12`, read by `BakePublicationGuard.AcquireIfRequested` (`AcDream.Bake/BakePublicationGuard.cs:18`); set by `BakeProcessRunner.cs:150/162` |
| `ACDREAM_NET_DROP_DIR` | `="out"`/`"in"`/anything-else (incl. unset) → `Both` (case-insensitive) | Selects which direction(s) — outbound, inbound, or both — the deterministic loss-injection decorator drops | Only takes effect when `ACDREAM_NET_DROP_PCT>0` (decorator is structurally absent otherwise). Drives real datagram loss on the live connection — the injection point for `tools/run-connected-loss-gate.ps1`. Never set during a normal/measurement run. | `Both` | `NetDiagnostics.NetDropDir` (`NetDiagnostics.cs:88-90,98-104`), consumed by `LossyTransportDecorator.WrapIfConfigured` (`Transport/LossyTransportDecorator.cs:21-22`), also read at `WorldSession.cs:901-907` (comment only) |
| `ACDREAM_NET_DROP_PCT` | `=<int 0-100>` (out-of-range or unparsable → 0) | Percent chance (post-handshake-arming, per droppable datagram) that the deterministic `LossyTransportDecorator` drops a packet in the configured direction(s) | **Fault injection.** `>0` wraps the real socket transport in a packet-dropping decorator for the whole session — genuinely breaks/delays delivery to exercise N1-N4 reliable-transport recovery. At 0 the decorator is never constructed (zero structural cost). Must be 0/unset for any normal run or non-loss-gate measurement. | `0` (off, decorator absent) | `NetDiagnostics.NetDropPercent` (`NetDiagnostics.cs:60-69,92-96`), consumed by `LossyTransportDecorator.WrapIfConfigured` (`:21`), wired at `WorldSession.cs:901-907` |
| `ACDREAM_NET_DROP_SEED` | `=<int>` (unparsable → `1`) | PRNG seed for the loss decorator (outbound seeded with `seed`, inbound with `~seed`) — same seed reproduces an identical drop pattern | Only matters when `ACDREAM_NET_DROP_PCT>0`; makes fault injection deterministic/reproducible for the connected loss gate | `1` | `NetDiagnostics.NetDropSeed` (`NetDiagnostics.cs:75-82`), consumed by `LossyTransportDecorator` (`:21-22`) |
| `ACDREAM_OPEN_CHARGEN` | `=1` | Campaign CC slice CC4 interim env/test-only seam: opens the character-creation screen (`gmCharGenMainUI`) automatically once Runtime's chargen view goes active, bypassing the real retail Create-Character-button transition. Fires once per mount (`_openOnStartConsumed` latch). | Own doc comment explicitly calls this "interim env/test-only" — Campaign CC (closed 2026-08-16, user-accepted) later wired the real Create button with its roster&lt;55-slot ghost gate, so this flag is now a bypass of that gate for automation/testing rather than the only way in. | `false` | `RuntimeOptions.OpenCharacterCreationOnStart``CharacterCreationUiController.cs:21,524-530` |
| `ACDREAM_UI_PROBE_DUMP` | `=1` | Enables the retail-UI automation probe's diagnostic dump path and feeds `RetailUiProbeBindings`/`RetailUiAutomationScriptRunner`. Also part of `RuntimeOptions.UiProbeEnabled` (`UiProbeDump \ | \ | UiProbeScript is set`). | `RuntimeOptions.UiProbeDump``LivePresentationComposition.cs:1465-1495`, `InteractionRetainedUiComposition.cs:1092-1100` |
| `ACDREAM_UI_PROBE_SCRIPT` | `=<path>` | Path to a script file the `RetailUiAutomationScriptRunner` executes against the retained UI (pointer/semantic-input command playback) for scripted UI regression testing. | Also flips `RuntimeOptions.UiProbeEnabled` true even without `ACDREAM_UI_PROBE_DUMP=1`. | `null` | `RuntimeOptions.UiProbeScript``InteractionRetainedUiComposition.cs:1094` |
| `ACDREAM_VULKAN_FORCE_UNSUPPORTED` | `=<feature-name>` (case-insensitive property name, e.g. `MultiDrawIndirect`) | Test knob (Slice V5): clears one named required Vulkan feature from the capability record to synthetically fail the gate, so the `NotSupportedException` → exit-code-4 → report path can be exercised on hardware that actually supports everything. | Deliberately breaks Vulkan startup when set to a matched feature name — this is a "make it fail on purpose" gate-testing flag, never appropriate for a normal or measurement run. | `null` → real capabilities used unmodified | `RuntimeOptions.VulkanForcedUnsupportedFeature``VulkanCapabilityRecord.Without` (`VulkanCapabilityRecord.cs:113-119`), consumed at `VulkanGraphicsContext.cs:339` |
| `ACDREAM_VULKAN_PROBE` | `=1` | Runs the standalone Vulkan capability-probe/bring-up harness (opens its own window, runs the capability gate, presents synthetic V6c/V6d verification scenes, captures one screenshot) **instead of** the real client composition host, then exits. | This flag ALONE gates entry (`GameWindow.cs:828`); the former `ACDREAM_RENDER_BACKEND=vulkan` co-requisite died with the OpenGL backend (its class doc was corrected 2026-08-24). | `false` → normal composition host | `RuntimeOptions.VulkanCapabilityProbe``GameWindow.cs:828``VulkanBringUpHost` |
| `ACDREAM_VULKAN_PROBE_FRAMES` | `=<int>` (non-negative) | Bounds the bring-up probe harness to N presented frames so it can run unattended in CI, instead of presenting until a human closes the window. | The frame budget never cuts a pending screenshot capture short — the loop stays open until the screenshot has been attempted even past the budget, so an unattended run's whole product (a PNG) is guaranteed. Zero (unset/unparseable/explicit `0`) keeps the interactive wait-for-close behavior. | `0` → interactive (wait for window close) | `RuntimeOptions.VulkanCapabilityProbeFrames``VulkanBringUpHost.cs:141-249` |
| `ACDREAM_DUMP_MOVE_TRUTH` | `=1` | Emits one `move-truth OUT` line per outbound movement record (MoveToState / AutonomousPosition): local resolved position vs the wire position/cell, ground contact, velocity (`MovementTruthDiagnosticController`). | **Automation apparatus, NOT a spent probe** — the canonical nine-stop soak (`tools/run-connected-r6-soak.ps1`) hard-gates on ≥2 of these lines per destination as its proof that production input produced outbound movement traffic; deleting it fails the soak at every stop (#437, deleted-and-restored 2026-08-24). Print volume follows the outbound send cadence. | off | `RuntimeOptions.DumpMoveTruth``GameWindow.cs``MovementTruthDiagnosticController` |
## Permanent diagnostics
| Flag | Value | What it does | Side effects | Default | Read by |
|---|---|---|---|---|---|
| `ACDREAM_CAPTURE_PLAYER_QUANTA` | `=<path>` (any non-whitespace path) | Opt-in JSON-Lines trace of every admitted player physics quantum (position/orientation/velocity/contact-plane snapshots at each stage boundary of `CPhysicsObj::UpdateObjectInternal`) | Appends+flushes one JSON line per physics quantum to the file (real file I/O on the physics tick when enabled); disabled path costs one static string null/empty check, no allocation. Read once into a mutable static property (settable via `ResetForTest`) rather than a typed options object. | unset (disabled, zero-alloc) | `PlayerPhysicsQuantumCapture` static class (`AcDream.Runtime/Gameplay/PlayerPhysicsQuantumCapture.cs:22`) |
| `ACDREAM_DUMP_MOTION` | `=1` | prints `UM`/`[UM_STALE]`/`[MOTIONDONE]`/`VU.land`/raw-hex wire dump lines tracing inbound `UpdateMotion` handling, remote ground-contact edges, and motion-done callbacks (bug-a/#32 stuck-cast subthread is temporary; core trace is long-lived) | print-only, but the raw-site reads in `LiveEntityNetworkUpdateController.cs` and `UpdateMotion.cs` fire on EVERY inbound motion/UM event (not cached) — `Environment.GetEnvironmentVariable` call per packet even when off; `UpdateMotion.cs`'s branch additionally builds a `StringBuilder` hex dump when on. Rule-5 violation (raw reads outside a diagnostics-owner class) at 5+ call sites | off | THREE independent readers: `PhysicsDiagnostics.DumpMotionEnabled` (owner, appears unconsumed — see Notes), `AnimationPresentationDiagnostics.FromEnvironment()` (App owner record, cached once at startup, consumed by `LiveEntityAnimationPresenter`), and raw `Environment.GetEnvironmentVariable` reads scattered across `LiveEntityNetworkUpdateController.cs` (4 sites) + `Core.Net/Messages/UpdateMotion.cs:163` + `Runtime/Physics/RuntimeRemotePhysicsUpdater.cs:630` |
| `ACDREAM_DUMP_PLAYSCRIPT` | `="1"` (ordinal) | Traces PhysicsScript playback: missing/empty script resolution, malformed `StartTime` entries, and other `[pes]`-prefixed hook-dispatch events | print-only (`Console.WriteLine`) at all 4 use sites (`:85-86,136,300,328`) | unset (off) | `PhysicsScriptRunner.DiagEnabled` (`PhysicsScriptRunner.cs:61-62`) — per-instance settable property seeded from the env var, not a shared static diagnostics-owner class |
| `ACDREAM_DUMP_SURFACES` | `="1"` (ordinal) | One-shot (per session) surface-format histogram dump for the atlas-opportunity audit — fires once after `_dumpFrameCounter>=600` OnRender ticks AND `_uploadMetadata.Count>=100` uploaded textures; writes to the host diagnostics directory | Doc comment claims "Zero cost when off" but `_uploadMetadata[name]=(w,h,fmt)` (`TextureCache.cs:1042`) is written **unconditionally on every texture upload regardless of the flag** — real (small) always-on dictionary-write cost. `TickSurfaceHistogramDumpIfEnabled` also re-reads `Environment.GetEnvironmentVariable` every OnRender frame (not cached) until the one-shot fires. Dump-write failures are caught and logged to stderr, not fatal. | unset (off) | `TextureCache` (`TextureCache.cs:102-113` fields, gate at `TextureCache.cs:802-812`, dump at `TextureCache.cs:814-829`), Phase N.6 slice 1 |
| `ACDREAM_FRAME_PROF` | `=1` | master toggle for the frame profiler: CPU frame time, GPU time samples, per-stage CPU attribution, per-frame alloc/GC, `[frame-prof]` report every ~5 s (doc: "permanent apparatus ... do not strip with session probes") | when on, samples `GC.GetAllocatedBytesForCurrentThread()` and stage-scope timing every frame (cheap, by design); its own XML doc claims a GPU-query self-disable tied to `ACDREAM_WB_DIAG=1` that `FrameProfiler.cs` says no longer exists — see Notes #1; startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.FrameProfEnabled` / `FrameProfiler` |
| `ACDREAM_PROBE_ENVCELL` | `=1` | emits one `[envcells]` line per indoor frame: `CellsRendered`/`TrianglesDrawn` + ourBldgs/otherBldgs/filter counts (phase a8 relic; its own render pass was removed but the probe was kept) | print-only; implicitly turned on whenever `ACDREAM_PROBE_VIS` is on (getter is `_probeEnvCellEnabled \ | \ | `RenderingDiagnostics.ProbeEnvCellEnabled` (backing field OR'd with `ProbeVisibilityEnabled`) |
| `ACDREAM_PROBE_INDOOR_ALL` | `=1` | master switch that reads as AND / writes as cascade across Walk, Lookup, Upload, Xform, Cull | print-only (every underlying probe is print-only); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.IndoorAll` (cascades to the 5 flags below) |
| `ACDREAM_PROBE_INDOOR_CULL` | `=1` (also set by `ACDREAM_PROBE_INDOOR_ALL=1`) | emits `[indoor-cull]` per culled cell entity with cull reason (visibleCellIds-miss / frustum / landblock) | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorCullEnabled` |
| `ACDREAM_PROBE_INDOOR_LOOKUP` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-lookup]` per visible cell entity/sec: render-data hit/miss, IsSetup, parts-hit/parts-miss tallies | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorLookupEnabled` |
| `ACDREAM_PROBE_INDOOR_UPLOAD` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-upload]` requested/completed lines per EnvCell id at `WbMeshAdapter`'s staged-drain time | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorUploadEnabled` |
| `ACDREAM_PROBE_INDOOR_WALK` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-walk]` per visible cell entity/sec: world position, parent cell, landblock/AABB-visible flags, "drew" flag | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorWalkEnabled` |
| `ACDREAM_PROBE_INDOOR_XFORM` | `=1` (also via `ACDREAM_PROBE_INDOOR_ALL`) | emits `[indoor-xform]` per visible cell entity/sec: cell-geometry SetupPart's composed world-matrix translation | print-only; startup-only (its DebugPanel/DebugVM mirror is unreachable — #434) | false (off) | `RenderingDiagnostics.ProbeIndoorXformEnabled` |
| `ACDREAM_PROBE_LOGIN_FRAMES` | `="1"` | Per-completed-frame login/portal-wormhole presentation classification (`world`/`tunnel`/`black`/`void`); logs `[login-frames]` on each classification transition | print-only. "Not a user setting; not in RuntimeOptions; not persisted" (doc comment). | unset (off) | `RenderPresentationDiagnostics.ProbeLoginFrames` (`LoginPresentationFrameProbe.cs:28-29`), consumed by `LoginPresentationFrameProbe.Process` |
| `ACDREAM_PROBE_NET` | `="1"` | Emits `[net-out]` (per outbound reliable message), `[net-tick]` (1 Hz WorldSession.Tick summary incl. reliable-transport rates), `[net-final]` (cumulative stats at Dispose), and `[cmd-gate]` (generation-gated command rejections) | print-only. Doc comment: "the counters themselves increment unconditionally in `TransportStats`; only the string work is gated" — i.e. the underlying stats tracking has a small always-on cost independent of this flag, but this flag itself gates only string/console formatting. | unset (off) | `NetDiagnostics.ProbeNet` (`NetDiagnostics.cs:56-57`), issue #260 probe family |
| `ACDREAM_PROBE_RESOLVE` | `=1` | gates one structured `[resolve]` line per `PhysicsEngine.ResolveWithTransition` call (in/target/out position+cell, ok-vs-partial, grounded/contact status, wall normal, walkable-polygon validity, responsible entity) (l.2a slice 1, general-purpose resolver probe) | print-only, ~30 Hz per moving entity while on | off | `PhysicsDiagnostics.ProbeResolveEnabled` |
| `ACDREAM_PROBE_REVEAL` | `="1"` | While a reveal destination's composite warmup is incomplete, emits one `[composite-warmup]` line/second: pending queue depth, scan state, upload-budget gate, first few unresolved GfxObj ids | print-only | unset (off) | `NetDiagnostics.ProbeReveal` (`NetDiagnostics.cs:115-116`), issue #260 |
| `ACDREAM_PROBE_REVEAL_TIMING` | `="1"` | Wall-clock attribution of each login/portal reveal hold: `[reveal-timing]` lines for `begin`/first-true readiness edges (render/composites/collision/gate/materialized), 1 Hz progress, and one `SUMMARY` line at viewport reveal; paired low-frequency `[reveal-resource]` snapshots report mesh staging/uploads/arena state, prepared-asset activity, composite warmup/uploads, managed memory, and tracked GPU residency | print-only; the probe object and render-resource sampler are not constructed when unset. When enabled, canonical resource owners are sampled only at begin, readiness edges, 1 Hz progress, and summary—not every frame. Use with `ACDREAM_FRAME_PROF=1` / `ACDREAM_FRAME_HISTORY` for per-frame CPU/GPU/alloc timing. | unset (off) | `StreamingDiagnostics.ProbeRevealTiming`, `RevealTimingProbe`, `RuntimeRenderFrameResourceDiagnosticsSource`, `PublicationTimingProbe` |
| `ACDREAM_PROBE_TUNNEL_FREEZE` | `=1` or `=N` | #419 RenderDoc apparatus: holds the teleport state in stable `Tunnel` after destination readiness and freezes the portal-space animation/roll at frame 72 (`=1`) or an explicit frame 2120 (`=N`); emits one `[tunnel-freeze]` line with the actual frame and retail Setup/animation ids | **behavior-changing diagnostic:** placement, world viewport reveal, and LoginComplete are intentionally withheld until transition cancellation/process exit. For static visual inspection only; never use in a performance or lifecycle measurement. | unset (off) | `StreamingDiagnostics.TunnelFreezeFrame`; consumed by `LocalPlayerTeleportPresentation` and `PortalTunnelPresentation` |
| `ACDREAM_PROBE_SOUND_WIRE` | `="1"` | One line per inbound server Sound event (`0xF750`) and per wire-sound play decision, with the drop reason when nothing plays — used to determine whether missing interior soundscapes are server- or client-side | print-only, consumed at `AudioHookSink.cs:159` and `EntityEffectController.cs:123` | unset (off) | `AudioDiagnostics.ProbeWireSoundsEnabled` (`AudioDiagnostics.cs:20-21`) |
| `ACDREAM_PROBE_USEABILITY_FALLBACK` | `=1` | gates a per-call log of `IsUseableTarget` calls that take the null-useability fallback path (creature/door/lifestone passes) (measures a real ace-vs-retail data gap, not a bug investigation) | print-only; measures how often ACE ships entities without `_useability` set | off | `PhysicsDiagnostics.ProbeUseabilityFallbackEnabled` |
| `ACDREAM_PROBE_VIS` | `=1` | emits `[vis]` line on root-cell CHANGE: visible cell ids, OutsideView poly/plane counts, per-cell plane counts, scissor-fallback count (phase u.2d repurposed the flag; its DebugPanel mirror is unreachable — #434) | print-only; ALSO implicitly enables the separate `ACDREAM_PROBE_ENVCELL` probe (its getter ORs with this flag — see Notes #3); startup-only in practice (its DebugPanel/DebugVM mirror is unreachable — #434)| false (off) | `RenderingDiagnostics.ProbeVisibilityEnabled` |
| `ACDREAM_REMOTE_VEL_DIAG` | `=1` | prints per-UM/per-tick remote-velocity and animation-cycle diagnostic lines; `Runtime/Physics/RemoteMotion.cs` carries diagnostic-only fields (`PrevServerPos`, `PrevServerPosTime`, `MaxRootMotionSpeedSinceLastUP`, `LastOmegaDiagLogTime`) unconditionally on every remote — small fixed per-instance memory regardless of the flag, not gated (long-lived remote-velocity/animation diagnostic, commit a.1) | print-only, but the raw-site reads in `LiveEntityNetworkUpdateController.cs` fire on every UM/tick even when off (rule-5 violation, `Environment.GetEnvironmentVariable` call per event, 6+ call sites) | off | THREE readers: `AnimationPresentationDiagnostics.FromEnvironment()` (App owner record, cached at startup, consumed by `LiveEntityAnimationPresenter` for `[SEQSTATE]`/`[CURRNODE]`/other part-diagnostic lines, throttled to 1/sec/entity) + raw `Environment.GetEnvironmentVariable` reads scattered across `LiveEntityNetworkUpdateController.cs` (6+ sites: `[UM_RAW]`, `[FWD_WIRE]`, `[VEL_DIAG]`, `[UPCYCLE_SRC]`, `[UM_STALE]`) + `RemoteServerControlledVelocityCycle.cs:68` (`[UPCYCLE]`) |
| `ACDREAM_DUMP_CELLS` | `=<comma list of hex cell ids>` | one-shot JSON dump of any cached EnvCell whose id matches the list, to `ProbeDumpCellsPath` (issue #98 fixture capture) — Standing fixture-extraction tooling (A6.P3/#98 lineage) for the physics replay harness; roundtrip-tested. Not investigation-scoped. | file I/O once per matching cell id (no-op on repeat); fixture-generation tool, not a perf-neutral no-op when ids are listed | off/unset | `PhysicsDiagnostics.ProbeDumpCellIds` (`ParseHexIdList`) |
| `ACDREAM_DUMP_CELLS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_CELLS` — Companion output-directory knob for ACDREAM_DUMP_CELLS. | print/file-path only; no effect unless `ACDREAM_DUMP_CELLS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpCellsPath` |
| `ACDREAM_DUMP_GFXOBJS` | `=<comma list of hex GfxObj ids>` | one-shot JSON dump of any cached GfxObj's polygon table + BSP root metadata matching the list, to `ProbeDumpGfxObjsPath` (issue #98 fixture capture) — Standing fixture-extraction tooling (A6.P3/#98 lineage), pair of DUMP_CELLS. | file I/O once per matching id (no-op on repeat) | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjIds` (`ParseHexIdList`) |
| `ACDREAM_DUMP_GFXOBJS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` — Companion output-directory knob for ACDREAM_DUMP_GFXOBJS. | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` |
| `ACDREAM_DUMP_SKY` | `=1` | Print-only: dumps decoded `SkyDesc` raw values on region load (`SkyDescLoader.cs`) and per-GfxObj `Surface.Type`/translucency flags on first upload (`SkyRenderer.cs`), plus gates a `TimeSync` console diagnostic in `GameWindow`. Built to resolve specific open questions about retail sky units and GfxObjReplace timing (2026-04-23 research), now answered but the dumps remain wired. — Generic sky-keyframe isolation dump (introduced with the phase-1 tint revert); a tool, not a bug probe. | Three independent reads of the SAME env var, only one of which (`RuntimeOptions.DumpSky`) goes through the typed options object; the other two are raw scattered reads (see Notes). `SkyRenderer.cs:582`'s raw read is in the App layer and has no architectural excuse for bypassing `RuntimeOptions``_options.DumpSky` was already available to that composition. `print-only` in all three sites. | off/unset | `RuntimeOptions.DumpSky` (typed) → `GameWindow.cs:704` (`TimeSyncDiagnostic`); **also** two independent raw `Environment.GetEnvironmentVariable` reads at `SkyDescLoader.cs:392` (Core) and `SkyRenderer.cs:582` (App) |
| `ACDREAM_DUMP_STEEP_ROOF` | `=1` | gates `[steep-roof] KILL-VELOCITY-APPLIED` in `PhysicsEngine.ResolveWithTransition` when retail's `kill_velocity` zeroes body velocity on steep-slope impact, plus per-frame plane-normal traces in `TransitionTypes`/`PlayerMovementController` — KEEP: observes LIVE divergence-register row AD-56 (the plumb-fall freeze on steep-but-walkable polys, restored 2026-08-07). The only runtime lens on that active divergence; delete only with the AD-56 row itself. | print-only | off/unset | `PhysicsDiagnostics.DumpSteepRoofEnabled` |
| `ACDREAM_HIDE_PART` | `=<int>` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. — Generic model-part isolation tool (issue #37 lineage but general-purpose since); a tool, not a bug probe. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | off/unset | `RuntimeOptions.HidePartIndex``LivePresentationComposition.cs:608``LiveEntityAnimationPresenter.cs:21,38,243` |
| `ACDREAM_PROBE_CELL` | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) — Standing cell-transit tracer (L.2a slice 1), pair of the permanent ACDREAM_PROBE_RESOLVE; recurs in every membership investigation. | print-only; low volume (only on actual cell crossings) | off/unset | `PhysicsDiagnostics.ProbeCellEnabled` |
## Temporary probes
Each row names the issue that owns it. **A temporary probe is deleted in
the same commit as its investigation's fix** — if you find one here whose
issue is closed, the strip was missed; delete both.
> **Probe debt, measured 2026-08-24:** 64 temporary probes existed, citing 21
> distinct issues with 14 already closed. [#435](ISSUES.md) part 1 stripped
> the 17 rows whose investigation had ended without the strip — see the
> Retired section below for their removal record — leaving 47. Part 2
> traced each of the (then-)14 unattributed rows to its introducing commit
> and stripped the 7 that belonged to closed investigations
> (`ACDREAM_A8_DUMP_PV`/Phase A8, `ACDREAM_DUMP_CLOTHING`/#37,
> `ACDREAM_DUMP_EDGE_SLIDE`/#32, `ACDREAM_DUMP_LIVE_SPAWNS`/Phase A8,
> `ACDREAM_DUMP_STEPUP`/L.2.3d-f, `ACDREAM_DUMP_VENDOR`/the vendor
> campaign, `ACDREAM_DUMP_VITALS`/#5). An eighth,
> `ACDREAM_DUMP_MOVE_TRUTH`, was deleted and then RESTORED the same day:
> it turned out to be automation apparatus, not a probe — the canonical
> nine-stop soak hard-gates on its output (see its row under Automation;
> #437 is the record). The rest of the attributed rows were reclassified
> into Permanent diagnostics as standing tools rather than investigation
> probes, leaving **31 temporary probes, every one attributed to an owning
> issue or campaign**. Each still costs a branch on its hot path even when
> unset, and a handful re-read the environment per frame rather than
> caching (see their side-effects column).
| Flag | Owning investigation | Value | What it does | Side effects | Read by |
|---|---|---|---|---|---|
| `ACDREAM_CLIP_DEBUG` | #176 | `=1` | forces the EnvCell SHELL pass to map every instance to clip slot 0 (no-clip) instead of its cell's portal-slice region | ALTERS RENDERED OUTPUT: shells draw whole/unclipped instead of trimmed — a visual isolation mode, not a log-only probe; no DebugPanel mirror | `RenderingDiagnostics.ClipDebugNoShellTrim` |
| `ACDREAM_DUMP_APPEARANCE` | #5 | `="1"` | Logs every `0xF625` ObjDescEvent + `0xF7DB` UpdateObject with body length, target guid, hex preview — used to debug remote-player appearance asymmetry | print-only (`Console.WriteLine`) | `WorldSession` static field `DumpAppearanceEnabled` (`WorldSession.cs:792-793`), raw scattered read, issue #5 diagnostic |
| `ACDREAM_DUMP_OPCODES` | #5 | `="1"` | Logs first occurrence of each genuinely-unhandled inbound opcode (deduped by opcode) | print-only. Must stay the LAST else-if in the dispatch chain per comment (else it would intercept handled opcodes) — currently correct. | `WorldSession` static field `DumpOpcodesEnabled` (`WorldSession.cs:788-789`, consumed `WorldSession.cs:2391-2398`), issue #5 diagnostic. Also mirrored (display-only, non-functional) via `DebugPanel.cs:241`/`DebugVM.cs:227`. |
| `ACDREAM_DUMP_SCENERY_Z` | #48 | `=1` | Per-spawn Z-placement diagnostic for procedural scenery (trees/bushes/rocks), added for issue #48 (the "trees-in-sky" bug). | **NOT print-only** — this is a real behavior fork, not just added logging. `LandblockBuildFactory.cs:167-178`: when the flag is on, the streaming worker calls a **separate, duplicate scenery-building method** (`BuildSceneryEntitiesForStreaming`, a full parallel reimplementation of GfxObj/Setup mesh resolution + placement inline in this file) instead of production's `LandblockPhysicsContentBuilder.HydrateProceduralScenery`. Any visual/measurement run taken with this flag set is exercising a different scenery-placement code path than production, which can drift from it silently. | `RuntimeOptions.DumpSceneryZ``SessionPlayerComposition.cs:280``LandblockBuildFactory.cs:23,42,168,335` |
| `ACDREAM_DUMP_TRANSIT_FAIL` | #345 | `=1` | buffers per-tick `[transit-fail-insert]`/`[transit-fail-stepup]`/`[transit-fail-walk]`/`[transit-fail-adjust]` trace lines into a `[ThreadStatic]` list and flushes them to console ONLY when a tick requested nonzero XY movement but delivered zero (self-selecting "stuck tick" predicate) | print-only, zero allocation when off (flag checked before touching any buffer per its own doc); buffer/list allocation only on ticks that are already stuck | `PhysicsDiagnostics.DumpTransitFailEnabled` |
| `ACDREAM_LIGHT_DEBUG` | #176 | `=<int>` (`int.TryParse`; unset/invalid → 0) | shader isolation mode uploaded as `uLightDebug` by `EnvCellRenderer` + `WbDrawDispatcher`: 0=off, 1=ambient-only vertex lighting, 2=kill dynamic point lights, 3=raw vLit visualization (texture ignored) | ALTERS RENDERED OUTPUT directly every draw pass (changes fragment-shader lighting/texturing) — not a log probe; no DebugPanel mirror | `RenderingDiagnostics.LightDebugMode` |
| `ACDREAM_PROBE_BUILDING` | l.2d slice 1 | `=1` | gates the multi-line `[resolve-bldg]` BSP-shadow-hit trace in `TransitionTypes.FindObjCollisions`, one-time `[entity-source]` registration logs in `GameWindow`, `[door-cycle]` UM dispatch trail, and a one-shot `[setstate-hex]` wire dump of the first `SetState` (0xF74B) packet in `WorldSession` | print-only; also un-gates the `PhysicsDiagnostics.LastBspHitPoly` diagnostic side-channel (a static field write in `BSPQuery`/`FlatBspQuery`, read back by the `[resolve-bldg]` line) — no gameplay effect, but an extra static-field write per BSP hit while on; heavy output (one multi-line entry per BSP hit per physics tick) | `PhysicsDiagnostics.ProbeBuildingEnabled` |
| `ACDREAM_PROBE_CELLSET` | a6.p5 | `=1` | gates `PhysicsDiagnostics.LogCellSetBuild`, one `[cellset-build]` line per `BuildCellSetAndPickContaining` call (seed cell, sphere XY, candidate list) from `CellTransit.cs:1468` | print-only; builds a `StringBuilder` of the candidate id list only when the flag is on | `PhysicsDiagnostics.ProbeCellSetEnabled` |
| `ACDREAM_PROBE_CELL_CACHE` | indoor walking phase d | `=1` | gates one `[cell-cache]` line per EnvCell first-cached in `PhysicsDataCache.CacheCellStruct` (poly counts, BSP root structure) | print-only; fires at most once per EnvCell (cache is no-op after first population); no DebugPanel mirror | `PhysicsDiagnostics.ProbeCellCacheEnabled` |
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |
| `ACDREAM_PROBE_GLSTATE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[gl-state]` line: depth/blend/cull/scissor/viewport/draw-FBO/color-mask/`glGetError` snapshot | print-only per its docstring; the actual state-snapshot/comparison call site lives outside `RenderingDiagnostics.cs` and was outside this pass's cited read sites | `RenderingDiagnostics.ProbeGlStateEnabled` |
| `ACDREAM_PROBE_INDOOR_BSP` | indoor walking phase 1 / cellar-lip wedge | `=1` | gates `[indoor-bsp]` (per `BSPQuery.FindCollisions` indoor call), `[neg-poly]` (near-miss polygon detail in `BSPQuery`), and `[stepdown-decide]` (step-down accept/reject inputs in `TransitionTypes`) trace lines | print-only; also un-gates the `LastBspHitPoly` diagnostic side-channel write (same as `ACDREAM_PROBE_BUILDING`) | `PhysicsDiagnostics.ProbeIndoorBspEnabled` |
| `ACDREAM_PROBE_INDOOR_LIGHT` | #176/#177 discriminator, a7.l1 | `=1` | rate-limited (1 Hz) `[indoor-light]` line from `LightManager.BuildPointLightSnapshot`: point-light pool set composition (pool/cellLess/registered/capped/byCell histogram) | print-only, explicitly "inert unless set" per the call-site comment (LightManager.cs:368-370); no DebugPanel mirror | `RenderingDiagnostics.ProbeIndoorLightEnabled` |
| `ACDREAM_PROBE_JUMP` | campaign ch round 2 | `=1` | gates the `[jump]` line in `PlayerMovementController.ReportJumpRefusal`, printed UNCONDITIONALLY (even when `OnInterfaceText` is null) to distinguish "branch never fired" from "branch fired, callback dropped it" | print-only; `Headless/Policies/HeadlessBotPolicy.cs`'s `JumpProbeHeadlessBotPolicy` doc comment references this flag as a companion but does not itself read it — it is a headless bot behavior meant to be run alongside `ACDREAM_PROBE_JUMP=1`, not a second consumer | `PhysicsDiagnostics.ProbeJumpEnabled` |
| `ACDREAM_PROBE_LOCAL_TELEPORT` | c4 route 3 d-t8 | `=1` | gates one `[local-tp]` line per local-player portal-arrival attempt (committed AND refused) from `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController.LogPortalArrivalAttempt` — the single Runtime chokepoint both graphical and headless hosts share | print-only; dual-host parity evidence (same line shape from both hosts) | `PhysicsDiagnostics.ProbeLocalTeleportEnabled` |
| `ACDREAM_PROBE_PARK` | issue #309 | `=1` | gates `[park]`/`[park-restore]` lines when a `RuntimeSetPositionState` placement parks or a cancelled park's withdrawal is rolled back | print-only, low volume (parks are rare); in a MULTI-session headless host, `HeadlessStaticStateAudit.ValidateProcessIsolation` THROWS `HeadlessConfigurationException` at startup if this (or any other process-global `Probe*`/`Dump*` boolean, `CollisionShadowSampleEvery`, or `PhysicsResolveCapture`) is enabled — refusal is waived only when `sessionCount==1` (logs loudly and proceeds instead) | `PhysicsDiagnostics.ProbeParkEnabled` |
| `ACDREAM_PROBE_PLACEMENT_FAIL` | issue #98 | `=1` | gates one `[place-fail]` line per Path-1 (Placement/Ethereal) `Collided` return in `BSPQuery.FindCollisions`, plus one per `Transition.DoStepDown` placement-insert rejection | print-only; low volume (fires only on actual rejection) | `PhysicsDiagnostics.ProbePlacementFailEnabled` |
| `ACDREAM_PROBE_POLY_DUMP` | a6.p3 slice 4, issue #98 | `=1` | gates one `[poly-dump]` line (full polygon geometry: cell, poly index, sides, plane, all vertices) per `AdjustSphereToPlane` push-back call | print-only; HEAVY output (one full-geometry dump per push-back call) — doc explicitly says "use briefly, then turn off" | `PhysicsDiagnostics.ProbePolyDumpEnabled` |
| `ACDREAM_PROBE_PORTAL_CHURN` | "throwaway apparatus — strip once the bound ships" | `=1` | one `[portal-churn]` summary per `PortalVisibilityBuilder.Build` call: per-cell pop/re-pop counts, re-enqueue totals, reciprocal-clip pre→post region growth | print-only | `RenderingDiagnostics.ProbePortalChurnEnabled` |
| `ACDREAM_PROBE_PUSH_BACK` | phase a6.p1 | `=1` | gates `[push-back]` (`BSPQuery.AdjustSphereToPlane`), `[push-back-disp]` (`BSPQuery.FindCollisions` 6-path dispatcher), `[push-back-cell]` (`Transition.CheckOtherCells` multi-cell BSP) lines | print-only; the `DebugVM.cs:380` "runtime mirror" is dead code — `DebugVM`/`DebugPanel` (`AcDream.UI.Abstractions/Panels/Debug/`) are never instantiated anywhere in `src/` (the ImGui frontend they required was removed at Campaign V slice V11); only the startup env var takes effect | `PhysicsDiagnostics.ProbePushBackEnabled` |
| `ACDREAM_PROBE_PVINPUT` | "throwaway apparatus — strip once the jitter source is pinned" | `=1` | one `[pv-input]` line/frame with 6-dp-precision `PortalVisibilityBuilder.Build` inputs (camera eye, player position, VP elements) + resulting flood-cell count; deliberately runs WITHOUT the heavier `[flap]` probe so the log stays diffable | print-only | `RenderingDiagnostics.ProbePvInputEnabled` |
| `ACDREAM_PROBE_REMOTE_SLIDE` | bug b, temporary — strip once two-client roof capture lands | `=1` OR `=<comma-separated hex GUID list>` | gates `[remote-slide-up]`/`[remote-slide-vec]`/`[remote-slide-snap]`/`[remote-slide-enq]` lines across `LiveEntityNetworkUpdateController`, `InterpolationManager`, `RuntimeRemotePhysicsUpdater`, `RuntimeRemoteSteadyStatePosition` tracing two candidate remote-slide "blip" producers | print-only; `BeginRemoteSlideAttribution`/GUID-stamping calls are UNCONDITIONAL at several call sites (self-guard is internal), so a `[ThreadStatic]` field write happens on every remote tick regardless of the flag (cheap, non-allocating); a GUID allow-list narrows output to specific entities for a readable two-client capture | `PhysicsDiagnostics.ProbeRemoteSlideEnabled` + `ProbeRemoteSlideGuids` (raw string parsed via `ParseHexIdList` unless it's the literal `"1"`) |
| `ACDREAM_PROBE_REMOTE_TELEPORT` | c4 route 4b-3, temporary | `=1` | gates one `[remote-teleport]` line per routed remote teleport arm in `LiveEntityNetworkUpdateController.ApplyRemoteContactRouting` | print-only; a 2026-08-04 fix moved the enabled-check to the CALL SITE because the probe's internal self-guard did not prevent `teleportStatus.ToString()` from being evaluated/allocated on every teleport regardless of flag state — now properly guarded | `PhysicsDiagnostics.ProbeRemoteTeleportEnabled` |
| `ACDREAM_PROBE_SEAMDRAW` | #176, "throwaway apparatus" | `"1"`/`"true"`/blank → default #176 Facility Hub cell set (7 fixed hex ids); otherwise comma-separated hex cell-id list | change-deduped + 2 s-heartbeat `[seam-cell]`/`[seam-snap]`/`[seam-ent]`/`[seam-mask]` lines from `EnvCellRenderer.Render` and `WbDrawDispatcher` describing per-instance transforms and resolved light-set identities at target cells | print-only | `RenderingDiagnostics.ProbeSeamDrawEnabled` / `SeamDrawTargetCells` |
| `ACDREAM_PROBE_STEP_WALK` | a6.p3 issue #98 | `=1` | gates `[step-walk]` lines at select points in the transition sub-step loop and step-down probe (requested vs adjusted offset, sphere positions, contact planes, walkable flags) | print-only; no DebugPanel mirror | `PhysicsDiagnostics.ProbeStepWalkEnabled` |
| `ACDREAM_PROBE_SWEPT` | phase w stage 0 | `=1` | gates one `[cell-swept]` line per `ResolveWithTransition` call comparing the transition's swept cell vs the legacy static `ResolveCellId` path | print-only | `PhysicsDiagnostics.ProbeSweptEnabled` |
| `ACDREAM_PROBE_TELEPORT` | 2026-06-22, "removable diagnostic" | `=1` | gates `[tp-probe]` lines (`LogTeleport`) at AIM/ENQ/BUILD/APPLY/PLACED teleport-pipeline events across `LocalPlayerTeleportController` and `RuntimeAcceptedPositionDriveController`, with cross-thread monotonic timestamps | print-only | `PhysicsDiagnostics.ProbeTeleportEnabled` |
## Deprecated
| Flag | Value | What it does | Side effects | Default | Read by |
|---|---|---|---|---|---|
| `ACDREAM_DEVTOOLS` | `=1` | logs a one-time "ImGui dev UI removed" notice; the only remaining functional consumer is `VulkanGraphicsContext.cs:184` (`enableOptionalExtensions: _options.DevTools`, selects optional Vulkan validation/debug-utils extensions) | real effect: turns on Vulkan validation/debug-utils extensions (can change perf and can surface validation-layer errors that don't occur when off) — NOT measurement-neutral for a perf gate; `GameWindow.DevToolsEnabled` is a hardcoded `false` const (dead — no ImGui dev UI exists to gate); `DevToolsInputCaptureSource(bool enabled)` explicitly discards its `enabled` ctor arg (`_ = enabled;`) — dead parameter, always reports `WantCaptureKeyboard=false` | off | `RuntimeOptions.DevTools` (typed, `Program.cs`/`RuntimeOptions.Parse`) |
| `ACDREAM_STREAM_RADIUS` | `=<int>` (non-negative) | Legacy override for the streaming near/far radii, applied on top of the quality-preset's radii at session-start composition. | **CLAUDE.md explicitly documents this as "legacy" and warns against using it for measurement.** Confirmed in code (`SessionPlayerComposition.cs:256-259`): `nearRadius = legacyRadius; farRadius = Math.Max(legacyRadius, farRadius)` — it FORCES `NearRadius` and only ever RAISES (never lowers) `FarRadius`. It is set once at session-start composition and is **silently discarded** by any later Settings quality change: `RuntimeSettingsController.ApplyQuality``RuntimeSettingsTargets.ApplyQuality``StreamingController.ReconfigureRadii` recomputes radii straight from the quality preset with no knowledge of this override. A measurement/gate run taken with this set is measuring a different streaming window than production and than any run that later touches Settings. | `null` → quality-preset radii unmodified (production default: High preset, Near 4 / Far 12) | `RuntimeOptions.LegacyStreamRadius``SessionPlayerComposition.cs:254-268` |
---
<!-- retired -->
## Retired
Flags that no longer exist, kept only so a stale script or an old research
document does not send someone hunting. Rows below this marker are exempt
from the "must still exist" check.
| Flag | Retired | Replacement |
|---|---|---|
| `ACDREAM_RUN_SKILL` | Client-side run-skill override for local motion prediction. Skills now arrive from the server (`LiveMovementStatsApplier`); the hardcoded fallback is 200. | none — server-authoritative |
| `ACDREAM_JUMP_SKILL` | As above. The fallback is 300, not the 200 that CLAUDE.md advertised. | none — server-authoritative |
| `ACDREAM_RENDER_BACKEND` | Selected the GL-vs-Vulkan backend. Campaign V deleted the OpenGL backend; Vulkan is the only one. Two comments still named it as a live co-requisite until 2026-08-24. | none |
| `ACDREAM_ANIM_SPEED_SCALE` | Animation-speed multiplier from the pre-retail-sequencer era; died with the 1.248x factor. | none |
| `ACDREAM_A8_AUDIT` | Phase A8 EnvCell batch/cull audit dump. Its only caller never existed; `EnvCellRenderer.CollectCellAuditLines` was unreachable and was deleted 2026-08-24. | `ACDREAM_PROBE_ENVCELL` |
| `ACDREAM_AIRBORNE_DIAG` | #42 airborne-sweep `[SWEEP]`/`[SWEEP-OBJ]` XY-drift trace. Investigation closed; stripped 2026-08-24 (#435) along with its 16 siblings below. | none |
| `ACDREAM_DUMP_ENTITY` | #119 tower-staircase HYDRATE/DRAW/WALK-REJECT entity watchlist. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_AUTOWALK` | Issue #63 server-initiated auto-walk trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_LIGHT` | #133 A7 dungeon-lighting `[light]`/`[light-detail]` trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_OUTSTAGE` | #131 outside-stage dynamics routing trace (also the `ACDREAM_DUMP_ENTITY` `[outstage-own]` watchlist consumer). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_PHANTOM` | #113 phantom-shell/phantom-objs draw-mechanism trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_REACH` | #334 broadphase candidate-disposition trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_REMOTE_LANDING` | Bug A / issue #32 remote ground-contact landing trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_SHELL` | #78 cell-shell opaque-pass render trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_STEP_HEIGHTS` | Issue #338 step-up/step-down height provenance trace (including its unconditional once-per-process `AnnounceStepHeightProbeOnce` self-report). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_STICKY` | R5-V3 issue #171 sticky-melee lifecycle/steer trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_SUPPORT` | Issue #337 `[support]`/`[geom]` collision-vs-visual classifier trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_TEXFLUSH` | #105 white-indoor-textures staged-upload trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_VIEWER` | #119-residual viewer/flood capture (tower-ascent replay). Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_PROBE_WALK_MISS` | Issue #83 indoor walkable-plane miss trace. Investigation closed; stripped 2026-08-24 (#435). | none |
| `ACDREAM_WIRE_MESH` | Issue #337 F2 overlay upgrade to real physics-BSP polygon edges. Investigation closed; stripped 2026-08-24 (#435) — F2 reverted to its proxy-cylinder overlay. | none |
| `ACDREAM_WIRE_RADIUS` | Companion radius knob for `ACDREAM_WIRE_MESH`. Stripped alongside it 2026-08-24 (#435). | none |
| `ACDREAM_A8_DUMP_PV` | Phase A8.F portal-frame visual-gate triage dump (camera-cell portal census + EXIT-PROJ/EXIT-CLIP/EXIT trace in `PortalVisibilityBuilder.Build`). Phase A8 closed; stripped 2026-08-24 (#435 part 2). | none |
| `ACDREAM_DUMP_CLOTHING` | Issue #37 humanoid-coat clothing/part-swap trace. #37 closed 2026-05-11; stripped 2026-08-24 (#435 part 2). | none |
| `ACDREAM_DUMP_EDGE_SLIDE` | Issue #32 L.2c edge-slide/cliff-slide branch trace (five `edge-slide:` lines). #32 closed 2026-08-07; stripped 2026-08-24 (#435 part 2). | none |
| `ACDREAM_DUMP_LIVE_SPAWNS` | Phase A8 indoor-visibility batch live-spawn/DROP trace. Phase A8 closed; stripped 2026-08-24 (#435 part 2). | none |
| `ACDREAM_DUMP_STEPUP` | L.2.3d/e/f step-up `stepup: enter/SUCCESS/FAILED` trace. Investigation closed; stripped 2026-08-24 (#435 part 2) — its content is still covered by the separate `[transit-fail-stepup]` line under `ACDREAM_DUMP_TRANSIT_FAIL`. | `ACDREAM_DUMP_TRANSIT_FAIL` |
| `ACDREAM_DUMP_VENDOR` | `[vendor-diag]` trace (~25 call sites) for two vendor-approach/split-stack regressions. Vendor campaign closed 2026-08-08; stripped 2026-08-24 (#435 part 2) along with its owner class `VendorDiagnostics.cs`. | none |
| `ACDREAM_DUMP_VITALS` | Issue #5 `PrivateUpdateVital`/`PlayerDescription`/parse-failure trace across 4 sites. #5 closed 2026-04-25; stripped 2026-08-24 (#435 part 2). | none |

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
# UI framework plan
**Date:** 2026-04-24 (design), shipped 2026-04-25
**Status:** **Phase D.2a and the D.2b retained gameplay UI have shipped.**
ImGui remains the `ACDREAM_DEVTOOLS=1` developer stack. Retail gameplay UI is
the independent `UiHost`/`UiRoot` retained tree under `AcDream.App/UI`, built
from production LayoutDesc/DAT assets. The stable cross-stack seam is game
state, ViewModels, and commands — not an `IPanelRenderer` backend swap.
**Status:** **Phase D.2a shipped**`AcDream.UI.Abstractions` + ImGui backend
+ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from
`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during
first-light integration — see the pivot note below. Phase D.2b (custom
retail-look backend) remains design-only.
**Owner:** lead engineer (erik) + Claude
Captures the UI strategy agreed via discussion on 2026-04-24. Documents
@ -53,12 +53,12 @@ panel, skills, spellbook, fellowship, allegiance, trade, options, map,
quest log, tooltips — and a first-class plugin API so plugin authors can
ship their own panels.
## Current strategy: two coexisting stacks, shared state contracts
## Strategy: two-phase, one stable interface
```
┌─────────────────────────────────────────┐
Developer UI ─ ImGui + IPanelRenderer │
Gameplay UI ─ UiRoot retained widgets
UI backend ─ ImGui (short-term) │ ← swappable
─ Custom retail (later)
├─────────────────────────────────────────┤
│ ViewModels + Commands (per panel) │ ← stable contracts
├─────────────────────────────────────────┤
@ -66,16 +66,14 @@ ship their own panels.
└─────────────────────────────────────────┘
```
- **Developer stack:** `AcDream.UI.Abstractions` panels render through
`IPanelRenderer` on ImGui. This stack is permanent for diagnostics, packet
inspection, settings development, and other devtools.
- **Gameplay stack:** `UiHost` owns a retained `UiRoot` tree. `LayoutImporter`
builds retail windows from LayoutDesc/DAT assets and focused `gm*UI`-style
controllers bind runtime values and actions.
- **Shared seam:** both stacks consume the same session state, ViewModels, and
command/event services. A panel is ported to retained UI by binding those
shared models to its DAT-authored tree, not by implementing
`IPanelRenderer` a second time.
- **Near term:** wire an ImGui-based overlay so we can iterate on game
logic fast — chat that actually sends + receives, inventory that
reflects real state, vitals bar that reads real HP/stam/mana. Looks
like a debugger, that's fine for now.
- **Later:** replace the visual layer with a custom toolkit that uses
retail dat assets (icons, panels, fonts) and matches retail's feel.
- **Always:** ViewModels and Commands stay stable across the swap. The
game logic never learns which backend is drawing it.
## Choice: Hexa.NET.ImGui for the short-term backend
@ -160,7 +158,7 @@ public sealed record EquipItemCmd(uint ItemGuid, EquipSlot Slot);
Dispatched to an `ICommandBus` that routes to the appropriate subsystem
(`WorldSession.SendSelect`, `ChatService.Send`, etc.).
### Layer 3a — ImGui developer backend
### Layer 3 — UI backend
New module: `src/AcDream.UI.ImGui/`. References `AcDream.UI.Abstractions`.
@ -170,34 +168,11 @@ New module: `src/AcDream.UI.ImGui/`. References `AcDream.UI.Abstractions`.
per frame.
- Keyboard / mouse / focus handled by ImGui natively.
This layer remains the permanent devtools surface. It is not the production
retail gameplay renderer.
Later: `src/AcDream.UI.Retail/` references the same `AcDream.UI.Abstractions`
and implements the same `IPanel` / `IPanelHost` interfaces — but draws
with our own retained-mode toolkit + retail dat assets.
### Layer 3b — retained retail gameplay UI
`src/AcDream.App/UI/` contains the GL-free widget tree, LayoutDesc importer,
window runtime, and panel controllers. Rendering dependencies enter through
small sprite/font/viewport resolver seams. Controllers consume the same
ViewModels and command services used by the ImGui panels, while binding
behavior to existing retail element ids instead of procedurally redrawing the
panel through `IPanelRenderer`.
## Plugin UI API
The shipped plugin-facing gameplay UI contract is
`AcDream.Plugin.Abstractions.IUiRegistry.AddMarkupPanel`: a plugin provides
KSML-style markup and a binding object; the host builds it into the retained
`UiRoot` tree. `IPanel`/`IPanelRenderer` remains a first-party developer-panel
contract and is intentionally not referenced by `Plugin.Abstractions`.
This makes plugin gameplay panels independent of ImGui while allowing them to
share the retained input, window, and DAT-sprite runtime. Registrations made
before the GL host exists are buffered. In builds where retail UI is disabled,
they remain registered but have no gameplay surface; the long-term release
configuration enables retained gameplay UI.
The following was the original pre-D.2b proposal and remains historical
context, not the shipped plugin contract:
## Plugin API (must be backend-agnostic)
```csharp
public interface IPanel
@ -252,34 +227,32 @@ walk around / take damage / regen.
to equip, drag target for future move.
- `CharacterPanel` — attributes, skills, XP.
### Sprint 3 — Plugin API hardening (superseded shape)
### Sprint 3 — Plugin API hardening
- Document the `IPanel` contract.
- The shipped route is `IUiRegistry.AddMarkupPanel`, not plugin-owned
`IPanel` implementations.
- Confirm plugins can subscribe to game events and expose retained markup
bindings without referencing App or ImGui assemblies.
- Port the smoke plugin to register a demo panel via the API.
- Confirm plugins can subscribe to game events AND draw UI through the
same interface.
### Sprint 4+ — More panels
Spellbook, allegiance, fellowship, trade, map, quest log, options.
Continue to expand `InventoryPanel` with drag-drop, split, appraise.
### D.2b — retained retail-look gameplay UI (shipped, expanding panel by panel)
### Later — Custom retail-look backend
`AcDream.App/UI` imports LayoutDesc trees and draws retail DAT assets through
the retained `UiRoot` runtime. ImGui remains available for devtools; shared
ViewModels and commands prevent duplicate game-state logic.
`AcDream.UI.Retail` implements `IPanelRenderer` with our own toolkit +
retail dat assets. Swap panels one at a time. ImGui overlay remains for
devtools.
## Non-goals for this first pass
- **Not** going to theme ImGui to look retail. Waste of effort when we'll
swap the backend. Devtools aesthetic is fine.
- **Not** byte-porting Keystone internals that are unavailable. Observable
widget behavior is recovered from the named client call sites, DAT
properties, and live retail evidence.
- **Not** hand-authoring first-party gameplay layouts where retail LayoutDesc
data exists. KSML-style markup remains the plugin/extension layout surface.
- **Not** porting retail's widget code. We use their ASSETS later, not
their widget implementation.
- **Not** building layout DSL / XAML-like markup. Panels register and
draw procedurally, same as ImGui.
## Alternatives considered
@ -298,13 +271,13 @@ ViewModels and commands prevent duplicate game-state logic.
**Mitigation:** code review every addition; if a feature only exists
in ImGui and the retail toolkit can't express it, don't add it.
- **Risk:** ImGui and retained controllers grow separate game-state truth.
**Mitigation:** one session model/ViewModel and one command path per
subsystem; each surface is only a projection.
- **Risk:** Swap to custom backend breaks a dozen panels simultaneously.
**Mitigation:** swap one panel at a time, keep ImGui rendering the
rest until all are ported.
- **Risk:** Plugin markup relies on App-only widget details.
**Mitigation:** keep `IUiRegistry` BCL-only, resolve bindings by contract,
and use a smoke plugin as the retained-runtime canary.
- **Risk:** Plugin authors write panels that only work in ImGui.
**Mitigation:** smoke plugin registers a panel early; use it as a
canary whenever backend changes.
- **Risk:** Hexa.NET.ImGui stops being maintained.
**Mitigation:** integration is small (<100 LOC), switching to

View file

@ -92,35 +92,6 @@ Goal: make every bad movement outcome explainable.
- Build real-DAT fixture capture for known walls, building ledges, rooftops,
slopes, landblock seams, and dungeon entrances.
Current shipped slices:
- 2026-04-30: cdb + TTD retail-observer toolchain (`tools/pdb-extract/`,
`tools/ttd-record.ps1`, `tools/ttd-query.ps1`) with PDB pairing checker
and ring-buffer trace replay. The "retail observer harness" line item.
- 2026-04 (pre-L.2 rename): `ACDREAM_DUMP_MOVE_TRUTH` paired
outbound/server-echo dumper in `GameWindow` covers outbound packet
fields + server echo + correction delta with cell-id mismatch.
- Pre-L.2: scenario-specific dumps `ACDREAM_DUMP_MOTION`,
`ACDREAM_DUMP_STEEP_ROOF`, `ACDREAM_DUMP_STEPUP`,
`ACDREAM_DUMP_EDGE_SLIDE` for the codepaths hit during prior bug chases.
- 2026-05-12 (slice 1): general-purpose probes via new
`AcDream.Core.Physics.PhysicsDiagnostics` static class.
`ACDREAM_PROBE_RESOLVE` emits one `[resolve]` line per
`PhysicsEngine.ResolveWithTransition` call (input/output pos+cell,
ok-vs-partial, grounded-in, contact-plane status, wall normal if hit,
walkable polygon valid, moving entity id).
`ACDREAM_PROBE_CELL` emits one `[cell-transit]` line per
`PlayerMovementController.CellId` change with old→new + position +
reason tag (`resolver`/`teleport`). Both flippable live via the
DebugPanel "Diagnostics" section — checkbox toggles take effect on
the next resolve, no relaunch required.
Remaining L.2a work: contact-plane probe (general, not just steep-roof),
ShadowObjectRegistry hit log ("you collided with entity X"), water probe,
real-DAT fixture-capture pipeline, and folding the older sticky-at-startup
`ACDREAM_DUMP_*` flags into `PhysicsDiagnostics` for unified runtime
toggling.
### L.2b - Movement Wire / Contact Authority
Goal: stop sending movement packets that claim more certainty than the local
@ -169,41 +140,6 @@ fallback.
- Audit `Setup.Radius` and cylinder fallback behavior against retail before
relying on them for conformance.
Current sub-direction (revised 2026-05-13 evening after slice 1 + 1.5
shipped and Holtburg-doorway capture analyzed — third reframe):
L.2d as scoped ("shape fidelity: Sphere / CylSphere / Building Objects")
is **essentially closed at the Holtburg site that motivated this phase**.
Building BSP collision works correctly — the slice-1.5 probe captured
real triangles in plausible world positions for `gfxObj=0x01000A2B` with
`bspR=13.99m`. The 121 wall hits the L.2a probe attributed to
`obj=0xA9B47900` were **side effects of the player already being pushed
back by a separate Door cylinder entity** at the same doorway threshold.
The actual blocker is a server-spawned **Door** entity — Setup
`0x020019FF` named `"Door"` — that ACE places at each Holtburg-town
building threshold (five doors total observed across `0xA9B40029`,
`0xA9B40154`, `0xA9B40155`). It registers as a Cylinder shadow entry
via the server-spawn path; its Cylinder collision blocks the player
walking into the doorway. That's **door-state handling**, a different
class of problem from L.2d's shape-fidelity scope — it touches network
(`CreateObject` PhysicsState bits), interaction (Use action on door
entity), animation (door open/close), and collision-state-toggle.
Recommend: **leave L.2d in "watch-and-wait" mode** with slice 1's probe
infrastructure in place. No more L.2d slices until a NEW shape-fidelity
bug is observed at a different site (dungeon walls, stairs, roofs) with
the probe-armed client. The door-state work becomes its own sub-phase
(probably nested under B.4 interaction or filed as a new L.2 sub-phase
like L.2g) scoped separately.
Full slice 1 + 1.5 handoff:
[docs/research/2026-05-13-l2d-slice1-shipped-handoff.md](../research/2026-05-13-l2d-slice1-shipped-handoff.md).
Design spec (now mostly historical, framing was wrong but probe
infrastructure shipped from it):
[docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md](../superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md).
Predecessor L.2a handoff:
[docs/research/2026-05-12-l2a-shipped-l2d-handoff.md](../research/2026-05-12-l2a-shipped-l2d-handoff.md).
### L.2e - Cell Ownership: Outdoor Seams, CELLARRAY, cell_bsp
Goal: the resolver knows which cell owns the movement and which adjacent cells
@ -228,62 +164,6 @@ client sees when observing acdream.
- Require conformance notes in tests or research docs for every AC-specific
algorithm ported under L.2.
### L.2g - Dynamic PhysicsState Toggling
Goal: server-driven post-spawn state changes (chiefly `ETHEREAL` flips) are
honored by the local collision stack.
Triggered 2026-05-12 evening by the L.2d slice 1.5 trace: the Holtburg
doorway blocker is a closed Door entity (Setup `0x020019FF`) whose
`PhysicsState.Ethereal` bit flips when the player Uses the door. The L.2d
shape-fidelity work doesn't cover this — the door's collision shape is
already correct; what's missing is honoring the *runtime* state change.
Scope is intentionally narrow:
- Parse inbound `GameMessageSetState (opcode 0xF74B)`.
- Plumb the new `PhysicsState` value into `ShadowObjectRegistry`'s cached
per-entity state so the existing `CollisionExemption.IsExempt(...)` check
sees the up-to-date bits.
- Verify the Holtburg inn-door scenario: walk into doorway → blocked, Use
door → door swings open AND player can walk through, auto-close after
30s → door closes AND player is blocked again.
- Confirm the existing `UpdateMotion` pipeline drives `(NonCombat, On/Off)`
on non-creature entities (door swing animation). If not, one-line fix.
Excluded from L.2g scope (deferred):
- Door-specific UX polish: "door is locked" sound, creature-AI bump-open.
- Any Door-specific class hierarchy — generic state-flip infrastructure
is enough; doors are the verification scenario, not a privileged case.
Lane: informal sixth lane "dynamic state." The existing five-lane table
treats per-entity state as static-after-spawn; L.2g makes it dynamic.
Full design spec:
[docs/superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md](../superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md).
M1 critical path: this slice unblocks the *"open the inn door"* demo
scenario.
Current shipped slice (2026-05-12):
| Commit | Subject |
|---|---|
| `2459f28` | `feat(phys L.2g slice 1): inbound SetState (0xF74B) parser` |
| `d538915` | `feat(phys L.2g slice 1): ShadowObjectRegistry.UpdatePhysicsState` |
| `536a608` | `feat(phys L.2g slice 1): WorldSession dispatches SetState (0xF74B) + hex probe` |
| `108e386` | `feat(phys L.2g slice 1): GameWindow routes SetState + extends [entity-source] log` |
Slice 1 is CODE-COMPLETE: parser + registry mutator + WorldSession
dispatcher + GameWindow subscriber. 6 new tests pass (3 parser + 3
registry). Build clean. Per-commit + final integration code reviews
all approved. **Visual verification deferred to Phase B.4b** — the
inbound SetState chain can't fire at runtime until B.4b finishes the
outbound Use handler. See
[docs/research/2026-05-12-l2g-slice1-shipped-handoff.md](../research/2026-05-12-l2g-slice1-shipped-handoff.md)
for full evidence + the 4 minor + 1 Important review notes.
## Named Retail Anchors
Primary source: `docs/research/named-retail/acclient_2013_pseudo_c.txt`.

View file

@ -1,72 +0,0 @@
# Phase N.5 perf baseline
**Captured:** 2026-05-08, against N.5 head (post-Task 12) on local machine.
**Method:** `ACDREAM_WB_DIAG=1` + character at Holtburg spawn position +
roaming. Numbers below are 5-second window medians from `[WB-DIAG]`.
## Holtburg courtyard (steady state)
| Metric | N.5 measured | N.4 (estimated*) | Gate |
|---|---|---|---|
| CPU dispatcher (median) | **1227 µs / frame** | ≥2500 µs / frame | ≤70% of N.4 → **PASS** |
| CPU dispatcher (p95) | 1303 µs / frame | — | — |
| GPU rendering (median) | unmeasured (see below) | — | within ±10% — **DEFERRED** |
| `drawsIssued` per 5s | 4.85M (= 1662 groups × ~580 fps) | far higher per frame | — |
| `drawsIssued` per pass (CPU GL calls) | **2** (1 opaque + 1 transparent indirect) | ~hundreds per pass | ≤5 → **PASS** |
| `groups` (working set) | 1662 | ~similar | sanity |
| Frame rate (inferred) | ~810 fps | ~100-200 fps | substantial uplift |
*N.4 baseline NOT measured directly in this run. The "≥2500 µs / frame"
estimate assumes N.4's per-group glBindTexture + glBindBuffer +
glDrawElementsInstancedBaseVertexBaseInstance hot path costs ≥1.5 µs per
group and N.4 has ~1700 groups in this scene, putting the GL portion alone
at ~2.5 ms before adding the entity-walk overhead. N.5's measurement
includes ALL dispatcher work (entity walk + group bucketing + 3 SSBO
uploads + 2 indirect calls + state changes) at 1230 µs total — comfortably
half of the lower bound estimate.
## Acceptance gates (spec §8.3)
- [x] **Visual identity to N.4** — confirmed at Task 10 USER GATE: Holtburg
courtyard renders identical, no missing entities, no z-fighting, no
exploded parts.
- [x] **CPU dispatcher time ≤ 70% of N.4** — N.5 measures 1.23 ms/frame
median; estimated N.4 ≥2.5 ms/frame; **comfortably under 70%**.
- [ ] **GPU rendering time within ±10% of N.4** — DEFERRED. The
`GL_TIME_ELAPSED` query polling never reports `avail != 0` in our
single-frame poll loop; the driver hasn't finalized the result by the
time we check. The fix is double-buffering (issue queryA on frame N,
read result on frame N+2). N.6 perf polish item.
- [x] **`drawsIssued` ≤ 5 per pass (CPU GL calls)** — exactly 2 indirect
calls per frame regardless of scene size.
- [x] **All tests green** — 70/70 in
`FullyQualifiedName~Wb|FullyQualifiedName~MatrixComposition`.
8 pre-existing failures in `MotionInterpreter` / `BSPStepUp` /
`PositionManager` / `PlayerMovementController` / `Dispatcher` are
carry-forward from before N.5 and unrelated to rendering.
- [N/A] **`ACDREAM_USE_WB_FOUNDATION=0` still works** — escape hatch
formally retired in N.5 ship amendment. `InstancedMeshRenderer`,
`StaticMeshRenderer`, and `WbFoundationFlag` deleted. Missing
bindless throws `NotSupportedException` at startup with a clear
error message. No fallback path.
## Visual verification (Task 14)
- [x] **Holtburg courtyard** — PASS at Task 10 USER GATE.
- [ ] **Foundry interior / dense static-object scene** — TODO Task 14.
- [ ] **Indoor → outdoor cell transition** — TODO Task 14.
- [ ] **Drudge / character close-up (Issue #47 close-detail mesh)** — TODO Task 14.
- [ ] **Magic content (Decision 2 additive fallback check)** — TODO Task 14.
- [ ] **Long-session sanity** — DEFERRED (N.6 watchlist; not load-bearing for ship).
## Open follow-ups for N.6
1. **GPU timer query double-buffering** — the current single-frame poll
pattern never sees `QueryResultAvailable=true`. Issue queryA on frame N,
queryB on frame N+1, read queryA on frame N+2. ~30 lines of state.
2. **Direct N.4 vs N.5 perf comparison** — re-run with `git checkout`ed N.4
SHIP (`c445364`) for a side-by-side measurement. Not load-bearing but
useful for N.6 ship message.
3. **Persistent-mapped buffers** — Decision 7 deferral. If profiling shows
the per-frame `glBufferData` cost is the residual hot spot, layer it on
top of the modern path.

View file

@ -1,98 +0,0 @@
# Phase N.5b — terrain perf baseline
**Captured:** 2026-05-09 at Holtburg town dueling field, radius=5, ~30s standstill.
## Methodology
Same build (commit at perf measurement: `da56063`), `ACDREAM_WB_DIAG=1`. The build
included a TEMPORARY `ACDREAM_LEGACY_TERRAIN=1` env-var toggle (since retired in T9
deletion of the legacy renderer) that routed Draw through the legacy renderer for
direct comparison. Both renderers were constructed and fed AddLandblock / RemoveLandblock
in parallel; only one drew per frame; the same Stopwatch wrapped whichever ran.
## Numbers
| Renderer | cpu_us median | cpu_us p95 | draws/frame | Visible LBs |
|---|---|---|---|---|
| **Legacy** (`TerrainChunkRenderer`) | 1.5 | 3.0 | 1 (1 chunk) | 132-143 (whole chunk) |
| **Modern** (`TerrainModernRenderer`) | 6.4-7.0 | 9-14 | ~36-51 | 36-51 (per-LB cull) |
(Legacy `draws=1` because its 16×16-LB chunking collapses radius=5's 121 visible
landblocks into a single chunk, dispatched as one `glDrawElements`. Modern issues
one `glMultiDrawElementsIndirect` with N=36-51 sub-commands.)
## Acceptance criterion
The N.5b spec acceptance criterion 5 read: "CPU dispatcher time at radius=5 ≥10%
lower than today's per-LB-binds path." The captured numbers show modern is ~4×
HIGHER on CPU at radius=5. **The criterion was wrong** — at radius=5 in Holtburg,
legacy's chunked path was already collapsed to one draw call. The architectural
wins of multi-draw indirect manifest at higher chunk counts (A.5 territory).
The spec is amended via this doc: ship N.5b on visual identity + structural
correctness rather than CPU savings at radius=5.
## Architectural wins of the modern path (real, even when CPU is higher)
1. **Zero `glBindTexture` per frame.** Bindless atlas handles are made resident
once at startup; the modern shader samples via `sampler2DArray(uvec2 handle)`.
Legacy issued 2 `glBindTexture(Texture2DArray)` calls per frame.
2. **Constant-cost dispatch.** As A.5 raises the streaming radius (next phase),
the visible chunk count grows. Legacy scales linearly: at radius=10 (4× chunks)
it's 4 `glDrawElements` calls; at radius=15 (≥9 chunks) it's 9+ calls. Modern
stays at exactly 1 `glMultiDrawElementsIndirect` regardless.
3. **Per-LB frustum culling.** Legacy culled at chunk granularity (16×16 LBs);
modern culls per-LB. At a typical Holtburg view, ~36-51 of 132 loaded LBs are
actually visible; legacy drew the entire 132-LB chunk (3.5× the visible work
pushed to GPU vertex/fragment stages, even though CPU dispatch was cheap).
## Why modern's CPU was higher at radius=5
Per-frame work in modern (in microseconds-ish budget on this scene):
- Walk all loaded slots checking visibility (~120 slots) → AABB test each
- Build DEIC array (51 entries × 20 bytes = 1020 bytes)
- `glBufferSubData(DRAW_INDIRECT_BUFFER, ...)` — driver memcpy
- 2× `glProgramUniform2(..., handle.low, handle.high)` for atlas handles
- `glBindVertexArray` + `glMemoryBarrier(GL_COMMAND_BARRIER_BIT)` + `glMultiDrawElementsIndirect`
Legacy's per-frame work:
- Bind 2 textures
- Bind one VAO (the chunk)
- One `glDrawElements`
The DEIC array build + buffer upload alone is ~3-5µs at radius=5 on this hardware,
which is the bulk of the modern overhead. At higher radius, this overhead amortizes:
the buffer is similar size, but the alternative (legacy's N draws) grows.
## Follow-up work
- **A.5 (next phase)** will exercise the higher-radius case where modern wins.
Capture a fresh baseline at radius=8 / 10 once A.5 lands.
- **N.6 perf polish** can investigate persistent-mapped buffers for the indirect
buffer, which would eliminate the per-frame `glBufferSubData`. Likely small win
at radius=5 (single ~1KB upload), bigger at higher radii.
- **GPU-side culling** (compute shader generating the DEIC array directly into
the indirect buffer) eliminates the CPU slot walk + DEIC build entirely. N.6 or
later territory; only worth it if profiling shows the CPU walk is hot.
## Lessons captured to memory
`memory/project_phase_n5b_state.md` records the high-value gotchas surfaced
during N.5b implementation. Three particularly bitable ones:
1. **`uniform sampler2DArray` + `glProgramUniformHandleARB` is unreliable.** Some
drivers (NVIDIA Windows in this case) reject the combination with
`GL_INVALID_OPERATION`. Use the `uniform uvec2` + `sampler2DArray(handle)`
constructor pattern instead — N.5's mesh_modern uses this, and N.5b's
terrain_modern adopted it after the black-terrain regression.
2. **`MaybeFlushTerrainDiag` underflow.** A naive median calc (`copy[N - nz/2]`)
underflows to `copy[N]` when only one sample has been recorded. Use
`copy[N - 1 - (nz - 1) / 2]` instead.
3. **Visual gate must actually be visually confirmed.** "Go" doesn't mean
"verified." During N.5b's gate the user said "go" without launching, which
masked the black-terrain regression for hours. The gate must include the
user reporting actual visual confirmation, not assent to proceed.

View file

@ -1,195 +0,0 @@
# Performance Tiers 2 + 3 — Future Roadmap
**Created:** 2026-05-10 during Phase A.5 polish.
**Status:** Future planning — not for current execution.
**Context:** A.5 shipped two-tier streaming with the entity dispatcher landing at ~3.5ms median (post-Bug-A and Bug-B fixes). Tier 1 (entity-classification cache) lands as A.5 polish and brings the dispatcher inside the 2.0ms spec budget. Tiers 2 + 3 are the "next big perf wins" beyond Tier 1.
---
## Background — why this exists
Discussion captured 2026-05-10: user observed 200-240 FPS at radius=12 on a Radeon 9070 XT @ 1440p and asked why an "old game like AC" doesn't deliver Unreal-level (1000+ FPS) on this hardware.
The honest answer: the bottleneck is *architectural*, not hardware. The CPU is single-threaded and rebuilds the entire draw plan from scratch every frame. Modern engines pre-bake static-world batches at content-cook time and rebuild only what changes.
AC's design — server-spawned per-entity world streamed at runtime — doesn't naturally batch the way Unreal's pre-cooked content does. Closing the gap requires backporting modern techniques while preserving AC's data model. Tiers 2 and 3 are that backporting work.
---
## Tier 2 — Static/dynamic split with persistent groups
**Estimated effort:** ~10-15 days (2-week phase).
**Estimated win:** entity dispatcher ~3.5ms → **~0.5-1ms median** at radius=12.
**Total frame time:** ~4-5ms → **~2-3ms = 400-600 FPS at standstill.**
### The core idea
Today, `WbDrawDispatcher._groups` (the dictionary of "(mesh + texture + blend) → list of instances to draw") is cleared and rebuilt from scratch every frame.
For trees, rocks, buildings, and other static entities (~95% of the world), the answer is identical every frame forever. Tier 2 makes the static-group instance buffers **persistent GPU-resident data**, just like Unreal's pre-baked world. The CPU only orchestrates "which groups are visible" per frame.
### Architectural shift
```csharp
class StaticInstancedGroup
{
public GroupKey Key;
public Matrix4x4[] Matrices; // grown as entities spawn
public BitArray ActiveSlots; // for free-list reuse
public bool NeedsGpuUpload; // dirty flag for delta upload
public Dictionary<uint, int> EntityToSlot; // for despawn lookup
public uint InstanceBufferOffset; // start of group's slice in global SSBO
}
```
**On entity spawn (atlas-tier static):** allocate a slot in each relevant group, write the matrix, mark dirty.
**On entity despawn:** free the slot, mark dirty.
**Per frame:**
- Static groups: LB-cull each group (cheap). For visible groups, flag for draw. **No matrix copy. No list rebuild.**
- Dynamic entities (~50 NPCs/players): today's per-frame walk-and-classify. Keeps the existing slow path for things that legitimately change every frame.
- Upload only the dirty groups' matrix slices (delta upload, not full reupload).
- Issue 2 multi-draw-indirect calls.
### Sub-decisions
**Frustum cull granularity at the group level:** at group level you can't reject individual instances; you draw the whole group or none of it. Two strategies:
- **Per-LB subgroups:** split each group into per-landblock subgroups. LB-frustum-culls reject subgroups whose LB is invisible. ~2K groups × ~5 LBs per group on average = ~10K subgroups. Each subgroup AABB cull is ~0.3 µs → ~3 ms per frame. Roughly a wash with today's per-entity cull.
- **Per-instance GPU cull (Tier 3):** compute pre-pass on the GPU writes which instances are visible to a draw-indirect buffer. ~0.05ms CPU. The right long-term answer.
For Tier 2 alone, per-LB subgroups are the recommended approach — keep CPU culling, just at coarser granularity than per-entity.
**Dynamic entities crossing LB boundaries:** when an NPC walks across a landblock boundary, it stays in the same group key but its "spatial bucket" changes. Solution: dynamic entities are tracked in a single global "dynamic group" outside the per-LB structure; they don't need spatial bucketing because there are only ~50 of them.
**Palette override invalidation:** server event swaps an NPC's clothing color → group key changes. Treat as despawn-from-old + spawn-into-new. NPCs are dynamic so this just rebuckets them.
**Animation overrides on static entities:** static entities don't animate. Trees don't bend (foliage wave is a vertex shader effect, not a group-key change). Buildings don't move. So the static path never invalidates.
**EnvCell visibility:** dungeon entities are gated by per-cell visibility state. Need to track which group instances are tied to which cell, and during visibility cull, gate per-cell. Keep using existing `ParentCellId` field on WorldEntity.
**Streaming load/unload integration:** when an LB unloads, all its static entity matrices need to be removed from their groups. Free-list management. Matches existing `LandblockSpawnAdapter` lifecycle.
### Effort breakdown
| Task | Days |
|---|---|
| Design + invariants document | 2 |
| Spawn-time slot allocator + free-list | 3 |
| Per-frame visibility + dirty-flag delta upload | 2 |
| Dynamic entity path (NPCs, projectiles) | 2 |
| Invalidation (palette/ObjDesc events) | 2 |
| EnvCell visibility integration | 1 |
| Streaming load/unload integration | 1 |
| Conformance testing | 2-3 |
| **Total** | **~10-15 days** |
### Risks
- **Slot management bugs** = double-frees or leaks (entities draw at random positions — visible).
- **Invalidation bugs** = stale matrices (entity teleports back to spawn point when palette changes).
- **Dynamic entity tracking** adds complexity around the static/dynamic boundary.
### Mitigations
- **Conformance test:** render a fixed scene through both pipelines, compare draw output. Adds CI infrastructure.
- **Per-frame validation in debug:** walk all groups, assert no orphan slots.
- **Hash invariant test:** static entities should produce stable group keys frame-over-frame. Add a debug assertion that fires once per frame in Debug builds.
---
## Tier 3 — GPU-side culling (compute pre-pass)
**Estimated effort:** ~1 month (longer phase).
**Estimated win:** entity dispatcher ~0.5-1ms (post-Tier-2) → **~0.05ms median.**
**Total frame time:** ~2-3ms → **~1.5-2ms = 600-1000+ FPS at standstill.**
### The core idea
Today (and after Tier 2), the CPU does per-LB or per-subgroup frustum culling and tells the GPU which groups to draw.
Tier 3 moves per-instance frustum cull to the GPU via a compute shader pre-pass. The CPU just uploads "here are all 1M instance matrices" once; the GPU compute shader writes which ones are visible to a draw-indirect buffer; the rasterizer draws only those.
This is the level Unreal is at. With this, per-frame CPU work for the entity dispatcher becomes essentially "tell the GPU what to do" + a tiny scratch upload.
### Why Tier 3 needs Tier 2 first
Without Tier 2's persistent group structure, GPU culling has nothing stable to operate on. The compute shader needs an addressable "here are the static instances" buffer to read from; that buffer only exists after Tier 2.
### Sub-decisions to be made
**Compute shader API:** OpenGL 4.3+ compute shaders are sufficient. We're already at GL 4.3+ for bindless. No additional capability requirement.
**Indirect draw command generation:** the compute shader writes a `DrawElementsIndirectCommand[]` buffer per pass. Render thread issues `glMultiDrawElementsIndirect` reading from that buffer. No CPU readback.
**LOD selection:** opportunity to add per-instance LOD selection in the compute shader (distance-based mesh detail). Not needed for A.5's scope; could be a Tier 4 follow-up.
**Per-light shadow map culling:** if shadows ship, GPU culling extends naturally to per-light frustum cull. Significant win for shadow rendering.
### Effort breakdown
| Task | Days |
|---|---|
| Compute shader design + GLSL implementation | 4 |
| Buffer layout coordination with Tier 2 | 2 |
| Silk.NET compute dispatch integration | 3 |
| Indirect command compaction logic | 4 |
| LOD selection (optional, ~stretch) | 4 |
| Validation: per-instance cull matches CPU cull within epsilon | 3 |
| Conformance + regression testing | 5 |
| **Total** | **~21-25 days, ~1 month** |
### Risks
- **GPU stalls** if the compute shader takes longer than expected (esp. on lower-end GPUs).
- **Sync overhead** between compute pre-pass and rasterizer pass.
- **Debugging difficulty** — GPU compute bugs are harder to diagnose than CPU bugs.
### Mitigations
- **Profile-driven design:** measure compute shader runtime on target hardware before committing.
- **Fallback path:** keep CPU cull as a runtime-toggleable option (env var) so we can A/B compare.
- **GPU debugging tools:** RenderDoc captures + frame-by-frame compute shader inspection.
---
## When to schedule these
**Tier 2:**
- Best fit: dedicated 2-week phase after a SHIP cycle. Treat it like a Phase B/C/N (i.e., name it Phase A.6 or N.7).
- Trigger: user wants to push radius beyond 12 (e.g., to 15 or 20 for true continent-scale horizon).
- Trigger: user wants to add 100+ active NPCs in a city without dropping below 240Hz.
**Tier 3:**
- Best fit: after Tier 2 has been live and stable for at least one cycle.
- Trigger: shadow map work begins (GPU cull + shadow cull share the same compute pre-pass infrastructure).
- Trigger: user wants 500+ FPS sustained for very-high-refresh scenarios (360Hz monitors, future hardware).
**Both:**
- Don't bundle with other phases. These are dedicated perf phases with their own brainstorm + spec + plan + SHIP cycles.
---
## What's "free" or smaller (out of Tier 1/2/3 scope but worth noting)
- **Plumb `JobKind` properly through `BuildLandblockForStreaming`** (~30 min). Today's Bug A patch wastes worker-thread CPU on hydration that gets thrown away for far-tier. Cleaner code, slight CPU savings on worker.
- **Eliminate `ToEntries` adapter allocation in `Draw`** (~15 min). Tiny win (~25 KB / frame). Could fold into Tier 1.
- **Persistent-mapped indirect buffer** (~2 days). Today's `glBufferData` per frame becomes a pre-mapped persistent buffer. Marginal win on RDNA 4; meaningful on lower-end GPUs.
- **Multi-thread mesh-build worker pool** (~1 day). 2.7s first-traversal horizon-fill drops to 0.7s with 4 workers. UX win on first walk-into-region.
These are good candidates for a "perf polish" mini-phase or to backfill into Tier 2.
---
## The architectural ceiling
Even with all three tiers, **a faithful AC client written in C# with bindless OpenGL tops out around 800-1500 FPS at radius=12 on RDNA 4 hardware**. Beyond that requires:
- Native C++ rendering core (eliminate .NET GC + JIT overhead)
- DX12/Vulkan API (eliminate driver state validation)
- Offline content cooking (eliminate runtime mesh/texture decode)
Each of those is a several-month undertaking and represents "becoming a different engine." The realistic target for acdream is 240-500 FPS at the user's monitor refresh, comfortably ahead of the visible-stutter threshold. Tier 1 + Tier 2 alone should deliver that for radius=12-15.
For "Unreal-level FPS at full quality," that's a different project.

View file

@ -1,193 +0,0 @@
# Phase N.6 slice 1 — perf baseline at Holtburg
**Created:** 2026-05-11.
**Spec:** [docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md](../superpowers/specs/2026-05-11-phase-n6-slice1-design.md)
**Measured against commit:** `25cb147` (Task 1 final — gpu_us fix + diag-gate symmetry follow-up)
**Purpose:** Capture authoritative CPU+GPU dispatch numbers so the next-phase decision (slice 2 vs C.1.5 vs Tier 2) rests on real data.
---
## §1. Setup
- **Hardware:** Radeon RX 9070 XT
- **Resolution:** 1440p (2560×1440)
- **Quality preset:** High (default)
- **Connection:** live ACE at `127.0.0.1:9000`
- **Character:** `+Acdream` at Holtburg
- **Sky / time:** clear midday (F7 → Noon, F10 → Clear)
- **Build:** Debug
- **Date measured:** 2026-05-11
- **Environment overrides:** `ACDREAM_WB_DIAG=1`, `ACDREAM_STREAM_RADIUS=<per-run>`
Note: `ACDREAM_STREAM_RADIUS=N` forces N₁=N (all N near-tier landblocks at full detail).
This is NOT the production A.5 default (N₁=4 / N₂=12), which was characterized in
CLAUDE.md as comfortable 200400 FPS at the default preset. These measurements
characterize the scaling curve — what happens as near-tier radius grows — not current
production behavior. FPS was not captured directly (no window-title screenshot per run);
it can be derived from `(1e6 / total_frame_time_us)` but the dispatcher's `cpu_us` is
only part of the frame (terrain, sky, particles, UI, GL submission overhead, and
swap-buffer wait are not included).
## §2. Dispatch CPU / GPU numbers
Each cell records the median of the last 3 `[WB-DIAG]` lines from a ~30s stable window.
`entSeen / entDrawn / groups / drawsIssued` are also from those lines (values per 5s bucket).
FPS column omitted — not captured per the note above.
| Radius | Motion | cpu_us median | cpu_us p95 | gpu_us median | gpu_us p95 | entSeen (per 5s) | entDrawn (per 5s) | groups | drawsIssued (per 5s) |
|--------|------------|---------------|------------|---------------|------------|------------------|-------------------|--------|----------------------|
| 4 | standstill | 3,208 | 3,313 | 93 | 95 | 16.9M | 15.5M | 1,216 | 1.65M |
| 4 | walking | 2,967 | 3,112 | 95 | 120 | 13.9M | 13.9M | 1,850 | 1.45M |
| 8 | standstill | 6,732 | 7,199 | 126 | 130 | 19.8M | 19.8M | 333 | 218K |
| 8 | walking | 6,572 | 6,927 | 96 | 113 | 18.1M | 18.0M | 534 | 245K |
| 12 | standstill | 12,853 | 13,525 | 344 | 507 | 19.6M | 19.6M | 541 | 184K |
| 12 | walking | 16,320 | 17,241 | 553 | 603 | 17.8M | 17.8M | 898 | 200K |
**Notable:** `meshMissing` counts at r4 standstill (~1.45M per 5s) drop to near-zero while
walking. This suggests the static-entity slow path's mesh-load lifecycle has some delay
before populating for newly-streamed content. Not fatal — doesn't affect rendered output —
but worth a follow-up issue in `docs/ISSUES.md` if it persists in normal play.
## §3. Surface-format histogram
From `ACDREAM_DUMP_SURFACES=1` at radius=12, ~30s after enter-world.
Output written to `%LOCALAPPDATA%\acdream\n6-surfaces.txt`.
- **Total unique GL textures:** 760
- **Total bytes (sum of W×H×4):** 96,387,584 (~96.4 MB)
**Top 10 (W, H) dimension buckets:**
| Dimensions | Count | Share |
|------------|-------|-------|
| 128×128 | 236 | 31% |
| 64×64 | 111 | 15% |
| 256×256 | 102 | 13% |
| 128×256 | 71 | 9% |
| 64×128 | 69 | 9% |
| 256×128 | 48 | 6% |
| 128×64 | 39 | 5% |
| 512×512 | 30 | 4% |
| 8×8 | 18 | 2% |
| 32×32 | 14 | 2% |
**Format distribution:**
| Format | Count | Share |
|---------------|-------|-------|
| RGBA8_DECODED | 760 | 100% |
All uploads land as RGBA8 regardless of source format (INDEX16, P8, DXT, BGRA, etc.
all decode through `TextureHelpers` before upload). The source-format diversity is real
but invisible to GL after the decode step.
**Top 10 (W, H, format) triples — atlas-opportunity input:**
Same as the dimension buckets above since there is only one format. The top-3 triples
(128×128, 64×64, 256×256) cover 449 of 760 surfaces = **59%**.
**Atlas-opportunity score: 59%** of surfaces fall into the top-3 (W, H, format) triples.
A conventional rule-of-thumb is that >30% concentration into the top buckets makes atlas
packing worth the implementation cost for memory savings; this measurement is well above
that. However, see §4 for why atlas is not the right next step despite the high score.
## §4. Conclusion + next-phase recommendation
### What the data shows
**The entity dispatcher is strongly CPU-bound.** At every radius, CPU dominates GPU by
3050×. At radius=12 standstill: 12.9 ms CPU vs 0.34 ms GPU. At radius=12 walking the
ratio is 16.3 ms CPU vs 0.55 ms GPU. There is no GPU bottleneck.
**GPU is wildly under-utilized.** The highest gpu_us p95 observed is 603 µs at radius=12
walking — against a 16,600 µs frame budget at 60 FPS. The GPU is working at roughly
3.6% of its 60fps capacity for entity rendering alone. Even accounting for terrain, sky,
particles, UI, and swap-buffer overhead, there is substantial headroom. The "GPU
comfortable" threshold (gpu_us p95 < 8,000 µs) is not even close to being challenged.
**CPU grows more than linearly with N₁ (near-tier radius), but sublinearly with
visible-LB count.** As N₁ grows from 4 → 8 → 12, median cpu_us grows from 3.2 ms →
6.7 ms → 12.9 ms — roughly 1.0× → 2.1× → 4.0× the r4 baseline. The visible-LB count
scales as `(2N+1)²`: 81 → 289 → 625, so CPU growth is sublinear in LB count (4.0×
vs 7.7× expected if every LB cost the same). Frustum culling discards most far LBs
early, but the outer per-LB walk still has to touch each one. The Tier 1 entity-
classification cache (`EntityClassificationCache`, shipped as #53) wins on the inner
loop (per-entity classification avoided on cache hits) but the outer walk dominates
as N₁ grows. This is exactly what the Tier 2 plan (persistent groups) at
`docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md` addresses by eliminating the
per-frame LB scan entirely.
**Radius=12 is not the production scenario.** `ACDREAM_STREAM_RADIUS=12` forces N₁=12
(625 near LBs at full detail). The production A.5 default preset is N₁=4 / N₂=12 (81
full-detail near + 544 terrain-only far), which CLAUDE.md already characterizes as
comfortable 200400 FPS at the default preset. The numbers above characterize the scaling
curve for headroom analysis, not the experience a typical player sees.
**Atlas opportunity is high (59%) but the win is memory-only — and modest.** With 96 MB
of textures and 59% in the top-3 dimension buckets, atlas consolidation would let the
top buckets share single `Texture2DArray` objects rather than each surface owning its
own 1-layer array. The primary wins of atlas — fewer sampler switches, fewer texture
binds — are already near-zero because bindless textures are made resident once at upload
and never bound per draw. The remaining win is the per-array metadata overhead × N
surfaces, which is bounded but not dramatic given all surfaces are already power-of-two
and same-format (RGBA8). Even on the optimistic side, the absolute memory saving is on
the order of low-MB to ~10 MB, not a 4050% halving. GPU is not bottlenecked on sampler
switches or memory bandwidth (0.6 ms gpu_us p95 at radius=12 walking demonstrates this
directly), so atlas adoption would cost 12 weeks of implementation risk for a memory
saving the process doesn't currently need at 96 MB.
### Recommendation
**Primary: do C.1.5 next (PES emitter wiring — portals, chimneys, fireplaces).** Four
reasons: (a) the production dispatcher is already comfortable at the default N₁=4 preset
per the CLAUDE.md notes; (b) the two slice-2 items that were "conditional on baseline"
data (atlas adoption and persistent-mapped buffers) are not justified — GPU is not
bottlenecked; (c) C.1.5 fills a visible content gap that has been open since C.1 shipped
and is in the roadmap queue ahead of N.6 slice 2; (d) C.1.5 stabilizes the particle path
before any future shader migration work in slice 2 touches `particle.frag`. Starting
point for C.1.5 scoping: `docs/plans/2026-04-27-phase-c1-pes-particles.md` lines 285295.
**Secondary (after C.1.5 lands): N.6 slice 2 with reduced scope.** The baseline data
justifies dropping atlas adoption and persistent-mapped buffers from slice 2 entirely.
What remains is a ~1-day cleanup: retire orphan `mesh.frag` (verify zero callers post-N.5
amendment), collapse dead `_handlesByOverridden` / `_handlesByPalette` legacy caches once
their callers are confirmed gone, migrate `particle.frag` to bindless sampling after C.1.5
stabilizes the path. Slice 2 is a cleanup sprint, not a performance phase.
**Tertiary option (if perf escalation becomes pressing): Tier 2 first.** The scaling
curve (3.2 → 6.7 → 12.9 ms as N₁ grows 4 → 8 → 12) confirms the per-LB walk is the
bottleneck — exactly what Tier 2's persistent-group structure at
`docs/plans/2026-05-10-perf-tiers-2-3-roadmap.md` addresses. Not urgent at the current
default N₁=4; worth revisiting if a future quality preset wants N₁=8 as default or if the
200400 FPS range at N₁=4 shrinks after more content is streamed.
**Decision rule for revisiting:** if future measurement at the default preset shows
cpu_us median > 5,000 µs or gpu_us p95 > 8,000 µs, re-open the escalation question.
Otherwise, hold the C.1.5 → reduced-slice-2 sequence.
## §5. Reproducing the measurements
Raw `[WB-DIAG]` output from each run was inspected live during measurement and the
median of the last three steady-state lines from each scenario was transcribed into §2.
The raw launch logs were not preserved — the captured medians in §2 are the canonical
record. To reproduce on the same hardware:
```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_WB_DIAG = "1"
$env:ACDREAM_STREAM_RADIUS = "4" # or 8, 12
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "baseline.log"
```
Stand still for ~30 s at the target radius (60 s at radius 12 to let streaming settle),
or walk N→E→S→W across one landblock. Then `Select-String -Path baseline.log -Pattern
"\[WB-DIAG\]" | Select-Object -Last 3` captures the steady-state numbers.
For the surface histogram, also set `$env:ACDREAM_DUMP_SURFACES = "1"`, stay in-world
~30 s after streaming has loaded ≥100 textures (the cache-size gate), then read
`$env:LOCALAPPDATA\acdream\n6-surfaces.txt`.

File diff suppressed because it is too large Load diff

View file

@ -1,320 +0,0 @@
# Phase C.1.5b handoff — issue #56 + EnvCell statics + animation-hook verification
**Created:** 2026-05-12, immediately after Phase C.1.5a merged to `main` (commit `88bda12`).
**Audience:** the fresh-session Claude (or human) picking up C.1.5b.
**Predecessor:** [C.1.5a portal PES wiring](../superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md) — slice 1, shipped.
---
## §1 Startup prompt (copy this into a fresh session)
Everything below this fence is the prompt to paste into a new Claude Code session. The detailed context the session needs lives in §2+ of this same file.
```
Pick up Phase C.1.5b — issue #56 (multi-emitter per-part collapse) first,
then EnvCell static-object DefaultScript dispatch + animation-hook
particle path verification.
## Context
Phase C.1.5a (portal PES wiring) merged to main 2026-05-12 (merge commit
88bda12). The PhysicsScriptRunner now fires Setup.DefaultScript on every
server-spawned WorldEntity via the new EntityScriptActivator. Visual
verification at the Holtburg Town network portal confirmed the mechanism
works end-to-end (10-hook portal script fires correctly, color +
persistence + orientation match retail), but exposed a pre-existing C.1
limitation now tracked as ISSUE #56: ParticleHookSink ignores
CreateParticleHook.PartIndex, so all 10 of the portal's emitters
collapse to one root position → compressed, partly-ground-buried swirl.
The C.1.5a final cross-task reviewer recommended #56 be resolved FIRST
in this slice, before the EnvCell static-object walker, because slice
2's natural visual gate (Holtburg inn interior fireplace, cottage
chimney) uses the same multi-emitter pattern — without #56 fixed,
slice 2 ships with the same visual gap.
## Read first (in order)
1. docs/plans/2026-05-12-phase-c1.5b-handoff.md (this file's §2+)
2. docs/ISSUES.md #56 (the per-part collapse problem with reproducible
identifiers from the C.1.5a verification session)
3. docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md §10
(slice 2 preview written during C.1.5a brainstorming)
4. docs/plans/2026-04-27-phase-c1-pes-particles.md lines 285295 (the
original C.1.5 scope source)
## Two slices in this session
### Slice A — issue #56 fix (per-part transform handling for static entities)
For static entities (portals, EnvCell statics, building decorations —
no animation), precompute the per-part offset from
Setup.PlacementFrames[Resting] at spawn time and surface those offsets
to the ParticleHookSink so SpawnFromHook can apply them. The handoff
doc §3 has the suggested architecture + decision space.
Acceptance: relaunch + walk to the Holtburg Town network portal. The
10 emitters should distribute across the portal Setup's parts instead
of collapsing — swirl extends vertically through the arch with
retail-like shape, not buried in the ground.
### Slice B — EnvCell static-object DefaultScript dispatch + animation-hook verification
Walk EnvCell.StaticObjects for newly-loaded landblocks; for each
StaticObject whose Setup has a non-zero DefaultScript, fire the
activator with a synthetic entity ID (suggested scheme: hash of
(landblockId, cellIndex, staticIndex) with a high-bit marker so it
doesn't collide with server guids — see handoff §4). Then verify the
animation-hook particle path (already shipped in C.1; just needs
visual confirmation): cast a spell on +Acdream and compare to retail.
Acceptance: Holtburg inn fireplace flames, cottage chimney smoke, and
a spell-cast particle effect on +Acdream all match retail.
## What this is NOT
- Not a renderer change. particle.frag stays as-is; bindless migration
waits for N.6 slice 2 after this slice lands.
- Not a perf phase. The N.6 baseline at radius=4 still holds; the
per-part precompute cost is bounded by N parts × M emitters per
spawned entity (small).
- Not adding new emitter types. Use the existing PES emitter data.
- Not touching the animated-entity path. For animated entities (NPCs,
monsters), per-part transforms vary per frame and would need a
per-tick refresh similar to UpdateEntityAnchor. Defer to a future
phase; C.1.5b stays scoped to static entities only.
## Suggested workflow
1. Read the handoff doc + the four referenced docs above.
2. Invoke superpowers:brainstorming to settle:
- For slice A: precompute-per-part-at-spawn vs render-thread-side-table
approach (handoff §3 has the tradeoff analysis).
- For slice B: the synthetic-entity-id scheme; whether the EnvCell
walker piggybacks LandblockSpawnAdapter or gets its own class.
- Visual verification locations.
3. After brainstorm: spec at
docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md (one spec
for both slices since they share the activator and tests), then plan
at docs/superpowers/plans/2026-05-13-phase-c1.5b.md, then execute
via superpowers:subagent-driven-development.
## Open issues from C.1.5a worth knowing
- #56 — multi-emitter per-part collapse. This slice's headline.
- #55 — meshMissing diagnostic spam at radius=4 standstill. LOW
severity, not blocking; only touch if you're already in the
dispatcher for unrelated reasons.
- Cold-path timing observation (C.1.5a Task 2 review): the activator
fires DefaultScript before pending-bucket entities are merged into
a loaded landblock. Mirrors existing _wbEntitySpawnAdapter pattern;
not a regression; defer.
## Three doc-drift items from C.1.5a (trivial — fold into the new spec)
1. C.1.5a spec §4 says "fifth (optional) parameter" — actually fourth.
2. C.1.5a spec §4 says "~50 lines" — file ships at 93 lines.
3. GpuWorldState.AddEntitiesToExistingLandblock (A.5 Far→Near
promotion path) does not fire the activator. No-op today because
promotion-tier entities are atlas-tier and the activator's
ServerGuid==0 guard would skip them anyway, but worth a code
comment explaining why the call is intentionally omitted there
(parallel to existing comments at the RemoveEntitiesFromLandblock
block in the same file).
Start by reading the handoff doc, then ask me what slice-A/slice-B
boundary feels right and what visual verification locations I want
to target.
```
---
## §2 What shipped in C.1.5a (so you don't re-do it)
### Commits on `main` (oldest to newest under merge `88bda12`)
| SHA | Title |
|---|---|
| `06d7fbd` | docs(vfx): Phase C.1.5a — portal PES wiring design spec |
| `ed5335b` | docs(vfx #C.1.5a): implementation plan + spec wiring-location fixes |
| `003c502` | feat(vfx #C.1.5a): add EntityScriptActivator (no wiring yet) |
| `e0529b0` | test(vfx #C.1.5a): real-emitter verification in OnRemove test + unused using |
| `44d8502` | feat(vfx #C.1.5a): wire EntityScriptActivator into GpuWorldState lifecycle |
| `65d833d` | feat(vfx #C.1.5a): construct EntityScriptActivator in GameWindow |
| `849690c` | refactor(vfx #C.1.5a): reuse SequencerFactory's capturedDats in resolver |
| `334f0c6` | fix(vfx #C.1.5a): seed entity rotation in activator so hook offset rotates |
| `9009318` | docs(vfx #C.1.5a): ship Phase C.1.5a + file issue #56 for per-part collapse |
| `88bda12` | Merge branch 'claude/lucid-burnell-aab524' — Phase C.1.5a |
### New files
- [`src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs`](../../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) — 93 lines including doc comments. Constructor `(PhysicsScriptRunner, ParticleHookSink, Func<WorldEntity, uint>)`; `OnCreate(WorldEntity)` resolves the entity's `Setup.DefaultScript.DataId`, seeds `_particleSink.SetEntityRotation(entity.ServerGuid, entity.Rotation)`, and calls `_scriptRunner.Play(scriptId, entity.ServerGuid, entity.Position)`; `OnRemove(uint serverGuid)` calls `_scriptRunner.StopAllForEntity(serverGuid)` + `_particleSink.StopAllForEntity(serverGuid, fadeOut: false)`.
- [`tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs`](../../tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs) — 4 xUnit `[Fact]` tests with mutation-check teeth verified during the C.1.5a code-quality reviews.
### Modified files
- [`src/AcDream.App/Streaming/GpuWorldState.cs`](../../src/AcDream.App/Streaming/GpuWorldState.cs) — fourth optional ctor parameter `EntityScriptActivator? entityScriptActivator = null`, field `_entityScriptActivator`, and two `?.OnCreate(entity)` / `?.OnRemove(serverGuid)` calls immediately after the matching `_wbEntitySpawnAdapter?.OnCreate` / `?.OnRemove` calls in `AppendLiveEntity` and `RemoveEntityByServerGuid`.
- [`src/AcDream.App/Rendering/GameWindow.cs`](../../src/AcDream.App/Rendering/GameWindow.cs) — new field declaration alongside `_wbEntitySpawnAdapter` and inline construction of the activator + resolver lambda inside the existing `OnLoad` block (~line 1620), passed to `GpuWorldState` as a named argument.
### What's working
- Server-spawned entities (`ServerGuid != 0`) with `Setup.DefaultScript.DataId != 0` fire that script through `PhysicsScriptRunner.Play` on enter-world.
- Multi-hook scripts dispatch all their hooks in order (timed by `StartTime` offsets — more retail-faithful than WB's "all at once" collection).
- `CreateParticleHook.Offset.Origin` rotates correctly from entity-local to world frame via the activator's `SetEntityRotation` seed.
- Despawn cleanly stops all scripts + emitters for the entity.
- 4 unit tests cover all three branches plus the rotation-seed correctness.
- Visual verification at the Holtburg Town network portal passed for the mechanism: 10-hook portal script fires correctly with matching color, persistence, orientation, multi-emitter dispatch.
## §3 Issue #56 decision space (slice A)
### The problem
`ParticleHookSink.SpawnFromHook` computes:
```csharp
var rotation = _rotationByEntity.TryGetValue(entityId, out var rot) ? rot : Quaternion.Identity;
var anchor = worldPos + Vector3.Transform(offset, rotation);
```
…where `worldPos` is `entity.Position` and `offset` is `cph.Offset.Origin`. The `CreateParticleHook.PartIndex` field is recorded into the per-handle tracking dict but never applied to the anchor. Retail's intended geometry is:
```
anchor = entityWorldPose × partLocalTransform[partIndex] × hookOffsetInPartLocal
```
Without the part transform multiplication, every emitter in a multi-emitter script lands at the same root position. Visible symptom: the Holtburg portal's 10 emitters compress to one point and the swirl appears partially buried because the offset's local-up direction goes off in world axes instead of the part's local axes.
### Where part transforms come from
For STATIC entities (no animation), per-part transforms come from `Setup.PlacementFrames[Resting].Frames[partIndex]` — see how `ObjectMeshManager.CollectParts` walks them in `references/WorldBuilder` (worktree-relative path; submodule must be initialized to read):
- For each `i` in `0..setup.Parts.Count`, the per-part transform is `Matrix4x4.CreateScale(setup.DefaultScale[i]) * Matrix4x4.CreateFromQuaternion(placementFrame.Frames[i].Orientation) * Matrix4x4.CreateTranslation(placementFrame.Frames[i].Origin)`.
- `DefaultScale` only applies when `SetupFlags.HasDefaultScale` is set.
- Fall back to `PlacementFrames[Default]` if `Resting` isn't present.
For ANIMATED entities (NPCs, monsters, the player), per-part transforms vary per animation frame and live in `AnimatedEntityState` / the animation tick. **Out of scope for C.1.5b.**
### Approach options
**Option A — precompute per-spawn, pass at activator-call time.**
`EntityScriptActivator` reads the Setup's `PlacementFrames[Resting]` once per spawn, builds a `Matrix4x4[] partTransforms` array, and passes it to a new sink method `_particleSink.SetEntityPartTransforms(entityId, partTransforms)` before calling `_scriptRunner.Play(...)`. `ParticleHookSink.SpawnFromHook` then reads `_partTransformsByEntity` to apply per-hook:
```csharp
var partXf = _partTransformsByEntity.TryGetValue(entityId, out var pts) && partIndex < pts.Length
? pts[partIndex] : Matrix4x4.Identity;
var anchor = worldPos + Vector3.Transform(Vector3.Transform(offset, partXf), rotation);
```
Pros: clean ownership (activator owns the lifecycle of part transforms keyed by entityId), matches existing sink-state patterns (`_rotationByEntity`, `_renderPassByEntity`), small code surface, fully testable.
Cons: stores per-entity array (matrix per part) — bounded but allocates. Doesn't compose with the animated-entity case (which would need per-tick refresh).
**Option B — render-thread side-table populated by the dispatcher.**
The `WbDrawDispatcher` already computes per-part world transforms each frame. Surface them via a side-table the sink queries. Per-frame.
Pros: free composition with animated entities (the dispatcher transforms whether the entity is animated or not).
Cons: render-thread / sink-thread coordination concern, bigger architectural surface, the dispatcher would need a new responsibility (publish part transforms) outside its draw-loop hot path. Risk of touching the modern bindless dispatcher's perf budget that N.5/N.5b worked to lock in.
**Option C — sink-side dat lookup on demand.**
`ParticleHookSink` calls `_dats.Get<Setup>(...)` on the hook fire to look up the part transform. Pros: zero state on activator. Cons: introduces dat coupling into the sink (currently dat-free), per-hook-fire dat lookup is a hidden allocation, doesn't compose with animated entities either, and we'd be reading the same Setup multiple times for the same entity.
### Recommended approach
**Option A.** It's the smallest surface, matches the existing sink-state pattern, doesn't expand any other layer's responsibilities, and the "doesn't compose with animated entities" downside is intentional — animated entities are explicitly out of scope and will get their own treatment later, possibly via Option B at that time.
### Test approach
Mirror the C.1.5a `OnCreate_SetsEntityRotationForHookOffsetTransform` test: construct an entity whose Setup has 2 parts (root at origin + part 1 lifted at (0, 0, 1)), fire a CreateParticleHook with `PartIndex=1` and `Offset.Origin=(0, 0, 0)`, assert the spawned particle's world position is `(0, 0, 1)` (the part's offset, not the root). Add a mutation check: delete the `SetEntityPartTransforms` line and confirm the test fails.
## §4 EnvCell static-object dispatch decision space (slice B)
### The problem
`EnvCell.StaticObjects` are interior decoration objects inside dungeon / building cells. Each StaticObject has a Setup reference and a placement frame. They have NO `ServerGuid` — they're dat-hydrated, not server-spawned.
Our `EntityScriptActivator.OnCreate` early-returns when `entity.ServerGuid == 0` (atlas-tier guard). So as-is, the activator won't fire DefaultScript for EnvCell statics.
### Two architectural questions
**Q1 — synthetic entity ID for tracking + cleanup.**
`PhysicsScriptRunner` keys active scripts by `(scriptId, entityId)`. `ParticleHookSink` keys per-entity emitter handles by `entityId`. EnvCell statics need a stable, unique 32-bit ID for these tables that won't collide with server guids (and won't collide between two EnvCell statics in different cells).
Suggested scheme:
```
uint syntheticId = 0xC0000000u
| ((landblockId & 0x0000FF00u) << 16) // landblock X byte bits 24-31 minus high marker
| ((landblockId & 0xFF000000u) >> 8) // landblock Y byte → bits 16-23
| ((cellIndex & 0x0000FFFFu) << 0); // bits 0-15: cell index within landblock
```
…leaving 4 bits for the static-object index within the cell. Adjust bit layout for the actual `(LandblockId, CellIndex, StaticIndex)` distribution. The `0xC0_______u` marker is **above** server guid range and **above** the anonymous-emitter range (`0x80_______u`) used by `ParticleHookSink._anonymousEmitterSerial`, so no collision.
Sanity check: `WorldEntity.ServerGuid` is `uint`; the `(scriptId, entityId)` dedupe key in the runner only needs uniqueness, not semantic meaning. Either scheme works as long as it's collision-free.
**Q2 — which adapter walks EnvCell.StaticObjects?**
Three options:
- **Option α — piggyback `LandblockSpawnAdapter`.** That adapter already walks `landblock.Entities` for atlas-tier mesh-ref counting. Extending it to also walk `EnvCell.StaticObjects` and fire DefaultScript via the activator keeps the per-landblock-load flow in one place. Cons: blurs the adapter's single responsibility.
- **Option β — new `EnvCellStaticActivator` class.** Mirror `EntityScriptActivator`'s shape but key by synthetic-id, walking each loaded landblock's EnvCells on load and firing per-static-object. Cons: more code; slight duplication of the activator pattern.
- **Option γ — `EntityScriptActivator` learns a "static-object" entry point.** Add `OnEnvCellStaticCreate(LoadedLandblock landblock, int cellIndex, int staticIndex, Setup setup, Vector3 worldPos, Quaternion worldRot)` to the existing activator. Compute the synthetic ID inside. Cons: signature creep on the activator.
Recommended: **Option β.** Keeps the existing activator's `WorldEntity`-shaped contract pure; the new class has a clean per-static-object contract; both share `_scriptRunner` and `_particleSink` instances so no architectural duplication, just two thin orchestrators.
### Lifecycle
EnvCell statics live as long as their parent landblock is loaded. On landblock unload, the new activator should stop all scripts for all its synthetic IDs from that landblock. Mirror `LandblockSpawnAdapter`'s `OnLandblockLoaded` / `OnLandblockUnloaded` lifecycle.
## §5 Animation-hook verification (slice B's quick half)
Already shipped in C.1: `MotionInterpreter` fires per-keyframe hooks through `IAnimationHookSink``ParticleHookSink`. We just haven't verified visually in the current codebase state.
Procedure:
1. Cast a spell on `+Acdream` (the test character likely has at least one spell + components configured — check or grant if needed).
2. Watch the cast-anim particle effect (sparkles, glyphs, etc.) — does it match retail's casting animation?
3. Optional: trigger an emote with a particle hook (the `\dance` / `\drink` emotes are good candidates if they have particle data).
If broken, file an issue with the symptom. If working, mark slice B complete on verification.
## §6 Verification locations
All in or near Holtburg, within ~30s of `+Acdream`'s spawn:
- **#56 fix re-verify** — the Town network portal used in C.1.5a. Same procedure as C.1.5a's Task 4 (see [the C.1.5a spec §8](../superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md)).
- **EnvCell chimney** — any cottage / inn within the Holtburg outer perimeter with a smoking chimney in retail. Confirm via dual-client.
- **EnvCell fireplace** — Holtburg Inn interior. Walk inside and stand near the fireplace. Confirm flame particles match retail.
- **Animation-hook verify** — cast a spell standing somewhere safe (outside any aggro range). Compare to retail.
## §7 File pointers for slice 2
- Particle pipeline (Core): [`src/AcDream.Core/Vfx/ParticleSystem.cs`](../../src/AcDream.Core/Vfx/ParticleSystem.cs), [`ParticleHookSink.cs`](../../src/AcDream.Core/Vfx/ParticleHookSink.cs), [`PhysicsScriptRunner.cs`](../../src/AcDream.Core/Vfx/PhysicsScriptRunner.cs).
- Activator (App): [`src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs`](../../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs).
- Streaming bridge (App): [`src/AcDream.App/Streaming/GpuWorldState.cs`](../../src/AcDream.App/Streaming/GpuWorldState.cs), [`LandblockSpawnAdapter.cs`](../../src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs).
- Renderer: [`src/AcDream.App/Rendering/ParticleRenderer.cs`](../../src/AcDream.App/Rendering/ParticleRenderer.cs) — **don't touch** in C.1.5b; bindless migration is N.6 slice 2.
- EnvCell loader: search for `LoadedCell` / `EnvCell.StaticObjects` in `src/AcDream.App/Streaming/` and `src/AcDream.Core/World/`.
- C.1.5a tests as a reference: [`tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs`](../../tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs).
## §8 Open questions to surface during brainstorming
- Slice A: does the C.1.5a final reviewer's "static-only fix is self-contained" claim hold up? (Section §3 Option A says yes; brainstorming should verify by checking `EntityScriptActivator`'s spawn path doesn't depend on animation state.)
- Slice B: which Setup field actually lives on `EnvCell.StaticObjects` — is it a `SetupId` reference or an inline Setup? Different shape changes the synthetic-ID hash input.
- Slice B: are EnvCell statics ALSO subject to the cold-path timing observation from C.1.5a Task 2 review (firing before the cell is rendered)?
## §9 Worktree cleanup reminder (one-time, from outside the worktree)
The C.1.5a worktree directory at `C:/Users/erikn/source/repos/acdream/.claude/worktrees/lucid-burnell-aab524` was not auto-removed because the controller session held a file lock. After this session ends, from any other directory:
```powershell
git -C "C:/Users/erikn/source/repos/acdream" worktree remove --force `
"C:/Users/erikn/source/repos/acdream/.claude/worktrees/lucid-burnell-aab524"
```
The branch `claude/lucid-burnell-aab524` was successfully deleted; only the worktree directory needs manual cleanup.

View file

@ -1,381 +0,0 @@
# The holistic building-render port plan (Phase B) — one drawing discipline
**EXECUTION STATUS (2026-06-11, post-BR-7): BR-2…BR-7 are ALL CODE-COMPLETE
on the branch — the render arc as the fused tasks T1T4 (T1 `579c8b0` frame
order; T2 `cf8a2c3`/`529dfcf`/`88f3ce1` flood fidelity, two retail constants
refuted by the conformance gate and kept at documented tolerances; T3
`a6aec8c` viewconeCheck; T4 `4a307d3` one-gate deletions), and BR-7 (T6,
collision A6.P4) as `6ec4cde` (signed OtherPortalId gate) + `abf36e2`
(BuildShadowCellSet flood) + `dbfbf85` (per-cell architecture: flood
registration, building channel, per-cell query, b3ce505 DELETED — closes
#99) + `ca4b482` (straddle-only outside-add, A6.P5 widening + #90
stickiness removed). Of the 4 #99-era Core reds, 3 flipped green as
designed (door apparatus + tick-13558 + tick-22760's blocking invariant);
the 4th (BSPStepUp D4) + 22760's lateral-slide delta proved to be a
SEPARATE pre-existing slide-response family — filed #116, D4 skipped with
the reference (probes show the cell-set layer innocent). Suites: Core
1416/0/2skip, App 225, UI 420, Net 294.
**T5 EXECUTED 2026-06-11 (the single comprehensive user gate) — PARTIAL
PASS.** ✅ Confirmed by the user: doors block both ways incl. off-center
(#99 visual), cellar descent/ascent clean + #108 grass-sweep GONE, inn
2nd floor clean (#97 closed), interiors stable through doorways incl.
edge-on, #109 far-door oscillation GONE, formerly-popping stairs now
STABLE at all ranges (the distance-pop class is dead). ❌ Remaining —
four filed render artifacts: **#117** aperture-shaped see-through
(doors/interiors through terrain hills + through nearer buildings — the
punch erases occluder depth), **#118** character clipped+vanishes for a
moment on house exit, **#119** old-tower stairs partially invisible +
extraneous barrel (pre-existing; `[up-null]` permanently-invisible mesh
lead in the T5 log), **#120** `[pv-ERROR]` in-place-propagation
convergence tripwire at depth 128 on the cottage cells (self-detected
T2 invariant break — investigate first). Rain-indoors not verifiable
(clear weather). NEXT: fix #120#117#118#119 at the mechanism
level, then a focused re-gate on just those spots.**
**Status: APPROVED + AMENDED (2026-06-11). EXECUTION DIRECTIVE CHANGED BY THE
USER: "I don't care if it is non-playable… I want everything ported, then we
test."** The per-phase playability constraint and per-phase user visual gates
are DROPPED. BR-2 through BR-6 execute as ONE continuous port (the fused
render discipline), with build + unit/conformance tests green at every commit
(engineering hygiene, not gates), and **ONE comprehensive visual test pass at
the end**. Rationale: the first BR-2 attempt failed precisely because the
phase slicing cut retail's frame order in half (the punch shipped without
entities-drawn-last and erased characters in apertures — reverted `88be519`);
the installment-must-be-a-complete-retail-behavior rule replaces the
playability rule. BR-7 (collision) runs as an independent track; BR-8b
(lighting) still wants the verification resume first.
Companion to the Phase A comparison:
[`docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md`](../research/2026-06-11-building-render-acdream-vs-retail-comparison.md)
(evidence appendices in
[`docs/research/2026-06-11-holistic-map/`](../research/2026-06-11-holistic-map/)).
Mandate: *"one solution that works every time I walk to a new landblock and
walk into a dungeon"* (2026-06-11).
---
## 0. The invariant (what "one drawing discipline" means, retail-cited)
Every phase below moves us toward — and no phase may move us away from — this
frame shape, which is retail's (Ghidra-cited in the comparison doc §2):
1. **Geometry is flattened at load** into surface-batched meshes (we already
do this). World geometry is **never geometrically clipped at draw time**.
2. **Untextured (solid) surface batches never draw** on building shells and
cell meshes (`skipNoTexture`); they do draw on plain objects.
3. **Portal polygons are not wall geometry.** They exist per frame only as
(a) flood admission tests (`ConstructView`: eye-side ε=0.0002 → clip vs
current view → cell loaded) and (b) **invisible depth writes** — far-Z
*punch* before an interior draws through an aperture; true-depth *seal*
on portals to the outside after the landscape draws.
4. **Cells draw whole, far→near, once** (frame stamp); the z-buffer plus the
punches/seals produce pixel-exact apertures.
5. **Objects and particles are culled per portal view** (sphere vs the view's
edge planes — `viewconeCheck`), never clipped, never scissored.
6. **One visibility computation feeds everything** — the PView flood. No
second BFS, no parallel gate, no distance constants in admission.
## 1. Keep-list (the code worth saving — explicitly not touched/rewritten)
- **Mesh pipeline**: `ObjectMeshManager` flatten + global VAO + bindless MDI
(`WbDrawDispatcher`) — retail-faithful architecture, confirmed by the
`ConstructMesh`/`RemoveNonPortalNodes` finding.
- **The flood port**: `PortalVisibilityBuilder` (homogeneous clipper, side
tests, reciprocal clip, exact-match skip) + conformance gates
(`CornerFloodReplayTests`, `Issue113MeetingHallFloodTests`) — BR-4 adjusts
constants/heuristics, it does not rewrite the clipper.
- **Membership** (P1 9/9 golden) + **straddle gate** (`414c3de`) +
**camera collision sweep** (verbatim `update_viewer`) + **znear=0.1** +
**#105 texture flush** + **two-tier streaming** + spawn/snap validation
(#107/#111/#112).
- Diagnostics/probes and the dat dump harness.
The M0 freeze list is superseded *for rendering only* by the 2026-06-11
mandate; nothing outside building/interior render + interior collision is in
scope.
## 2. Phases
Ordering rule: each phase lands green (build + full suites + named visual
gate) and the client stays playable after every phase. Conformance pins come
from the dat harness + the flood replay harnesses; retail constants are cited
inline when ported.
### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)
**Premise falsified before implementation (the BR-1 pre-check,
`Diagnostic_ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses
every portal fill** — all four extraction paths skip `Stippling.NoPos`
positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`,
`PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`),
and the Holtburg fills have no negative surface. The planned "draw-time
surface gate" has nothing to gate.
**What shipped instead — the equivalence pin**
(`StipplingSurfaceEquivalenceTests`): 2,607 polys across 13 building models +
13 environments, **zero violations both directions** — `NoPos ⇔ untextured
surface`. Our build-time skip is therefore *proven equivalent* to retail's
draw-time `skipNoTexture` rule on this content; the
`portal-poly-suppression-criterion` divergence closes as
equivalent-with-proof. The pin fails loudly if future content breaks the
invariant (the cue to implement the draw-time gate then).
**Consequences (the honest part):**
- The **#113 phantom residual is NOT GfxObj fills** — it cannot be, they
never reach a vertex buffer. The "root cause #2" attribution from the
e46d3d9 session is corrected; the e46d3d9 user-gate observations (filter
removed phantom/doors) were confounded — the filter was a provable mesh
no-op on both shells and door parts.
- The phantom's plausible true sites are cell-side: flood-admitted stair
CELLS drawn with a pass-all slice when the assembler hands them no slot
(`RetailPViewRenderer.cs:71` draws ALL visible cells; `NoClipSlice`
default), and/or stair-cell STATICS drawn unclipped + un-viewcone'd by
design (`object-lists-skip-portal-view-gate`, confirmed). **BR-2's first
task is a 10-minute probe at the hall bisect spot pinning which** —
the closure moves to BR-2/BR-3 (shells) and BR-5 (statics).
- **Closes:** the `portal-poly-suppression-criterion` divergence (as
proven-equivalent); #113's closure moves to BR-2/BR-3/BR-5.
- **Shipped:** the pre-check + equivalence pin tests; no production code
(none needed).
### BR-2 — Aperture depth machinery (punch / seal / clear)
**What:** port the invisible depth writes:
(a) wire `DrawExitPortalMasks` (today an unwired no-op) as a depth-only draw
of each outside-leading portal polygon, software-clipped to its view slice
(the `ClipToRegion` math already exists), at the portal's **true projected
depth** (retail `maxZ2`) — after the landscape slices, indoor roots;
(b) add the **far-Z punch** (retail `maxZ1`) on building-aperture flood
success on the outdoor + look-in paths, before the interior cells draw;
(c) replace the per-slice scissored `ClearDepthSlice` AABB clear with
retail's discipline: one full depth clear between the outside stage and the
interior stage, gated on whether any seal was drawn (`portalsDrawnCount`);
(d) on the look-in path, draw interior-through-aperture **before** the shell
mesh (retail `DrawBuilding` order) so the shell's depth closes everything
outside the punch.
- **First task (from BR-1's falsification):** the 10-minute probe at the
hall bisect spot — when the phantom is visible, log per stair cell
(0x100..0x106) whether it drew with a real clip slot or the pass-all
`NoClipSlice`, and whether its statics drew — pinning the phantom's true
draw site (shells → fixed here/BR-3; statics → BR-5).
- **Closes:** #108 (outdoor terrain sweeping across the upstairs door — the
missing true-depth seal is the confirmed `missing-portal-depth-fence`
divergence); the outdoor-root depth-discipline gap; part of #109; the
#113 phantom residual if the probe pins it on pass-all shell slices.
- **Acceptance:** cellar↔main-floor walk shows no grass sweep (user gate);
phantom-spot check at the hall (user gate, replaces the old BR-1
acceptance); new harness fact: seal depth = portal plane depth inside the
clipped aperture polygon (GL readback test or probe assertion); suites
green.
- **Size:** ~3 commits (~80 lines of GL + clipper reuse per the area
estimate, plus the clear re-shape and order swap).
### BR-3 — Retire the geometric shell chop; whole-shell far→near draws
**What:** remove `gl_ClipDistance` as the *enforcement* mechanism for cell
shells (both the outdoor-scoped enable from `927fd8f`/`9ce335e` and the
never-enabled indoor half — i.e. #114 closes by *deleting* the chop, not
perfecting it). Shells draw whole, far→near per `OrderedVisibleCells`
(already the order), drawn-once. Clip regions remain for admission, punch
shapes, and (BR-5) object culling. The landscape-through-aperture pass keeps
its per-slice plane clip for now (open Q: `LScape::draw` internals) — revisit
after BR-2 proves the seal protects terrain.
- **Closes:** #114 (chopped stairs / vanished candle area / barrel-through-
wall were artifacts of clipping geometry retail never clips) — jointly
with BR-2. Removes the 8-plane budget + slot-0 PASS-ALL as load-bearing
for shells.
- **Acceptance:** meeting-hall interior + multi-room cottages render
unchopped from indoor and outdoor eyes (user gate vs the #114 screenshot
set); phantom stays gone (BR-1 unaffected); flood replay gates green.
- **Order constraint:** must not land before BR-2 (the depth fence replaces
the chop's job at apertures).
- **Size:** ~2 commits (mostly deletions + the draw-order assertion).
### BR-4 — Shell-draw-driven floods + flood fidelity
**What:** make the building's own draw the flood trigger, retail-shaped:
pair the shell GfxObj's `PortalRef.PortalIndex` with its `BuildInfo.Portals`
entry (the `outdoor_portal_list` correspondence) and, when a shell survives
the cull for a view slice, run each aperture through the ported
`ConstructView(CBldPortal)` chain under that slice. Then remove the
non-retail machinery the trigger replaces: the 48 m seed constant, the
Chebyshev≤1 candidate gather, the `EyeInsidePortalOpening` full-view rescue;
adopt retail constants (ε=0.0002; in-plane rejects for building portals);
add the 1-px screen-space vertex dedup to `ClipToRegion` output (retail's
fixpoint floor) and switch late view growth to in-place propagation
(`AddToCell`/`FixCellList`/`AdjustCellView` shape), removing the
`MaxReprocessPerCell=16` cap; make `MergeBuildingFrame` union views instead
of first-wins and retire single-slot consumers (`CellIdToSlot[0]`); bind
nested floods to their originating slot (the `building_view` latch).
- **Closes:** #109 (binary 48 m pop + first-wins view loss + missing punch
are its named mechanisms); the flood-stability family (edge-on doorway
residuals); enables interior-visible-through-window parity.
- **Acceptance:** flood replay harnesses extended: (a) building flood
triggers with no distance constant — admission matches the
clip-survival rule across an eye sweep; (b) two-aperture cell holds two
views; (c) growth propagates without the cap on a portal-dense fixture;
#109 spot user gate; suites green.
- **Size:** ~45 commits (trigger + pairing; constants/dedup; growth
in-place; merge union; deletions).
### BR-5 — Per-view object + particle culling (viewconeCheck)
**What:** port `Render::viewconeCheck`: per view slice, lift the per-edge
eye planes (each NDC edge + the eye defines a plane — the `view_vertex.plane`
analog) and sphere-test every entity and emitter against the slice before
draw; route particles through the same gate and the same clip/punch
discipline (delete the `BeginDoorwayScissor` AABB path); fix the
outdoor-root unattached-emitter drop; gate the weather pass on
`is_player_outside` (player cell, not viewer root).
- **Closes:** particles-through-walls (candle flames in other buildings);
rain-indoors-through-doorways; the neighbour-room object over-inclusion
half of the old #114 report.
- **Acceptance:** flame-through-wall spot at Holtburg (user gate); a
conformance fact pinning sphere-vs-slice culling on a fixture; no
regression in entity draw counts outdoors (perf probe within noise).
- **Size:** ~3 commits.
### BR-6 — One gate: consolidate visibility + delete legacy paths
**What:** make the PView flood the only visibility computation:
remove the per-frame ACME BFS (`CellVisibility.ComputeVisibilityFromRoot`)
by folding its remaining consumers (lighting indoor flag etc.) onto PView/
membership outputs; delete or quarantine the confirmed legacy remnants
(`InteriorRenderer`, `IndoorDrawPlan` consumers of the old path, the
`clipRoot==null` second render branch, the dormant exit-mask wiring once
BR-2 rewires it, duplicate frustum implementation); one frustum, one
center/radius window.
- **Closes:** the `dual-live-visibility-computations` inconsistency class
(the one-gate rule, `feedback_render_one_gate`); removes the surface area
where two gates disagree (future flap-class bugs).
- **Acceptance:** gate-audit re-run shows ONE visibility computation per
frame; every deletion verified by a launch + the visual gate set; suites
green.
- **Size:** ~3 commits, mostly deletions (each independently revertable).
### BR-7 — Interior collision: per-cell shadow lists (A6.P4, verified) — ✅ CODE-COMPLETE 2026-06-11 (`6ec4cde`+`abf36e2`+`dbfbf85`+`ca4b482`; visual confirmation rides T5)
**What:** ship the A6.P4 architecture with the investigation's corrections:
registration builds the cell set by sphere-overlap portal flood (not an XY
grid; crosses landblocks), per-cell `shadow_object_list` iteration on the
query side (`CheckOtherCells` runs env AND shadow objects per other cell),
buildings dispatch through a per-LandCell building channel
(`CSortCell.building` shape), `OtherPortalId` widened to signed with the
`>= 0` gate (sign-extension Ghidra-proven). Then remove the `b3ce505`
stopgap, the A6.P5 `hasExitPortal` widening, and the #90 stickiness
workaround.
- **Closes:** #99 (doors block from both sides), very likely #97; retires
three flagged workarounds.
- **Acceptance:** A6.P4 spec acceptance (doors block both ways at Holtburg
inn + cottages; #98 cellar ascent stays fixed — `CellarUp` harness green);
capture/replay comparison on the door apparatus; suites green.
- **Size:** the A6.P4 spec's estimate stands (~5 commits); independent of
BR-2..BR-5 — may run in parallel with them.
### BR-8 — Feel tier: camera, lighting, LOD (post-discipline polish)
- **BR-8a Camera (#115, verified root cause; can land any time):** damp the
sought eye FROM the published collided viewer each frame (retail
`PlayerPhysicsUpdatedCallback` shape) and apply the computed player fade
over the 0.45→0.20 m band. Acceptance: cramped-interior turn feel (user
gate). ~12 commits.
- **BR-8b Lighting (pending verifier confirmation):** interior sun mask
(never sun-light interiors), static cell-light burn-in (all lights, not
8-nearest), viewer light, per-object light selection, surface
luminosity/diffuse. Acceptance: side-by-side interior look vs retail
screenshots. Phase-sized; spec before code.
- **BR-8c LOD + dedup (low):** per-part degrade selection beyond humanoids;
frame-stamp draw dedup. Optional per-cell interleave for draw-order parity
is explicitly NOT planned (z-buffer makes it unnecessary; revisit only on
evidence).
- **Picking refinements** (all-low area): defer; file as issues when the
port changes what is clickable.
## 3. What this plan deliberately does NOT do
- No per-frame BSP traversal of ordinary geometry (retail doesn't either).
- No rewrite of the mesh/MDI pipeline, the flood clipper, membership, or
streaming (keep-list).
- No `leaf_cells`/`CPartCell` port (path dormant in the 2013 binary — needs
runtime proof first).
- No transparency-sorting work yet — that area's map is still re-running;
fold its findings in as a BR-9 candidate after review (the AlphaList
deferral machinery is already decompiled in the Area 1 file).
## 4. Explicitly out of scope — tracked follow-ups (NOT covered by BR-1…BR-8)
Completing BR-1 through BR-8 lands the building/interior **drawing
discipline** and the collision rearchitecture. It does **not** cover the
items below. They are named here so the boundary of what the campaign
delivers is written down, not assumed — each becomes its own roadmap item or
issue, none blocks BR-1…BR-8.
- **FU-1 — Transparency / draw-sorting (→ BR-9 candidate).** Retail's
`DrawSortCell` + AlphaList deferral (decompiled in
`2026-06-11-holistic-map/wf1-gfxobj-draw.md`) governs water surfaces,
translucent windows, and alpha-blend ordering. The area's *map never
completed* (agent hit the token limit), so there are no divergences yet —
scope it before promoting to BR-9. **Severity: medium; user-visible as
wrong window/water compositing.**
- **FU-2 — Dungeon visibility scaling (#95).** The 8 phases are
Holtburg-building-shaped. Dungeons share the EnvCell/portal discipline so
they benefit *automatically*, and BR-4's tighter flood admission
(no-distance-constant + screen-clip rejection + cell-loaded gate)
**plausibly** shrinks #95's 135-cells/frame blowup — but #95 is a
disconnected-landblock *seeding* problem that BR-4 is not guaranteed to
fix. **Re-measure #95 after BR-4/BR-6 land; if still blown, it needs its
own phase.** Do not assume the building port closes it.
- **FU-3 — Distance LOD / degrades (= BR-8c, optional).** Per-part degrade
selection beyond humanoids; far models stay base-detail until picked up.
- **FU-4 — Picking refinements** (4 low-severity divergences,
`wf2-picking-selection.md`). Defer; file as issues if/when the port
changes what is clickable (e.g. building shells, baked fills).
- **FU-5 — The ~30 open questions** live in the comparison doc §6
(`2026-06-11-building-render-acdream-vs-retail-comparison.md`). The
load-bearing ones are referenced inline in the phases that consume them
(e.g. `LScape::draw` clip behavior for BR-2/BR-3, the near-W constant,
`DrawPortal` mode-3 seal-on-failure for unstreamed interiors); the rest
are pinned during implementation, not before.
- **FU-6 — Verification top-up.** ~36/76 divergences remain UNVERIFIED (the
overnight resume was stopped to preserve budget; both runs are resumable
by ID — see comparison §7). Run a cheap resume before **BR-8b lighting**
scoping (the one phase that leans on unverified rows) and before promoting
FU-1 to BR-9.
## 5. Sequencing summary
```
BR-1 (surface gate) — ✅ RESOLVED as already-equivalent (pin shipped,
no production code; #113 closure moved to
BR-2/3/5 — see BR-1 section)
BR-2 (depth punch/seal) — FIRST implementation phase; opens with the
phantom-site probe; enables BR-3
BR-3 (delete shell chop) — closes #114 with BR-2
BR-4 (draw-driven floods) — closes #109; flood fidelity
BR-5 (viewconeCheck) — particles/objects through the same gate;
closes the phantom if it is statics-side
BR-6 (one gate + deletions) — consolidation after the discipline is in
BR-7 (collision A6.P4) — independent track; may interleave with BR-2..5
BR-8 (camera/lighting/LOD) — feel tier; BR-8a may land early
```
Every phase: `dotnet build` + full suites green, conformance pins added with
retail citations, named user visual gate, roadmap/ISSUES updated in the same
session, and the render digest updated when a phase closes one of the named
bugs.
## 6. Approval asks
1. Approve the plan shape + ordering (BR-1 → BR-8, BR-7 parallel-capable).
2. Approve the deletions implied by BR-3/BR-6 (shell-chop enforcement,
ACME BFS visibility, legacy render branches) — all on the strength of the
cited evidence that retail has no counterpart.
3. Note the verification caveat: ~36/76 divergences still carry UNVERIFIED
(resume in flight); BR-1..BR-3's load-bearing claims are either verified
or dat-confirmed locally, so approval need not wait on the rest.

View file

@ -1,177 +0,0 @@
# Phase R — retail motion & animation stack, ground-up reconstruction
Date: 2026-07-02. **Mandate (user, verbatim intent):** a complete new
movement + animation system, verbatim retail equivalent — all movement,
inbound and outbound, all animation, for players, NPCs, and monsters. No
frozen code, no bandaids, no guessing, no approximation. Total overhaul.
**Execution shape:** a staged verbatim RECONSTRUCTION, not a big-bang
cutover — each stage builds a retail class 1:1 with a conformance harness,
cuts consumers over, and DELETES the legacy path in the same stage. "New
system" is the destination; the client keeps running between stages. The
cdb-golden technique is proven (183/183 live-retail dispatch conformance in
S2a) and is the acceptance mechanism for every stage: the user's eyes are a
final sanity pass only.
Supersedes the L.2g S3S6 slice plan (S1/S2/S5 landed and are absorbed as
components). This doc is the plan of record; the deviation map
(`docs/research/2026-07-02-inbound-motion-deviation-map.md`), the funnel
pseudocode (`…-s2-inbound-funnel-pseudocode.md`), and the 2026-06-04
sequencer deep-dive are its research base.
## The retail module map to reconstruct (per physics object, ALL classes)
```
CPhysicsObj (local player, remote player, NPC, monster — ONE pipeline)
├─ MovementManager unpack_movement 0x00524440 (10-way dispatch),
│ │ PerformMovement, MotionDone relay
│ ├─ CMotionInterp raw_state + interpreted_state + pending_motions,
│ │ DoMotion / DoInterpretedMotion / StopInterpretedMotion,
│ │ apply_raw/interpreted/current_movement, my_run_rate,
│ │ HitGround / LeaveGround / ReportExhaustion, jump family
│ └─ MoveToManager movement types 6/7/8/9 (MoveToObject/Position,
│ TurnToObject/Heading), node stepping, arrival radii,
│ fail distance, CanCharge walk/run selection
├─ CPartArray → MotionTableManager pending_animations, add_to_queue 0x0051bfe0,
│ │ remove_redundant_links 0x0051bf20,
│ │ CheckForCompletedMotions 0x0051be00 → AnimationDone
│ └─ CMotionTable::GetObjectSequence 0x00522860
│ │ same-substate fast path (change_cycle_speed +
│ │ subtract/combine_motion), link path (get_link +
│ │ style-default double-hop), re_modify, is_allowed
│ └─ CSequence anim-node DLList, append_animation 0x00525510,
│ remove_cyclic_anims, clear_physics, velocity/omega
│ accumulators, update/update_internal + apply_physics
│ (root motion), placement frames, hook dispatch
├─ PositionManager InterpolationManager [PORTED ✓ L.3] + StickyManager
│ + ConstraintManager
└─ per-tick UpdateObjectInternal 0x005156b0 order:
CPartArray.Update (CSequence root motion) → PositionManager.adjust_offset
(chase REPLACES) → Frame.combine → UpdatePhysicsInternal → FULL transition
sweep (ALL entities — retires the remote no-sweep fork) → DetectionManager /
TargetManager / MovementManager.UseTime / CPartArray.HandleMovement /
PositionManager.UseTime. `process_hooks` is inside UpdatePositionInternal,
before the sweep and manager tail (corrected 2026-07-19 from the named
retail body; see `docs/research/2026-07-19-r6-update-object-order-pseudocode.md`).
```
## Already-verbatim components (absorbed, not rewritten)
- `MotionSequenceGate` (S1) — is_newer 3-stamp gate, live-validated.
- Inbound funnel: `MoveToInterpretedState` / `ApplyInterpretedMovement` /
`DispatchInterpretedMotion` / verbatim `contact_allows_move` (S2a) +
183-case live-trace conformance suite.
- `InterpolationManager` (L.3) — chase constants re-verified 2026-07-02.
- Outbound: `RawMotionState::Pack` default-difference, MoveToStatePack
trailer, JumpPack (L.2b/D6.2b, golden-byte tests); dual command catalogs
(L.1b); `adjust_motion`/`apply_run_to_command`/`apply_raw_movement`/
`get_state_velocity` (D6).
Everything else is reconstruction scope — especially: `AnimationSequencer`
internals (becomes the CSequence port's host or is replaced by it), the
three per-tick drive paths in `GameWindow.TickAnimations`, `RemoteMotionSink`
(temporary S2b seam — dissolves into GetObjectSequence), `RemoteMoveToDriver`,
`ServerControlledLocomotion`, the motion half of `PlayerMovementController`,
the 300 ms stop-detection window, NPC UP hard-snaps.
## Stage plan (each: pseudocode → harness → port → cutover → DELETE legacy → register sweep)
- **R1 — CSequence verbatim. SHIPPED 2026-07-02** (commits 1371c2a1 P0/P1, 778744bf P2, 5138b8fb P3, 658b91d8 P4, 9147344a P5, +P6): the verbatim core (AnimSequenceNode/CSequence/FrameOps, 56 conformance tests) + the AnimationSequencer adapter rehost deleting the legacy epsilon/stale-head/safety-cap/per-node-flag mechanisms; root motion flows through Advance(dt, Frame) = retail update(Frame*). Registers AD-33/AD-34. Node list, framerate/rate math, velocity+
omega accumulators (set/combine/subtract), update/update_internal root
motion, apply_physics, placement frames, hook dispatch. Goldens: dat
MotionData fixtures + a cdb trace of append_animation/remove_cyclic_anims
args (script pattern: tools/cdb/l2g-observer.cdb). Cutover: becomes the
sequencer core behind the existing AnimationSequencer API, then the API
narrows to retail's.
- **R2 — GetObjectSequence + MotionTableManager. SHIPPED 2026-07-02 (pending
the stage visual pass).** Fast path,
link path (restores the walk↔run link pose — old S4), re_modify (modifier
blend — retires AP-73), pending_animations + remove_redundant_links +
CheckForCompletedMotions → AnimationDone→MotionDone chain (old S3).
RemoteMotionSink's single-cycle pick DELETED — GetObjectSequence decides.
Progress: Q0 pins (dc54a3e4) + Q1 MotionState (2345da30), Q2 CMotionTable all-4-branch
GetObjectSequence + statics (98f58db9, 44 tests), Q3 MotionTableManager+
pending_animations (aa65990a, 47 tests), Q4 adapter cutover (3b9d9bb6 —
SetCycle/PlayAction → PerformMovement; Fix B / fast-path / stop-anim
fallback / G17 gate DELETED; 11-scenario trace conformance a6235a36).
Q5 RemoteMotionSink DELETED (d82f07d4 — funnel → MotionTableDispatchSink →
PerformMovement; AP-73 retired; spawn/despawn run initialize_state/
HandleExitWorld; live smoke green: MOTIONDONE chain firing in-world).
Q6 sweep done with Q5. OUTSTANDING: the ONE user visual pass (walk↔run
stride continuity, turn-while-running legs, emote overlay, stop settle).
R3 prep done in parallel: research base 8eff3978 + W0 pins cd0289be (all
10 ambiguities resolved, adversarially verified).
- **R3 — CMotionInterp completion. SHIPPED 2026-07-03 (pending the stage
visual pass).** pending_motions/MotionDone, DoMotion, jump family
(jump_charge_is_allowed/motion_allows_jump verbatim — the misattribution
found in S2a), HitGround/LeaveGround/ReportExhaustion, enter_default_state.
LOCAL PLAYER unified (edge-driven CommandInterpreter altitude; the
synthesis layer + UpdatePlayerAnimation deleted).
Trail: W-1 research 8eff3978, W0 pins cd0289be (A1-A10 adversarially
verified), W1 state completion 86649591, W2 pending_motions 37167991,
W3 jump family af476444, W4 ground transitions + K-fix18 DELETED
e214acdf, W5 one-DoInterpretedMotion + zero-tick flush df7b096d
(discovery: the dispatch RESULT gates queue+state writes — sink returns
bool), W6 local-player unification fb7beb70 (map fc5a2cda; discoveries:
ChargeJump never wired, CurrentHoldKey shadow-field staleness, the
autonomous-flag clobber). Registers through the arc: retired AP-73/
AP-74/AP-78/TS-34/IA-4 + the Fix-B-class inventions; added AD-36
(narrowed), AP-75/76/77, TS-35/36/37/38. EXPECTED-DIFFS for the visual
pass: #45 sidestep factor + ANIM_SPEED_SCALE retired (local now matches
remotes), auto-walk-at-run walk-pace legs (R4), ApplyServerRunRate echo
live through fast re-speed. R3+R2 share ONE visual pass.
- **R4 — MoveToManager verbatim. SHIPPED 2026-07-03 (pending the stage
visual pass).** Types 6/7/8/9 (TurnToObject/TurnToHeading — the dropped
D9/DEV-5 commands), node stepping, arrival, fail distance, CanCharge.
RemoteMoveToDriver + ServerControlledLocomotion.PlanMoveToStart + B.6
auto-walk all DELETED.
Trail: research base 988304e1, V0 pins 386b1ce5 (P1-P7 resolved; P1
autonomous-echo gate + P3 heading_diff mirror adversarially sealed, P3
down to instruction bytes), V1 command-selection family e0d2492c
(GetCommand + CanCharge fast-path + MoveToMath), V2 the verbatim manager
addc8e97 (all 33 members, 101 tests, seam-injected harness), V3 wire
completion a144e873 (mt 8/9 parse + full params exposure + sticky
trailer), V4 remote cutover 7016b26c (per-remote manager, P4
TargetTracker adapter, retail unpack dispatch; retired AD-8/AD-9/AP-8/
AP-9; smoke log verified clean), V5 local-player cutover b3decdfa (P1
gate ported verbatim — ACE's autonomous echo dropped before unpack;
B.6 deleted; TS-36 bound — input/jump/teleport cancel through the retail
interrupt chain; run-rate re-anchored to PD skills + mt-6/7 my_run_rate,
echo tap deleted with NO AD row; MoveToComplete client seam widened to
natural completion for AD-27; adversarial-review fixes: remote HitGround
relay, mt-8 wire_heading degrade, remote curTime clock), V6 register/
docs sweep (AD-34 widened with the MoveToNode rename, NEW TS-39 StickTo/
Unstick no-op seams → R5, TS-33 extended with the orientation-diff gap,
TS-21/AD-25/AP-24/AP-30 re-anchored). EXPECTED-DIFFS for the visual
pass: melee-range stop distance (retail cylinder distance — the AD-8
max() class is gone), auto-walk legs now walk/run per CanCharge with
real turn cycles during corrections, walk-pace close-in demote.
OUTSTANDING: the stage visual pass (folds into the pending R2+R3 pass).
- **R5 — MovementManager + MovementSystem facade.** One per-object pipeline
for every entity class; StickyManager (stick_to_object — the motionFlags
0x100 bit) + ConstraintManager ports; GameWindow's OnLiveMotionUpdated
shrinks to parse→MovementSystem.HandleMovementEvent.
- **R6 — per-tick UpdateObjectInternal order.** Retail tick order for ALL
entities incl. the transition sweep for remotes (retires the L.3-M2
no-sweep fork + the path A/B split + the 300 ms stop window + NPC UP
hard-snap special cases). GameWindow.TickAnimations sheds its motion
logic entirely (Code Structure Rule 1).
- **R7 — outbound autonomy cadence.** ShouldSendPositionEvent (0x006b45e0)
+ the MTS/AP stamp split — retires TS-33. Outbound becomes 100% verbatim.
- **R8 — cutover audit.** grep-sweep for legacy motion code, register
reconciliation (every AD/AP/TS motion row retired or re-justified), full
live protocol (walk/run/toggle/turn/circle/stop/jump/MoveTo/TurnTo,
player+NPC+monster), ONE final user visual pass.
## Standing rules for every stage
1. Decomp is the oracle; ACE the interpretation aid; cdb/TTD the runtime
arbiter when they disagree (the S0/S2 pattern).
2. No stage ships without its conformance fixtures. Golden sources: dat
tables, cdb traces (both observer + actor side), captured wire logs.
3. Every commit: build + full suite green; register rows added/retired in
the same commit; roadmap stage table updated on stage completion.
4. New code lives in `src/AcDream.Core/Physics/Motion/` (pure logic; GL-free)
with App seams only for rendering handoff (Structure Rule 2).
5. Delete, don't gate: when a stage cuts over, the legacy path is REMOVED in
that stage (mandate: no frozen code, no bandaids).

View file

@ -1,177 +0,0 @@
# Automated world-lifecycle gate
**Status:** Complete (2026-07-20)
**Owner:** M3 stabilization / R6 rendering-lifecycle rebaseline
**Depends on:** `WorldRevealReadinessBarrier`, retained-UI automation,
`FrameProfiler`, connected local ACE
## Outcome
Create a deterministic connected Release gate that catches the structural
failures which previously required someone to stare at the client for several
minutes:
- the first login frame cannot expose world geometry before the destination's
render meshes, composite textures, and collision data are all ready;
- every portal/recall lifetime crosses the same readiness edge, materializes
once, and returns to the normal viewport once;
- dungeon entry and exit use the same lifecycle without grey frames,
stale-source geometry, or an uncollidable destination;
- graceful logout followed by a fresh reconnect starts a new reveal lifetime
and cannot inherit readiness or resources from the previous process;
- repeated destinations do not cause cumulative memory, owner-count,
allocation, update-time, or GPU-time growth;
- each stable checkpoint produces machine-readable state plus a screenshot for
later human comparison.
This is a correctness and resource-lifetime gate. Screenshots preserve visual
evidence, but automation does **not** claim retail visual equivalence. Color,
composition, animation feel, and subtle geometry artifacts remain user gates.
## Canonical runtime contract
`WorldRevealReadinessBarrier` remains the only definition of destination
readiness. It will expose a structured snapshot rather than forcing telemetry
to repeat the readiness formula. A snapshot records the destination cell,
indoor/outdoor classification, required radius, render publication, composite
texture readiness, collision readiness, an impossible/unhydratable claim, and
the final `Ready` result.
A focused App-layer `WorldRevealLifecycleTelemetry` owns diagnostic reveal
generations. It has no GL calls and no knowledge of `GameWindow`. For each
login or portal generation it records:
1. begin;
2. destination acquisition;
3. each distinct readiness transition;
4. the first frame where the normal world viewport is actually eligible to
draw;
5. completion or cancellation.
Making the normal viewport visible before `Ready` is a hard invariant failure,
not a warning. A newer generation retires the older one, so stale asynchronous
completion cannot satisfy the new destination.
`GameWindow` only supplies facts and invokes the controller at existing
lifecycle seams. It does not gain a new feature body.
## Production automation seam
The retained-UI automation runner gains runtime commands through an injected
interface:
- `wait world-ready [timeout-ms]` waits on canonical lifecycle state instead
of a guessed sleep;
- `wait materialized <occurrence> [timeout-ms]` waits for the authoritative
portal completion count;
- `checkpoint <name>` emits one JSON record containing reveal state,
landblock/entity/animation and resource-owner counts, managed memory, and
the latest frame-profiler summary;
- `screenshot <name> [timeout-ms]` queues a default-framebuffer capture after
the complete world and retained UI have drawn, then waits for the render
thread to finish the PNG;
- existing `input` commands perform deterministic turns and movement through
`InputDispatcher`.
All state mutation and GL readback stay on the update/render thread. The
external gate only launches the process, observes files/logs and OS process
metrics, and requests normal `WM_CLOSE` shutdown.
## Connected route
The gate runs two capped/uncapped-aware sessions against local ACE:
1. fresh login → wait for canonical readiness → checkpoint + screenshot;
2. outdoor dense scene → turn → stable checkpoint + screenshot;
3. world-edge scene → turn → stable checkpoint;
4. known dungeon destination → checkpoint + screenshot → local movement;
5. return outdoors → checkpoint + screenshot;
6. revisit the first dense scene → stable resource comparison;
7. graceful close;
8. reconnect the same account → repeat the fresh-login readiness and
screenshot gate → graceful close.
The existing seven-destination R6 soak remains the longer performance route.
The lifecycle gate is shorter and more diagnostic; it composes with that soak
rather than replacing it.
## Hard failures
- world viewport observed before the active reveal snapshot is ready;
- destination never reaches readiness or materialization within the bounded
connected test timeout;
- missing or duplicate reveal/materialization/completion edges;
- missing checkpoint JSON or screenshot, zero-sized/corrupt screenshot;
- absent required destination landblock/EnvCell/collision readiness;
- retained per-owner resources after delete/session teardown in deterministic
tests;
- same-location resource counts or memory/per-frame allocation grow beyond the
route's documented relative tolerance;
- unhandled exception, access violation, OOM, GL/device loss, disconnect,
unexpected WeenieError, nonzero exit, or failure of graceful shutdown.
Absolute FPS is recorded, never compared across local and RDP sessions. The
gate compares like-for-like samples within one process and reports CPU and GPU
frame time separately.
## Implementation sequence
1. Add a structured readiness snapshot and exhaustive barrier tests.
2. Add lifecycle telemetry as a pure App owner with stale-generation,
cancellation, duplicate, and early-visible tests.
3. Add the automation runtime interface, deterministic waits, checkpoints, and
screenshot request/completion protocol.
4. Add the render-thread screenshot owner and structured resource snapshots.
5. Wire the owners at login, F751 portal start, TAS placement/completion, world
draw eligibility, and session teardown.
6. Add the connected route and PowerShell report/gate orchestration.
7. Run capped and uncapped Release gates, fix root causes, and retain artifacts.
8. Extract the now-protected streaming/portal/reveal frame orchestration from
`GameWindow` without changing the accepted lifecycle trace.
9. Run focused tests, full Release build/test, connected gates, graceful-close
verification, and documentation reconciliation.
## Completion evidence
- Focused App tests cover readiness composition, stale generations,
cancellation, duplicate lifecycle edges, early-visible rejection, artifact
capture, automation timeouts, and resource snapshots.
- `dotnet build AcDream.slnx -c Release` succeeds. The complete repository
suite is 6,480 passed / 5 skipped; the 17 existing test-project warnings
remain tracked by issue #228.
- The final two-process connected report passed with no failures. The capped
process completed fresh login, Aerlinthe, Rynthid, Facility Hub, Holtburg,
and an Aerlinthe revisit in 244.424 seconds. The uncapped process then
reconnected the same account and completed in 61.717 seconds.
- Both processes closed through `WM_CLOSE`, received the authoritative
character-logoff confirmation, exited with code 0, and were matched to
ACE's exact client UDP endpoint and `PacketHeader Disconnect` record.
- Every checkpoint had zero pending live teardowns, landblock retirements,
staged mesh uploads, composite warmups, and reveal invariant failures.
- All six PNG artifacts were valid and inspected. Login, indoor dungeon,
dungeon exit, Aerlinthe revisit, and reconnect showed complete world
geometry rather than a grey or partially revealed destination.
- The uncapped reconnect sampled 172.7 FPS / 5.79 ms at the checkpoint; its
latest profiler window measured 5.7 ms CPU p50, 6.1 ms p95 and 5.7 ms GPU
p50, 6.0 ms p95 with zero pacing time. This is machine-specific throughput
evidence, not a cross-display FPS requirement.
- The Aerlinthe revisit retained balanced owners and stayed inside the
documented relative memory/resource tolerances. One circuit is not claimed
as a permanent memory plateau proof; the longer seven-destination R6 soak
remains the endurance gate.
- `WorldRevealCoordinator` now owns the protected lifecycle outside
`GameWindow`; the accepted trace did not change after extraction.
## Landed commits
- `db45b81f` — expose canonical readiness snapshots and lifecycle telemetry;
- `354c2adc` — add deterministic checkpoints and framebuffer screenshots;
- `b03371c0` — add the connected lifecycle/reconnect gate;
- `68578fa5` — port retail graceful character logout and transport disconnect;
- `a4ef5788` — centralize the reveal lifetime in `WorldRevealCoordinator`;
- `f7b09617` — preserve singleton endpoint/checkpoint collections in the gate.
The remaining visual work is deliberately outside this gate: the R6
locomotion/collision/projectile/teleport retail comparison, the two-client
portal-out/materialization observer comparison, and issue #225's translucent
lifestone/particle ordering comparison.

View file

@ -1,352 +0,0 @@
# GameWindow Slice 1 — selection and interaction ownership
**Status:** COMPLETE — landed 2026-07-21.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 1.
**Baseline:** `cf50ee3d`; `GameWindow.cs` is 15,723 lines before this slice.
**Behavior rule:** Existing retail behavior moves unchanged. Any defect found by
the extraction audit is fixed in a separately identified commit before the
mechanical cutover.
## Landing record
- Plan/oracle: `c2713837`.
- Exact-incarnation lifetime hardening: `047a4c83`.
- Retail pending keyboard-pickup presentation: `52dbb574`.
- Read-only query extraction: `e74f2ca9`.
- Stateful interaction owner: `fa8d5232`.
- `GameWindow` cutover: `d2bb5af4`.
- Three-review correction pass: `5acc3f01`.
The correction pass unified retail's one-inventory-request-at-a-time owner
across every mutation surface, separated optimistic/rollback/authoritative
placement notices, made response completion atomic and reentrancy-safe, and
bound pending work to exact object identities and placement tokens.
Final metrics: `GameWindow.cs` 15,723 → 14,912 lines and 205 → 191 methods;
fields remained 278. Release build passed, the focused selection/inventory
gate passed, the full suite passed 6,558 tests with five intentional skips,
and the retail-conformance, architecture/integration, and adversarial reviews
all finished with no actionable findings.
## 1. Outcome
`GameWindow` stops owning world selection and selection-driven interaction.
After this slice it retains only composition and narrow lifecycle calls:
- construct the query/controller;
- feed the completed render-selection frame;
- drain outbound interactions at the existing retail frame boundary;
- forward input actions and MoveTo completion;
- forward entity hidden/removal and session-reset edges.
Two App owners replace the current body:
1. `WorldSelectionQuery` owns read-only world selection, eligibility,
classification, naming, closest-hostile lookup, target bounds, and range
queries over canonical live state.
2. `SelectionInteractionController` owns selection intents, click routing,
Use/PickUp ordering, speculative facing/movement, deferred close-range
requests, and the input-action boundary.
Core `SelectionState` remains the sole selected-object truth.
`ItemInteractionController` remains the sole retail ItemHolder policy owner.
`LiveEntityRuntime` remains the sole GUID/incarnation/visibility owner.
## 2. Retail and project oracles
No AC algorithm is invented in this slice. Move the existing ports with these
references attached:
- [`2026-07-17-retail-world-selection-pseudocode.md`](../research/2026-07-17-retail-world-selection-pseudocode.md)
— render-coupled part selection, selection sphere, vivid target, click pulse.
- [`2026-07-10-retail-toolbar-interaction-pseudocode.md`](../research/2026-07-10-retail-toolbar-interaction-pseudocode.md)
— target-mode precedence, ItemHolder Use, selection notices.
- [`2026-07-17-retail-external-container-looting-pseudocode.md`](../research/2026-07-17-retail-external-container-looting-pseudocode.md)
— corpse/container open, range, pending loot placement.
- [`2026-07-12-death-and-auto-target-pseudocode.md`](../research/2026-07-12-death-and-auto-target-pseudocode.md)
— synchronous selection clear and reentrant Auto Target.
- `Render::GfxObjUnderSelectionRay @ 0x0054C740`.
- `ACCWeenieObject::SetSelectedObject @ 0x0058C2E0`.
- `CPlayerSystem::OnAction @ 0x00561890`.
- `CPlayerSystem::PlaceInBackpack @ 0x0055D8C0`.
- `ItemHolder::UseObject @ 0x00588A80`.
- `UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`.
Existing registered adaptations remain adaptations; this structural slice does
not silently “improve” them:
- IA-19: automatic acquisition is narrowed to hostile non-player monsters.
- AD-11: ACE null-useability fallback.
- AD-27: natural `MoveToComplete` is the client completion seam.
- AP-23: speculative local Use movement and 3/2/0.6-unit radius buckets.
Their file pointers move in the same commit as their implementation bodies.
## 3. Invariants
### 3.1 Render selection
- The renderer builds a private list, then publishes it only at
`CompleteFrame`; input reads the previous complete frame.
- A hit carries server GUID **and local entity ID**. The query accepts it only
if `LiveEntityRuntime` still resolves the same current, visible top-level
incarnation.
- Plain selection excludes self. Active target mode includes self.
- A successful hit starts the HIGH click-light pulse before target-mode or
selection routing.
- Target-mode success or rejection consumes the click and never falls through
to ordinary selection/use.
- A miss does not clear selection. A miss during target mode leaves target mode
active.
- Drag release may pulse the target, but does not select it.
- Vivid target markers deliberately query materialized state so selected
objects remain indicated through walls and long distances. Picking and new
combat acquisition use visible interaction state only.
### 3.2 Input and outbound ordering
- Keep the one `InputDispatcher.Fired` subscription in `GameWindow`.
- Combat attack handling, Press/DoubleClick filtering, and retained-UI capture
occur before delegation exactly as today.
- The controller handles only the currently implemented semantic actions:
previous selection, closest monster, left selection, double-left selection,
Use selected, and PickUp selected.
- Input callbacks enqueue Use/PickUp. The queue drains after local movement
output and before inbound packet dispatch.
- The queue drains only its boundary snapshot; reentrant enqueue waits until
the next frame.
### 3.3 Use/PickUp and movement
- `ItemInteractionController` decides Use/open/target mode. The new controller
does not copy ItemHolder policy.
- Far target: install speculative MoveTo and send exactly one wire request
immediately. ACE owns its arrival callback.
- Close target: install TurnToObject and defer the **first** request until
natural `MoveToComplete(None)`.
- Cancel/error completion never sends.
- The movement intent preserves target cylinder radius/height, the AP-23 use
radius, and the 7.5-unit CanCharge prediction.
- Every queued/deferred request is bound to the captured live incarnation and
revalidated before sending.
- Pickup preserves destination container and placement. Retail pending-slot
presentation occurs before the request.
### 3.4 Lifecycle
- Hidden, logical removal, session reset, and GUID replacement invalidate old
hits and pending actions immediately.
- Token-specific pending cleanup is allowed even after GUID replacement;
GUID-only selection cleanup retains the existing incarnation guard so old
teardown cannot clear a newly selected replacement.
- Session reset atomically clears selection history, outbound queue, deferred
action, item target mode/busy state, published selection frame, and click
pulse.
- `CombatTargetController` must not auto-acquire during `SessionReset`.
- No new GUID map, selection store, visibility store, or retry timer is added.
## 4. Target interfaces
### 4.1 Render hit identity
`RetailSelectionPart` and `RetailSelectionHit` carry `LocalEntityId` alongside
`ServerGuid`. `RetailSelectionScene.Pick` returns the complete hit. Its lighting
source is also keyed by both identities so a reused GUID cannot inherit an old
click pulse.
`RetailSelectionScene.Reset` clears building/published lists, frame keys,
frustum state, and pulse state. Geometry resolution receives a narrow internal
source interface so publication behavior is testable without installed DAT.
### 4.2 `WorldSelectionQuery`
The query receives:
- canonical `LiveEntityRuntime`;
- `ClientObjectTable`;
- the shared `RetailSelectionScene`;
- player GUID and current player position providers;
- camera/viewport and cursor providers;
- a DAT selection-sphere resolver;
- the existing Setup-cylinder resolver.
It exposes read-only operations for:
- pick at cursor/screen point with exact-incarnation validation;
- current interaction record/entity lookup;
- item type, name, and description;
- creature/hostile/health eligibility;
- closest visible hostile result without mutating selection;
- combat camera target;
- vivid target information and selection sphere;
- useability/pickupability;
- AP-23 use radius, close-range, CanCharge, and target movement data;
- external-container cylinder range.
The query never mutates selection, queues work, sends packets, or starts
movement.
### 4.3 `SelectionInteractionController`
The controller receives:
- `SelectionState` and `WorldSelectionQuery`;
- `ItemInteractionController` provider;
- a typed Use/PickUp transport;
- a narrow movement-intent sink;
- toast/system diagnostic sinks;
- Auto Target setting provider.
It owns `OutboundInteractionQueue` and an incarnation-bound
`PendingPostArrivalAction`. Public methods form the complete host seam:
```text
HandleInputAction
PickAtCursor / PlaceDraggedItem
SelectClosestCombatTarget
GetSelectedOrClosestCombatTarget
SendUse / SendPickUp
DrainOutbound
OnNaturalMoveToComplete
OnEntityHidden / OnEntityRemoved
ResetSession
```
The controller does not subscribe directly to Silk input and does not own
rendering, combat state, the session, or item policy.
## 5. Execution sequence
### Commit 0 — plan and defect registration
- Land this plan.
- Register #230 for GUID-incarnation/session-reset leakage.
- Register #231 for F-key pickup missing the pending destination reservation.
- No runtime behavior change.
### Commit 1 — lifecycle identity hardening (#230)
- Add local-entity identity to render parts/hits and selection lighting.
- Add scene reset and deterministic publication tests.
- Validate published hits against current visible incarnation.
- Bind pending post-arrival actions to a live record/local identity.
- Clear only the matching pending token on teardown, including GUID reuse.
- Make session reset clear scene and item interaction state.
- Prevent combat Auto Target during `SessionReset`.
- Keep this root-cause correction independently bisectable.
### Commit 2 — retail pending pickup presentation (#231)
- Add one ItemInteractionController entry point for a validated world pickup.
- Publish `PendingBackpackPlacementRequested` before calling the existing
placement delegate.
- Route F-key pickup through it with destination/placement unchanged.
- Do not optimistically move ownership; server confirmation/failure remains
authoritative.
### Commit 3 — extract `WorldSelectionQuery`
- Move every read-only selection/classification/range body from `GameWindow`.
- Preserve materialized-versus-visible semantics.
- Extend the live animation read seam with current motion so dead eligibility
does not depend on `GameWindow.AnimatedEntity`.
- Rewire cursor, vivid target, toolbar health, combat camera, external-container
range, drag target, and door-name reads through the query.
- Delete the corresponding `GameWindow` methods.
### Commit 4 — extract `SelectionInteractionController`
- Move world click routing, selection mutation, closest-target selection,
outbound queue, Use/PickUp, movement intent, deferred completion, and
hidden/removal/reset handling.
- Compose `ItemInteractionController`; do not duplicate its policies.
- Update IA-19/AD-11/AD-27/AP-23 file pointers.
- Add focused controller tests for every invariant in §3.
### Commit 5 — input cutover and host cleanup
- Replace the six selection/interaction switch cases with one controller
delegation call at the same ordered position.
- Forward render-frame, update-frame, MoveTo completion, hidden/removal,
session-reset, cursor, and drag hooks.
- Delete old fields/records/wrappers from `GameWindow`.
- Prove `GameWindow` no longer owns a selection algorithm or interaction
request body.
### Commit 6 — review, gate, and ledger close
- Run retail-conformance, architecture/integration, and adversarial read-only
reviews against the complete slice diff.
- Fix confirmed findings and repeat review until clean.
- Run focused Core/App tests, Release solution build, and full Release suite.
- Update code-structure ownership status, roadmap/current queue if necessary,
both session instruction files, divergence pointers, and durable interaction
memory.
- Record the new `GameWindow` line/field/method counts.
## 6. Automated acceptance matrix
### Render/query
- Building frame is invisible until `CompleteFrame`.
- Last complete frame remains queryable while the next frame builds.
- Empty completion clears prior hits.
- Hidden, pending, deleted, and replaced incarnations reject stale hits.
- A replacement GUID is not pickable until its own part is published.
- Pulse is identity-bound and cannot color a replacement.
- Plain pick excludes self; target-mode pick includes self.
- Vivid target can use materialized state while combat candidates cannot.
- Closest hostile excludes self, players, pets, friendly NPCs, dead, Hidden,
pending, and deleted objects.
### Interaction
- Pulse precedes target-mode and selection routing.
- Target-mode success/rejection consumes the click without selection drift.
- Single click selects; double click captures the clicked GUID and queues one
activation.
- Miss preserves selection and target mode.
- Previous selection preserves current SelectionState semantics.
- Queue drains after movement and only once per captured boundary.
- Far Use/PickUp sends once; close sends only on natural completion.
- Cancel/error/Hidden/delete/reset sends zero.
- Delete then same-GUID create sends zero to the replacement.
- Useability and pickupability fallbacks remain byte-for-byte behaviorally
equivalent.
- Creature and Stuck pickup rejection messages remain exact.
- Corpse double-click/Use opens it and never attempts to pick up the corpse.
- F pickup reserves the first destination slot before wire send.
- Pickup destination/placement survive the deferred path.
### Lifecycle/input
- Session reset clears previous selection, target mode, busy state, queue,
pending completion, published picks, and pulse; repeated reset is safe.
- SessionReset cannot trigger combat auto-target.
- Modal/retained UI capture still prevents controller invocation upstream.
- Escape target-mode cancellation remains ahead of window-close behavior.
## 7. Connected gate
Run one normal capped Release client and verify:
1. select a nearby door/NPC and see the click pulse and target corners;
2. double-click/Use the door or NPC;
3. select a distant corpse, press R, approach, and open it;
4. double-click one loot item and use F on another; both reserve the expected
backpack slot and settle after ACE confirmation;
5. enter combat, select a hostile, use closest-target and previous-selection;
6. kill the selected target with Auto Target enabled and observe only a hostile
replacement;
7. portal or relog and confirm no old selection/pulse/action survives.
Any failure returns to the owning commit. No retry, grace period, or
GUID-only suppression is acceptable.
## 8. Subagent policy for this slice
The implementation is tightly coupled across `GameWindow`, live lifetime,
render publication, input order, and player MoveTo. One primary agent performs
all edits. Independent subagents are useful for read-only retail,
architecture, and adversarial reviews; they do not edit the shared slice
concurrently.

View file

@ -1,392 +0,0 @@
# GameWindow Slice 2 — live animation presentation ownership
**Status:** Complete 2026-07-21. Automated and three-agent review gates pass;
connected visual regression gate is carried to the final campaign gate.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 2.
**Baseline:** `9ad8113c`; `GameWindow.cs` is 14,912 lines, 278 fields, and
191 methods before this slice.
**Result:** `GameWindow.cs` is 14,546 lines, 277 fields, and 190 methods.
The slice also closed stale-schedule ABA across replacement, appearance
rebinding, static rebind, and borrowed sequencer buffers. Forty-one focused
Release tests and the full 6,575-pass / 5-skip Release suite are green.
**Behavior rule:** This is an ownership extraction over the accepted R6 object
frame plus one named-retail conformance correction: short AnimFrames retain
the prior/rest pose of trailing CPartArray parts. It must not otherwise change
sequence time, root motion, collision, manager order, hook semantics, part
transforms, render eligibility, or effect attachment.
## 1. Outcome
`GameWindow` stops owning final animated-part presentation. A focused
`LiveEntityAnimationPresenter` consumes the already-completed scheduler output
and publishes the exact same draw and effect state:
- final drawable `MeshRef` transforms;
- indexed rigid part poses and availability;
- `EntityEffectPoseRegistry` publication;
- live static-animation frame handoff;
- legacy animation interpolation state;
- one-time `MotionDone` binding;
- presentation-specific sequence/part diagnostics.
The shipped owners remain unchanged:
- `LiveEntityAnimationScheduler` owns ordinary-object clocks, PartArray
advance, root motion, one movement owner, control-hook capture, and the
retail manager tail;
- `RetailStaticAnimatingObjectScheduler` owns the separate retail
`static_animating_objects` time/workset and its deferred process-hooks tail;
- `EquippedChildRenderController` owns equipped-child composition;
- `AnimationHookFrameQueue` owns incarnation-scoped semantic/presentation-hook
delivery;
- `LiveEntityRuntime` remains the only identity, incarnation, component, and
spatial-lifetime owner.
`GameWindow.AdvanceLiveObjectRuntimeCore` retains only the explicit frame-order
calls between these owners.
## 2. Retail and project oracles
No new AC algorithm is introduced. The extraction is pinned to:
- [`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md)
`UpdatePositionInternal`, `process_hooks`, and manager-tail ordering.
- [`2026-07-19-r6-complete-root-frame-pseudocode.md`](../research/2026-07-19-r6-complete-root-frame-pseudocode.md)
— complete root `Frame`, ordinary/static worksets, part/effect publication.
- [`memory/project_animation_runtime.md`](../../memory/project_animation_runtime.md)
— appearance-update and description-before-enter-world lifetime invariants.
- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`.
- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`.
- `CPhysicsObj::process_hooks @ 0x00511550`.
- `CPhysicsObj::animate_static_object @ 0x00513DF0`.
- `CPartArray::Update @ 0x00517DB0`.
- `CPartArray::UpdateParts @ 0x005190F0`.
- `CPartArray::AnimationDone @ 0x00517D30`.
- `CPhysicsObj::MotionDone @ 0x0050FDB0`.
- `Frame::combine @ 0x005122E0`.
ACE/DatReaderWriter and the installed retail DAT remain secondary fixtures for
motion-table/animation schema interpretation. Named retail is authoritative
for ordering and transform semantics.
## 3. Fixed frame-order contract
One ordinary live-object frame remains:
```text
local player pre-network object phase
selection outbound boundary
publish PhysicsScript clock
LiveEntityAnimationScheduler.Tick
RetailStaticAnimatingObjectScheduler.Tick
LiveEntityAnimationPresenter.Present
EquippedChildRenderController.Tick
RetailStaticAnimatingObjectScheduler.ProcessHooks
translucency advance
AnimationHookFrameQueue.Drain
effect-root refresh
attached-emitter refresh
live-light refresh
particle visibility
particle simulation
PhysicsScript simulation
```
The presenter advances no clock and drains no hook. It only consumes poses
produced earlier in the same frame. Static `process_hooks` remains after live
parts and equipped children, as retail's `animate_static_object` requires.
## 4. Ownership and interfaces
### 4.1 Animation component state
Move `GameWindow.AnimatedEntity` and `GameWindow.AnimatedPartTemplate` to
top-level App rendering types:
```text
LiveEntityAnimationState
LiveAnimationPartTemplate
```
`LiveEntityAnimationState` remains the exact component stored in
`LiveEntityRecord.AnimationRuntime`. It owns no GUID map and no lifetime. It
retains:
- the exact `WorldEntity`, Setup, Animation, and optional sequencer;
- legacy low/high/current frame and framerate;
- ObjScale, part template, and part availability;
- prepared local-player pose and complete root-frame scratch;
- reusable `MeshRefs`, effect-part-pose, and diagnostics scratch.
Appearance updates mutate this same component and preserve sequencer/playback
state. Logical replacement creates a new component.
### 4.2 Presentation context
Define a narrow App interface equivalent to:
```csharp
interface ILiveAnimationPresentationContext
{
uint LocalPlayerGuid { get; }
MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record);
}
```
`GameWindow` implements only these constant-time lookups. The presenter cannot
reach windowing, GL, networking, input, streaming, physics, or arbitrary
`GameWindow` methods.
### 4.3 Presenter
Use an API equivalent to:
```csharp
void Present(
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules);
void PrepareAnimation(
LiveEntityRecord record,
LiveEntityAnimationState state);
```
The presenter receives:
- canonical `LiveEntityRuntime` provider for exact-record validation;
- `EntityEffectPoseRegistry`;
- `ILiveAnimationPresentationContext`;
- a late-bound `ILiveStaticPartFrameSource` (the static scheduler is created
after the presenter);
- startup-resolved hidden-part diagnostic index;
- typed animation diagnostic flags.
The presenter owns its own reusable `LiveEntityRecord` snapshot. It does not
borrow `LiveEntityAnimationRuntimeView`'s reusable enumerator snapshot across
the synchronous `EffectPoseChanged` callback boundary. Nested enumeration by a
subscriber therefore cannot invalidate the outer presentation pass.
Each schedule carries the exact `LiveEntityRecord`, `WorldEntity`, animation
component, `ObjectClockEpoch`, and projection-mutation version which produced
it. The presenter validates all of those identities before consuming ordinary
or static frames and again before each externally observable publication. A
dictionary key/local ID is an index only; it is never proof of incarnation.
The static prepared-frame source is queried late, only after this validation.
Its take operation receives the exact record, entity, animation component,
object-clock epoch, and projection/residency versions; it validates those
tokens before clearing and returning prepared frames.
It keeps no GUID map, no animation clock, no packet/session reference, and no
render-backend object.
### 4.4 Diagnostics
Move the surviving `SEQSTATE`, `CURRNODE`, `PARTSDIAG`, and `MOTIONDONE` gates
behind one typed animation diagnostic owner. Environment variables are read
once; the presenter does not perform per-frame environment lookups. Throttle
state follows the exact animation component lifetime instead of living on the
remote-motion component.
## 5. Transform invariants
For every part index, preserve these two deliberately different transforms.
### Drawable visual transform
```text
Setup DefaultScale
× animated part orientation
× animated part translation
× entity ObjScale
```
This is the existing renderer/hydration contract. It scales geometry and the
part's translated placement.
### Rigid effect/attachment transform
```text
animated part orientation
× translation(animated origin × entity ObjScale)
```
Retail `CPartArray::UpdateParts` scales the part-frame origin only. Setup
DefaultScale is `gfxobj_scale`, not rigid part-frame state, so particles,
lights, attachments, and multipart collision must never inherit it.
Additional invariants:
- Sequencer frames win when present. Retail updates only
`min(CPartArray.num_parts, AnimFrame.num_parts)`; missing trailing frames
retain both their previous drawable visual transform and their previous
indexed rigid/effect pose (the two distinct initial rest transforms on first
presentation) instead of snapping either channel to identity.
- Legacy animation interpolates origin linearly and orientation spherically,
clamps interpolation to `[0,1]`, and wraps over the inclusive frame span.
- Live static prepared frames override the ordinary schedule for that owner.
- `ComposeParts == false` changes no mesh or indexed pose.
- Non-drawable/debug-hidden parts remain in indexed effect poses but not
drawable `MeshRef`s.
- `PartAvailability` is published unchanged and controls effect part lookup.
- Mesh identity, surface overrides, and list reuse remain unchanged.
- Entity mesh/indexed-part state is installed before effect-pose publication.
## 6. MotionDone invariants
- Bind before any sequencer can complete; repeated calls are idempotent.
- The scheduler validates the exact owner before invoking `PrepareAnimation`.
- Resolve the player/remote `MotionInterpreter` at callback time because that
consumer may be created after the animation component.
- Never capture a legitimate null interpreter permanently.
- Never complete a motion from pose publication; `AnimationHookFrameQueue`
remains the semantic `AnimationDone` boundary.
- Logical removal/replacement stops the old component from advancing; no
presenter-owned callback queue survives it.
- The callback revalidates the stable captured record, entity, and animation
component before resolving a same-GUID interpreter. It deliberately does
not capture object-clock/projection epochs: the same retail CPartArray and
completion consumer survive legitimate rebucketing and leave/re-enter.
GUID reuse still cannot deliver an old completion to a replacement.
- `MOTIONDONE` diagnostics observe but never alter queue state.
## 7. Execution sequence
### Commit 0 — plan and baseline
- Land this plan.
- Record exact baseline metrics and current frame order.
- Register no new divergence; this slice moves an accepted mechanism.
### Commit 1 — extract component types and pure presenter
- Move animation component/template types out of `GameWindow`.
- Add the presentation context and typed diagnostics owner.
- Implement `LiveEntityAnimationPresenter` by moving the current composition
body line-for-line, retaining the baseline trailing-part fallback initially.
- Add focused tests before runtime cutover.
### Commit 2 — cut over runtime ownership
- Construct the presenter beside the scheduler.
- Route scheduler preparation, local-player pre-advance, spawn-time binding,
and final presentation through it.
- Delete `TickAnimations`, `EnsureMotionDoneBinding`, nested animation types,
and presentation-only remote diagnostic fields from `GameWindow`.
- Keep child/static-hook/effect ordering visible in
`AdvanceLiveObjectRuntimeCore`.
### Commit 3 — retail trailing-part conformance
- Record the `CPartArray::UpdateParts` minimum-count/retain behavior in the
R6 pseudocode note.
- Preserve both visual and rigid prior/rest trailing transforms.
- Add first-presentation and later-shorter-frame conformance tests plus
ordinary/live-static/DAT-static parity coverage.
- Update the divergence register in this commit if the audited register
contains a corresponding row; introduce no silent behavior delta.
### Commit 4 — review, gate, and ledger close
- Run retail-conformance, architecture/integration, and adversarial read-only
reviews against the complete slice.
- Fix every confirmed finding and repeat review until clean.
- Run focused animation, scheduler, static, effect, projectile, remote-motion,
and local-player tests.
- Run Release solution build and full Release suite.
- Record new `GameWindow` line/field/method counts.
- Update code structure, roadmap, milestones, issues if any, durable animation
memory, `AGENTS.md`, and `CLAUDE.md`.
## 8. Automated acceptance matrix
### Composition
- Sequencer pose composes every part and preserves surface overrides.
- Missing sequencer trailing parts retain their previous/rest indexed pose.
- Legacy current/next frame interpolation and inclusive wrapping are exact.
- Legacy hidden/single-frame owners compose only when scheduled.
- Static prepared frames override ordinary frames exactly once.
- `ComposeParts == false` preserves prior mesh/pose state.
### Visual versus rigid transforms
- Setup nonuniform DefaultScale affects drawable geometry only.
- ObjScale affects drawable geometry and translated placement.
- Rigid effect poses scale only the translated origin.
- Nondrawable and debug-hidden parts remain available to effects.
- Unavailable parts remain indexed but fail effect-part lookup.
### Lifetime and ordering
- A removed/replaced animation component is skipped by the runtime-view
snapshot and cannot publish into its replacement.
- A schedule prepared for incarnation A is rejected if A is withdrawn,
replaced, or reprojected before presentation, including same-local-ID ABA.
- Reentrant deletion/replacement during pose publication prevents every later
write for the stale owner; no borrowed scratch buffer crosses a callback.
- Effect-pose publication sees the entity's already-installed indexed parts.
- Same-frame animation hooks drain only after current parts and children.
- Static process-hooks runs only after static parts and children are current.
- Presentation allocates no per-frame mesh/effect lists in steady state.
### Motion completion
- Binding is idempotent.
- A consumer created after binding receives completion.
- Player and remote resolution select the correct interpreter.
- Missing consumer is a no-op.
- Diagnostics do not duplicate `MotionDone`.
- A legitimate rebucket/object-clock rebase preserves the binding.
- Delete plus GUID/local-ID reuse rejects completion from the old component.
### Cross-owner order and reconciliation
The presenter-level portions below are covered by Slice 2. The full
instrumented owner-order trace and non-advancing spatial-reconcile assertion
remain assigned to Slice 6, where the update sequence becomes one testable
orchestrator instead of a `GameWindow` method. Slice 2 preserves that order but
does not claim an end-to-end orchestrator test prematurely.
- One instrumented object-frame test pins presenter, children, static hooks,
fades, ordinary hook drain, effect roots, emitters, lights, particles, and
scripts. A PES-created particle first simulates on the following frame.
- Authoritative spatial reconciliation performs only root/effect refresh,
child composition, emitter refresh, and light refresh; it advances no
animation, hook, fade, particle, script, or object clock.
- Appearance shrink from three parts to one preserves playback while removing
stale MeshRefs, indexed poses, availability, and effect-registry entries.
- Ordinary, live-static handoff, and DAT-static composition share conformance
fixtures for scale, sparse availability, and surface overrides.
- An equipped child consumes the just-published parent indexed pose in the
same frame, including ObjScale and nested attachment composition.
- An animated projectile's trail/light anchor observes the same-quantum root
and part pose, never the previous frame.
`EquippedChildRenderController` callback-mutation hardening is an adjacent
Slice-6 frame-orchestration concern. Slice 2 does not claim it: the known
reentrancy case is recorded as a follow-up unless a focused test demonstrates
the existing traversal is already safe.
- A nested animation-runtime enumeration triggered by `EffectPoseChanged`
cannot overwrite or truncate the presenter's private outer snapshot.
## 9. Connected gate
Run one normal capped Release client and verify behavior remains unchanged:
1. local idle, walk, run, strafe, turn, jump, combat, and stop transitions;
2. observe a remote player walk/run/turn without pose stalls or blips;
3. open/close a door and kill a creature; static/reactive final poses persist;
4. equip/unequip armor and switch melee/bow/wand without animation loss;
5. fire an arrow and cast a projectile spell; trails remain attached to the
current missile/hand pose;
6. cast a protection and recall/portal; body effects follow current parts;
7. portal/relog and confirm no old pose or completion reaches a replacement.
Any failure returns to the owning commit. No fallback pose cache, delayed
retry, grace period, or GUID-only suppression is acceptable.
## 10. Subagent policy
One primary agent performs all edits. Read-only agents independently audit
retail conformance, architecture/lifetime, and adversarial tests before the
plan and after every implementation correction. No subagent edits the shared
slice concurrently.

View file

@ -1,414 +0,0 @@
# GameWindow Slice 3 — complete live-session ownership
**Status:** Complete 2026-07-21.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 3.
**Baseline:** `9a150e24`; `GameWindow.cs` is 14,546 lines, 277 fields, and
190 methods before this slice.
**Behavior rule:** Preserve the accepted connect, EnterWorld, inbound dispatch,
chat/command, and retail graceful-close behavior while correcting the
named-retail character-list, unattended-selection, canonical-account, and
disconnect-state gaps discovered by the slice audit. This slice changes App
ownership and fixes proven session-lifetime defects; it does not add a retained
character-selection UI or redesign the transport.
**Progress ledger:**
- [x] A0 — retail lifecycle conformance corrections (`aea957f8`): retail
active/deleted CharacterSet parsing, validated unattended selection,
canonical-account F657, exact F653 confirmation, negotiated-state
Disconnect symmetry, closed-channel/flood-safe confirmation wait, and
deterministic shutdown tests. Release: 6,605 passed / 5 skipped.
- [x] A — owned subscription primitives (`7d452aa6`): exact nested
GameEvent registration tokens, reverse/idempotent subscription ownership,
transactional wiring construction, and adversarial unregister,
reentrancy, replacement, and concurrent-dispose coverage. Release: 6,621
passed / 5 skipped.
- [x] B — session event and command routers (`961bdd07`): focused inbound
event and outbound command owners, exact reverse teardown, copied/in-flight
callback acceptance gates, inactive-before-EnterWorld publication,
concurrent and reentrant disposal serialization, and delayed-dialog
indirection that makes displaced transports both inert and collectible.
Three-agent review clean. Release: 6,636 passed / 5 skipped.
- [x] C — convergent session reset (`4f31a508`): fixed ordered manifest,
all-attempt named transaction, retry-observable owner resets, canonical
live-runtime convergence/identity gate, retained-world preservation,
retail dialog completion semantics, and actual-owner A → failed drain →
retry → clean-state coverage. Three-agent review clean. Release: 6,659
passed / 5 skipped.
- [x] D — complete lifecycle controller (`d9ccf8a6`): sole App ownership
of resolve/create/bind/Connect/select/EnterWorld/Tick/stop/reconnect,
generation-gated reentrancy, pre-publication command activation,
convergent staged teardown, transport-free deterministic session tests,
and invalid-endpoint validation before UDP allocation. Three-agent review
clean. Release: 6,707 passed / 5 skipped.
- [x] E — GameWindow cutover (`6a5d9e2e`): one controller field, focused
lifecycle host, transactional event/command binding, call-time borrowed
session resolution, controller-backed retained/dev UI command providers,
unified shutdown, and structural regression gates proving the displaced
fields and lifecycle bodies remain absent. Three-agent review clean.
Release: 6,714 passed / 5 skipped.
- [x] F — final review, connected gate, and documentation closeout. All three
independent review tracks finished clean. Release: 6,714 passed / 5 skipped.
The 306-second connected lifecycle gate passed capped login, five travel/
revisit checkpoints, exact graceful exit, and uncapped fresh-process
reconnect; both client processes exited normally. The sole warning was 25
expected world-edge landblock misses already classified by the gate.
## 1. Outcome
`LiveSessionController` becomes the sole App owner of one exact
`WorldSession` lifetime:
- endpoint resolution and session construction;
- event routing before the first network receive;
- Connect → CharacterList validation → first-character selection → EnterWorld;
- active command-bus publication;
- per-frame Tick;
- graceful stop, replacement/reconnect, and disposal;
- session-generation and reentrant-operation gates;
- failure convergence back to a clean offline state.
`LiveSessionEventRouter` owns typed session subscriptions and exact
unsubscription. It routes into focused entity/environment/chat/vital and Core
state handlers but owns no entity map, renderer, UI model, or gameplay state.
`LiveSessionResetPlan` owns the ordered, failure-isolated reset transaction but
does not become a second state owner.
At slice exit, `GameWindow` retains one `_liveSessionController` field plus
narrow composition/lifecycle callbacks. `_liveSession`, `_commandBus`,
`_combatChatTranslator`, `TryStartLiveSession`, `ClearInboundEntityState`, and
`WireLiveSessionEvents` are removed. Every outbound caller resolves the current
borrowed session from the controller at call time; no feature caches it.
## 2. Retail and shipped oracles
The accepted transport behavior remains pinned to:
- [`2026-07-20-retail-graceful-logout-pseudocode.md`](../research/2026-07-20-retail-graceful-logout-pseudocode.md);
- `Proto_UI::LogOffCharacter @ 0x00546A20`;
- `CharacterSet::UnPack @ 0x004FE340`;
- `gmCharacterManagementUI::EnterGame @ 0x004ED440`;
- `CPlayerSystem::LogOnCharacter @ 0x0055F890`;
- `CPlayerSystem::RequestLogOff @ 0x00562DD0`;
- inbound login-message dispatch `@ 0x0055C963`;
- `CPlayerSystem::ExecuteLogOff @ 0x0055D780`;
- `ClientNet::ExitWorldDisconnect @ 0x00541E00`;
- `ClientNet::LogOffServer @ 0x00543EF0`;
- `SharedNet::SendOptionalHeader @ 0x00543160`;
- ACE `CharacterHandler.CharacterLogOff` and
`Session.SendFinalLogOffMessages`;
- Holtburger's connect/PlayerCreate/LoginComplete flow as a protocol
cross-check, not as the logout oracle where its shortcut differs from retail;
- [`2026-07-20-automated-world-lifecycle-gate.md`](2026-07-20-automated-world-lifecycle-gate.md),
whose accepted connected trace proves F653 confirmation, transport
disconnect, endpoint release, and fresh-process reconnect.
The controller must preserve these fixed facts:
1. Complete routing is installed before `Connect`; Connect can synchronously
publish initial server time before character selection.
2. Retail's CharacterList contains separate active and deleted collections.
EnterGame requires a selected GUID in the active collection whose
`GetGreyedOutFor` result is not positive. acdream's unattended auto-entry is
an explicit adaptation: select the first active, non-greyed character and
reject an empty/all-greyed list. A future retained character-select screen
is separate M4/UI work.
3. `WorldSession.EnterWorld` retains its shipped request → ServerReady →
CharacterEnterWorld order. F657 uses `CharacterList.AccountName`, the
canonical server-returned spelling, rather than the startup username.
4. Closing an in-world session sends the eight-byte `[0xF653, active GUID]`
request, drains without App world callbacks until the server's exact
four-byte opcode-only F653 confirmation, sends transport Disconnect, and
only then releases the socket. Once ConnectRequest negotiation has supplied
receiver id/iteration, character-select, entering-world, and failed
post-negotiation sessions also send transport Disconnect before socket
release. `WorldSession.Dispose` remains the one wire owner.
5. The existing close-only adaptation in divergence row AD-44 remains: acdream
exits instead of returning to a retained character-select connection.
6. The registered fixed ConnectResponse delay (UN-6) and initial
LoginComplete-readiness gap (TS-28) remain explicit Core.Net follow-ups.
They are not copied into, disguised by, or claimed by the App controller.
No new automatic network retry or logout shortcut is introduced. Core.Net
changes stay surgical: retail CharacterSet parsing/selection data, exact F653
confirmation, and negotiated-state Disconnect symmetry. The accepted packet
codec, handshake loops, confirmation wait, and receive-thread design are not
rewritten.
## 3. Proven baseline defects closed by this slice
The pre-slice audit found real lifetime defects, not merely style debt:
- repeated startup can overwrite an active controller/session without stopping
the exact prior lifetime;
- no-character and failure paths can leave a `LiveCommandBus` whose delegates
capture a disposed session;
- the no-character path leaks `CombatChatTranslator` subscriptions;
- anonymous event wiring cannot be explicitly detached;
- `GameEventDispatcher.Unregister(type)` cannot safely distinguish an old
registration from a replacement;
- the reset boundary leaves player identity, Core character state, Turbine
rooms/cookie, shortcuts/components, combat/item mana/social state, motion
metadata, and player-mode references from the prior session;
- one reset-stage failure can prevent later network/effect cleanup;
- a synchronous handler can request stop/reconnect while `Tick`, Connect, or
EnterWorld is still unwinding.
Each is fixed at its owner. No suppression flag, delay, retry loop, or stale-GUID
filter is acceptable.
## 4. Architecture and interfaces
### 4.1 Controller state
`LiveSessionController` exposes:
```csharp
LiveSessionStartResult Start(RuntimeOptions options, ILiveSessionLifecycleHost host);
LiveSessionStartResult Reconnect(RuntimeOptions options, ILiveSessionLifecycleHost host);
void Stop();
void Tick();
WorldSession? CurrentSession { get; } // borrowed; never cache
ICommandBus Commands { get; } // NullCommandBus outside active InWorld
bool IsInWorld { get; }
ulong SessionGeneration { get; }
```
The controller uses an injected internal operations seam for deterministic
tests (resolve, create, connect, select, enter, tick, dispose). Production
operations call the real `WorldSession`; tests do not open UDP sockets. This is
an App orchestration seam, not a duplicate protocol implementation.
Every start attempt and accepted session has a monotonically increasing
generation. Lifecycle calls made synchronously from a host/event callback mark
the current attempt non-accepting and defer stop/reconnect until the outer
operation unwinds. The outer attempt rechecks its generation after every
callback and blocking network phase, so it cannot publish an old session after
reentrant replacement.
Replacement order is fixed:
1. disable outbound commands and mark the router non-accepting;
2. detach exact subscriptions and session-scoped translators;
3. gracefully dispose the exact old `WorldSession`;
4. detach the borrowed session from the host;
5. execute the complete reset plan to convergence;
6. create and bind the new session transactionally;
7. Connect, validate/select, EnterWorld;
8. publish active commands only after success.
### 4.2 Lifecycle host
`ILiveSessionLifecycleHost` is a narrow composition boundary, not a context
bag. It has focused lifecycle calls only:
- bind one exact session and return an owned `LiveSessionBinding`;
- reset session-scoped App state;
- report connecting/connected status;
- apply the selected character identity;
- apply post-EnterWorld UI/settings state;
- detach the exact session.
Entity, environment, and effect packets continue through focused sink
interfaces. Later Slice 4 replaces the GameWindow entity sink with the two
planned live-entity integration controllers without changing the session
controller.
### 4.3 Exact subscription ownership
`LiveSessionEventRouter` stores named delegates against one exact event source,
sets `Accepting = false` before teardown, and unsubscribes in reverse order.
Construction is transactional: a partially created router unwinds every prior
registration.
`ObjectTableWiring.Wire`, `CombatStateWiring.Wire`, and
`GameEventWiring.WireAll` return idempotent owned registrations. The GameEvent
dispatcher gains registration tokens that restore/remove only when their exact
handler is still current. Nested registration A → B is safe when A disposes
first: disposing B skips the retired A node and restores the session-owned
predecessor (not a dead callback).
The router groups dependencies by focused responsibility:
- live entity/physics/effect sink;
- environment/time sink;
- Core object/combat/spell/player/social state bridge;
- chat/vital bridge;
- command router.
There is no `GameWindowContext`, GUID table, renderer registry, or generic
service locator.
### 4.4 Command lifetime
`LiveSessionCommandRouter` owns one `LiveCommandBus`, the server/chat routing,
and the exact session captured by its handlers. The controller exposes this bus
only while that session is the active in-world generation. Before stop or
replacement it returns `NullCommandBus`, so a retained panel can never send via
the displaced socket.
Turbine channel resolution and labels move with the chat command/event owner.
`ClientCommandController` remains the retail command behavior owner; GameWindow
supplies its existing focused bindings during composition.
### 4.5 Reset convergence
`LiveSessionResetPlan` is an ordered list of named owner-reset operations. It
runs every stage even when one fails, aggregates errors, and forbids a new
session from starting until all stages converge. The plan clears:
- mouse-look, player mode/cameras, auto-entry, and teleport/reveal transit;
- session origin/counters and selected-character identity;
- equipped/external-container projections;
- object, spell, magic, combat, item-mana, local-player, friends, squelch,
Turbine-chat, shortcut, and desired-component state;
- interaction/selection and selection-presentation state;
- liveness and canonical live entities (including GpuWorldState persistence);
- remote teleport, pending F754/F755 effects, and deferred animation hooks.
`LocalPlayerState.Clear`, `TurbineChatState.Reset`, and `SquelchState.Clear`
are added to their real state owners. Chat history is intentionally retained
across an in-process reconnect, matching the existing UI behavior, but its
local-player GUID resets to zero. Vitals GUID, diagnostic player GUID, run/jump
skills, character options, motion table, movement truth/shadow, active toon,
shortcuts, and desired components reset to pre-login values.
## 5. Implementation sequence
### Commit A0 — retail lifecycle conformance corrections
- Write one consolidated pseudocode note for `CharacterSet::UnPack`,
`gmCharacterManagementUI::EnterGame`, `CPlayerSystem::LogOnCharacter`, and
negotiated logout/disconnect ordering.
- Parse active and deleted CharacterList collections with cursor/truncation
tests and preserve status/account/slot/boolean fields.
- Add the unattended first-active-non-greyed selector and canonical-account
EnterWorld input.
- Require an exact four-byte F653 confirmation and send transport Disconnect
for every negotiated session state.
- Keep UN-6, TS-28, and AD-44 explicitly registered and unchanged.
### Commit A — owned subscription primitives
- Add nested-safe owned registrations to `GameEventDispatcher`.
- Make ObjectTable, CombatState, and GameEvent wiring return disposables.
- Add exact teardown, nested replacement, reverse disposal, and idempotence
tests in Core.Net.
### Commit B — session event and command routers
- Add focused live-session event source/sink contracts.
- Route every current direct WorldSession event exactly once.
- Move chat/Turbine/vital routing and the live command bus out of GameWindow.
- Own `CombatChatTranslator` and all wiring registrations transactionally.
- Test post-dispose silence, partial-construction unwind, nested events, and
router A/B replacement.
### Commit C — convergent session reset
- Add missing reset APIs to Core state owners.
- Add `LiveSessionResetPlan` with named, failure-isolated stages.
- Compose the complete current session manifest and test mutated A → reset →
clean B state, including one throwing stage.
### Commit D — complete lifecycle controller
- Move resolve/create/Connect/CharacterList/selection/EnterWorld/stop/reconnect
into `LiveSessionController`.
- Add generation and operation-depth gates.
- Add typed start results for Disabled, MissingCredentials, NoCharacters,
Connected, Deferred, and Failed.
- Test ordering, duplicate/reentrant start/stop/reconnect, Connect and
EnterWorld failures, tick generation, and exact-once cleanup.
### Commit E — GameWindow cutover
- Implement the narrow host and packet sink interfaces.
- Replace all direct session field reads with borrowed controller resolution.
- Replace both UI command-bus providers with `controller.Commands`.
- Delete the three old lifecycle/wiring methods and the displaced fields.
- Preserve OnLoad, frame Tick, and shutdown stage order.
### Commit F — review corrections and documentation
- Run three independent read-only reviews: retail conformance,
architecture/lifetime, and adversarial tests.
- Fix every confirmed issue and repeat review until clean.
- Run focused App/Core.Net tests, Release build, and the full Release suite.
- Run the existing connected login/command/chat/portal/graceful-close/
fresh-process-reconnect gate when local ACE is available.
- Update code structure, roadmap, milestones, issues/divergence if needed,
durable session memory, `AGENTS.md`, and `CLAUDE.md`.
## 6. Automated acceptance matrix
### Controller
- Routing is complete before Connect's synchronous server-time callback.
- First active non-greyed CharacterList entry, GUID, name, canonical account,
and active-list index are exact; deleted/greyed entries are rejected.
- Empty list, Connect failure, EnterWorld failure, and binding failure leave no
active session/router/bus and clean exactly once.
- Duplicate `Start` is idempotent and cannot disturb a healthy active scope;
explicit `Reconnect` gracefully stops A before B state is reset/wired.
- Stop/reconnect requested during bind, Connect, selected-character callback,
EnterWorld, or Tick cannot resurrect the outer generation.
- Tick reaches only the exact current session; stopped/disposed Tick is a no-op.
- Stop and Dispose are idempotent.
### Router and command bus
- Every direct session event routes once while accepting.
- Dispose is reverse-order, idempotent, and post-dispose events are ignored.
- Partial construction unwinds prior registrations.
- GameEvent A/B nested ownership restores the nearest live predecessor.
- Old command delegates are unreachable before old-session disposal and cannot
send after replacement.
- Domain-handler exceptions follow an explicit policy without corrupting
subscription ownership.
### Reset
- Every named stage is attempted once even when another throws.
- Failure blocks construction of session B and reports all stage errors.
- Player identity, Vitals/Chat identity, diagnostic GUID, selected-character
metadata, Core state, social/chat-room state, projections, live runtime,
pending effects, and hooks all return to pre-login values.
- Chat history is retained but no old own-GUID classification remains.
- Live runtime convergence removes the old persistent player classification.
### Integration
- `GameWindow` has no `_liveSession`, `_commandBus`, or
`_combatChatTranslator` field.
- `TryStartLiveSession`, `ClearInboundEntityState`, and
`WireLiveSessionEvents` no longer exist.
- No substantial session callback body remains in `GameWindow`.
- WorldSession retains the 8-byte request → exact 4-byte confirmation →
Disconnect order and extends Disconnect symmetry to every negotiated state.
- Release build and full suite are green.
## 7. Connected gate
Run a normal capped Release client against local ACE and prove:
1. login selects the same first character and reveals a complete world;
2. system chat, say/tell/channel/server commands, vitals, combat state, and
shortcuts/components still update;
3. one portal/recall completes with the same reveal lifecycle;
4. native close logs the F653 request/confirmation and transport Disconnect;
5. ACE releases the exact endpoint;
6. a fresh process reconnects cleanly with no old character state, command
route, entities, effects, or persistent GUIDs.
The gate is behavioral and resource-oriented. It does not create a new visual
approval pause; final campaign visual acceptance remains after Slice 8.
## 8. Review policy
One primary agent performs edits. Read-only agents independently audit retail
conformance, architecture/lifetime, and adversarial tests after each ownership
commit. Confirmed findings are fixed at their root and re-reviewed. No reviewer
edits the shared worktree, and no callback facade back into a substantial old
GameWindow body counts as extraction completion.

View file

@ -1,442 +0,0 @@
# GameWindow Slice 4 — live-entity App integration
**Status:** Complete 2026-07-21.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 4.
**Baseline:** `ed9dc366`; `GameWindow.cs` is 14,310 lines, 274 fields, and
190 methods before this slice. Release baseline: 6,714 passed / 5 skipped.
**Behavior rule:** Preserve the accepted live-object identity, packet FIFO,
independent timestamp authorities, render/physics/effect lifetime, and connected
R6 behavior while moving their App integration to focused owners. Correct only
proven retail-conformance defects with an explicit oracle and test; do not hide
them inside a mechanical extraction.
**Progress ledger:**
- [x] A — canonical seams: `LiveWorldOriginState`, single-bind runtime-component
lifecycle bridge, extracted `RemoteMotion`, and canonical physics-host
ownership. Landed in `d68c83d1`, `5882b308`, and `fcb66198`. The final
exact-record host checkpoint passed three clean reviews plus 6,743 Release
tests / 5 skipped on 2026-07-21.
- [x] B — pure appearance/projection helpers and controller-level
characterization tests. Landed in `69a2ca0c`. Exact appearance/collision
replacement, default-pose resolution, local-shadow ownership, and recursive
retained projection withdrawal passed three clean reviews plus 6,799 Release
tests / 5 skipped on 2026-07-21.
- [x] C — hydration create/materialize/landblock-recovery ownership and direct
CreateObject/streaming cutover. Landed in `d10c5f2d`; 6,854 Release tests
passed / 5 skipped after three clean retail, architecture, and adversarial
re-reviews on 2026-07-21.
- [x] D — ObjDesc/Parent/Pickup/ready/withdraw ownership and exact retained
identity across leave-world/re-entry. Landed in `fe551496`; 6,877 Release
tests passed / 5 skipped after three clean retail, architecture, and
adversarial re-reviews on 2026-07-21.
- [x] E — Delete/prune and retryable exact-incarnation teardown ownership.
- [x] F — Position/Vector/State/Movement controller, same-generation CreateObject
tail, independent authority gates, and direct inbound cutover.
- [x] G — `GameWindow` cleanup, three-agent review cycle, full Release suite,
connected gates, documentation, and durable memory closeout.
## 1. Outcome and non-goals
This slice creates two cooperating App owners:
- `LiveEntityHydrationController` owns CreateObject, ObjDesc, Parent, Pickup,
Delete/prune, DAT-backed appearance hydration, first materialization,
landblock rehydration, ready publication, projection withdrawal, and exact
App-component teardown over `LiveEntityRuntime`.
- `LiveEntityNetworkUpdateController` owns accepted Position, Vector, State,
and Movement routing into the existing motion, physics, projectile,
presentation, and teleport owners. It also consumes the same-generation
CreateObject update tail without reconstructing the entity.
`LiveEntityRuntime` remains the only owner of server GUID ↔ local identity,
incarnation, accepted snapshots, independent authority versions, spatial
worksets, and teardown tombstones. Neither new controller may contain a GUID
dictionary or cache an incarnation outside the runtime record it is currently
validating.
This is not a new entity model, physics algorithm, renderer, packet decoder, or
DAT reader. It does not retire the documented future-packet, visibility-PVS,
or null-parent-queue divergences. It does not redesign the accepted update
thread or `RetailInboundEventDispatcher` FIFO.
## 2. Retail and shipped oracles
The extraction is pinned to:
- `SmartBox::HandleCreateObject @ 0x00454C80`;
- `ACCObjectMaint::CreateObject @ 0x00558870`;
- `SmartBox::ProcessObjectNetBlobs @ 0x00454B20` and
`QueueBlobForObject @ 0x00451B90`;
- `SmartBox::HandleReceivedPosition @ 0x00453FD0`;
- `SmartBox::DoPickupEvent @ 0x00452240` and
`DoParentEvent @ 0x00452290`;
- `SmartBox::HandleDeleteObject @ 0x00451EA0`;
- `CPhysicsObj::set_description @ 0x00514F40`;
- `CPhysicsObj::set_state @ 0x00514DD0`;
- `CPhysicsObj::change_cell @ 0x00513390`;
- `CPhysicsObj::exit_world @ 0x00514E60` and
`leave_world @ 0x005155A0`;
- [`2026-07-13-retail-projectile-vfx-pseudocode.md`](../research/2026-07-13-retail-projectile-vfx-pseudocode.md);
- [`2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md`](../research/2026-07-18-retail-object-liveness-and-mesh-reclamation-pseudocode.md);
- [`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md);
- ACE and Holtburger object-update handlers as interpretation and protocol
cross-checks, never as replacements for the named-retail ordering.
The following rules are load-bearing:
1. One logical object exists per accepted `INSTANCE_TS`. Equal generation
mutates it; newer generation tears down and replaces it; older generation
is ignored using retail's wrap-safe comparison, including the `0x8000`
boundary.
2. A fresh CreateObject constructs, describes, attaches, enters world, then
replays queued blobs. An equal-generation CreateObject applies ObjDesc,
Parent or Position/Pickup, Movement, State, Vector, then WeenieDesc without
recreating the object.
3. Position, Movement, State, Vector, ObjDesc, Teleport, ForcePosition,
ServerControlledMove, and Instance authorities remain independent. After
every reentrant callback, the exact record and relevant authority version
are revalidated.
4. Movement consumes strict `MOVEMENT_TS` before it can discover a stale
server-controlled timestamp; equality on the latter is accepted.
5. Parent, Pickup, and Position share `POSITION_TS`. Pickup unparents and
leaves world; a later fresh Position unparents and restores the same object
identity/resources.
6. `leave_world` is not deletion. It clears cell/shadow/contact/transient
presentation while retaining PartArray, movement, animation, scripts,
effects, timestamps, and identity. Logical destruction alone performs
`exit_world` manager cleanup.
7. Inbound processing is one FIFO. Nested heterogeneous packets wait until the
active packet reaches its complete tail.
## 3. Architecture and interfaces
### 3.1 Shared origin state
`LiveWorldOriginState` owns the session-scoped world center currently spread
across `_liveCenterX`, `_liveCenterY`, and `_liveCenterKnown`:
```csharp
bool IsKnown { get; }
double CenterX { get; }
double CenterY { get; }
bool TryInitialize(double worldX, double worldY);
void Recenter(double worldX, double worldY);
void Reset();
```
Hydration, accepted Position/teleport, streaming, and render consumers share
this state directly. No GameWindow callback conceals origin mutation.
### 3.2 Runtime component lifecycle
Introduce:
```csharp
interface ILiveEntityRuntimeComponentLifecycle
{
void TearDown(LiveEntityRecord exactRecord);
}
```
`DeferredLiveEntityRuntimeComponentLifecycle` is a composition-only,
single-assignment bridge. Construction order is fixed:
1. construct the unbound bridge;
2. construct `LiveEntityRuntime` with the bridge;
3. construct effect, projectile, presentation, child, and related owners;
4. construct hydration;
5. bind the bridge exactly once to hydration;
6. construct and bind the network-update sink;
7. only then permit a live session to start.
Calling before bind fails fast; second bind is rejected. This removes the
circular GameWindow teardown façade without making runtime mutation optional.
The existing `CompositeLiveEntityResourceLifecycle` remains the sole logical
mesh/script registration transaction; hydration does not become a second
resource owner.
### 3.3 Canonical runtime components
Move `RemoteMotion` out of `GameWindow` into `AcDream.App.Physics` without
semantic changes. Store physics-host resolution on the exact
`LiveEntityRecord` (or an explicit runtime-owned component index) rather than
the duplicate `_physicsHosts` GUID dictionary. Local-player host publication
uses the same focused owner, not a second identity table. Preserve the full-cell
binding and remote-placement contract.
### 3.4 Hydration API
The controller surface is narrow and event-shaped:
```csharp
void OnCreate(WorldSession.EntitySpawn spawn);
void OnObjDesc(ObjDescEvent.Parsed update);
void OnDelete(DeleteObject.Parsed update);
void OnPrune(LiveEntityPruneCandidate candidate);
void OnPickup(PickupEvent.Parsed update);
void OnParent(ParentEvent.Parsed update);
bool TryAcceptParentForRender(ParentEvent.Parsed update);
void OnLandblockLoaded(uint landblockId);
bool EnsureWorldOriginAndRecover(
LiveEntityRecord expected,
ulong positionAuthorityVersion,
WorldSession.EntitySpawn acceptedSpawn);
void OnEntityReady(uint guid);
bool WithdrawWorldProjection(uint guid);
```
Hydration receives focused concrete collaborators/ports: runtime and object
table, the sole `DatCollection` and shared DAT lock, physics data cache,
spawn/animation/collision builders, spatial state/origin/streaming, and the
existing effect/projectile/child/presentation/light owners. It must not receive
a generic GameWindow context or a bundle of callbacks to old substantial
methods.
ObjDesc mutates the captured exact entity. Acquire-before-publish rollback and
post-callback incarnation checks preserve local ID, body, motion, physics host,
effect owner, and parent state. Parent-on-spawn may reenter; the canonical
snapshot is reread afterward. A materialized object only rebuckets; a pending,
never-materialized object hydrates once. Landblock load never replays
create-time logical resource activation.
### 3.5 Network-update API
The network owner accepts Position, Vector, State, Movement, and the equal-
generation CreateObject tail. It resolves through `LiveEntityRuntime`, captures
the relevant authority version, calls existing focused runtime owners, and
revalidates exact record plus authority after every callback.
Hydration calls a once-bound `ILiveEntityNetworkUpdateSink` for the
same-generation tail. Network position recovery calls
`EnsureWorldOriginAndRecover`; no substantial dispatch returns to GameWindow.
Local ForcePosition, teleport presentation, projectile corrections, shadow
updates, movement animation, and static/headless branches retain their shipped
ordering.
### 3.6 Direct composition
At slice exit these callbacks point directly to the new owners:
- session Create/Delete/Pickup/Parent/ObjDesc/Position/Vector/State/Movement;
- streaming landblock-loaded rehydration;
- liveness prune;
- equipped-child parent acceptance and entity-ready replay;
- runtime exact-component teardown.
`GameWindow` keeps controller construction plus narrow frame calls. The old
handler, hydration, collision-registration, appearance-rebind, ready,
leave-world, and teardown method bodies must be absent, not retained as
one-line façades.
## 4. Commit sequence and gates
### A — canonical seams
- Add and test `LiveWorldOriginState`.
- Add and test the fail-fast, single-bind lifecycle bridge.
- Extract `RemoteMotion` mechanically; retarget all 51 App-test references.
- Canonicalize physics-host ownership and remove `_physicsHosts` only after
resolution/teardown/local-player tests prove equivalence.
- Run App remote-motion, runtime, physics-host, session-reset, and structural
suites; full Release build/test; three-agent review and re-review.
### B — presentation helpers and characterization
- Extract `AppearanceUpdateState`, appearance rebind, default-pose, and
collision-build helpers behind focused App types.
- Preserve the exact DAT lock and existing extracted WorldBuilder pipeline.
- Add controller-harness fakes that require no Silk/GL context.
- Characterize current leave-world failure ordering rather than silently
redesigning it.
- Run appearance, animation, spawn-adapter, collision, resource-lifecycle,
and new characterization suites; full Release build/test; review cycle.
Completed in `69a2ca0c`. The checkpoint also pinned the retail `add_child` /
`set_parent` / `leave_world` boundary required by C and D: parent timestamps
are staged before validation, accepted relationships are committed one at a
time, and world withdrawal recursively removes the exact parent-first
projection subtree while retaining logical identity and retry ownership.
Release build completed with zero warnings and errors; 6,799 tests passed with
five intentional skips; retail, architecture, and adversarial re-reviews were
clean.
### C — create and materialization
- Implement hydration CreateObject registration, first materialization,
pending deferral, landblock recovery, and ready publication.
- Preserve prior-incarnation cleanup failure aggregation and rollback.
- Route session CreateObject and streaming callbacks directly.
- Tests: loaded create order; pending→loaded; materialized rebucket; same/
older/new/wrapped generations; nested callback replacement; registration
failure/retry; no duplicate mesh/script/effect activation.
- Full Release build/test; review cycle.
Completed in `d10c5f2d`. `LiveEntityHydrationController` now owns the exact
CreateObject registration transaction, world-origin bootstrap, pending
deferral, first DAT projection materialization, landblock recovery, and the
ready/PES replay boundary. Same-generation CreateObject supersession carries
a monotonic integration version and a persistent retry obligation, so an
older reentrant materialization cannot publish or erase the newer canonical
snapshot. Parent and pickup creates complete through their retail cell-less
paths, while interrupted initial hydration reuses the same animation
sequencer and completed owners retain their exact phase and callback queues.
Session CreateObject and streaming callbacks route directly to the extracted
owner; the old GameWindow bodies are gone. Release build completed with zero
warnings and errors; 6,854 tests passed with five intentional skips; retail,
architecture, and adversarial re-reviews were clean.
### D — appearance, parenting, pickup, withdrawal
- Move ObjDesc, Parent, Pickup, child-ready, collision registration, and
projection withdrawal.
- Tests: identity/component preservation; acquire failure rollback; stale or
reentrant ObjDesc; parent-before-create canonical reread; Pickup/Parent
leave-world; Position re-entry with unchanged local ID/resources.
- Bind child callbacks directly and remove their GameWindow façades.
- Full Release build/test; review cycle.
Completed in `fe551496`. `LiveEntityHydrationController` now owns accepted
ObjDesc, Parent, Pickup, child-ready, and exact leave-world/re-entry routing.
ObjDesc carries an independent authority/retry transaction and reports success
only when the current visual projection is published; delayed landblock or
Position recovery therefore refreshes the player paperdoll exactly once.
Attached objects re-realize through the equipped-child owner without requiring
a world Position, retain their local identity/resources, and restore their
complete descendant chain synchronously. Ready publication pins the exact
create, Position, ObjDesc, projection-mutation, projection-kind, spatial, and
entity authorities across every callback. The retail absent AnimationFrame
default is placement zero. Release build completed with zero warnings and
errors; 6,877 tests passed with five intentional skips; retail, architecture,
and adversarial re-reviews were clean.
### E — delete and exact teardown
- Move Delete/prune, teardown-plan construction, leave-world component cleanup,
and retry tombstone behavior.
- Bind runtime bridge and liveness directly.
- Tests: unknown/stale/current Delete; callback deletion; dual failure;
teardown retry; same-GUID newer generation during old teardown; reset
convergence; no surviving renderer/collision/script/effect/light owner.
- Full Release build/test; review cycle.
Completed in `f38822c4`. `LiveEntityRuntimeTeardownController` now owns the
retryable App-component half of retail `CObjectMaint::DeleteObject` and keeps
the stable cleanup plan on the exact record tombstone. Hydration rejects the
local player before every lookup or side effect, gates remote Delete by the
exact Instance timestamp, discards only unknown-owner pending effects, and
routes the 25-second prune through the same transaction. Successful cleanup
steps never replay after a failure; a same-GUID replacement cannot be mutated
by the old incarnation's delayed cleanup. The derived remote stop-observation
state also has a focused session owner instead of a loose GameWindow
dictionary. Full Release validation passed 6,891 tests with five intentional
skips and no errors; all three corrected-diff re-reviews were clean.
### F — network-update owner
- Move Movement, Vector, State, and Position routing plus the equal-generation
CreateObject tail.
- Preserve FIFO and independent authorities; test stale, equal, wrap, nested,
and callback-invalidated updates for every channel.
- Preserve ForcePosition, server-controlled move, teleport, projectile,
shadow, static, airborne, autonomous, animated, and headless branches.
- Route session callbacks directly and remove old GameWindow handlers.
- Full Release build/test; review cycle.
Completed in `aa90c646`. `LiveEntityNetworkUpdateController` now owns accepted
Position, Vector, State, and Movement routing;
`LiveEntitySameGenerationUpdateRouter` owns the retail equal-generation
CreateObject tail. `LiveEntityInboundAuthorityGate` pins the exact record,
independent channel authority, and velocity authority before any reentrant App
callback. `LiveEntityMotionRuntimeController` owns the shared physics-host,
Setup, MoveTo/Sticky, Hidden-target, and server-controlled-cycle policy behind
a once-bound construction bridge. ForcePosition preserves retail's
blip-then-immediate-F7B1 order and stamps the exact Position snapshot serialized
on the wire. Focused tests cover stale/equal/wrapped timestamps, callback
replacement, GUID reuse, projectile/body/remote Vector precedence, mixed
same-generation routing, bridge binding, and the exact 56-byte autonomous
position payload. All three corrected-diff re-reviews were clean.
### G — closeout
- Structural tests prove both controllers lack identity dictionaries and
`GameWindow` lacks every moved field/type/method/body.
- Run all live-runtime, object-table, parent/attachment, appearance/animation,
effect/light/projectile, remote-motion/physics, inbound FIFO, liveness,
stress, session-reset, and router suites.
- Run `dotnet build AcDream.slnx -c Release` and full Release tests.
- Run the connected R6 route and graceful reconnect gate, then user visual
checks for inventory/equip/parenting, death/corpse, and portals when
available.
- Update architecture, roadmap, milestones, issues/divergence register, this
ledger, `AGENTS.md`/`CLAUDE.md` when current-state pointers change, and
durable GameWindow/live-runtime memory.
Completed 2026-07-21. `GameWindow` fell from the Slice-3 baseline of 14,310
lines to 10,301 lines; its top-level field and method declarations are now 267
and 163. Release build completed with zero warnings/errors and the full suite
passed 6,940 tests with five intentional skips. The 310-second connected R6
route passed login, Caul/Sawato/Rynthid/Aerlinthe/Sawato-revisit/Holtburg/
Caul-return materialization, turn sampling, three locomotion/jump/combat
exercises, and graceful close. Live-server population churn, previously known
missing DAT VFX diagnostics, and empty world-edge landblocks remained warnings,
not lifecycle failures. Slice 5 is the next structural owner boundary.
## 5. Adversarial acceptance matrix
Automated acceptance includes:
- equal/old/new/wrapped `INSTANCE_TS`, including `0x8000`;
- full same-generation call trace and WeenieDesc tail ordering;
- nested heterogeneous FIFO and exact authority invalidation;
- fresh loaded create and pending→loaded without replay;
- loaded→loaded rebucket and loaded↔pending oscillation;
- ObjDesc identity/body/motion/host/effect/parent preservation;
- Parent/Pickup→cell-less→Position re-entry with unchanged local ID;
- unknown/stale/current Delete, callback delete, GUID reuse, and retry;
- state side-effect order and movement/ForcePosition timestamp order;
- projectile and remote-position corrections across landblocks;
- bridge unbound/second-bind failures;
- session reset with zero surviving live owners;
- 96-owner and repeated-recall stress with no resource growth;
- source/reflection ownership gates.
Connected acceptance includes:
- capped login and populated radar/world;
- remote creature motion and target/status projection;
- wield/dewield, bow/wand/melee switching, ammo parenting, inventory pickup;
- open and loot a corpse after approach;
- kill/death/corpse lifetime;
- recall and portal travel across landblocks, revisit, graceful close, and
fresh-process reconnect;
- uncapped post-reconnect performance checkpoint.
## 6. Review discipline
Every ownership commit receives three independent read-only reviews:
1. retail conformance — symbols, ordering, timestamps, and documented
divergences;
2. architecture/integration — identity ownership, dependency direction,
update-thread rules, lifecycle symmetry, and GameWindow reduction;
3. adversarial testing — malformed/stale packets, callback reentrancy,
registration failure, GUID reuse, landblock churn, reset, and leaks.
The primary agent reproduces and fixes confirmed findings, reruns focused and
full gates, and requests re-review until no actionable finding remains. No
subagent writes the implementation.
## 7. Explicit carried divergences
Slice 4 preserves, rather than disguises:
- AD-32 future-generation non-effect packet dropping;
- AP-65 picked-up data-only weenie retention;
- AP-69 approximate 384-unit visibility envelope;
- TS-32 missing complete child-before-parent null-object queue;
- the equal-generation PES-table refresh and VectorUpdate airborne/state
adaptations recorded during Slice 4 planning.
These remain visible in the divergence register and are not evidence that the
ownership extraction itself is incomplete.

View file

@ -1,470 +0,0 @@
# GameWindow Slice 5 — landblock presentation
**Status:** Complete 2026-07-21 (`4a205a3e`).
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 5.
**Baseline:** `2b10e91e`; `GameWindow.cs` is 10,301 lines, 267 fields, and
163 methods. Release baseline: 6,940 passed / 5 skipped. The connected R6
baseline passed seven destinations/revisits, three movement/jump/combat
exercises, and graceful close in 310 seconds.
**Behavior rule:** Preserve the accepted two-tier streaming quality, immutable
per-job EnvCell transaction, render-readiness barrier, collision/light/script/
plugin lifetime, and retryable detach-first retirement. This is an ownership
move. Any proven defect needed to make that boundary safe gets its own oracle,
test, and bisectable commit rather than being hidden in a mechanical move.
## Progress ledger
- [x] A — freeze production order and introduce explicit build/publication
seams plus structural guards.
- A1 landed in `4f965d06`: accepted results now enter one staged
`LandblockPresentationPipeline` transaction; `GpuWorldState` returns an
exact spatial-publication receipt before retryable render-pin/script
activation; retries preserve FIFO suffixes, pending-live merges, and
promotion identity across policy/generation changes. Three corrected-diff
reviews are clean. Release gate: 6,955 passed / 5 skipped.
- A2 landed in `090b0354`: every production load now captures a specified
immutable origin (including valid map origin `(0,0)`) and carries it through
worker construction, priority/supersession, completion, Far normalization,
and publication. Origin-less or mismatched request-aware work fails before
it can publish. Three corrected-diff reviews are clean. Release gate: 6,965
passed / 5 skipped.
- [x] B — capture one immutable world-origin snapshot per load and extract the
serialized DAT-backed `LandblockBuildFactory`.
- Landed in `5af101b2`: `LandblockBuildFactory` now owns the complete
serialized worker-side DAT transaction, immutable height-table snapshot,
stab/scenery/interior hydration, complete EnvCell build, and physics-DAT
bundle. `GameWindow` only constructs the owner and passes its `Build`
operation to the streamer; it no longer contains any build helper body.
Hermetic tests pin Far/Near/Promote shape, zero-submesh visibility cells,
missing-record behavior, the exact supplied DAT gate, non-interleaved full
Near transactions, exception recovery, and immutable inputs. The installed
Holtburg gate independently derives visibility completeness from
`LandBlockInfo.NumCells`. Three corrected-diff reviews are clean. Release
gate: 6,979 passed / 5 skipped; `GameWindow.cs` is 9,573 lines.
- [x] C — extract terrain, visibility, EnvCell, building, and render-pinning
publication.
- Landed in `1029f837`: `LandblockRenderPublisher` owns captured-origin
terrain publication, complete visibility-cell transactions, AABBs,
building registries, EnvCell commit/preparation, and matching retirement
operations. Invalid EnvCell identities fail before any presentation
mutation; visibility-only cells and zero-submesh transactions remain
first-class. Tests pin building-before-EnvCell order, duplicate and failed
completion behavior, foreign receipts, post-pin preparation, real
publish-to-retire balance, and hidden AABB ownership. Three corrected-diff
reviews are clean. Release gate: 6,988 passed / 5 skipped;
`GameWindow.cs` is 9,490 lines.
- [x] D — extract physics/cache/collision publication and cross-landblock
reflood ownership.
- Landed in `3613d393`: `LandblockPhysicsPublisher` now owns captured-origin
terrain/cell/portal/building physics, cached GfxObj shapes, mutually
exclusive multipart BSP or Setup fallback collision, exact static-owner
replacement, final reflood, demotion, and full removal. Every Setup shape
shares the retail logical entity ID; building shells remain exclusive to
the per-landcell building channel. Reapply retires stale cell/building/UCG
state and omitted static owners by landblock prefix, rebinds `CurrCell` to
the replacement object, and preserves adjacent owners even when collision
footprints cross the seam. Three corrected-diff reviews are clean. Release
gate: 7,008 passed / 5 skipped; zero-warning build; `GameWindow.cs` is
9,043 lines.
- [x] E — extract static lighting, translucency, default-script, and plugin
presentation; compose the small `LandblockPresentationPipeline`.
- Landed in `ea0da8c8`: render, physics, and DAT-static publishers now
preflight immutable receipts before mutation and retain exact per-stage
progress through retry. `LandblockStaticPresentationPublisher` owns static
lights, translucency lifetime, and plugin projection; `GpuWorldState` and
`EntityScriptActivator` explicitly rebind same-ID static snapshots without
replaying Setup defaults or resetting animation/light/translucency state,
while omitted IDs take full teardown. Source changes and duplicate IDs fail
before presentation mutation, missing DefaultAnimation never claims an
owner, and fallible reconciliation precedes pending-map consumption. Three
corrected-diff reviews are clean. Release gate: 7,026 passed / 5 skipped;
`GameWindow.cs` is temporarily 9,073 lines because production wiring lands
before checkpoint G deletes the old callback body.
- [x] F — move retryable demotion/full-retirement presentation under the
pipeline without changing `LandblockRetirementCoordinator`'s ledger.
- Landed in `ea7ffbc1`: the pipeline now owns a focused
`LandblockPresentationRetirementOwner` and its exact detach-first ledger;
`GameWindow` no longer owns a coordinator or retirement callback body.
Full and Near retirements preserve their distinct terrain/physics/live
owner matrices, successful stages and entity cursors never replay, and
reentrant Begin/Advance requests serialize without mutating an active
ticket enumeration. A committed visibility-observer failure retains the
detach receipt, completes presentation teardown, drains deferred work,
then rethrows. Constructor identity guards enforce one world-state,
publisher, lighting, and translucency graph, while explicit concrete and
legacy controller overloads make ignored presentation callbacks
impossible. Three corrected-diff reviews are clean. Release gate: 7,039
passed / 5 skipped; build green with 17 pre-existing test-project warnings;
`GameWindow.cs` is 9,011 lines.
- [x] G — cut `StreamingController` and `GameWindow` directly to the pipeline;
delete old bodies, scratch state, and callback facades.
- [x] H — three-agent corrected-diff review, full Release suite, deterministic
lifecycle gate, nine-destination resource soak, documentation, and durable
memory closeout.
- Landed in `4a205a3e`: native shutdown now keeps the live-session reset
dependencies alive until deferred/reentrant disposal actually converges;
source and behavioral tests pin that completion barrier. Both connected
harnesses scan the final post-close logs for shutdown/disposed-resource
failures. The resource gate uses an already-warm Caul → Sawato → Caul
plateau instead of comparing a cold first load to a warmed cache, and
every route command blocks on its exact portal-materialization count.
- All three final reviews are clean. Release gate: 7,054 passed / 5 skipped.
The capped/reconnect lifecycle gate passed in 311.4 seconds. The
synchronized nine-stop soak passed in 394.1 seconds; Caul return → plateau
changed working/private memory by only +56.4/+48.4 MiB and update p95 by
0.8 → 0.7 ms. `GameWindow.cs` closes Slice 5 at 8,811 raw lines, 247
fields, and 153 methods—1,490 lines below the Slice-5 baseline and 6,912
lines (44%) below the campaign baseline.
Every checked checkpoint is committed as a bisectable architectural unit.
Documentation records the exact commit and accepted test count as work lands.
## 1. Outcome and non-goals
At slice exit, `GameWindow` only composes and retains the landblock presentation
owner. It no longer builds a landblock from DAT records, hydrates scenery or
interiors, constructs a physics DAT bundle, publishes terrain/cells/collision/
lights/plugins, or advances presentation retirement.
The final owner graph is deliberately not a replacement god object:
```text
StreamingController (tier, region, generation, priority, budget)
│ accepted Loaded / Promoted / Unloaded result
LandblockPresentationPipeline (transaction order only)
├── LandblockBuildFactory worker-only DAT transaction
├── LandblockRenderPublisher terrain/cells/EnvCell/building/render pins
├── LandblockPhysicsPublisher terrain/cell/portal/static collision
├── LandblockStaticPresentation light/translucency/script/plugin projection
└── LandblockRetirementCoordinator detach-first retry ledger
GpuWorldState (spatial buckets, pending live projections, tier, AABB)
```
`StreamingController` remains the sole desired-tier and stale-generation
authority. `GpuWorldState` remains the sole spatial-bucket and retained-live-
projection owner. `LandblockStreamer` remains the sole worker queue. Existing
renderers, physics/cache registries, lighting, scripts, plugin state, and
`WorldRevealCoordinator` remain independent owners.
This slice does not change Near/Far radii, mesh or texture quality, world view
distance, portal visibility, GPU frame fences, the draw graph, live-entity
identity, or the update/render phase order. It does not re-port algorithms
already inventoried under `Rendering/Wb` or introduce another DAT reader.
## 2. Retail, WorldBuilder, and shipped oracles
The extraction preserves the following retail order and already-shipped
adaptations:
- `LScape::grab_visible_cells @ 0x00504EC0`: initialize buildings and gather
visible cells before static/dynamic activation;
- `CLandBlock::init_buildings @ 0x0052FD80`;
- `CLandBlock::grab_visible_cells @ 0x0052F460`;
- `CLandBlock::init_static_objs @ 0x00530A40` and
`CLandBlock::init_dyn_objs @ 0x0052F3B0`;
- `CLandBlock::destroy_static_objects @ 0x0052FA50`,
`CLandBlock::Destroy @ 0x0052FAA0`, and
`CLandBlock::release_all @ 0x0052FCF0` for static/building/cell teardown;
- `CObjCell::init_objects @ 0x0052B420`
`CPhysicsObj::recalc_cross_cells @ 0x00515A30`; acdream's per-landblock
`RefloodLandblock` remains the registered asynchronous adaptation;
- `PView::InitCell @ 0x005A4B70` and
`CEnvCell::find_visible_child_cell @ 0x0052DC50`;
- `CBuildingObj::find_building_collisions @ 0x006B5300`;
- `CPhysicsObj::add_shadows_to_cells @ 0x00514AE0`;
- `SmartBox::UseTime @ 0x00455410`; acdream's shared reveal coordinator is the
asynchronous equivalent of retail's synchronous cell readiness;
- [`worldbuilder-inventory.md`](../architecture/worldbuilder-inventory.md),
especially the one-job/one-complete-EnvCell-transaction and sole-DAT-reader
rules;
- the current render and two-tier streaming digests, including logical
withdrawal before fence-delayed GPU reuse and the prohibition on reducing
view distance as an optimization;
- [`2026-07-13-envcell-landblock-transaction.md`](../research/2026-07-13-envcell-landblock-transaction.md)
and
[`2026-07-18-retail-texture-resource-lifetime-pseudocode.md`](../research/2026-07-18-retail-texture-resource-lifetime-pseudocode.md).
The named retail decomp is the oracle for lifecycle ordering. The extracted
WorldBuilder code is the oracle for already-ported terrain, scenery, EnvCell,
mesh, and texture mechanics. ACE and the retained WorldBuilder reference tree
are cross-checks, not new dependencies.
## 3. Load-bearing invariants
### 3.1 Worker build
1. One `Load` captures landblock ID, job kind, generation, and the accepted
world-origin center on the update thread. The worker never reads the
update-thread-confined `LiveWorldOriginState`.
2. All DAT reads for one build execute under the existing single-reader gate.
The worker performs CPU work only: no GL call, renderer/physics publication,
registry mutation, or shared pending cell bag.
3. Far reads the heightmap only and emits no static entities, EnvCells, or
physics bundle. Near/Promote emits hydrated stabs, procedural scenery,
interiors, a complete `PhysicsDatBundle`, and one immutable
`EnvCellLandblockBuild`.
4. Every valid EnvCell is present in the visibility transaction even when it
has zero drawable shell submeshes. Geometry IDs retain their current
deterministic arithmetic.
5. Missing records keep the current skip/null semantics. A failed build has no
externally visible partial publication.
### 3.2 Publication
For an accepted Near load or promotion, the transaction order remains:
1. publish terrain and rebase it to the captured origin;
2. commit the complete cell-visibility snapshot and AABB;
3. populate physics data and publish terrain/cells/portals/buildings;
4. commit building registries and the EnvCell renderer transaction;
5. publish static BSP/cylinder collision, lights, translucency, and DAT-static
plugin snapshots; reflood the landblock;
6. publish/merge the canonical spatial bucket in `GpuWorldState`, acquiring
ordinary and synthetic EnvCell render IDs and activating static defaults;
7. only after the pins exist, replay schema-aware EnvCell mesh preparation;
8. only after spatial publication, restore retained live projections.
Render readiness means Near tier plus real uploaded ownership for every
required ordinary and synthetic EnvCell render ID. Dictionary publication is
not readiness. Duplicate Near completion is a no-op; stale Far cannot erase a
Near layer. Promotion-before-base and the accepted Near-completion-while-Far-
desired synthesis remain `StreamingController` policy.
### 3.3 Demotion and retirement
1. `GpuWorldState.DetachNearLayer` or `DetachLandblock` commits first. No
presentation cleanup failure can resurrect spatial membership.
2. Near→Far keeps terrain and all live projections, but removes DAT statics,
static scripts/classification, lights/translucency/plugins, indoor physics,
cell visibility, building registries, and EnvCells.
3. Full retirement additionally releases terrain and full landblock physics.
Persistent player projection is rescued and still-live server projections
are parked for rebucketing with the same identity.
4. The retryable order is exactly: mesh references → static scripts →
classification → entity lights → entity translucency → DAT-static plugin
projection → terrain for Full only → physics remove/demote → cell visibility
→ building registry → EnvCell renderer.
5. Successful stages and entity cursors never replay. Same-ID replacement is
fenced until every required stage converges, including across controller or
radius reconfiguration.
6. GPU range/layer reuse remains fence-delayed by `GpuFrameFlightController`.
## 4. Architecture and interfaces
### 4.1 Immutable build request
Introduce a request equivalent to:
```csharp
public readonly record struct LandblockBuildRequest(
uint LandblockId,
LandblockStreamJobKind Kind,
ulong Generation,
int OriginCenterX,
int OriginCenterY);
```
The generation remains for matching and diagnostics; the builder never uses it
to decide residency. Results carry the same origin snapshot so apply cannot
combine worker geometry from one center with render-thread placement from a
later center.
### 4.2 DAT build factory
`LandblockBuildFactory` receives `IDatReaderWriter`, the one shared DAT gate,
the immutable height table, and worker-safe mesh/physics-cache collaborators.
Its public surface is one operation:
```csharp
LandblockBuild? Build(in LandblockBuildRequest request);
```
The extracted `BuildPhysicsDatBundle`, stab hydration, scenery generation,
interior/EnvCell construction, and terrain sampling remain private focused
helpers. No method accepts `GameWindow`, `LiveWorldOriginState`, GL objects, or
presentation registries.
### 4.3 Focused publishers
`LandblockRenderPublisher` owns terrain slot publication, AABB, cell
visibility, building registries, EnvCell commit, static render pin/preparation,
and a typed immutable diagnostics snapshot.
`LandblockPhysicsPublisher` owns cache population, terrain/cell/portal/building
physics, static multipart BSP and exclusive Setup-cylinder/sphere fallback,
demotion/full removal, and final cross-cell reflood.
`LandblockStaticPresentationPublisher` owns idempotent static light and
translucency replacement, static script activation/removal at the existing
spatial registration boundary, and DAT-static plugin spawn/removal projection.
The pipeline calls concrete publishers in the fixed order. Publish/retire
interfaces expose no DAT reader. Diagnostics move with the owner and the Debug
UI reads a snapshot rather than `GameWindow` scratch counters.
### 4.4 Policy cutover
`StreamingController` keeps result validation and calls the pipeline only after
generation/tier/duplicate/retirement-fence checks. It must not gain DAT, GL,
physics, plugin, light, or live-GUID state. `GpuWorldState` keeps its buckets,
pending Near/render-ID parking, and live rescue. Existing hidden presentation
side effects may be made explicit only when tests prove identical ordering and
balance.
`GameWindow` retains construction/wiring plus one pipeline field. Structural
tests forbid these methods from returning:
```text
BuildLandblockForStreaming
BuildLandblockForStreamingLocked
BuildPhysicsDatBundle
BuildSceneryEntitiesForStreaming
BuildInteriorEntitiesForStreaming
EnsureEnvCellMeshesAfterPin
ApplyLoadedTerrain
ApplyLoadedTerrainLocked
AdvanceLandblockPresentationRetirement
```
## 5. Checkpoint execution
### A — characterization and seams
- Add owner-call trace tests around the current production order and payload
multiplicity.
- Introduce immutable build/publication request/result seams without changing
the live call graph. The typed apply-diagnostics snapshot moves with its
concrete render owner in C instead of creating a temporary A-stage owner.
- Add dependency/reflection guards: no origin-state read from worker, no DAT in
publish/retire interfaces, no residency/generation map in the pipeline.
### B — origin snapshot and build factory
- Capture origin with every enqueued load; preserve it through priority queue,
completion, promotion, and Far synthesis.
- Move the complete serialized DAT build verbatim into
`LandblockBuildFactory`; keep `DatCollection` access behind
`IDatReaderWriter` and the shared gate.
- Test Far/Near payload shape, repeat-build determinism, EnvCell completeness,
physics-DAT closure, missing records, lock serialization, and zero DAT reads
during publication.
### C — render publication
- Extract terrain upload/rebase, CellVisibility, AABB, building registry,
EnvCell renderer commit, render-ID pin/preparation, and diagnostics.
- Test visibility-only cells, zero-submesh EnvCells, promotion-before-base,
real render readiness, reapply idempotency, and release balance.
### D — physics publication
- Extract physics cache commit, terrain/cell/portal/building setup, static
multipart BSP/cylinder registration, reflood, demote, and full removal.
- Use two adjacent landblocks to prove one retirement cannot erase a neighbor's
seam collision. Pin Far terrain-only, Near reapply, promotion, demotion, and
full unload symmetry.
### E — static presentation and transaction coordinator
- Extract static lights/translucency/plugin projection and make existing static
script ownership explicit without replaying defaults.
- Compose the small `LandblockPresentationPipeline`; add exact order,
multiplicity, rollback/blocked-publication, and owner-balance tests.
### F — retirement
- Move production retirement callbacks under the pipeline while keeping
`LandblockRetirementCoordinator` as the exact retry ledger.
- Inject failure before/after each boundary; prove only unfinished suffixes and
exact entity cursors resume. Test Near and Full requirements separately.
### G — direct cutover and cleanup
- Route `LandblockStreamer` build and `StreamingController` apply/retire calls
directly to the new owners.
- Delete the old GameWindow bodies, callback facades, building-registry and
apply-diagnostic scratch state that now have focused owners.
- Add structural source tests and remeasure GameWindow lines/fields/methods.
**Complete in `c79d0a49`.** Production constructs one concrete pipeline;
legacy callback constructors are internal test seams and structural tests keep
them out of the composition root. The old GameWindow apply methods and their
building/diagnostic scratch state are deleted. A retained
`StreamingOriginRecenterCoordinator` now joins complete old-window
presentation retirement to teleport recenter and session cancellation before
the shared origin may be reused. Reviews closed full-window overlap, radius
reconfiguration, sealed-dungeon bootstrap, committed-callback retry,
reentrancy, fast-relogin, and ordinary-logout gaps. Three final reviews are
clean; the zero-warning Release build and 7,052 tests pass with five skips.
`GameWindow.cs` was 8,793 raw lines at checkpoint G, 1,508 below the Slice-5
baseline and 6,930 (44%) below the campaign baseline.
### H — closeout
**Complete in `4a205a3e`.** H proved the landed graph under deterministic and
connected lifecycle churn, corrected shutdown-stage ordering so session reset
converges before its streaming dependencies are disposed, and strengthened the
resource oracle to compare already-warm steady-state revisits.
- Run focused App/Core/Net tests after each checkpoint.
- After each code checkpoint, run three independent read-only reviews: retail/
WorldBuilder conformance, architecture/integration, and adversarial failure
analysis. Fix confirmed findings and repeat review until clean.
- Run `dotnet build AcDream.slnx -c Release` and the full Release test suite.
- Run deterministic first-login/portal lifecycle automation and the connected
nine-destination resource soak. Verify retirement/preparation queues drain,
residence matches the region, resource counts remain bounded, and graceful
close succeeds.
- Update the divergence register pointers in IA-14, AD-2, AD-22, and any other
row whose cited ownership moved. Keep AD-6, AD-24, AP-31, and TS-52 unless a
separate retail mechanism actually lands.
- Update architecture, milestones, roadmap, issues if needed, synchronized
`AGENTS.md`/`CLAUDE.md`, and durable decomposition/render memory.
The final IA-14/AD-2/AD-22 audit found no new retail divergence. IA-14 and
AD-22 still point at the correct WB/mesh owners; AD-2 now names the extracted
origin-recenter/presentation owners. AD-6, AD-24, AP-31, and TS-52 remain open
as required.
## 6. Acceptance matrix
Automated acceptance includes:
- Far heightmap-only and complete immutable Near/Promote payloads;
- deterministic static/scenery/interior IDs, bounds, offsets, cells, and shell
geometry IDs across repeated builds;
- single-reader DAT gate and no publish-time DAT access;
- exact owner-call sequence for load, promotion, Far synthesis, demotion, full
unload, replacement, and failure retry;
- stale/duplicate completions invoke no presentation participant;
- complete render pins and real readiness before reveal;
- static lights/translucency/scripts/plugins and mesh references remain
balanced through load → reapply → demote → promote → full unload;
- adjacent-landblock collision survives one neighbor's retirement;
- first login with unknown origin starts no build, and stale placeholder/old-
center completions cannot publish or satisfy reveal;
- reflection/source gates keep moved methods and scratch ownership out of
`GameWindow` and keep policy/state out of the pipeline.
Connected acceptance includes fresh login, outdoor travel, world-edge empty
blocks, dungeon travel, same-location revisit, turn sampling at every stop,
movement/jump/combat exercises, portal reveal, and graceful close. No view-
distance or quality reduction is an acceptable way to pass the resource gate.
## 7. Known risks and explicit traps
- A worker must not read a later live origin than the job it is building.
- A Near promotion may complete before its Far base; parking behavior and
required render IDs must survive unchanged.
- Duplicate Near/ForceReload must not stack lights, scripts, plugin events,
meshes, collision, or EnvCells.
- Demotion must never retire live entities or terrain.
- Publication must not be mistaken for uploaded render readiness.
- Failed retirement must survive controller/radius replacement.
- Do not move `WbDrawDispatcher` or any draw-graph body; that is Slice 7.
- Do not reintroduce worker-side `RegisterCell`, global pending renderer lists,
legacy rendering, or another DAT reader.
- Do not reduce Near/Far radii, texture detail, or mesh quality.

View file

@ -1,545 +0,0 @@
# GameWindow Slice 6 — update-frame orchestration
**Status:** Complete 2026-07-22.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 6.
**Baseline:** `d4ecac1d`; `GameWindow.cs` is 8,811 raw lines, 247 fields, and
153 methods. Release baseline: 7,054 passed / 5 skipped. The connected
lifecycle gate and synchronized nine-stop resource soak both pass.
**Behavior rule:** This is an ownership extraction. Preserve the accepted
retail object/network order, asynchronous streaming-readiness adaptation,
portal transit, input semantics, and camera presentation. A defect exposed by
the boundary receives its own deterministic oracle and bisectable fix; it is
never hidden behind a retry, delay, suppression flag, or reordered callback.
## Progress ledger
- [x] A — freeze the complete production phase graph and introduce the typed
orchestration contract plus structural/order guards.
- [x] B — extract the pre-network live-object presentation phase and the
non-advancing post-network spatial reconciler; remove callbacks into
`GameWindow` from `RetailLiveFrameCoordinator`.
- [x] C — extract streaming-origin convergence, observer selection, dungeon
collapse, streaming tick, and rescued-entity rebucketing.
- [x] D — extract dispatcher/raw-mouse/combat input sampling without changing
movement-command or UI-capture semantics.
- [x] E — extract the complete local-player teleport owner and wire it between
the existing post-network liveness and player-mode auto-entry phases.
- [x] F — extract the shared player-mode lifecycle plus fly/chase/player
camera presentation and cut production over to one
`UpdateFrameOrchestrator`.
- [x] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
frame state; run focused corrected-diff reviews after each ownership edge.
- [x] H — full Release suite, connected lifecycle/reconnect gate, synchronized
resource soak, documentation, durable memory, and line/field/method closeout.
Every checked checkpoint is committed as one bisectable architectural unit.
Documentation records the exact commit and accepted test count as work lands.
Checkpoint A introduced the internal `AcDream.App.Update` contract, one typed
`UpdateFrameClock`/`IPhysicsScriptTimeSource`, the twelve-phase outer-order
oracle, exact teardown and invalid-delta tests, and guards against a transitive
`GameWindow` clock capture. Production now publishes PES time once per update;
18 focused Release tests and the full 2,709-test App project pass, and all
three corrected-diff reviews are clean.
Checkpoint B moved the complete local/ordinary/static/effect body into
`LiveObjectFrameController`, made `RetailLiveFrameCoordinator`'s
object → inbound batch → command → spatial-reconcile barrier fully typed,
and removed animation/runtime/projectile back-references to `GameWindow`.
`GameWindow.cs` is now 8,716 raw lines. The corrected diff passes 239 focused
Release tests, the 2,732-test App suite, and the full 7,090-test Release suite
(5 fixture/environment skips); all three independent reviews are clean.
Checkpoint C moved retained origin convergence, readiness, observer selection,
sealed-dungeon collapse, streaming publication, and persistent live-projection
rescue into `StreamingFrameController`. Production keeps streaming publication
ahead of input and the live object/network barrier, so a same-frame
CreateObject can enter the newly resident bucket. The focused 146-test Release
selection, 2,754-test App suite, and complete 7,112-test Release suite (5
fixture/environment skips) pass. Login-before-position, portal hold, outdoor
and sealed/SeenOutside EnvCells, pending recenter, exact rescue rebucketing,
GUID deletion, and session reset are pinned; all three corrected-diff reviews
are clean. `GameWindow.cs` is now 8,569 raw lines.
Checkpoint D moved semantic hold delivery, movement sampling, autorun,
instant mouse-look, cursor capture, raw filtering/idle handling, and combat
intent into `GameplayInputFrameController` and focused typed owners. Devtools
keyboard capture pauses all gameplay movement without clearing the autorun
latch; retained chat suppresses held keys while autorun continues. Every
focus, teleport, camera/player-mode, reset, and shutdown boundary now releases
mouse-look through one lifecycle path and clears stale RMB orbit state.
Production combat requests, automatic hostile selection, UI-capture probes,
and movement-truth diagnostics no longer retain callbacks into `GameWindow`.
The 2,787-test App suite and complete 7,145-test Release suite pass (5
fixture/environment skips); all three corrected-diff reviews are clean.
`GameWindow.cs` is now 8,205 raw lines.
## 1. Outcome and non-goals
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
host delta to `UpdateFrameOrchestrator`. `GameWindow` composes concrete owners;
it no longer selects a streaming observer, advances animations/effects,
samples raw mouse input, sequences inbound network traffic, advances portal
transit, or updates gameplay cameras.
The owner graph is explicit rather than a list of anonymous callbacks:
```text
UpdateFrameOrchestrator
├── live teardown convergence retry retained tombstones
├── UpdateFrameClock normalized host time / PES clock
├── StreamingFrameController residence/readiness pre-pass
├── GameplayInputFrameController semantic input + mouse/combat intent
├── RetailLiveFrameCoordinator retail SmartBox object barrier
│ ├── LiveObjectFrameController local/ordinary/static/effect tails
│ ├── LiveSessionController inbound network drain
│ ├── RetailLocalPlayerFrame CommandInterpreter position tail
│ └── LiveSpatialReconciler non-advancing authoritative refresh
├── LiveEntityLivenessController expiry after accepted inbound state
├── LocalPlayerTeleportController local transit/reveal/tunnel state
├── PlayerModeAutoEntry one-shot ready-world entry
├── PlayerModeController enter/exit/current-player lifetime
└── CameraFrameController fly/chase/player presentation
```
Concrete names may be narrowed during checkpoint A's source audit, but the
ownership boundaries and order do not change. The orchestrator receives a
small immutable per-frame input and concrete phase owners. It does not receive
a hundred delegates, one mega `IUpdateFrameServices` context, or a service
locator, and no transitive update collaborator calls a substantial method back
on `GameWindow`.
This slice does not change simulation rate, object quantum rules, movement
speed, interpolation, streaming radii, reveal readiness, portal visuals,
particle range, combat cadence, key bindings, camera formulas, render order,
or shutdown order. It adds no GL work and does not move retained-UI timers out
of their render seam. Slice 7 owns the draw graph; Slice 8 owns final
composition and Silk callback cleanup.
## 2. Retail oracle and accepted adaptation
The object/network barrier is pinned by
[`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md):
- `CPhysicsObj::update_object @ 0x00515D10` applies the strict object quantum;
- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30` advances the PartArray,
position/physics candidate, then processes semantic hooks;
- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0` runs Detection → Target →
Movement → `CPartArray::HandleMovement` → Position → Particle → Script;
- `SmartBox::UseTime @ 0x00455410` advances object/physics time before the
inbound queue and then calls `CommandInterpreter::UseTime`;
- `gmSmartBoxUI::UseTime @ 0x004D6E30` owns UI/viewport time outside that
object barrier.
At the retail host level, `Client::UseTime @ 0x00411C40` calls
`UIElementManager::UseTime` before `SmartBox::UseTime`; the UI time broadcast
reaches `gmSmartBoxUI::UseTime`. Retail also publishes player-camera work from
the physics/player callback rather than from one post-network camera tail.
acdream currently advances teleport/UI-camera presentation after its
object/network/command barrier. Slice 6 preserves that existing order as
registered TS-53; it does not cite `gmSmartBoxUI` as proof that the current
post-network placement is retail-exact.
acdream preserves two documented presentation adaptations:
1. only semantic `AnimationDone` executes at retail's in-object hook slot;
every other captured animation hook retains the current deferred path after
final root, part, and equipped-child poses are published, including
particles, audio, lights, translucency, `CallPES`, and default-script
chaining (TS-50);
2. shared particle simulation and PhysicsScript queues tick once per host
update after both worksets rather than once per object's admitted quantum.
This also means the current shared Particle → Script order does not reproduce
the static workset's retail Script → Particle → hooks order (TS-51).
Asynchronous landblock streaming has no one-call retail equivalent. Its
convergence pre-pass remains before inbound CreateObject dispatch so a newly
accepted projection can find a resident bucket in the same frame. This
pre-pass does not advance an object's retail clock. `WorldRevealCoordinator`
remains the single login/portal readiness owner.
## 3. Fixed production phase order
The complete host-update graph is frozen as follows:
1. retry canonical live-entity teardown tombstones; an `AggregateException` is
logged and later phases continue, while any other exception propagates;
2. normalize the host delta, advance the shared PES clock once, and publish it;
3. advance pending origin retirement/recenter, select the observer, tick
streaming, and rebucket rescued persistent entities;
4. tick `InputDispatcher`, consume one raw mouse sample, and advance combat
attack intent;
5. enter the non-reentrant inbound-event envelope and advance the pre-network
live-object phase:
- local object and pre-network movement/jump output;
- queued selection/use output after movement edges;
- ordinary live objects;
- the distinct static-animation workset;
- equipped children and static hook completion;
- fades, every deferred non-`AnimationDone` hook, and current effect/light
anchors;
- particle visibility and particle simulation;
- PhysicsScript queues;
6. drain inbound session traffic inside one `GpuWorldState.MutationBatch`;
7. run the local player's post-network `CommandInterpreter` position check;
8. perform the ordinary post-network non-advancing root/child/emitter/light
reconcile;
9. advance liveness expiry from the absolute client timer;
10. activate/tick local teleport transit. A Place edge commits the player and
live root, immediately performs its own non-advancing reconcile, then
notifies world reveal and continues tunnel state;
11. evaluate one-shot player-mode auto-entry;
12. update fly or chase/player camera presentation. If the local player was
first created by this frame's inbound pass, publish its non-advancing
projection, immediately reconcile a second time, then update the camera.
Those three reconcile edges—ordinary post-network, teleport Place, and
inbound-created-player projection—are separate accepted production edges. Any
future consolidation is a behavior change with its own oracle, not part of
this extraction.
An invalid/non-positive host delta becomes zero for the PES/object/particle/
tunnel delta but never blocks inbound traffic, absolute-time liveness,
recenter convergence, or camera-state publication. Raw mouse idle uses its
independent monotonic input clock. A missing gameplay camera/input owner never
suppresses outer phases; the streaming controller may skip observer-driven
streaming when it has no valid live or offline observer, while origin
retirement still advances. Devtools keyboard capture pauses player/camera
control including autorun; retained chat capture suppresses held WASD while
autorun continues; mouse capture ends mouse-look but does not suppress
keyboard, combat, network, streaming, teleport, or teardown.
## 4. Architecture and interfaces
### 4.1 Per-frame input
Use an immutable value equivalent to:
```csharp
public readonly record struct UpdateFrameInput(double HostDeltaSeconds);
```
Derived facts are read by their owning controller from stable runtime owners
or from a narrowly typed state source. Do not pass `GameWindow`, expose its
private fields wholesale, or create a service locator. Mutable player/session
owners that legitimately change during login are represented by one focused
state/source interface per domain, not anonymous closures into window methods.
`UpdateFrameClock` is constructed before any materializer/network owner that
needs the current PES time. It owns the normalized host/PES clock and exposes a
narrow `CurrentScriptTime` source; `_physicsScriptGameTime` does not remain a
`GameWindow` field captured by later owners. Absolute liveness time and raw
mouse idle time remain distinct monotonic sources and are not derived from the
normalized simulation delta.
### 4.2 Object phase
`LiveObjectFrameController` owns the body currently in
`AdvanceLiveObjectRuntimeCore`. It composes existing focused owners and keeps
their retail order; it does not absorb their algorithms or identity maps.
`LiveSpatialPresentationReconciler` owns the current no-time refresh body.
`RetailLiveFrameCoordinator` depends on those typed owners plus the existing
session and local-player owners. Its public `Tick` remains the one explicit
object → network → command → reconcile barrier.
`RetailInboundEventDispatcher` continues wrapping the complete object phase.
Reentrant packets queue until the current object's manager tail completes.
The hot path remains allocation-free after warmup.
The extraction inventory includes the complete transitive update graph, not
only `RetailLiveFrameCoordinator`: `RetailLocalPlayerFrameController` loses
substantial window captures in favor of a focused local-player slot, movement
input source, outbound owner, and projection owner;
`LiveEntityAnimationPresenter` receives a concrete animation-presentation
context instead of retaining `GameWindow` as
`ILiveAnimationPresentationContext`; scheduler clock/player/projectile sources
are similarly narrow. Existing small value/policy callbacks are acceptable
only when they do not call a substantial body back on the window or create a
second identity/state owner.
### 4.3 Streaming phase
`StreamingFrameController` owns observer selection and the sealed-dungeon
gate. It receives the existing `StreamingOriginRecenterCoordinator`,
`StreamingController`, `LiveWorldOriginState`/center state, camera/player
state, physics current cell, `GpuWorldState`, and `LiveEntityRuntime` through
typed seams. It never builds, publishes, or retires a landblock; those remain
Slice 5 owners.
The controller preserves:
- no pre-login hardcoded-Holtburg streaming;
- real-player-origin readiness before first streaming;
- destination-origin pinning while in portal space;
- authoritative player landblock during the login hold;
- offline fly-camera observation only outside live in-world mode;
- sealed EnvCell collapse using the cell's own landblock identity;
- teleport-hold suppression of the stale source-cell dungeon gate;
- rescued-entity rebucketing after the accepted streaming tick.
### 4.4 Input and camera phases
`GameplayInputFrameController` owns per-frame dispatcher hold delivery,
UI-capture transitions, raw mouse sample/filter/idle handling, combat attack
ticking, and the complete mouse-look/cursor-capture lifetime. It is the single
owner of mouse-look state, saved cursor mode, begin/end/hide/restore, and
movement teardown across press/release, capture change, focus loss, teleport,
session reset, player-mode exit, and shutdown. Action press/release callbacks
remain on `InputDispatcher` and movement stays owned by
`PlayerMovementController`.
`PlayerModeController` owns enter/exit/toggle construction and the current
player-controller, physics-host, chase-camera, and chase-mode slot. Manual
input, `PlayerModeAutoEntry`, and local teleport activation all enter through
that one owner. Streaming, camera, local-frame, and teleport owners receive
narrow read sources from it; no path calls the substantial current
`EnterPlayerModeNow`/exit bodies back on `GameWindow` or duplicates player-mode
state.
`CameraFrameController` owns held fly-camera controls, retail chase offsets,
post-network player presentation sampling, and both chase-camera updates. It
does not own physics, target selection, or camera formulas. The local player
created by the inbound pass gets a non-advancing projection before its first
draw exactly as today, followed by the current second spatial reconcile before
the camera samples that projection.
### 4.5 Teleport phase
`LocalPlayerTeleportController` owns only the current local-player transit
state machine and presentation edges. It composes
`TeleportTransitCoordinator`,
`TeleportViewPlaneController`, `PortalTunnelPresentation`,
`WorldRevealCoordinator`, streaming priority, and the session LoginComplete
send. Checkpoint E first extracts the currently window-owned local aim,
destination, hold/forced, activation, placement, reset, and session-clear
state into this owner plus a concrete local placement collaborator where
needed. It does not reuse `RemoteTeleportPlacement`, and network/session sinks
do not retain `AimTeleportDestination` or reset callbacks into `GameWindow`.
`LiveEntityLivenessController`, tombstone retry, and `PlayerModeAutoEntry`
remain separate focused phases; teleport does not absorb them.
### 4.6 Composition, thread, and lifetime
The clock and focused dynamic sources are constructed before their first
consumer. The final orchestrator is composed exactly once at the end of a
successful `OnLoad`; `OnUpdate` fails fast if that invariant is broken rather
than running a nullable partial graph. Every phase mutates state only on the
existing update/render thread. Streaming worker completion remains behind
`StreamingController.Tick`; the extraction adds no lock, task, or async entry.
Phase controllers borrow their input, session, streaming, portal, camera, and
render-presentation owners; they do not dispose shared dependencies. Any new
event subscription has explicit symmetric detach. During shutdown the update
collaborators remain alive through the first-stage `LiveSessionController`
reset, then retire before their underlying input/camera/portal/streaming
resources. Existing retryable shutdown ordering remains authoritative.
## 5. Checkpoints and acceptance
### A — contract and guards
- record the exact production phase graph and every mutable dependency;
- correct and pin the retail oracle for physics-hook retention/reentrant
append, inactive object behavior, and static Script → Particle → hooks; pin
the existing TS-50/TS-51 divergences without reordering runtime behavior in
this ownership-only slice;
- add an explicit orchestrator order test covering all twelve phases and the
three conditional reconcile edges;
- pin normalized simulation/PES/particle/tunnel time separately from absolute
liveness and raw-mouse idle clocks for NaN, both infinities, non-positive
values, and finite values above `float.MaxValue`;
- pin missing live/offline camera behavior and the separate devtools keyboard,
retained-chat keyboard, and mouse-capture contracts;
- pin teardown retry success, `AggregateException` log-and-continue, other
exception propagation, and next-frame retry;
- add source guards that prevent `OnUpdate` or the coordinator from regaining
substantial callback bodies, and prevent any transitive update owner from
retaining `GameWindow` or one mega service context;
- verify current production traces before cutover.
### B — object/network barrier
- move the pre-network animation/effect body into
`LiveObjectFrameController`;
- move no-time root/child/effect refresh into
`LiveSpatialPresentationReconciler`;
- replace coordinator callbacks with typed collaborators;
- preserve semantic/visual hook separation, owner-incarnation revalidation,
static-after-ordinary order, ordinary Particle-before-Script, the current
shared-tail TS-51 adaptation, and once-per-host tails;
- delete `AdvanceLiveObjectRuntimeCore` and
`ReconcileLiveObjectSpatialPresentation` from `GameWindow`.
Focused tests cover `RetailObjectActivityGate` and
`RetailObjectQuantumClock`: parented/cell-less/Frozen suspension, inactive
Particle/Script-only time, Hidden+Active adjustment/hooks/manager time, and
separate ordinary epsilon/Min/Max/Huge plus static epsilon/Huge boundaries.
They also cover incomplete physics-hook
retention, same-drain animation-hook append, deferred `CallPES`/default-script/
translucency routing, physics-hook append deferred until the next drain,
completed physics-hook removal preserving the snapshotted successor, static
hooks, callback GUID reuse, and reentrant inbound packets. Particle then Script
must run exactly once for an empty world,
multiple ordinary/static owners, invalid delta, and reentrant inbound work;
the one published PES clock remains visible from hook capture through the
script tick.
Checkpoint B removes the coordinator, animation presenter/scheduler, runtime
view, static-root, and projectile callbacks into `GameWindow`. The callback
composition still retained by `RetailLocalPlayerFrameController` is explicitly
deferred: checkpoint D owns its input/capture sources and checkpoint F owns its
mutable player-mode/controller/session sources. A structural test permits that
one named legacy owner only; D/F must remove the exception before final cutover.
### C — streaming frame
- extract readiness/recenter advancement, observer selection, dungeon gate,
streaming tick, and rescued projection rebucketing;
- test login-before-player-spawn, outdoor/dungeon/portal-hold/offline observer
cases, missing live/offline camera sources, and cell-landblock identity;
- prove same-frame streaming completion precedes CreateObject projection into
the resident bucket, unknown pre-login origin enqueues nothing, and recenter
retirement continues while ordinary streaming is gated;
- verify no build/publication/retirement ownership leaks back from Slice 5.
### D — input frame
- extract dispatcher tick, UI mouse-capture edge, raw sample/filter/idle path,
combat intent tick, and the one cross-lifetime mouse-look/cursor owner;
- test capture entry while mouse-look is active, zero-idle drift stop,
production action ordering, devtools-vs-chat autorun semantics, mouse-only
capture, and focus/teleport teardown;
- prove every non-input phase still runs under each capture mode;
- retain diagnostic semantic input as the unattended connected gate seam.
### E — local teleport and post-network world frame
- extract the currently window-owned local-player teleport state and every
aim/destination/activation/place/reset/session port before moving its tick;
- retain liveness and auto-entry as separate orchestrator calls around pending
transit activation, readiness/hold, tunnel events, materialization, and
reveal completion;
- test loaded and pending destinations, forced-readiness boundary, same-cell
respawn, LoginComplete ordering, delete/reset, and no second reveal owner;
- pin Place → immediate reconcile → reveal and old-GUID liveness candidate →
inbound delete/recreate generation safety;
- preserve the completed portal visuals and world-reveal barrier unchanged.
**Completed 2026-07-22.** `LocalPlayerTeleportController` now owns the full
local transit lifetime and its immutable network handoff, while
`PlayerModeController` is the sole writer of controller, physics-host,
player-mode, and chase-camera slots. Local animation, shadow synchronization,
sealed-dungeon classification, viewport aspect, and approach-completion
handoff are focused collaborators rather than window callbacks. Portal entry
retires the one-shot auto-entry guard, host publication is transactional, and
teleport work revalidates the live generation after every reentrant edge.
Focused App/Core tests cover destination readiness, forced holds, same-cell
placement, LoginComplete order, reset/disposal, GUID reuse, reentrancy,
host rollback, auto-entry idempotence, and completion-mailbox ordering. The
full App suite is green at 2,814 passed / 3 skipped. `GameWindow.cs` is 7,351
lines at this checkpoint, down from the 15,723-line campaign baseline.
### F/G — camera, cutover, and deletion
- extract player-mode enter/exit/toggle and current-owner state before camera
or teleport cutover; retain `PlayerModeAutoEntry` only as its one-shot
readiness guard;
- extract fly camera, retail chase adjustment, local post-network projection,
combat target tracking, and chase-camera publication;
- pin inbound-created projection → immediate second reconcile → camera, with
exact root/child/emitter/light refresh counts and no duplicate time advance;
- compose `UpdateFrameOrchestrator` only after all dependencies exist;
- replace `OnUpdate` with profiler scope plus one `Tick` call;
- remove obsolete frame fields/facades and prove there is no back-reference to
`GameWindow` from an extracted owner;
- measure line/field/method count. The target is below 8,000 lines, but the
ownership/ordering exit criteria outrank the count.
**Checkpoint F completed 2026-07-22.** `CameraFrameController` now owns fly
held input, retail chase adjustment, the inbound-created-player projection
and second reconcile, both chase-camera publications, and combat-target
tracking. `RetailLocalPlayerFrameController` and
`LocalPlayerProjectionController` resolve current identity, session, world,
shadow, and player slots through focused typed runtimes instead of callbacks
into `GameWindow`; the camera receives only the two-read player-presentation
seam. Projection and frame owners remain composition locals, so the window
retains no duplicate lifecycle state. Focused Release tests are green at
37/37, the App suite at 2,820 passed / 3 skipped, and the full suite at 7,179
passed / 5 skipped. All three corrected-diff reviews are clean.
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
campaign baseline.
**Checkpoint G completed 2026-07-22.** `GameWindow.OnUpdate` now owns only the
update profiler scope and one `UpdateFrameOrchestrator.Tick` call. Teardown,
absolute liveness time, local timestamp publication, expiry deletion,
player-mode auto-entry, live-origin initialization, Hidden PartArray
boundaries, remote teleport placement, and all twelve live-entity session
packet routes resolve through typed runtime owners; the former transitive
window callbacks and obsolete frame fields are gone. The App suite is green
at 2,823 passed / 3 skipped. Focused orchestration, lifecycle, presentation,
teleport, origin, and session tests are green, and all three corrected-diff
reviews are clean. `GameWindow.cs` is 7,026 raw lines / 241 fields / 108
methods, down 1,785 lines and 45 methods from the Slice-6 baseline and 8,697
lines (55.3%) from the 15,723-line pre-extraction class.
### H — release and connected gates
After all three independent corrected-diff reviews are clean:
1. run every focused App/Core/Net/UI test project;
2. run `dotnet build AcDream.slnx -c Release`;
3. run the full Release suite;
4. run the connected lifecycle/reconnect gate;
5. run the synchronized nine-stop resource soak with movement, turn, jump,
combat, portal materialization waits, fatal-log scan, and graceful close;
6. compare lifecycle, outbound movement, materialization, entity/animation,
frame-profile, resource, and teardown traces with the Slice 5 baseline;
7. audit the divergence register, architecture, milestones, roadmap, issues,
`AGENTS.md`, `CLAUDE.md`, and durable memory.
The slice requires no new visual behavior. If connected screenshots or traces
change, return to the responsible checkpoint; do not defer the difference to
Slice 7.
**Completed 2026-07-22.** The complete Release suite is green at 7,182 passed /
5 fixture or environment skips. The deterministic lifecycle/reconnect gate
passed in 314.195 seconds across capped login/travel checkpoints, graceful close,
and a fresh-process reconnect. Its only warning was the expected set of 25
world-edge empty landblocks. The synchronized nine-stop resource soak passed in
393.581 seconds with movement, turn, jump, combat, teleport waits, fatal-log scan,
and graceful exit. The final Caul plateau held the same 21,025 entity and 13
animation-owner counts as the earlier Caul return, with update-frame p95 at
0.8 ms; working/private memory increased by 112.2/111.0 MiB across that retained
route, inside the existing deterministic gate. The soak's only diagnostics were
35 known DAT-driven missing VFX table/emitter records and the same 25 expected
world-edge misses. The divergence audit keeps TS-50, TS-51, and TS-53 unchanged
and found no new behavior adaptation. Final `GameWindow` size is 7,026 raw
lines / 241 fields / 108 methods, versus 8,811 / 247 / 153 at Slice 6 entry and
15,723 / 278 / 205 at the campaign baseline.
## 6. Review gate for every checkpoint
After each implementation checkpoint:
1. run focused tests and inspect the complete diff;
2. retail-conformance review checks named functions, exact order, Hidden/
quantum semantics, and adaptation boundaries;
3. architecture review checks dependency direction, absence of a replacement
god object/service locator, same-thread mutation, and symmetric teardown;
4. adversarial review checks independent clocks, callback reentrancy,
owner/GUID reuse, login/portal transitions, capture modes, missing observer
sources, teardown failures, and early-return phase suppression;
5. fix every confirmed root cause and repeat review until clean;
6. build/test Release and commit the checkpoint before starting the next one.
## 7. Divergence and documentation bookkeeping
TS-50, TS-51, and the newly documented existing host-order TS-53 remain until
their exact adaptations are ported; this slice must not overclaim retail
parity. IA-14/AD-22 and the lifetime/resource rows are audited against the
final owner graph but changed only if their mechanism actually changes. Any
newly discovered unavoidable adaptation gets a register row in the same
commit. The Slice 6 closeout updates the code-structure table,
roadmap, milestones, session docs, and
`project_gamewindow_decomposition.md` with exact commits, test counts, gates,
and final metrics.

View file

@ -1,602 +0,0 @@
# GameWindow Slice 7 — render-frame orchestration
**Status:** Complete 2026-07-22.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 7.
**Baseline:** `9512404e`; `GameWindow.cs` is 7,026 raw lines, 241 fields, and
108 methods. The Release suite passes 7,182 tests / 5 fixture or environment
skips. The 314-second lifecycle/reconnect gate and 394-second synchronized
nine-stop resource soak pass.
**Behavior rule:** This is an ownership extraction. Preserve every accepted
world/PView/alpha/private-viewport/UI draw edge, render-thread publication
edge, GL-state boundary, and GPU-flight failure contract. Do not combine the
extraction with a render-quality change, visibility correction, frame-order
port, or resource-lifetime redesign.
## Progress ledger
- [x] A — freeze the complete render graph, correct the architecture SSOT, and
introduce data-only frame contracts plus deterministic order/failure tests.
- [x] B — extract paperdoll, panel-layout/devtools, and render-diagnostics leaf
owners while `GameWindow` still orders the frame.
- [x] C — extract frame-resource begin/upload/publication and live display,
and weather/display foundations that do not depend on camera/root
classification.
- [x] D — extract frame-data classification and reusable world scratch without
duplicating live identity, spatial, visibility, or resource ownership; then
run the dependent audio, sky, lighting, and listener preparation.
- [x] E — replace `RetailPViewDrawContext`'s callback bag with a typed
`RetailPViewPassExecutor` and data-only inputs.
- [x] F — extract `WorldSceneRenderer`, including both shared-alpha scopes,
PView/fallback world branches, particles, debug world draw, and completion.
- [x] G — compose `RenderFrameOrchestrator`, cut `GameWindow.OnRender` to one
handoff, and delete obsolete frame bodies, fields, helpers, and callbacks.
- [x] H — corrected-diff reviews, full Release suite, connected lifecycle and
soak gates, framebuffer comparison, documentation, memory, and metrics.
Every checked checkpoint is one bisectable architectural commit. A checkpoint
does not complete while a new owner merely delegates a substantial body back to
`GameWindow`.
## 1. Outcome and non-goals
At slice exit, `GameWindow.OnRender` supplies one immutable delta/viewport input
to `RenderFrameOrchestrator`. The orchestrator owns the GPU-flight transaction
and composes focused frame-resource, world-scene, private-presentation, and
diagnostic phases. `GameWindow` remains the construction and shutdown shell; it
does not keep the draw graph, PView pass callbacks, frame scratch, paperdoll
assembly, or performance-title body.
The target ownership graph is:
```text
RenderFrameOrchestrator
├── RenderFrameResourceController GPU slot + resource BeginFrame/upload
├── WorldRenderFrameBuilder camera/root/light/classification facts
├── WorldSceneRenderer accepted world draw graph
│ ├── RetailPViewRenderer visibility/order owner
│ ├── RetailPViewPassExecutor concrete GL pass implementation
│ └── WorldRenderDiagnostics probes/debug counters/signatures
├── PrivatePresentationRenderer portal + paperdoll + retained UI + ImGui
│ ├── PaperdollFramePresenter
│ └── DevToolsFramePresenter
└── RenderFrameDiagnosticsController title/resource/frame snapshots
```
Names may narrow as code lands, but the ownership boundaries do not collapse
into one replacement god object. No new owner may receive `GameWindow`, a broad
service locator, or an anonymous list of callbacks into the window.
This slice does **not** change:
- the viewer-cell render root or player-cell lighting root;
- synthetic outdoor-cell behavior or the null-root safety path;
- portal flood, building pre-gates, clip-plane/scissor limits, punch/seal
depth behavior, shell lift, viewcone culling, or draw distances;
- shared-alpha sort keys, scope boundaries, blend/cull behavior, or immediate
EnvCell/private alpha;
- weather, fog, point-light selection, particle visibility/range, terrain
quality, FOV, frame pacing, or audio-listener formulas;
- portal-space, paperdoll, retained gameplay UI, ImGui, or screenshot visuals;
- resource ownership, shutdown/disposal order, or three-frame GPU retirement;
- TS-53's retained-UI time placement on the render seam.
Issue #225's translucent lifestone/particle comparison remains a separate user
visual gate. Passing this structural slice must not mark it resolved.
## 2. Retail oracle and accepted adaptations
This extraction reuses the existing named-retail research rather than
reinterpreting the draw algorithms:
- `SceneTool::BeginScene @ 0x0043DAD0` establishes the black scene-clear
baseline used by portal CreatureMode replacement;
- `SmartBox::DrawNoBlit @ 0x00454C20` updates the viewer and invokes the normal
world render before post-world selection work;
- `SmartBox::RenderNormalMode @ 0x00453AA0` owns landscape/inside dispatch and
the final retail alpha flush;
- `RenderDeviceD3D::DrawInside @ 0x0059F0D0`,
`PView::DrawInside @ 0x005A5860`, and
`PView::ConstructView @ 0x005A57B0` own the portal traversal;
- `PView::DrawCells @ 0x005A4840` fixes the outside-landscape → alpha flush →
conditional depth clear → exit masks → shells → object-list order;
- `LScape::draw @ 0x00506330`, `GameSky::Draw @ 0x00506FF0`,
`DrawBuilding @ 0x0059F2A0`, `DrawBlock @ 0x005A17C0`, and
`DrawObjCellForDummies @ 0x005A0760` constrain the landscape/building/cell
phases;
- `AddMeshToAlphaList @ 0x0059C230` and
`FlushAlphaList @ 0x0059D2E0` constrain alpha submission/flush boundaries;
- `CPhysicsPart::UpdateViewerDistance @ 0x0050E030` and
`CShadowPart::insertion_sort @ 0x006B5130` constrain transformed DAT
SortCenter/CYpt and stable far-to-near ordering;
- `CreatureMode::Render @ 0x004529D0` constrains portal and paperdoll private
viewports;
- `SceneTool::EndFrame @ 0x0043FB30` proves retail gameplay UI precedes retail
profiler/debug overlays; placing ImGui above gameplay UI is acdream's IA-12
modern coexistence adaptation, not literal Keystone parity.
The readable pseudocode oracle is
[`docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`](../research/2026-06-05-retail-pview-indoor-render-pseudocode.md),
with the current full reference in
[`docs/research/2026-06-02-retail-render-pipeline-full-reference.md`](../research/2026-06-02-retail-render-pipeline-full-reference.md).
Shared-alpha ordering is pinned separately by
[`docs/research/2026-07-18-retail-shared-alpha-list-pseudocode.md`](../research/2026-07-18-retail-shared-alpha-list-pseudocode.md).
This slice preserves, rather than retires, the registered modern adaptations:
IA-8/9/10/12/14/15; AD-16/17/19/24;
AP-21/32/33/34/35/85/92/115/116/117;
TS-6/7; and TS-53. Source pointers move with ownership in the same commit, but
mechanism/risk language changes only if behavior actually changes.
## 3. Frozen production render order
The complete accepted order is:
1. call `GpuFrameFlightController.BeginFrame` outside the render `try`;
2. begin the composite-texture frame and cache tick;
3. begin, in order, dispatcher, EnvCell, portal depth, world text, retained-UI
text, clip, terrain, lighting UBO, then profiler frame state for the current
GPU slot;
4. select the portal replacement viewport; reset particle visibility when it
is active;
5. choose portal black or normal fog clear color, force depth writes, clear
color/depth/stencil, and establish CW/back-face frame-global state;
6. run GL-state/surface diagnostics;
7. advance render-thread publication in order: `WbMeshAdapter.Tick`, destination
reveal preparation/evaluation, then `ParticleRenderer.BeginFrame`;
8. begin ImGui, tick weather, and begin retail selection-scene accumulation;
9. when a normal camera/world viewport is active:
- begin the one frame-scoped `RetailAlphaQueue` collection; its landscape
`Flush` drains the first sub-scope and `EndFrame` later drains the final
world sub-scope without a second begin;
- resolve camera/projection/frustum and begin particle/terrain visibility;
- publish world-reveal visibility/completion;
- preview audio/display settings and publish the listener;
- resolve player lighting cell and collided viewer render cell;
- build prior-visible-cell point-light candidacy, sun/fog/lighting UBO, and
animated/equipped classification;
- construct the synthetic outdoor root/building candidates when required;
- run the null-root safety world or the one `RetailPViewRenderer.DrawInside`
graph;
- draw the post-world Scene particles/weather fallback and debug geometry;
- sample current-frame DebugVM visible-landblock and nearest-collision facts
before the developer UI can consume them;
- close the final shared-alpha scope and publish terrain/cell particle
visibility;
10. complete retail selection-scene accumulation;
11. draw the portal CreatureMode replacement viewport;
12. render the private paperdoll FBO before its 2-D widget blit;
13. tick/update-cursor/draw retained gameplay UI;
14. render ImGui menus and developer panels above gameplay UI;
15. capture a requested default-framebuffer screenshot;
16. publish window-title, resource-dump, and frame-profile diagnostics;
17. close the exact GPU frame once in `finally`.
Within `RetailPViewRenderer.DrawInside`:
1. construct the main PView; merge per-building exterior floods only for the
synthetic outdoor root and build separate look-in frames for interior roots;
2. assemble/upload the one clip arena, prepare EnvCell batches, partition
statics/dynamics, and classify outside-stage dynamics;
3. draw each outside slice early: sky, terrain, and outdoor statics;
4. draw building look-ins: all far-Z punches, opaque shells, then transparent
shells plus statics/dynamics/emitters;
5. draw outside-stage dynamic meshes, Scene particles, and weather late;
6. draw one interior-root unattached Scene-particle pass;
7. flush the **landscape** shared-alpha scope;
8. for an interior root, perform the full depth clear, then write true-depth
exit seals; for an outdoor root, retain world depth and use far-Z entry
punches;
9. draw opaque EnvCell shells once, then transparent shell cells immediate
far-to-near;
10. draw cell statics as one cross-cell batch, then their particles;
11. draw the surviving main-flood dynamics last, then their particles; look-in
and outside-stage dynamics remain in their earlier landscape phases.
The landscape alpha flush must never move across the depth clear. The final
world alpha close must never move after portal/paperdoll private viewports.
## 4. Architecture and interfaces
### 4.1 Frame transaction
Use immutable values equivalent to:
```csharp
internal readonly record struct RenderFrameInput(
double DeltaSeconds,
int ViewportWidth,
int ViewportHeight);
internal readonly record struct WorldRenderFrameOutcome(
int VisibleLandblocks,
int TotalLandblocks,
bool NormalWorldDrawn);
```
Portal-viewport selection and drawing belong to `PrivatePresentationRenderer`,
which runs after the world phase. If a combined public outcome becomes useful,
the top-level orchestrator composes it only after private presentation; the
world owner never claims that a later viewport was drawn.
`RenderFrameOrchestrator` receives a small fixed set of typed phase owners. It
does not receive every renderer separately, a mutable frame-services bag, or
`GameWindow`.
`BeginFrame` remains outside the `try`: if it fails, no `EndFrame` is attempted.
After begin succeeds, every failure closes exactly once. Render failure plus
close failure becomes the current explicit `AggregateException`; close-only
failure propagates directly. Alpha, particle-visibility, and selection-scene
completion retain their current non-finally behavior; changing that recovery
contract requires separate approval and evidence.
### 4.2 Borrowed resources and shutdown
The frame orchestrator and phase owners borrow the already-created GL/render
owners and do not dispose them. The developer-tools backend is itself the
canonical owner of its ImGui bootstrap and disposes that bootstrap when the
existing retryable shutdown transaction retires the backend; the frame
presenter only borrows it. Shutdown keeps its proven dependency order: wait for
submitted work; withdraw UI/render publications; release textures and mesh
owners; then dispose GL backing stores, with `GpuFrameFlightController` last
among render resources. Slice 8 may group construction/disposal after the
render cutover is stable.
`DisplayFramePacingController` is the focused shared owner for requested VSync,
live display preview, monitor/window callbacks, and the separate
`OnFrameRendered` pacing event. Render preparation and window lifecycle wiring
invoke that typed owner directly; neither calls back into a substantial
`GameWindow` method. Its existing disposal remains in the Slice 7 shutdown
transaction.
### 4.3 World frame data
`WorldRenderFrameBuilder` owns reusable classification scratch:
- animated/equipped entity IDs;
- synthetic outdoor node and candidate building cells;
- previous-frame drawable cells used by point-light candidacy;
- camera/root/classification facts with one-frame borrowed lifetime.
The builder owns only facts available before PView execution. Slice-local
visible/outdoor/unattached particle-owner IDs belong to
`RetailPViewPassExecutor`. The current interior partition is produced by
`RetailPViewRenderer.DrawInside` and remains in its borrowed result/diagnostic
snapshot; it is never stored as stale pre-draw builder state.
It queries canonical `LiveEntityRuntime`, `GpuWorldState`, player-mode/camera,
physics cell graph, cell visibility, and landblock presentation sources. It
does not copy their dictionaries or create a second visibility computation.
Returned collections are borrowed until the next build; tests pin this
lifetime and consumers cannot retain them.
### 4.4 PView pass execution
Replace the current `RetailPViewDrawContext` callback bag with a data-only
`RetailPViewFrameInput`, a typed cell-visibility source, and one explicit
`IRetailPViewPassExecutor`/`RetailPViewPassExecutor`. The executor owns the GL
resources needed for these named operations:
```text
SetTerrainClip
DrawLandscapeSlice
DrawLandscapeSliceLate
ClearInteriorDepth
DrawExitPortalMask
DrawLookInPortalPunch
DrawUnattachedSceneParticles
FlushLandscapeAlpha
DrawCellParticles
DrawDynamicsParticles
EmitDiagnostics
```
`RetailPViewRenderer` remains the retail ordering owner and calls these typed
operations. The executor may not call `GameWindow` or make independent
visibility/order decisions. Existing borrowed `RetailPViewFrameResult` scratch
semantics remain explicit.
### 4.5 Presentation and diagnostics
`PaperdollFramePresenter` owns dirty state, DAT-backed clone/re-dress, pose
selection/application, and FBO rendering. Inventory/session paths call
`MarkDirty`; they do not mutate its internals.
`PrivatePresentationRenderer` owns portal viewport, paperdoll, retained UI,
ImGui, and screenshot order. A focused panel-layout/devtools owner handles menu
actions and is also reused by framebuffer resize; it cannot call a substantial
window method.
`WorldRenderDiagnostics` owns pass-local PView/GL/scissor state, render
signatures, and world-pass timings. `RenderFrameDiagnosticsController` owns
FPS/frame-time accumulation, final visible-landblock counters, performance
title/resource publication, and the public render-frame snapshot. Only
render-derived DebugVM providers move in Slice 7. Update/session/streaming
providers and the cross-domain world-lifecycle resource sampler stay at the
composition boundary until Slice 8 rather than turning either diagnostics
owner into a replacement service locator. Diagnostics observe the frame; they
never choose a draw branch or mutate canonical world ownership.
## 5. Implementation checkpoints
### A — contracts, architecture, and oracles
- correct the stale render-pipeline and per-frame architecture sections;
- add `RenderFrameInput`, phase interfaces, and a recording orchestrator;
- pin the outer resource/world/private/diagnostic order across normal,
portal-replacement, login-wait, and screenshot outcome shapes; defer the
concrete indoor/outdoor/null-root branch tests to the world builder and scene
owner in D/F; camera is an initialized runtime invariant, so the slice does
not invent a new graceful missing-camera path;
- pin the exact GPU begin/end exception matrix;
- add source guards against `GameWindow` back-references, mega contexts, and a
new PView delegate bag;
- capture the current connected framebuffer/resource baseline.
Completed 2026-07-22. `RenderFrameOrchestrator` now owns the inert five-owner
outer transaction contract; `GpuFrameFlightController` implements its narrow
lifetime seam. Twenty-six focused render/GPU tests pin value-only contracts,
required-owner construction, exact phase prefixes, per-phase input propagation,
and every begin/render/close failure combination. Release build and the full
suite pass (7,199 passed / 5 skipped after adding 17 tests). All three
corrected-diff reviews are clean; production `GameWindow.OnRender` is unchanged.
### B — leaf presentation and diagnostics
- extract paperdoll refresh/pose/dirty/FBO ownership;
- extract panel-layout reuse and devtools menu/panel rendering;
- extract pass-local render signature, PView/GL/scissor probes, and timing state
into `WorldRenderDiagnostics`; extract FPS, final counts, title/resource
publication, and the public render snapshot into
`RenderFrameDiagnosticsController`;
- preserve the pre-presentation render-derived DebugVM sampling site separately
from the post-screenshot title/resource publication site; leave non-render
DebugVM providers and cross-domain world-lifecycle sampling at composition;
- keep call sites in their current `GameWindow.OnRender` positions.
Completed 2026-07-22. Paperdoll clone/re-dress/pose/FBO work now belongs to
`PaperdollFramePresenter`; retained surfaces are narrow and optional, while
live identity and DAT pose application resolve through focused adapters.
`DevToolsFramePresenter` owns the ImGui frame, menus, panels, layout, and input
actions while borrowing the bootstrap; `ImGuiDevToolsFrameBackend` explicitly
owns and disposes that bootstrap at shutdown.
`WorldRenderDiagnostics`, `DebugVmRenderFactsPublisher`, and
`RenderFrameDiagnosticsController` own the former render probes, debug facts,
title/resource cadence, and public snapshot. The production handoff now reports
the observed login/world and screenshot outcomes, preserves the original shared
TERRAIN→FRAME diagnostic transaction, and keeps the devtools-off scan path
allocation-free. `GameWindow.cs` is 6,270 raw lines, 60.1% below the 15,723-line
campaign baseline. Sixty-two focused tests, the Release build, and the complete
suite pass (7,255 passed / 5 skipped). Retail, architecture, and adversarial
corrected-diff reviews are clean.
### C — frame resources and live preparation
- extract ordered per-resource `BeginFrame` calls, clear/state establishment,
histogram probe, mesh upload, reveal evaluation, and particle begin;
- extract weather accumulation and display foundations that do not consume the
camera/root classification, preserving their current render-time positions;
- consolidate requested-VSync/live-preview, monitor callbacks, and
`OnFrameRendered` pacing through `DisplayFramePacingController`, while
leaving its disposal in the existing shutdown transaction;
- prove `WbMeshAdapter.Tick` remains before reveal evaluation and drawing;
- prove dispatcher begin occurs once for the whole world/paperdoll frame.
Completed 2026-07-22. `RenderFrameResourceController` now owns the exact
resource-begin → atmosphere/GL-clear → live-upload/reveal/particle transaction,
with a single captured GPU slot shared by every phase. Focused sources own the
login reveal cell, teleport render state, upload timing, and render-time weather
clock. `DisplayFramePacingController` owns requested VSync, the event-refreshed
monitor-rate cache, software pacing, and the post-render callback without adding
a monitor query to the frame hot path. Borrowed frame owners are withdrawn before
render teardown; portal fallback ownership transfers before the throwable sink
bind; pacing releases its borrowed surface/profiler before their owners close.
`GameWindow.cs` is 6,181 raw lines, 60.7% below the 15,723-line campaign
baseline. Twenty-one focused tests, the Release build, and the complete suite
pass (7,269 passed / 5 skipped). Retail, architecture, and adversarial
corrected-diff reviews are clean.
### D — world frame builder
- move animated/equipped classification, camera/root selection, outdoor-node
building gather, and prior-visible-cell light scratch out of the window;
- after those facts exist, run focused audio/listener, live preview, sky PES,
sun/fog/point-light, and lighting-UBO preparation; do not build an interim
`GameWindow` mega-context;
- keep PView-generated particle-owner sets in `RetailPViewPassExecutor` and the
post-draw partition in the borrowed PView result/diagnostic snapshot;
- pin viewer-cell versus player-cell roles, portal replacement, login wait,
fallback/null-root, sealed/SeenOutside, and borrowed scratch reset;
- preserve allocation-free warmed frames and hysteretic scratch retention.
Completed 2026-07-22. `WorldRenderFrameBuilder` now owns the ordered camera,
visibility, live settings/listener, player-lighting versus collided-viewer root,
sky/lighting/fog, animated-ID, and building-candidate preparation. Reusable
animated/building/light-cell collections retain their explicit one-frame
borrowed lifetime; PView drawable-cell feedback returns through the builder and
is copied before the renderer reuses its own scratch. The diagnostic sky-PES
experiment moved to `SkyPesFrameController`, and fog startup overrides now enter
through typed `RuntimeOptions`. The frame graph is withdrawn before its borrowed
equipped/effect/audio owners during shutdown. `GameWindow.cs` is 5,715 raw
lines, 63.6% below the 15,723-line campaign baseline. Twenty-nine focused tests,
the Release build, and the complete suite pass (7,281 passed / 5 skipped).
Retail, architecture, and adversarial corrected-diff reviews are clean.
### E — typed PView pass executor
- replace `Action`/`Func` draw callbacks in `RetailPViewDrawContext`;
- move landscape early/late, scissor/clip, depth seal/punch, particle passes,
landscape alpha flush, and PView diagnostics to the executor;
- add a recording executor test for the complete `DrawInside` call sequence;
- verify no executor operation rebuilds portal visibility or retains a borrowed
frame result across calls.
Completed 2026-07-22. `RetailPViewRenderer` is now the retail visibility and
ordering owner only; `RetailPViewPassExecutor` owns the concrete clip upload,
EnvCell/entity, landscape, portal depth, particle, alpha, and diagnostic pass
implementations. The old callback bag and broad window closures are gone.
`WorldRenderDiagnostics` now owns every PView print-on-change signature and
probe sink, while a focused terrain-diagnostics controller preserves the
shared five-second terrain/frame publication transaction across fallback and
PView draws. Real `DrawInside` fixtures cover outdoor, interior-exit,
no-outside, entity/static/dynamic particle, transparent-shell, look-in, and
second-call scratch-reset behavior; structural tests pin concrete forwarding,
no delegate/back-reference leakage, no borrowed frame/result retention, and
early shutdown withdrawal. `GameWindow.cs` is 5,303 raw lines, 66.3% below the
15,723-line campaign baseline. Release build and the complete suite pass
(7,295 passed / 5 skipped). Retail, architecture, and adversarial
corrected-diff reviews are clean.
### F — world scene owner
- extract alpha/selection/particle-visibility begin/end and the normal-world
conditional;
- extract sky/terrain/null-root fallback and unified PView branches;
- extract post-world particles/weather and debug collision drawing;
- return a small `WorldRenderFrameOutcome` for diagnostics/presentation;
- delete the world draw body and helper closure from `GameWindow`.
Completed 2026-07-22. `WorldSceneRenderer` now owns the normal-world
transaction, one shared-alpha frame, retail selection accumulation, PView versus
null-root fallback branching, post-world particles/weather, visibility
publication, and world diagnostics. `WorldScenePassExecutor`, focused runtime
sources, and `WorldSceneDiagnosticsController` carry the concrete GL and data
seams without retaining `GameWindow`, delegates, or borrowed PView products.
Failed frames abort PView, pass routing, particle visibility, alpha, and
selection in reverse ownership order; one shared `RenderFrameGlStateController`
restores scissor/stencil/blend/clip/mask/depth/cull/program/buffer state before
the next clear. Normal portal-depth exits now restore the same cull-off
convention, and alpha cleanup preserves the original draw failure while
attempting every source reset. `GameWindow.cs` is 4,765 raw lines, 69.7% below
the 15,723-line campaign baseline. Release build and the complete suite pass
(7,313 passed / 5 skipped). Retail, architecture, and adversarial
corrected-diff reviews are clean.
### G — final orchestration cutover
- compose concrete resource, world, private-presentation, and diagnostic owners;
- replace `OnRender` with one `RenderFrameOrchestrator.Render` handoff;
- remove obsolete fields/helpers/callbacks; prove immediate frame owners retain
no direct `GameWindow`/anonymous-delegate back-reference, and document rather
than deny recursive paths through borrowed canonical UI/input owners;
- preserve the existing shutdown dependency order while making the
developer-tools backend's pre-existing bootstrap ownership explicit;
- measure line/field/method counts. Ownership decides completion; the expected
signal is roughly 5,0005,500 lines before Slice 8 composition cleanup.
Completed 2026-07-22. `GameWindow.OnRender` now creates one value-only
`RenderFrameInput` and hands the complete frame to `RenderFrameOrchestrator`.
Focused preparation, world-scene, private-presentation, and diagnostic owners
preserve the accepted resource → ImGui begin → weather → world → portal →
paperdoll → retained UI → ImGui submit → screenshot → diagnostics → GPU close
order. Recovery now closes an active ImGui frame through the owning Silk
controller and restores the exact text-render GL state after success or
failure. Screenshot dimensions travel in the frame input rather than through a
window callback, and immediate frame owners have no direct `GameWindow` or
anonymous-delegate back-reference. `GameWindow.cs` is 4,666 raw lines / 196
fields / 70 methods: 11,057 lines (70.3%) below the 15,723-line campaign
baseline. The Release build and complete suite pass (7,341 passed / 5 skipped),
and retail-conformance, architecture, and adversarial corrected-diff reviews
are clean.
### H — release and connected gates
After three clean corrected-diff reviews:
1. run focused App/Core render, PView, alpha, portal, screenshot, and resource
tests;
2. run `dotnet build AcDream.slnx -c Release`;
3. run the complete Release suite;
4. run `tools/run-connected-world-lifecycle-gate.ps1 -SkipBuild`;
5. run `tools/run-connected-r6-soak.ps1 -SkipBuild`;
6. require code-zero graceful exits, exact lifecycle checkpoints/PNGs, no
fatal/invariant/reveal/pending-resource failures, and unchanged same-location
resource tolerances;
7. compare the six existing lifecycle/reconnect PNGs (`capped_login`,
`aerlinthe_first`, `facility_hub`, `holtburg_after_dungeon`,
`aerlinthe_revisit`, and `uncapped_reconnect`) with the Slice 6 baseline;
8. audit architecture, divergence pointers, roadmap, milestones, issues,
`AGENTS.md`, `CLAUDE.md`, and durable memory.
Completed 2026-07-22. The 315.6-second lifecycle/reconnect gate passed at
`9d7df1bf` with six valid PNG checkpoints, code-zero graceful capped exit, and
fresh-process uncapped reconnect. Side-by-side comparison with the Slice 6
artifacts preserved world geometry, UI/paperdoll layering, private viewports,
and depth/presentation; only live weather, particle timing, authoritative
vitals, and small scripted camera-settling differences changed. The unchanged
nine-stop gate then passed in 395.2 seconds with nine materializations, all
Caul/Holtburg movement exercises, code-zero graceful shutdown, update p95 at
or below 0.8 ms, and same-location Caul plateau deltas of +85.7 MiB working set
and +59.3 MiB private memory. Two immediately preceding identical-binary runs
crossed the coarse process-residency limit while every other deterministic
lifecycle, timing, and shutdown check passed; #232 records the need to add
canonical managed/GPU/cache owner snapshots to that gate instead of weakening
its leak threshold. Release build and the complete suite remain green at 7,341
passed / 5 skipped. Slice 7 is closed with no new retail divergence.
The accepted baseline at docs-only commit `9512404e` is behavior-equivalent to
production cutover `e91f3102`: 7,182 passed / 5 skipped, 314.195-second
lifecycle/reconnect, and 393.581-second soak. Exact counters are recorded in
`memory/project_gamewindow_decomposition.md` and the Slice 6 closeout ledger.
Building/doorway poses, an open paperdoll, transient portal-exit visuals, and
#225 remain user visual gates; the stable post-materialization PNGs do not
overclaim them.
## 6. Mandatory review gate
After every implementation checkpoint, three read-only reviews run:
1. **Retail conformance:** named anchors, PView/alpha/depth/private-viewport/UI
order, and registered adaptation boundaries.
2. **Architecture/integration:** focused owner scope, no replacement god object,
no service locator/back-callback, render-thread mutation, borrowed lifetime,
GL-state symmetry, and shutdown dependency direction.
3. **Adversarial:** begin/end failures, missing resources, portal/login
branches, stale borrowed scratch, callback reentrancy, frame spikes,
mid-frame resource churn, and GUID/session replacement.
The primary agent fixes confirmed findings at their root, then requests
corrected-diff re-review until clean. Each checkpoint then runs focused tests,
Release build/full suite, divergence/doc audit, and lands as one commit.
## 7. Final acceptance matrix
### Automated
- exact orchestrator order for normal, login, portal, indoor, outdoor, and
fallback frames, plus construction-time rejection of missing required phase
owners;
- exact GPU begin/end/aggregate failure behavior;
- PView landscape/look-in/flush/clear/seal/shell/static/dynamics ordering;
- screenshot after retained UI/ImGui, before title/resource/frame-profile
diagnostics, and before GPU `EndFrame`;
- dispatcher frame begin once across world and paperdoll scopes;
- frame scratch reset/reuse after skipped, portal, normal, and failed frames;
- no `GameWindow` reference or anonymous draw delegate bag in extracted owners;
- existing PView replay, shared-alpha, particle visibility, portal, paperdoll,
GPU-fence, screenshot, resource, and structural suites remain green;
- Release build and all tests green;
- connected lifecycle/reconnect and synchronized soak reports pass.
### In-client campaign gate
- outdoor dense scenes, terrain, sky, weather, particles, and selection match;
- buildings, doorway look-in/out, depth seals/punches, and dynamics stay stable;
- sealed dungeons keep correct visibility and lighting;
- portal replacement and destination reveal preserve the accepted flow;
- paperdoll, retained gameplay UI, cursor, and ImGui layering are unchanged;
- no resource growth, GPU reset, render exception, or shutdown regression.
## 8. Fixed assumptions
- all render/GL mutation remains on the existing render thread;
- `DatCollection` remains the only DAT reader;
- `LiveEntityRuntime`, `GpuWorldState`, cell visibility, session, and resource
owners remain canonical and are not copied;
- existing render resources are borrowed through Slice 7 and disposed by the
current shutdown transaction;
- frame-local results and scratch collections are borrowed until the next
frame/call unless explicitly copied;
- no quality/range/performance tradeoff is introduced to make the extraction
easier;
- missing optional diagnostic/UI owners skip only their own presentation and
never suppress world drawing or GPU frame closure.

View file

@ -1,132 +0,0 @@
# GameWindow Slice 8 — Checkpoint G Runtime Settings Plan
## Objective
Replace `GameWindow`'s duplicated pre-window and `OnLoad` settings paths with
one two-phase `RuntimeSettingsController`. The controller owns persistence and
live settings state; render, window, audio, UI, and streaming objects remain
borrowed targets. This is a structural cutover, not a redesign of the settings
format or retail option behavior.
Two carried behaviors are now explicit rather than accidental. Saved FOV is
applied during startup even when developer tools are disabled; previously it
was incorrectly gated on construction of `SettingsVM`. The named-retail
`/framerate` live toggle and notice remain intact, while acdream's already
shipped cross-launch ShowFps persistence is documented as AP-121 rather than
being attributed to the retail command body.
## Frozen behavior and ordering
1. Construct the controller before `Window.Create` and load Display, Audio,
Gameplay, Chat, and the `default` Character bag exactly once.
2. Resolve the preset plus environment overrides once for the immutable startup
snapshot. `WindowOptions.VSync` comes from the pacing policy initialized with
that snapshot; `WindowOptions.Samples` comes from the same resolved quality.
3. After window/GL input, camera, DAT effects, and optional audio exist, but
before devtools or world/render factories, apply startup settings in this
order: monitor-aware pacing, resolution/fullscreen, field of view, then audio.
4. Downstream terrain, dispatcher, render-range, and streaming factories read
the same startup quality snapshot. The legacy startup stream-radius override
remains at its current composition boundary.
5. Bind one complete `RuntimeSettingsTargets` only after window, UI (when
enabled), terrain atlas, dispatcher, render-range, and streaming exist.
Binding performs no display, quality, UI-lock, or audio replay.
6. Settings-panel Save preserves its callback order: keybindings, Display,
Audio, Gameplay, Chat, Character. Each section retains its existing
persistence/error boundary.
7. Display Save persists first, applies resolution/fullscreen, publishes the
new display snapshot, then reapplies quality. A changed MSAA sample count is
logged as restart-required and is not applied to the live GL context.
8. Runtime quality reapply preserves the existing order: alpha-to-coverage,
terrain anisotropy, render-range publication, streaming reconciliation,
completion budget.
9. `/framerate`, UI lock, loot permits, and combat preferences mutate the
controller's canonical state, synchronize the optional Settings draft using
the existing draft-preservation rules, and retain their existing save-error
behavior.
10. Enter-world changes the active toon key, loads that character bag even when
devtools are disabled, and publishes it to the optional Settings view model.
Session reset restores the startup/default character context before the
later identity-reset operation restores the key to `default`.
11. Per-frame display/audio preview reads the optional Settings draft through a
typed source. Particle range and FPS visibility read the same source so an
unsaved draft still previews exactly as it does today.
12. Shutdown first withdraws the Settings view-model and runtime-target loans;
the controller never disposes the window, renderer, UI, streaming, audio, or
input objects it borrows.
## Types and ownership
- `IRuntimeSettingsStorage` is the test seam for the five settings bags.
`JsonRuntimeSettingsStorage` is the production owner of the one concrete
`SettingsStore` and exposes that store only for retained-window layout
persistence.
- `RuntimeSettingsSnapshot` is the immutable startup value.
- `IRuntimeSettingsStartupTarget` receives the one startup Display/Audio apply.
- `IRuntimeSettingsTargets` receives future display-window, quality, and UI-lock
changes. `BindRuntimeTargets` stores it without invoking it.
- `IRuntimeSettingsPreviewSource` exposes current Display/Audio preview values
and whether a live draft exists.
- `RuntimeSettingsController` owns persistence, current values, active toon,
resolved quality, and the optional `SettingsVM` binding. It also implements
the combat settings source; no second gameplay-state mirror remains.
- `SilkRuntimeDisplayWindowTarget`, `RuntimeSettingsStartupTargets`, and
`RuntimeSettingsTargets` are App-layer adapters over borrowed runtime objects.
## Automated gates
- Construction loads every bag once and populates one immutable snapshot.
- Startup apply is ordered, exactly once, and independent of GL in controller
tests.
- Runtime target binding produces zero calls; unbinding prevents future target
calls while state and persistence still advance.
- Display Save ordering, quality transition, and restart-required MSAA logging.
- Quality target receives one complete resolved value per reapply.
- Draft preview/cancel, external FPS mutation, UI lock, loot permits, and combat
option synchronization.
- Toon activate/load/reset/save behavior with and without a Settings view model.
- Save failures preserve each established state/target boundary.
- Source-boundary test proves `GameWindow` no longer constructs or loads a
`SettingsStore`, owns persisted settings fields, or contains quality/display
feature bodies.
- Focused App tests, full App tests, Release solution build, and full Release
suite.
## Review and commit gate
After the focused tests pass, the existing retail-conformance,
architecture/integration, and adversarial reviewers inspect the complete diff
in parallel. Confirmed findings are corrected and the same reviewers repeat
until clean. Documentation, roadmap, issues, memory, `CLAUDE.md`, and
`AGENTS.md` are reconciled in the same bisectable Checkpoint G commit.
## Implementation result
- `RuntimeSettingsController` is constructed before `Window.Create`; all five
bags load once and `WindowOptions` consumes its immutable startup snapshot.
- Startup substeps commit independently, so an audio failure retries audio
without replaying already successful pacing/display/FOV work.
- Late target binding is inert and reversible; expected-instance SettingsVM
withdrawal cannot detach a replacement binding.
- The concrete quality adapter preserves all five application steps and the
unchanged MSAA restart boundary.
- Combat changes merge their three fields into a live Gameplay draft rather
than overwriting unrelated unsaved fields; successful external changes also
advance the SettingsVM persisted baseline so Cancel cannot resurrect stale
command state.
- UI-lock target, persistence, and baseline publication form one explicit
convergence state. A transient failure leaves the same requested value
retryable instead of letting the equality fast-path strand unfinished work.
- Enter/reset/toon changes load and publish Character state independently of
developer-tool availability.
- `GameWindow` no longer constructs or loads `SettingsStore`, owns persisted
settings mirrors, or contains display/quality/settings feature bodies.
- Candidate metrics: 3,663 raw lines / 162 fields / 37 methods, down 394 lines /
36 fields / 17 methods from Checkpoint F and 12,060 lines (76.7%) from the
campaign baseline.
- Thirty-five focused App settings/boundary tests, all 43 `SettingsVMTests`, the
UI Abstractions Release suite (534 pass), and the App Release suite (3,183
pass / 3 intentional skips) pass. All three independent corrected-diff
reviews are clean. The production App Release build has zero warnings/errors;
the solution Release build retains the 17 existing #228 test-project warnings;
and the complete Release suite passes 7,553 tests / 5 intentional skips.

View file

@ -1,208 +0,0 @@
# GameWindow Slice 8 — Checkpoint H Resource Ownership Plan
## Objective
Replace the remaining implicit, constructor-local, and nullable-field resource
ownership in `GameWindow` with explicit single-owner transactions. This is a
behavior-preserving lifetime cutover: draw/update order, portal presentation,
retained UI behavior, settings behavior, and the frozen shutdown order do not
change.
Checkpoint H does not move the complete shutdown manifest; that is Checkpoint
J. It supplies the typed roots and retry semantics that J will consume.
## Frozen invariants
1. Every GL/kernel-owning object is published to exactly one owner immediately
after successful construction.
2. Renderers borrow the terrain atlas and dedicated sky shader. They never
dispose those resources. The renderer borrower retires before the sole
resource owner releases the borrowed atlas/shader.
3. A leaf factory that throws before returning must release every GL name it
created. Outer phase/lifetime rollback cannot recover a name that was never
published.
4. Bindless residency is a resource prefix: partial handle acquisition rolls
back, successful non-residency is not replayed after a later failure, and
textures are not deleted while a resident handle remains.
5. Retained UI input is quiesced before the live-session barrier and physically
detached afterward, independently of runtime/host disposal.
6. Exactly one retained-UI ownership chain exists. Before runtime construction
the lease owns the `UiHost`; after a runtime object is published the lease
retains that exact partial/full runtime and delegates final disposal to it.
Constructor or initialization failure never leaves a host in a local only.
7. Update and render roots publish atomically as one frame graph. Silk callbacks
resolve only through the slot; an empty/withdrawn slot is safely inert.
8. Portal tunnel ownership starts in one fallback slot. Transfer clears the
fallback only after the complete local-teleport controller factory returns;
any earlier failure leaves the same tunnel reachable by shutdown.
9. Successful disposal is never replayed. Failed disposal remains retryable.
Terminal abandonment is explicit and retains references for native/process
fallback rather than silently losing ownership.
## Typed owners
### `OwnedResourceSlot<T>` and `GameRenderResourceLifetime`
- `OwnedResourceSlot<T>` acquires through a factory, rejects duplicate
publication, exposes a typed borrower, and clears ownership only after
`Dispose` succeeds.
- `GameRenderResourceLifetime` contains separate slots for `TerrainAtlas` and
the dedicated sky `Shader`.
- `GameWindow` acquires both through this owner. Shutdown releases the sky
renderer and terrain renderer first, then the two resource slots, then GL.
### `RetailUiRuntimeLease`
- The lease exists before `OnLoad` and acquires the `UiHost` through a factory.
- Runtime construction and initialization are separate. The runtime is stored
in the lease before initialization begins.
- Initialization failure attempts immediate cleanup. Successful cleanup
rethrows the original initialization error; cleanup failure reports both and
leaves the exact runtime in the lease for lifetime retry.
- The lease exposes idempotent `QuiesceInput` and retryable `DeactivateInput`
operations against the retained host.
- If no runtime was constructed, lease disposal directly disposes the host. If
a runtime exists, runtime disposal remains the one path that disposes the
host. There are never two host disposers.
- Explicit terminal abandonment is allowed only after a failed disposal pass;
it makes later disposal inert while retaining the unresolved references for
the native/process fallback handled by Checkpoint J.
### `GameFrameGraphSlot`
- `IGameUpdateFrameRoot` and `IGameRenderFrameRoot` are the narrow frame seams.
- `UpdateFrameOrchestrator` and `RenderFrameOrchestrator` implement those seams.
- `Publish(update, render)` is atomic and one-shot until withdrawal.
- `Tick`/`Render` are inert before publication and after withdrawal.
- Shutdown withdraws the pair only after live-session convergence and before
any frame borrower retires.
### `TransferableResourceSlot<T>`
- `Acquire` owns one fallback resource.
- `Transfer(factory)` retains fallback ownership while the complete destination
factory runs and clears it only after that factory returns successfully.
- Failed acquisition, resource preparation, destination construction, binding,
and normal shutdown prefixes are covered without double disposal.
## Exception-safe GL leaves
### Shader program construction
`ShaderProgramConstruction` uses a narrow `IShaderProgramBuildApi`. It tracks
the vertex shader, fragment shader, and linked program as soon as each name is
returned. Compile, attach, link, detach, or cleanup failure attempts every
remaining rollback operation. A clean rollback preserves the original failure;
rollback failures are aggregated with it. `Shader.Dispose` becomes idempotent
and retryable.
### Terrain atlas construction and residency
`GlTextureConstructionTransaction` tracks every texture name allocated by
terrain, alpha, and fallback paths. `TerrainAtlas.Build`, `BuildAlphaAtlas`, and
`BuildFallback` allocate only through that transaction. The transaction commits
only after the complete `TerrainAtlas` object exists; failure rolls back all
names in reverse acquisition order and reports cleanup failures with the
original error.
`BindlessTexturePair` owns the terrain/alpha residency pair. Partial acquisition
releases the first handle. Release attempts both handles, remembers successful
substeps, and is retryable. `TerrainAtlas.Dispose` releases residency first,
then independently deletes both texture names through the existing retryable
shutdown transaction.
## Production integration order
1. Construct render-resource lifetime, UI lease, frame slot, and portal
fallback slot before `Window.Create`/`OnLoad` acquisitions.
2. Acquire the atlas through `GameRenderResourceLifetime`; construct
`TerrainModernRenderer` as a borrower.
3. Acquire `UiHost` through `RetailUiRuntimeLease`, wire input/assets, construct
and publish `RetailUiRuntime` before initialization, then expose host/runtime
only as borrower aliases.
4. Acquire/prepare portal presentation in the fallback slot.
5. Acquire the dedicated sky shader through the render-resource lifetime, then
construct `SkyRenderer` as a borrower.
6. Transfer the portal fallback only through the complete
`LocalPlayerTeleportController` factory.
7. Construct update/render roots as locals and publish them atomically through
`GameFrameGraphSlot`.
8. Update/render callbacks resolve only through the slot.
9. Shutdown quiesces/deactivates UI through the lease, withdraws frame roots,
disposes the UI lease and renderer borrowers, releases atlas/sky roots, then
reaches GL.
## Automated gates
- owned-resource acquire/borrow/release, duplicate acquire, transient failure,
retry, and no replay;
- UI host acquisition, runtime constructor failure, Initialize failure with
successful cleanup, transient cleanup then lifetime retry, persistent cleanup
then explicit abandonment, separate input quiescence/deactivation, and exact
host disposal count;
- frame pair atomic publication, duplicate publication, update/render routing,
withdrawal, pre-publication/post-withdrawal silence, and republish policy;
- portal fallback acquisition/preparation/transfer failures plus successful
transfer and every shutdown prefix, with exact one-resource disposal;
- vertex/fragment compile failure, program creation/link failure, and cleanup
failure after every created shader/program name;
- terrain/alpha/fallback texture allocation and upload failure after every
created name, reverse rollback, cleanup aggregation, and committed-name
non-deletion;
- bindless first/second acquisition failure, release partial failure/retry, and
no deletion before residency release;
- source boundary proves direct frame-root fields, constructor-local atlas/sky
ownership, direct UI host/runtime ownership, and nullable portal fallback are
removed from `GameWindow`;
- focused App tests, production App Release build, solution Release build, and
complete Release suite.
## Review and commit gate
The existing architecture/integration, retail-conformance, and adversarial
reviewers inspect the complete tracked and untracked diff independently. Every
confirmed finding is corrected and all three repeat until clean. Architecture,
roadmap, milestones, issues, memory, `CLAUDE.md`, and `AGENTS.md` are reconciled
in the same bisectable Checkpoint H commit.
## Implementation result — 2026-07-22
Checkpoint H landed the planned ownership graph without changing the accepted
update/render order or gameplay behavior:
- `GameRenderResourceLifetime` owns the terrain atlas and dedicated sky shader;
renderers borrow them and retire first.
- `RetailUiRuntimeLease` owns Host/runtime construction, initialization, input
cutoff, retryable disposal, and explicit terminal abandonment as one chain.
- `GameFrameGraphSlot` publishes update/render roots atomically and makes empty
or withdrawn callbacks inert.
- `TransferableResourceSlot.AcquirePrepared` publishes the portal presentation
before mesh preparation, retries the same partial resource, and refuses
transfer until preparation has committed.
- GL name creation records the returned name before post-command validation.
Shader/program, texture, text-renderer, buffer/VAO, bindless-residency, and
texture-binding operations advance ownership only after an always-on checked
GL commit boundary.
- Failed construction cleanup is retained by `GlConstructionCleanupLedger`;
exact pending names and binding-restore obligations remain retryable, while
successful cleanup stages never replay.
Four corrected-diff cycles closed issues found by the architecture, retail,
and adversarial reviewers: partial bindless mutation, lost GL rollback names,
constructor-local text resources, reentrant UI ownership, portal preparation,
unchecked production GL calls, leaked texture binding, and the bindless post-
commit verification gap. All three final reviews are clean.
Automated acceptance:
- focused Checkpoint H ownership/lifetime gate: 61 passed;
- App Release suite: 3,236 passed / 3 intentional skips;
- production App Release build: zero warnings and zero errors;
- complete solution Release suite: 7,606 passed / 5 intentional skips; solution
build has zero errors and the 17 existing test-project warnings tracked by
#228;
- `GameWindow.cs`: 3,689 raw lines / 162 fields / 37 methods, versus 15,723 /
278 / 205 at the campaign baseline.
No AC-specific algorithm or intended retail behavior changed, so Checkpoint H
adds no retail-divergence row. Checkpoint I is the next active unit.

View file

@ -1,374 +0,0 @@
# GameWindow Slice 8 Checkpoint I — ordered production composition
**Status:** Complete 2026-07-22.
**Parent:**
[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md),
Checkpoint I.
**Integrated baseline:** `c791f138`; the reviewed Checkpoint-H tree plus the
complete-world-reveal history is merged. `GameWindow.cs` is 3,689 raw lines,
162 fields, and 37 methods. The Release build succeeds with the 17 warnings
tracked by #228 and all 7,606 tests pass / 5 intentionally skip.
**Behavior rule:** startup ownership and failure behavior may become explicit;
accepted input priority, settings, session/reset, update/render order, world
presentation, and retail gameplay behavior do not change.
## 1. Outcome
Replace the 2,149-line `GameWindow.OnLoad` body with one fixed production
composition pipeline. `GameWindow` remains the native host and write-only
publication shell. It does not become a service locator, and no extracted
phase stores or calls back into `GameWindow`.
The final startup shape is:
```text
GameWindow.OnLoad
-> GameWindowPlatformAcquisition.Acquire
-> GameWindowCompositionPipeline.Run
1 Host input / camera
2 Content / effects / audio
3 Settings / optional devtools
4 World / render resources
5 Interaction / retained UI
6 Live presentation / landblock publication
7 Streaming / session / local player / teleport
8 Atomic update + render roots
9 LiveSessionHost.Start, and nothing after it
```
Every phase consumes only the exact immutable prior results it needs and
returns a cohesive typed result. Results are borrowers. Newly acquired owners
move immediately into the existing lifetime slots, where shutdown remains the
sole release authority.
## 2. Fixed architecture
### 2.1 Platform prelude
`GameWindowPlatformAcquisition` is the exact production method used by
`OnLoad`. Its order is fixed:
1. create GL;
2. publish GL to the lifetime shell;
3. create the input context;
4. publish input;
5. return borrowed platform handles.
GL publication must happen before the input factory runs. An input-factory
failure leaves GL lifetime-owned for normal shutdown; local rollback never
double-disposes it. Viewport, diagnostics, camera, and GPU-frame resources are
not part of this narrow prelude.
### 2.2 Pipeline contract
Use nine explicit phase interfaces and immutable result records. The pipeline
holds no cumulative context and stores no final runtime graph. Locals pass only
the required sub-results forward.
Representative result grouping:
```csharp
HostInputCameraResult(InputGraph, CameraGraph, HostRenderGraph)
ContentEffectsAudioResult(ContentCatalogs, EffectsGraph, AudioGraph?)
SettingsDevToolsResult(ResolvedQuality, DevToolsGraph?)
WorldRenderResult(TerrainBuildContext, RenderFoundation)
InteractionUiResult(InteractionGraph, RetainedUiGraph?)
LivePresentationResult(LiveEntityGraph, WorldPresentationGraph)
SessionPlayerResult(StreamingGraph, SessionGraph, PlayerGraph, FrameParts)
FrameRootResult(LiveSessionHost)
SessionStartResult
```
Nested records are cohesive typed graphs, not dictionaries, `object` bags,
general service providers, or wrappers around the window.
### 2.3 Publication seam
`GameWindow` implements narrow write-only phase publishers. Publication methods
only validate single assignment and assign the existing shell fields. They do
not construct resources, invoke gameplay behavior, or provide runtime lookup.
The pipeline and phases release the publisher reference when `Run` returns.
Platform publication is narrower still so GL can publish before input
creation. Checkpoint J will move the exact shutdown manifest into the lifetime
owner; Checkpoint I does not duplicate or reorder that manifest.
### 2.4 Acquisition and rollback
Every concrete phase uses `CompositionAcquisitionScope` and named typed leases:
- unpublished acquisitions roll back in strict reverse dependency order;
- publication or transfer removes the resource from local rollback
immediately;
- lifetime-owned resources are never released locally;
- cleanup continues after an independent cleanup failure;
- the original construction failure and cleanup failures are retained;
- retry invokes only unfinished cleanup actions;
- successful cleanup never replays;
- duplicate publication and duplicate transfer are rejected.
This is not a generic shutdown bag. Long-lived owners remain in their focused
H-era slots and are retired in dependency order by the existing shutdown
transaction.
## 3. Frozen phase order and dependencies
### Phase 1 — host input and camera
Acquire viewport target, GPU-frame owner, world render diagnostics, keyboard
and mouse sources, input dispatcher, movement/camera bindings, mouse-look
cursor, orbit/fly camera, framebuffer camera target, and raw pointer owner.
Preserve:
- keyboard/mouse source attach before dispatcher attach;
- dispatcher attach before raw `MouseMove` attach;
- raw pointer attach before retained-UI device wiring;
- physical framebuffer size remains distinct from logical window size.
### Phase 2 — content, effects, and audio
Open the sole `DatCollection`; load magic, animation, collision, emitter,
particle, hook-frame, physics-script, lighting, and translucency owners. Audio
is optional, but failure can be swallowed only after its entire unpublished
prefix has converged cleanup. Hook-router registrations are reversible leases.
The physics-script advancement gate resolves the later entity-effect authority
through one typed deferred source; it must not capture the window or a null
snapshot.
### Phase 3 — settings and optional devtools
Apply the immutable startup settings snapshot after camera/audio exist and
before world/render/UI factories consume it. Compose optional devtools with a
transactional input/bootstrap/backend/framebuffer-target lease. Disabled
devtools acquire nothing. A devtools construction failure disables the feature
only after cleanup converges; cleanup failure fails startup rather than
stranding callbacks or native state.
Devtools live facts use focused sources for render diagnostics, canonical
world state, player mode, and player controller. No callback captures the host.
### Phase 4 — world and render resources
Load Region/environment/height/blend data, validate bindless support, then
construct terrain atlas, terrain shader, scene lighting, debug/text resources,
terrain renderer, WB mesh/render infrastructure, texture/sampler caches, and
the render foundation.
Required order includes bindless before atlas/terrain, and the final immutable
height/blend/surface build inputs are captured directly for worker use.
Constructor prefixes for debug lines, bitmap font, OpenGL graphics device,
particle batcher, global mesh buffers, portal depth, particles, samplers, and
terrain renderer must have a strong transactional guarantee.
### Phase 5 — interaction and retained UI
Compose combat target/attack, external-container and item interaction,
character/magic controllers, radar/chat/cursor assets, `UiHost`, and the
retained runtime lease. Retained bindings to later session, reveal, player,
diagnostic, and live-entity authorities use focused deferred sources. Host,
partial runtime, mounted runtime, and input bindings retain exact ownership.
### Phase 6 — live presentation and landblock publication
Compose WB/spawn adapters, canonical `GpuWorldState`, `LiveEntityRuntime`, live
motion/projectile/animation/effect/presentation owners, selection/paperdoll,
EnvCell/portal/sky/particle renderers, landblock publishers, and render
diagnostics. Bind the canonical live-runtime slot immediately after runtime
creation. Anonymous retained event handlers become named reversible bindings.
Portal tunnel ownership remains acquire -> prepare -> transfer. The fallback
slot and final controller never co-own the same resource.
### Phase 7 — streaming, session, local player, and teleport
Compose and publish the streamer before `Start`, then streaming/recenter/reveal,
session/hydration/network/liveness, player-mode/local-animation/shadow,
gameplay-input, commands, action-router, live-session host, and teleport
owners. All UI and devtool late sources bind here.
The current premature frame-root publication defect is removed: command
bindings and gameplay-action attachment complete in Phase 7, before Phase 8.
### Phase 8 — atomic frame roots
Construct all update/render leaves locally, then both roots, and publish the
pair once through `GameFrameGraphSlot` as the final phase action. A failure
before publication leaves the slot empty. A failure after publication leaves
the lifetime owner responsible for exactly one paired graph. No callback can
observe a half-pair.
### Phase 9 — terminal session start
Invoke only `LiveSessionHost.Start`. It is the absolute last startup operation.
There is no publication, callback attachment, command binding, or diagnostics
work afterward.
## 4. Focused live sources and reversible edges
Replace current host captures with focused authorities:
- `ILocalPlayerIdentitySource` for the server GUID;
- `ILocalPlayerModeSource` for player/fly state;
- a player-controller slot for current movement facts;
- `RenderRangeState` for near radius;
- `LiveWorldOriginState` for center coordinates;
- external-container state for current container;
- focused deferred session command/host authority;
- a canonical world-state source that cannot freeze the pre-runtime
placeholder;
- typed item/use/pick/selection/debug-fact operations;
- immutable phase-4 terrain worker inputs.
Add expected-owner unbind tokens for one-shot deferred composition slots that
can be bound before a later fallible edge. A failed phase must be disposable
and retryable without stale bindings or “already bound” failures.
Named reversible bindings replace anonymous subscriptions for projection
visibility, projection pose, entity-ready, appearance-applied, hook sinks, and
streamer start/stop.
## 5. Implementation checkpoints
1. **I.1 — contracts and executable oracle:** pipeline, platform prelude,
acquisition scope, fake phases, prefix/failure tests.
2. **I.2 — platform + Phase 1:** exact GL/input publication and host
input/camera cutover.
3. **I.3 — Phase 2:** content/effects/audio, reversible hooks, transactional
OpenAL construction.
4. **I.4 — Phase 3:** settings/devtools, complete optional rollback.
5. **I.5 — Phase 4:** world/render foundation and strong constructor
guarantees.
6. **I.6 — Phases 56:** retained UI, live presentation, landblock publishers,
typed late sources and event leases.
7. **I.7 — Phase 7:** streaming/session/player/teleport and all pre-frame
bindings.
8. **I.8 — Phases 89:** atomic roots, terminal start, delete old `OnLoad`
body, structural gates.
9. **I.9 — corrected-diff review, full gates, docs/memory, one bisectable
Checkpoint-I closeout commit.**
Small implementation commits may be used while the checkpoint is active, but
each must build and preserve production startup. The final I commit closes the
ledger only after the complete pipeline is live.
### Current implementation progress — 2026-07-22
- I.1I.4 are committed: the executable pipeline/rollback oracle, platform
acquisition, host input/camera, content/effects/audio, and settings/devtools
phases are the production startup path.
- I.5 is complete. `WorldRenderCompositionPhase` now owns the ordered Region,
environment, mandatory bindless, terrain-atlas, shader, lighting, debug/HUD,
terrain, WB, texture, and sampler foundation. `GameWindow` is a write-only
publisher for this phase and no longer contains the Phase-4 construction
body.
- Every fallible GL constructor in the Phase-4 prefix now publishes each name
into retryable construction ownership before later GL work. Failure testing
covers every composition boundary, every disposable publication, partial HUD
construction, reverse cleanup, retry, and no replay.
- The I.5 tree is 3,522 raw `GameWindow.cs` lines. The App gate passes 3,373
tests / 3 intentional skips; the complete Release suite passes 7,745 tests /
5 intentional skips. Production App builds with zero warnings/errors; the
solution retains only the 17 warnings tracked by #228.
- I.6 (retained UI plus live-presentation composition) is complete. Its detailed
two-commit ownership and failure-test plan is
[`2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md`](2026-07-22-gamewindow-slice-8-checkpoint-i6-ui-live-presentation.md).
Phase 5 now composes interaction and retained UI through typed dependencies,
exact-owner late bindings, and transactional rollback. Phase 6 now composes
the canonical live runtime/world, presentation/effect controllers,
selection/radar, landblock publishers, and portal/sky/particle resources
under the same ownership contract. `GameWindow.cs` is 2,840 raw lines; the
App gate passes 3,407 tests / 3 intentional skips and the complete Release
suite passes 7,779 tests / 5 intentional skips. I.7 session, streaming, and
hydration composition is complete; its ownership, rollback, and gate
contract is
[`2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md`](2026-07-22-gamewindow-slice-8-checkpoint-i7-session-player.md).
- I.7 removes the former 432-line Phase-7 body. Streaming, session, hydration,
local-player, combat, and teleport startup now compose through one typed
result; all late edges have named reversible exact-owner tokens. The
spawn-claim DAT range memo has a focused owner. `GameWindow.cs` is 2,479 raw
lines. App Release passes 3,420 tests / 3 skips; the complete Release suite
passes 7,792 tests / 5 skips with the same 17 warnings tracked by #228. I.8
frame-root publication and terminal session start is the active cut. Its
detailed session-binding, atomic-root, and terminal-start plan is
[`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md).
I.8a is complete: the session factory, command targets, and sole gameplay
input subscriber now belong to Phase 7 and publish before any frame root.
I.8b composes the unchanged update/render graph in a focused phase, publishes
the pair through an exact owned slot lease, and moves lifecycle resource
sampling out of the window. I.8c makes the exact session start and its
existing diagnostics the terminal `OnLoad` operation. `GameWindow.cs` is
1,910 raw lines.
- I.9 found and corrected one structural defect: production manually repeated
the phase order instead of invoking the executable pipeline used by failure
tests, and several later phases received real platform/settings results only
through surrounding captures. Production now enters the same fixed pipeline;
platform and settings are explicit typed phase inputs and concrete phases
reject mismatched result instances. Session start remains the terminal
operation. Three corrected-diff passes covering behavior order, architecture/
ownership, and adversarial failure/reentrancy are clean. `GameWindow.cs` is
1,945 raw lines. App Release passes 3,431 tests / 3 intentional skips; the
complete Release suite passes 7,803 tests / 5 intentional skips with only the
17 pre-existing test-project warnings tracked by #228. No retail behavior or
divergence changed. Checkpoint I is complete; Checkpoint J is active.
## 6. Automated acceptance
### Platform and pipeline
- Success trace is exactly `gl.factory -> gl.publish -> input.factory ->
input.publish`.
- GL-factory failure never calls input; input-factory failure retains GL in the
lifetime owner.
- Failure at each of nine phases runs exactly the preceding prefix and no
suffix.
- Every phase receives the exact immutable prior result instance.
- Success trace is phases 18 followed by terminal session start.
- Failure after frame publication withdraws the pair exactly once.
### Acquisition and concrete phases
- A/B/C prefix tests prove reverse rollback, transfer, aggregation, retry, and
no replay.
- Every concrete production phase has injected fault points at each
acquisition/attach/bind/publication edge.
- Optional audio/devtools continue only after successful rollback.
- Hook and event registrations unregister exactly once.
- Deferred slots unbind only the exact owner and reject stale release.
- Streamer start is reversed on later Phase-7 failure.
- Portal fallback/controller never co-own.
- Phase 8 never exposes one frame root without the other.
### Structural and repository gates
- `OnLoad` is a small platform/pipeline forwarder.
- No phase stores `GameWindow`, accepts a callback facade for substantial
host methods, or uses a service locator / `object` dictionary.
- `GameWindow` contains no construction body larger than one paragraph.
- `AcDream.Core` gains no App/GL dependency.
- App production build has zero warnings/errors; solution warnings remain only
the 17 tracked by #228.
- Focused tests, all App tests, and full Release suite pass.
- Connected lifecycle/reconnect and synchronized soak gates remain green when
the local ACE/visual environment is available.
## 7. Review gate
After the complete production cutover, run three independent read-only reviews:
1. retail/behavior-order conformance;
2. architecture, dependency direction, ownership, and shutdown symmetry;
3. adversarial failure, partial construction, reentrancy, and stale-binding
analysis.
Fix every confirmed finding at its root and repeat review until all three are
clean. No review finding is waived merely because the happy-path tests pass.
## 8. Documentation and commit exit
Checkpoint I closes only when the code-structure ledger, architecture, roadmap,
milestones, issues, `memory/project_gamewindow_decomposition.md`, `AGENTS.md`,
and `CLAUDE.md` agree on the same production pipeline and measured class size.
No retail-divergence row is added for a behavior-preserving ownership move; any
discovered behavioral adaptation is recorded in the same commit that keeps it.

View file

@ -1,237 +0,0 @@
# GameWindow Slice 8 — Checkpoint I.6 UI and Live Presentation
**Status:** Complete; I.7 active
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
**Scope:** Ordered-composition phases 5 and 6 only
## 1. Outcome
Remove the interaction, retained gameplay UI, live-entity presentation, and
landblock-publication construction bodies from `GameWindow.OnLoad`. The window
remains a write-only publication surface and runtime coordinator; it does not
remain an implicit service locator for callbacks created during startup.
I.6 lands as two independently buildable commits:
1. **I.6a — interaction and retained UI composition.**
2. **I.6b — live presentation and landblock publication composition.**
Neither commit changes retail behavior, rendering order, input priority,
network messages, selection policy, DAT lookup, or resource quality. This is a
structural ownership move, so it does not add a divergence-register row.
## 2. I.6a — interaction and retained UI
### 2.1 Owned construction
`InteractionRetainedUiCompositionPhase` constructs, in the existing order:
1. combat attack controller;
2. combat target controller;
3. external-container lifecycle;
4. item interaction controller;
5. optional retained UI host;
6. cursor feedback and retail cursor resources;
7. character-sheet and magic runtimes;
8. retained mouse and keyboard bindings;
9. chrome/icon/font assets;
10. chat, radar, persistence, and optional probe owners;
11. uninitialized retained UI runtime mount.
The non-UI combat and item owners exist even when retail UI is disabled. The
disabled path acquires no UI host, input callback, font, cursor, probe, or
runtime resource.
### 2.2 Focused authorities
Callbacks that formerly captured `GameWindow` resolve through typed owners:
- `DeferredLiveSessionUiAuthority` exposes the current session, command bus,
link facts, character account, and the exact outbound operations needed by
interaction/UI. It binds to the Phase-7 `LiveSessionController` with an
expected-owner token and safely supplies disconnected/no-op values before
that bind.
- `DeferredSelectionUiAuthority` exposes world picking, use/pickup dispatch,
closest-target selection, range/health classification, vivid-target facts,
and camera facts. Phase 6 binds its exact query/controller pair.
- `DeferredRadarSnapshotSource` returns an empty-but-valid snapshot until
Phase 6 publishes the canonical `LiveEntityRuntime`/`GpuWorldState` view. It
must never retain the pre-runtime placeholder world.
- `DeferredWorldLifecycleAutomationSource` lets the optional UI probe mount in
Phase 5 while Phase 7 supplies reveal/resource facts later.
- the already focused local-player identity, controller, mode, external-
container, settings, and input-dispatch sources replace raw host fields.
- a shared `DeferredRenderFrameDiagnosticsSource` exists regardless of whether
developer tools are enabled; Phase 6 binds the diagnostics owner once.
Every deferred bind returns or is paired with an expected-owner unbind. A stale
release can neither clear a newer owner nor make retry report “already bound”.
Deactivation makes all callbacks inert before runtime teardown.
### 2.3 Ownership and publication
`InteractionRetainedUiResult` carries the stable owners required by later
phases. `InteractionUiLateBindings` owns the retained input-capture lease and
the late authorities; `RetailUiRuntimeLease` remains the sole owner of the
optional host/runtime resources. Construction uses
`CompositionAcquisitionScope`; the owner is published only after all mandatory
interaction resources and every enabled retained-UI resource are complete.
`IGameWindowInteractionRetainedUiPublication` publishes exact field identities
needed by existing steady-state and reset code. Publication is write-only and
rejects replacement.
Normal shutdown and construction rollback preserve these stages:
1. deactivate late sources and release retained input capture;
2. dispose the retained-UI lease, which detaches device callbacks, the mounted
runtime, and the host in its established order;
3. dispose item/external-container/combat owners.
The existing retained UI lease remains the sole host/runtime owner until its
full lifecycle is migrated; the phase owner controls it but never duplicates
its resources.
### 2.4 I.6a automated gates
- exact success order and disabled-UI acquisition trace;
- injected failure after every construction, attach, mount, and publication
edge;
- reverse rollback, retry after partial cleanup, and no replay;
- late authority default behavior, exact-owner unbind, stale-token rejection,
deactivation, and rebind after rollback;
- session operations preserve the former outbound message selection;
- radar cannot observe a pre-runtime `GpuWorldState` placeholder;
- retained mouse-before-keyboard wiring and input capture are unchanged;
- publication identity and replacement rejection;
- App Release build/tests, then full solution Release build/tests;
- three independent self-review passes: retail behavior, architecture and
ownership, adversarial failure/reentrancy.
### 2.5 I.6a completion evidence
I.6a moved the complete Phase-5 body into
`InteractionRetainedUiCompositionPhase`. Exact-owner leases now guard retained
input capture plus the late session, selection, radar, view-plane, diagnostics,
inventory, and automation bindings. The old `GameWindow` item-use/pick/query
wrappers are deleted; shared Vitals identity, mouse-before-keyboard ordering,
direct contained-item use, and the disabled-retail-UI path remain unchanged.
The focused success/failure/rollback matrix passes 28 tests. The App Release
gate passes 3,401 tests / 3 intentional skips, the complete Release suite
passes 7,773 tests / 5 intentional skips, and the solution build has only the
17 warnings already tracked by #228. `GameWindow.cs` is 3,246 raw lines after
I.6a. Behavior, ownership, and adversarial self-reviews found no remaining
actionable issue. I.6b is the next active cut.
## 3. I.6b — live presentation and landblock publication
### 3.1 Owned construction
`LivePresentationCompositionPhase` constructs, in existing order:
1. deferred live-component lifecycle;
2. landblock and entity spawn adapters;
3. static animation/script activation owners;
4. canonical `GpuWorldState`;
5. `LiveEntityRuntime`, followed immediately by canonical slot bind;
6. motion, projectile, withdrawal, light, animation, equipped-child, effect,
presentation, and remote-teleport owners;
7. WB dispatcher and retail selection scene/query/controller;
8. optional paperdoll renderer/presenter;
9. EnvCell/portal/clip/tunnel/sky/particle render resources;
10. landblock render/physics/static publishers and presentation pipeline;
11. render diagnostics and canonical late-source bindings.
The phase consumes immutable Phase-4 terrain/build inputs. It does not read
mutable `GameWindow` terrain fields.
### 3.2 Named reversible edges
Replace anonymous subscriptions with named lease owners:
- live projection visibility → WB presentation residency;
- live projection visibility → particle presentation visibility;
- equipped-child projection pose → live-entity lights.
Each lease stores the exact delegate and publisher, detaches once, supports
retry if a later release fails, and is inert before dependent teardown.
Phase-7 entity-ready and appearance-applied subscriptions are deliberately
handled in I.7, where both publishers exist.
### 3.3 Canonical publication and late binding
The result publishes one canonical world/runtime identity. Phase 5 radar and
selection authorities bind only after those identities exist. The live-runtime
slot binds immediately after runtime construction and receives an exact-owner
unbind token for rollback.
Landblock-loaded hydration uses a deferred sink owned by the result. Phase 7
binds it to `LiveEntityHydrationController`; no Phase-6 callback captures a
future nullable controller.
Portal tunnel ownership remains:
```text
fallback slot acquire -> prepared transfer -> final frame owner
```
The fallback slot and final owner never co-own the tunnel.
### 3.4 I.6b automated gates
- exact production construction order and canonical slot-bind position;
- failure at every construction/subscription/bind/publication edge;
- canonical runtime/world identity through radar, selection, devtools, and
frame diagnostics sources;
- loaded, pending, rebucketed, hidden, and deleted live records retain the
existing presentation behavior;
- event leases detach exact delegates once and survive callback reentrancy;
- no duplicate mesh, particle, light, script, selection, or collision owner;
- portal fallback acquire/prepare/transfer failure matrix;
- landblock publisher ordering and retirement symmetry;
- App Release build/tests, full Release build/tests, divergence/source audit;
- three independent self-review passes as in I.6a.
### 3.5 I.6b completion evidence
I.6b moved the complete Phase-6 body into
`LivePresentationCompositionPhase`. The phase now owns the canonical live
runtime/world identity, live presentation and effect controllers, WB draw and
landblock publication owners, selection/radar authorities, optional paperdoll,
and portal/sky/particle resources. Named exact-owner leases replace the former
anonymous Phase-6 subscriptions. A deferred landblock-loaded sink preserves
the Phase-7 hydration edge without capturing a future window field.
Construction uses transactional acquisition ownership throughout, including
portal-fallback and sky-shader lifetime rollback. The focused binding,
rollback, retry, stale-release, and source-boundary tests pass. The App Release
gate passes 3,407 tests / 3 intentional skips, the complete Release suite
passes 7,779 tests / 5 intentional skips, and the clean solution build retains
only the 17 test-project warnings tracked by #228. `GameWindow.cs` is 2,840
raw lines after I.6b.
Behavior/fidelity, architecture/ownership, and adversarial failure reviews
found no remaining actionable issue. I.7 session, streaming, and hydration
composition is the next active cut.
## 4. Production cutover rules
- Each subcommit leaves `OnLoad` calling the new phase and leaves later phases
consuming the typed result; no duplicate inline construction remains.
- Existing steady-state code may temporarily read published fields until I.8,
but new phase code cannot capture the window.
- I.6a may bind its selection/session/diagnostic authorities at the existing
Phase-6/7 construction sites. I.6b moves the Phase-6 binds into its result;
I.7 moves the remaining Phase-7 binds into the session/streaming phase.
- No broad interface exposes the window. Dependencies are explicit records of
stable owners and focused sources.
- No behavior change is accepted merely to simplify composition.
## 5. Exit evidence
I.6 is complete only when both production phase bodies have moved, the old
inline blocks and host callback wrappers are deleted, the late-source and
failure matrices pass, full Release gates are green, documentation/memory
reflect the new boundary, and both commits are individually buildable.

View file

@ -1,202 +0,0 @@
# GameWindow Slice 8 — Checkpoint I.7 Session and Player Composition
**Status:** Complete 2026-07-22
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
**Scope:** Ordered-composition Phase 7 only
## 1. Outcome
Move the complete streaming, session, hydration, local-player, and teleport
construction body out of `GameWindow.OnLoad` without changing accepted
movement, network, world-reveal, combat, portal, or render-frame behavior.
`GameWindow` remains a write-only publication surface and shutdown shell.
This is a structural ownership move. It does not alter retail algorithms or
add a divergence-register row.
## 2. Exact production order
`SessionPlayerCompositionPhase` preserves the current order:
1. resolve near/far streaming radii from the immutable quality snapshot and
legacy override;
2. construct and start `LandblockStreamer` under immediate rollback ownership;
3. construct `StreamingController`, origin recentering, runtime settings
targets, and `WorldRevealCoordinator`;
4. construct the network-update bridge, sealed-dungeon classifier,
projection materializer, and world-origin coordinator;
5. construct and bind `LiveSessionController` plus local physics timestamps;
6. construct teardown, deletion, hydration, network-update, liveness, and
inbound-session owners;
7. bind component teardown, landblock hydration, parent acceptance,
same-generation network updates, entity-ready, and appearance-applied;
8. bind live combat operations;
9. construct mouse-look/gameplay input, streaming-frame, effect-frame,
spatial reconciliation, local animation/shadow/projection/frame, and
live-object frame owners;
10. construct player-mode and auto-entry owners and bind developer commands;
11. transfer the prepared portal tunnel into the sole local-teleport owner;
12. bind the teleport network sink and retained selection view plane;
13. publish one complete `SessionPlayerResult` and transfer rollback ownership.
No live connection starts in this phase. Terminal session start remains Phase
9 so a server cannot publish into a partial frame graph.
## 3. Typed inputs and outputs
`SessionPlayerDependencies` contains only stable App owners and state sources.
It never stores `GameWindow` or accepts a callback to a substantial window
method. Phase results supply content, render, interaction, and live-presentation
owners directly.
`SessionPlayerResult` carries the exact owners required by Phase 8 and
shutdown:
- streamer, streaming controller, origin recenter, and world reveal;
- live-session, hydration, network-update, liveness, and inbound-session
controllers;
- gameplay input, streaming-frame, local animation/shadow, live-object frame,
player-mode, and auto-entry owners;
- local teleport owner;
- one named runtime-binding aggregate.
`IGameWindowSessionPlayerPublication` publishes those exact identities only
after all mandatory resources and binds succeed. Replacement is rejected
before any field changes.
## 4. Focused owners and seams
### 4.1 Spawn-claim classifier
Move the memoized indoor spawn-claim range check from `GameWindow` into
`DatSpawnClaimHydrationClassifier`. It retains the exact cell-number boundary,
DAT lock, and one-claim memoization. The world-reveal coordinator receives its
typed `IsUnhydratable` method rather than a window callback.
### 4.2 Runtime bindings
`SessionPlayerRuntimeBindings` owns named exact-owner leases and releases them
in reverse order. Successful releases are removed immediately; failed releases
remain retryable without replaying completed work.
The aggregate owns:
- retained-UI live-session authority;
- runtime settings targets;
- live parent acceptance;
- hydration network bridge;
- exact `EquippedChildRenderController.EntityReady` delegate;
- exact `LiveEntityHydrationController.AppearanceApplied` delegate;
- combat attack operations;
- camera-pointer gameplay frame;
- developer player-mode and command-bus targets;
- local teleport network sink;
- retained selection view-plane authority.
The live-runtime component lifecycle remains attached through the Phase-6
binding owner because it must outlive session teardown and stay callable until
`LiveEntityRuntime.Clear` completes. Landblock-loaded hydration may use that
same late lifetime when its publisher outlives the session result.
### 4.3 Portal transfer
The prepared tunnel follows the existing single-owner transaction:
```text
Phase 6 fallback slot -> Phase 7 LocalPlayerTeleportController
```
If the destination factory throws, the fallback retains the tunnel. Once the
controller exists, Phase 7 immediately owns it for rollback. A later failure
disposes the controller and tunnel exactly once; the fallback is already empty.
### 4.4 Streamer and session rollback
The streamer is scope-owned before `Start`. Any later Phase-7 failure disposes
it, which joins the worker and releases pending work. The live-session
controller is also scope-owned immediately. Rollback order is:
1. detach external bindings;
2. dispose local teleport if transfer completed;
3. dispose live-session ownership;
4. dispose the streamer;
5. aggregate and retain any incomplete cleanup for retry.
Normal shutdown remains behaviorally ordered: quiesce input, gracefully retire
the live session and reset graph, withdraw frame borrowers, detach Phase-7
bindings, stop the streamer, clear live entities, then release presentation and
GL owners.
## 5. Publication and cutover
- `OnLoad` calls one `SessionPlayerCompositionPhase.Compose(...)` paragraph.
- The old radius, streamer, session, hydration, player, and teleport
construction body is deleted.
- Phase 8 consumes `SessionPlayerResult`; it does not rediscover the new owners
through nullable window fields.
- Existing steady-state/reset code may temporarily read published fields until
Checkpoint I.8 completes the root cutover.
- No second GUID map, world state, input subscriber, portal owner, or session
authority is introduced.
## 6. Automated gates
### Construction and rollback
- exact construction trace, including streamer-start and portal-transfer
positions;
- failure before and after streamer start stops the worker exactly once;
- failure at every bind/publication boundary releases in reverse order;
- partial cleanup retries only failed edges and never replays completed ones;
- portal destination-factory failure preserves fallback ownership;
- later failure disposes the transferred teleport owner exactly once;
- publication rejects replacement atomically.
### Binding and lifetime
- exact-owner unbind and stale-token rejection for every new deferred seam;
- `EntityReady` and `AppearanceApplied` detach the exact delegates once;
- callbacks are safe before bind and inert after deactivation where the source
contract permits it;
- live-runtime component teardown stays bound until live records are cleared;
- session/UI/command targets cannot retain a failed Phase-7 graph;
- runtime settings targets do not replay startup values when bound;
- no duplicate input, combat, streamer, hydration, or portal owner.
### Behavior and structure
- live/offline gates and initial placeholder origin remain unchanged;
- near/far and legacy radius selection are byte-for-byte equivalent;
- spawn-claim cell boundaries preserve the former memoized behavior;
- session packet routing, hydration order, combat target selection, player-mode
auto-entry, and portal presentation remain unchanged;
- `GameWindow` contains no Phase-7 construction body or spawn-claim algorithm;
- App Release tests, clean solution Release build, and full Release tests pass;
- source/divergence audit and three self-review passes are clean.
## 7. Exit evidence
I.7 is complete when Phase 7 is the production path, every Phase-7 external
edge has explicit reversible ownership, failure and retry gates pass, the old
window body is gone, documentation/memory report the measured class size and
test totals, and the commit is independently buildable. I.8 then composes the
update/render roots and terminal session start over the typed result graph.
## 8. Completion evidence
- `SessionPlayerCompositionPhase` is the production Phase-7 path. The former
432-line radius/streaming/session/hydration/player/teleport block is absent
from `GameWindow`.
- `SessionPlayerRuntimeBindings` plus exact-owner tokens cover every Phase-7
late edge. Focused tests cover reverse retry, rebind, and stale-token safety.
- `DatSpawnClaimHydrationClassifier` owns the former window memo and exact
indoor-cell boundaries, including missing/empty DAT cases and reset.
- `GameWindow.cs` is 2,479 raw lines, down 13,244 lines (84.2%) from the
15,723-line campaign baseline.
- App Release: 3,420 passed / 3 intentional skips. Complete Release suite:
7,792 passed / 5 intentional skips. Clean solution build: 0 errors and the
17 pre-existing test warnings tracked by #228.
- Behavior/order, architecture/ownership, and adversarial rollback reviews are
clean. No gameplay mechanism changed and no divergence row was introduced.

View file

@ -1,250 +0,0 @@
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
**Status:** Complete 2026-07-22
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
**Scope:** Complete Phase 7's session-facing owners, then implement ordered
composition Phases 8 and 9.
## 1. Outcome
Finish the production startup graph without changing accepted input, update,
render, network, portal, or UI behavior:
1. complete Phase 7 before any frame root is visible;
2. construct update and render roots locally and publish the pair atomically;
3. start the optional live session as the absolute final startup operation.
The current ordering defect is structural: `GameWindow.OnLoad` publishes the
frame pair before the combat/diagnostic command targets and sole gameplay input
subscriber are attached. I.8 moves those pre-frame owners into the Phase-7
result, so native callbacks can never observe an incomplete input/command
graph.
This is an ownership move. Retail algorithms and wire behavior remain
unchanged, so no divergence-register row is added.
## 2. I.8a — complete session/player composition
**Completed 2026-07-22.** `LiveSessionRuntimeFactory` now owns the exact
reset/router/command graph without a window reference. Phase 7 publishes the
session host, exact-owner combat and diagnostic command bindings, and the sole
gameplay input subscriber before Phase 8 can expose a frame root. Desired
components have a focused canonical snapshot owner. The component-lifecycle
handoff into the earlier live-presentation phase is an exact, retryable
adoption lease, so any later Phase-7 fault rolls it back without stranding or
replaying teardown.
### 2.1 Focused session runtime factory
Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing
domain-specific construction of:
- `LiveSessionHostBindings`;
- reset manifest bindings;
- selection and entered-world callbacks;
- event and command routers;
- inventory, character, and social session bindings.
The factory receives focused owners and state slots, never `GameWindow` and
never a callback to a substantial window method. Move the existing reset and
router bodies verbatim. Preserve the exact reset order and the existing
retail citations around player-module identity/shortcut/component cleanup.
Add `DesiredComponentSnapshotState` beside `ShortcutSnapshotState` so session
routers mutate one focused state owner. `GameWindow.DesiredComponents` remains
an ABI-compatible read-only projection over that owner. Existing
`LocalPlayerIdentityState`, `LocalPlayerControllerSlot`, and
`ShortcutSnapshotState` remain canonical; no mirrored values are introduced.
### 2.2 Commands and sole gameplay subscriber
At the tail of `SessionPlayerCompositionPhase`, after player mode and teleport
exist but before result publication:
1. create `LiveSessionHost` through the focused factory;
2. create and bind `LiveCombatModeCommandController`;
3. create and bind `RuntimeDiagnosticCommandController`;
4. create `GameplayInputCommandController` and priority targets;
5. create and attach the sole `GameplayInputActionRouter`.
Add exact-owner `BindOwned` tokens to the combat and diagnostic command slots.
The existing gameplay router already owns both subscriptions transactionally;
Phase 7 scope-owns it immediately after construction and before `Attach`.
Extend `SessionPlayerResult` and its publication seam with the exact
`LiveSessionHost`, command owners, and optional gameplay router. Publication
rejects replacement before changing any field. Failure before publication
disposes the router, detaches command targets, and rolls back the existing
Phase-7 suffix in reverse order.
## 3. I.8b — atomic frame-root composition
**Completed 2026-07-22.** `FrameRootCompositionPhase` now constructs the
unchanged render and update graphs from explicit prior-phase results and
focused runtime owners. `GameFrameGraphSlot.PublishOwned` publishes and
withdraws one exact pair. Optional lifecycle automation has a Phase-8 binding
owner, and `WorldLifecycleResourceSnapshotSource` samples the same canonical
owners without window callbacks or mirrored counters. The Phase-8 contract now
receives the Phase-2 content result explicitly because scripts and particles
are real frame dependencies; this removes a hidden reach-back through the
window shell.
Add `FrameRootCompositionPhase` implementing the existing
`IFrameRootCompositionPhase` contract. Its dependencies are focused App owners;
prior phase results supply all resources created during startup.
Preserve the exact current construction order:
1. teleport/login/GL-state and render-live preparation;
2. render resource begin/clear/live phases;
3. weather, sky-PES, world environment, camera, visibility, settings preview,
root, animated-object, and building sources;
4. terrain, PView, scene-pass, and world-scene diagnostics;
5. optional lifecycle automation;
6. retained/devtools/private presentation;
7. render orchestrator;
8. live-frame coordinator, camera frame, and update orchestrator;
9. atomic update/render pair publication as the final Phase-8 action.
Add `GameFrameGraphSlot.PublishOwned`. Its lease withdraws only the exact pair
it published, is idempotent, and cannot withdraw a later replacement. Scope-own
the lease immediately. A failure before publication leaves the slot empty; a
failure after publication withdraws the exact pair unless result publication
has transferred lifetime to the window shutdown owner.
### 3.1 Automation and resource snapshots
Add `WorldLifecycleResourceSnapshotSource` now rather than retaining
`CaptureWorldLifecycleResourceSnapshot` on the window. It samples the same
canonical world, live-runtime, effect, particle, light, script, mesh, texture,
GPU-memory, managed-memory, render-diagnostic, and frame-profiler owners.
This advances the owner-extraction portion of Checkpoint K; K still changes
checkpoint timing, acknowledgement barriers, JSONL validation, and soak
comparison. No checkpoint semantics change in I.8.
`FrameRootRuntimeBindings` owns the optional automation late binding and any
other Phase-8 external edge. It uses reverse, retryable, no-replay cleanup.
Shutdown withdraws the frame graph first, then detaches these frame-owned
bindings before borrowed session/presentation owners retire.
### 3.2 Publication
`FrameRootResult` contains:
- exact update and render roots;
- optional lifecycle automation owner;
- Phase-8 runtime bindings;
- the `LiveSessionHost` borrowed from Phase 7 for terminal start.
`IGameWindowFrameRootPublication` stores only owners needed by steady-state or
shutdown. Replacement is rejected atomically.
## 4. I.8c — terminal start and window cutover
**Completed 2026-07-22.** `SessionStartCompositionPhase` invokes the exact
Phase-7 host through the Phase-8 result and owns the existing missing-credential
and failed-start diagnostics. The phase call is the final statement in
`GameWindow.OnLoad`; no binding, publication, allocation, or diagnostics setup
follows it.
Add `SessionStartCompositionPhase` implementing
`ISessionStartCompositionPhase<FrameRootResult>`. It invokes only
`LiveSessionHost.Start(RuntimeOptions)` and preserves the existing diagnostics
for missing credentials and failed startup.
`GameWindow.OnLoad` ends with:
```text
Phase 7 Compose
Phase 8 Compose and atomic pair publication
Phase 9 Start
return
```
There is no callback attachment, binding, publication, diagnostics setup, or
other work after Phase 9. Delete the old inline frame construction, session
factory/reset/router methods, snapshot method, and terminal status switch.
The existing typed nine-phase `GameWindowCompositionPipeline` remains the
executable order/failure oracle. Production `OnLoad` uses the same phase
interfaces and result chain explicitly because later-phase dependency records
are assembled from earlier concrete results; it must not introduce a retained
mega-context, service locator, or delegate façade around `GameWindow`.
## 5. Ownership and rollback order
Before frame publication, Phase-7 rollback is:
1. gameplay router detach;
2. diagnostic and combat command unbind;
3. remaining session/player bindings;
4. teleport, live session, and streamer teardown.
After frame publication, Phase-8 rollback is:
1. exact frame-pair withdrawal;
2. automation/other frame binding detach;
3. no disposal of resources borrowed from earlier successful phases.
Normal shutdown retains the frozen barrier order:
1. logical input/command quiescence;
2. live-session convergence;
3. physical input callback detach;
4. frame-pair withdrawal;
5. frame then session/player late-binding detach;
6. remaining session, live-entity, effect, render, content, GL, and native
owners.
## 6. Automated gates
### Session/runtime
- exact live-session host construction and reset trace;
- no `GameWindow` field or delegate capture in the focused factory;
- exact-owner combat/diagnostic bind, competing-owner rejection, idempotent
release, and rebind;
- gameplay router attaches before frame publication and rolls back from either
partial subscription;
- failed Phase-7 publication leaves no command/input/session target behind.
### Frame roots
- exact render/update leaf construction order;
- frame slot is empty at every pre-publication fault;
- exact pair publishes once and both native callbacks see the same generation;
- rollback withdraws only the published pair and never a later replacement;
- automation disabled path acquires nothing;
- automation enabled path binds once, detaches exactly, and samples the same
resource values as the former window method;
- frame result publication rejects replacement atomically.
### Terminal start and structure
- terminal start runs only after frame publication and every input/command
attachment;
- missing credentials and failed starts preserve current diagnostics;
- no operation follows `Start`;
- `GameWindow` contains no live-session binding factory, frame-root
construction body, resource-snapshot algorithm, or direct frame-pair
publication;
- no backend dependency enters Core and panels remain on UI abstractions;
- App Release tests, clean solution Release build, and complete Release suite
pass;
- behavior/order, architecture/ownership, and adversarial failure reviews are
clean.
## 7. Commit sequence
1. `docs(architecture): plan frame roots and terminal session start`
2. `refactor(app): complete session startup composition`
3. `refactor(app): compose atomic frame roots`
4. `refactor(app): make session start terminal`
Each implementation commit is independently buildable and preserves the
protected pre-existing `TransitionTypes.cs`, `.test-out/`, and `logs/` changes.
I.9 performs the complete Checkpoint-I corrected-diff review and documentation
closeout after these cuts land.

View file

@ -1,180 +0,0 @@
# GameWindow Slice 8 Checkpoint J — lifetime and shutdown
**Status:** Complete 2026-07-22.
**Parent:**
[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md),
Checkpoint J.
**Integrated baseline:** `530b4bd8`; Checkpoint I is complete, the production
startup path uses the tested nine-phase pipeline, `GameWindow.cs` is 1,945 raw
lines, App Release passes 3,431 tests / 3 skips, and the complete Release suite
passes 7,803 tests / 5 skips.
**Behavior rule:** preserve the accepted disconnect, reset, callback cutoff,
frame withdrawal, GL dependency, and native-window-last order. This checkpoint
changes lifetime ownership and failure reporting, not gameplay behavior.
**Result:** `GameWindowLifetime` now owns the immutable typed root snapshot,
retryable staged transaction, structured terminal report, and native window.
Persistent physical-detach failures are retried and reported without stranding
later owners; session convergence and GPU drain remain hard barriers. Clean
native release drops the completed transaction/root graph, while terminal
abandonment retains it. `GameWindow.cs` is 1,625 raw lines, down 14,098 lines
(89.7%) from the campaign baseline. The App Release gate passes 3,441 tests / 3
skips; the complete Release gate passes 7,813 / 5 skips; the solution build has
only #228's 17 existing test warnings. Three corrected-diff review passes are
clean and no retail-divergence row changed.
## 1. Outcome
Replace `GameWindow.CreateShutdownTransaction` and the window-owned transaction
field with one pre-window `GameWindowLifetime`. The lifetime owns:
- the single staged `ResourceShutdownTransaction`;
- the first immutable typed shutdown-root snapshot;
- the native window root once `Window.Create` succeeds;
- explicit `Active`, `RetryableIncomplete`, `Complete`,
`CompleteWithCleanupFailures`, and `AbandonedIncomplete` state;
- one immutable report returned by every terminal repeated call.
`GameWindow.OnClosing` performs a synchronous non-terminal attempt while the GL
context is current. `GameWindow.Dispose` retries the retained transaction and
then releases the native window. A persistent hard barrier forces the named,
logged last-resort native fallback and terminal abandonment; it never reports a
clean shutdown.
## 2. Typed ownership boundary
`GameWindowShutdownRoots` is a teardown-only aggregate of cohesive records, not
a runtime service locator. It exposes no gameplay operations and is captured
exactly once when shutdown first starts:
1. `IngressShutdownRoots` — quiescence, command/input logical cutoff, physical
callback owners, session controller, and native callback binding.
2. `FrameShutdownRoots` — atomic frame publication, frame/session bindings, and
UI/session late bindings.
3. `LiveShutdownRoots` — retained UI, interaction/streaming owners, canonical
live runtime, hook/effect/audio owners, and effect state.
4. `RenderShutdownRoots` — GPU-flight barrier, private/render frontends,
texture/mesh/render resources, dedicated atlas/sky roots, and construction
cleanup.
5. `PlatformShutdownRoots` — DAT mapping, input context, and GL.
The records retain exact object identities. Operations call those identities
directly; no stage stores `GameWindow`, a callback facade into it, a dictionary,
or an untyped service provider. The window may retain borrowed fields until the
host itself dies, but only `GameWindowLifetime` drives release after capture.
## 3. Transaction policy
Extend `ResourceShutdownOperation` with a defaulted policy:
```csharp
ResourceShutdownOperationPolicy.HardBarrier
ResourceShutdownOperationPolicy.ReportAndContinue
```
Existing call sites remain hard by default. All operations in a stage are
attempted independently. Successful operations never replay. A hard failure
remains pending and protects later stages. A report-and-continue operation is
retried once; a second failure becomes a structured cleanup failure, marks that
operation settled, and allows the stage to converge. The transaction exposes
its current stage and immutable cleanup failures without swallowing exceptions.
Only physical ingress removal is soft. Logical quiescence, live-session
convergence, frame withdrawal, owner disposal, GPU drain, DAT/input/GL release,
and all dependency barriers remain hard.
## 4. Frozen stage order
The manifest outside `GameWindow` uses this exact order:
1. `host and session barriers`
- stop accepting callbacks;
- deactivate combat/diagnostic/retained/gameplay/pointer/dispatcher/device
paths;
- quiesce retained/devtools input;
- dispose the live-session controller and require converged disposal.
2. `physical ingress cleanup` — settings VM, retained/gameplay/pointer/
dispatcher/device bindings, retained/devtools input, and native window
callbacks; every operation is report-and-continue.
3. `frame borrowers` — withdraw the exact atomic frame pair, then frame-root and
session/player bindings.
4. `session dependents` — late bindings, mouse presentation, retained UI,
combat/item/container owners, streamer, and equipped children.
5. `live entities` — clear canonical runtime while teardown callbacks live.
6. `effect dispatch edges` — live bindings, effect advance, hook registrations.
7. `live entity dependents` — lights, presentation, remote teleport, effect
state, and audio.
8. `submitted GPU work` — hard wait barrier.
9. `render frontends`.
10. `shared texture owners`.
11. `mesh adapter`.
12. `remaining render owners`.
13. `dedicated render resources` — sky shader then terrain atlas.
14. `failed render construction cleanup`.
15. `frame flight owner`.
16. `content mappings`.
17. `input context`.
18. `OpenGL context`.
19. Native window release/fallback outside the transaction and last.
The former late native-callback operation moves to stage 2. Logical cutoff makes
copied callbacks inert before the potentially long session close, so a broken
event remove cannot prevent F653/transport teardown or strand unrelated owners.
## 5. State and reporting
`GameWindowLifetimeReport` contains status, blocked stage, structured soft
cleanup failures, and the retained hard/native error when present.
- `TryComplete(roots)` captures roots once and never performs native fallback.
It is used by `OnClosing`.
- `CompleteAndReleaseNativeWindow(roots)` retries hard work, then disposes the
native window. If hard convergence still fails, it retains the blocked-stage
report, marks eligible partial owners abandoned, releases the native fallback,
and becomes `AbandonedIncomplete`.
- Clean transaction + no soft failures becomes `Complete`.
- Clean transaction + soft failures becomes `CompleteWithCleanupFailures`.
- Reentrant completion is inert while the outer call owns progress.
- Every terminal repeated call returns the same report and touches no owner.
- Native window publication is single-owner, pre-Run-safe, and released once.
## 6. Implementation sequence
1. Add operation policy, structured cleanup-failure exposure, and focused
transaction tests for transient/persistent soft cleanup, mixed hard/soft
stages, retry/no-replay, and reentrancy.
2. Add `GameWindowLifetime`, report/state types, typed root records, and the
manifest factory. Test every stage trace using fakes, optional-null roots,
persistent session/GPU barriers, persistent soft detach, clean/native-failure
finalization, terminal abandonment, and repeated/reentrant calls.
3. Publish the native window immediately after creation; replace the window
transaction/manifest with one typed root capture and the two narrow lifetime
calls. Remove all shutdown feature bodies and update source-shape tests to
inspect the focused owner.
4. Exercise constructed-never-run, Load-never-fired, partial composition
prefixes, normal close then Dispose, direct Dispose, transient retry, and
optional audio/devtools/retained-UI absence.
5. Run behavior-order, architecture/ownership, and adversarial failure review
passes; correct every finding. Run focused tests, App Release, solution
Release build, and complete Release tests.
6. Reconcile architecture, roadmap, milestones, issues, AGENTS/CLAUDE, and
durable memory; commit Checkpoint J as one bisectable ownership unit.
## 7. Acceptance
- No shutdown stage or substantial release body remains in `GameWindow`.
- No lifetime/manifest type stores `GameWindow` or a general callback facade.
- Session and GPU hard failures never run protected dependent stages.
- Persistent physical-detach failure does not strand frame/live/render/content/
input/GL owners and produces `CompleteWithCleanupFailures`.
- Both frame roots become unreachable before any borrower retires.
- Every acquired owner releases once; absent owners are no-ops; failed owners
retry without replaying completed work.
- Direct, repeated, concurrent-safe/reentrant, close-then-dispose, and every
partial-load path are deterministic.
- Native window release is last. Hard non-convergence becomes named terminal
abandonment; repeated calls are inert and preserve the same report.
- App production builds without warnings; only #228's 17 test warnings remain;
focused, App, and complete Release suites pass.
- No retail-divergence row is added unless review discovers an actual behavior
change; this ownership correction itself is behavior-preserving.

View file

@ -1,225 +0,0 @@
# GameWindow Slice 8 Checkpoint K — canonical soak snapshots
**Status:** Complete 2026-07-22 (`bca41487`, corrected gate semantics
`6c5e0604`).
**Parent:**
[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md),
Checkpoint K.
**Integrated result:** `GameWindow.cs` is 1,622 raw lines, App Release passes
3,451 tests / 3 skips, and the complete Release suite passes 7,823 tests / 5
skips.
**Issue:** [`#232`](../ISSUES.md#232--nine-stop-soak-process-memory-gate-lacks-canonical-owner-snapshots).
**Behavior rule:** this checkpoint changes diagnostic capture and connected-gate
evidence only. It does not change rendering, streaming, gameplay, resource
budgets, or the existing process working/private-memory thresholds.
## 1. Outcome
Make every scripted `checkpoint <name>` a deferred, acknowledged render-frame
barrier. A checkpoint is written only after private presentation and the normal
render diagnostics phase have both observed the same immutable
`RenderFrameOutcome`. Its JSON contains the canonical world/reveal/resource
owner snapshot and that exact frame outcome. The script cannot execute the next
command until serialization and both writes succeed, fail, or are explicitly
cancelled during shutdown.
The nine-stop soak then consumes exactly nine ordered checkpoints, gates
canonical owner stability at the Caul return → Caul plateau same-location pair,
labels server-controlled entity/animation population changes as workload
warnings, and retains process residency as an unchanged secondary guard.
## 2. Runtime contract
### 2.1 Acknowledgement token
Replace the synchronous automation method with a request contract equivalent
to:
```csharp
enum RetailUiAutomationCheckpointStatus
{
Pending,
Succeeded,
Failed,
Cancelled,
}
interface IRetailUiAutomationCheckpoint
{
RetailUiAutomationCheckpointStatus Status { get; }
string? Error { get; }
}
bool TryRequestCheckpoint(
string name,
out IRetailUiAutomationCheckpoint? checkpoint,
out string error);
```
The production token also retains the assigned request sequence and validated
name across the update → render edge. Only the controller can transition it,
exactly once, from Pending to a terminal state.
`RetailUiAutomationScriptRunner` stores one token together with the active
command index. While Pending, repeated ticks return without enqueueing again or
advancing the command. Success clears the token and advances once. Failure or
cancellation clears it and stops on the token's exact error. Disposal still
releases held input; runtime shutdown is responsible for cancelling queued
tokens.
### 2.2 FIFO request owner
`WorldLifecycleAutomationController` becomes the sole FIFO owner and an
`IDisposable` frame-phase participant:
- validation failure creates no request and consumes no sequence;
- accepted requests receive one monotonic sequence and enter one FIFO;
- a successful render drains accepted requests in order;
- each request serializes one `WorldLifecycleCheckpoint` containing its
sequence/name, current reveal state, the exact current `RenderFrameOutcome`,
and one canonical resource snapshot;
- Succeeded is published only after the JSONL append and named JSON write both
complete;
- capture/serialization/I/O failure marks only that request Failed with
contextual sequence/name error and does not throw out of rendering;
- shutdown marks every still-pending request Cancelled with an explicit error;
- enqueue after shutdown fails synchronously and cannot create an orphan token.
The controller is adopted by `FrameRootRuntimeBindings` separately from its
retained-UI late binding, so the binding detaches and pending requests cancel
under the existing retryable frame-root lifetime.
### 2.3 Post-diagnostics render phase
Add a narrow `IRenderFramePostDiagnosticsPhase` with a no-op implementation.
`RenderFrameOrchestrator` calls it exactly once after:
1. resource preparation;
2. world rendering;
3. private presentation and screenshots;
4. `IRenderFrameDiagnosticsPhase.Publish`;
5. then checkpoint drain;
6. finally the existing GPU-flight close.
If an earlier render phase fails, post-diagnostics is not called and the FIFO
remains pending for the next successful frame. An unexpected post-phase throw
uses the existing render failure/recovery path, although production capture and
I/O failures are represented on tokens rather than thrown. No previous-frame
diagnostic value is paired with a current-frame owner snapshot:
`WorldLifecycleResourceSnapshotSource.Capture(RenderFrameOutcome)` takes
visible/total landblocks from the supplied outcome and reads the already-
published aggregate FPS/frame-time snapshot only after diagnostics runs.
## 3. Artifact and soak schema
`WorldLifecycleCheckpoint` adds the exact frame outcome. Existing reveal and
resource fields retain their names. The route adds these nine commands in this
order at the end of each settled destination block:
1. `caul-baseline`
2. `sawato-baseline`
3. `rynthid`
4. `aerlinthe`
5. `sawato-return`
6. `holtburg`
7. `caul-return`
8. `sawato-plateau`
9. `caul-plateau`
`run-connected-r6-soak.ps1` reads the JSONL only after the route completes and
requires one row for every expected name, exact sequence 19, exact order, and
the current process id. It embeds those records in the report.
Every checkpoint hard-gates zero:
- pending live teardowns;
- pending landblock retirements;
- staged mesh uploads and bytes;
- composite warmup work.
Every route teleport supplies an explicit identity quaternion so the timed turn
begins from a reproducible heading. The same-location Caul return → Caul
plateau comparison requires exact equality for the deterministic loaded/total
world domain, while visible-landblock and authoritative entity/animation/live-
entity changes are named workload warnings. Teardown/retirement queues, staged
work, and composite warmup remain hard zero at every checkpoint. Mesh, atlas,
tracked-GPU, composite/particle-texture, and VFX owner counts may retire and
shrink normally; any growth is a hard leak failure when the visible and
authoritative workload is stable, and a named owner-growth warning when that
workload changed. Particles, emitters, and active scripts are always transient
workload warnings rather than silent exclusions. The report therefore says
which canonical owner grew without mistaking legitimate retirement for a leak.
Managed used/committed deltas are reported diagnostically. Existing
working/private-memory, update-p95, and allocation-p50 thresholds remain byte-
for-byte unchanged as the secondary residency/performance guard.
## 4. Implementation sequence
1. Add token states and change the automation runtime/script runner to the
one-token-per-command polling contract. Cover delayed success, repeated
pending ticks, failure, cancellation, invalid request, and disposal while
pending.
2. Refactor `WorldLifecycleAutomationController` into the FIFO request owner;
pass the frame outcome into snapshot capture and write it into artifacts.
Cover FIFO order, sequence stability, write failure, no duplicate drain,
shutdown cancellation, and enqueue-after-dispose.
3. Add the post-diagnostics phase to `RenderFrameOrchestrator`, wire the
controller through `FrameRootCompositionPhase`, and adopt its lifetime
separately from the late UI binding. Pin exact phase order, skipped drain on
prior failure, post-phase failure recovery, and partial-composition cleanup.
4. Add the nine route commands. Parse/validate JSONL in the soak, attach the
canonical checkpoints to the report, add zero-work and same-location owner
gates, and preserve the old process/performance formulas verbatim.
5. Run behavior/order, architecture/ownership, and adversarial failure review
passes. Correct findings and re-run focused App tests, App Release, Release
build, and the complete Release suite.
6. Run two fresh-process connected nine-stop routes before closing #232. Both
must exit gracefully, produce exact ordered checkpoints, pass canonical
owner/cache gates, and pass the unchanged process residency guard.
7. Reconcile architecture, roadmap, milestones, issues, AGENTS/CLAUDE, and
durable memory; commit Checkpoint K as its own bisectable diagnostic unit.
## 5. Automated acceptance
- one checkpoint command creates one request, one sequence, and one artifact;
- a pending token blocks all later script commands without duplicate enqueue;
- render failure delays rather than loses or duplicates the request;
- diagnostics publication precedes same-frame capture;
- independent accepted requests drain FIFO and retain name/sequence identity;
- capture/write failure reaches the script as the exact failed-token error;
- shutdown reaches pending scripts as explicit cancellation;
- no controller/runtime reference points back to `GameWindow`;
- all nine route names and sequences are exact and appear in the soak report;
- canonical pending/staging/warmup work is zero at every checkpoint;
- Caul return → plateau deterministic owners/caches are unchanged, while
authoritative workload deltas are explicitly warned;
- working/private-memory thresholds and update/allocation formulas are
unchanged;
- focused, App, and complete Release suites pass with no new warnings.
No visual gate is required for K because it changes diagnostic observation
only. Checkpoint L owns the final connected framebuffer and user visual gate.
## 6. Completion evidence
The focused checkpoint suite passes 74 tests. The Release build succeeds with
only the 17 pre-existing test-project warnings tracked by #228. Two independent
fresh-process routes produced all nine ordered render-frame checkpoints and
closed gracefully:
- `connected-r6-soak-20260722-201335.report.json`: 403.139 seconds; Caul
return-to-plateau working/private deltas +20.2/+10.3 MiB; update p95 1.5 ms.
- `connected-r6-soak-20260722-202025.report.json`: 403.021 seconds; Caul
return-to-plateau working/private deltas +165.0/+157.2 MiB; update p95 1.3 ms.
Every checkpoint had zero pending teardown/retirement, staged upload work, and
composite warmup work. Neither run found unconfounded canonical owner growth,
and both passed the unchanged process-residency and performance limits.
The first connected implementation run also proved that exact cache equality
was the wrong leak predicate: visible landblocks and authoritative entities
changed while mesh/GPU/cache owner counts decreased through legitimate
retirement. The corrected contract therefore hard-fails owner *growth* when
the workload is stable, reports named growth warnings when the authoritative
or visible workload changed, and permits shrinking. It does not weaken the
process limits or hide an identified owner leak.

View file

@ -1,160 +0,0 @@
# GameWindow Slice 8 Checkpoint L — campaign closeout
**Status:** Automated closeout complete 2026-07-22; final user visual matrix
pending.
**Parent:**
[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md),
Checkpoint L.
**Integrated baseline:** `6c5e0604`; Checkpoints AK are implemented.
`GameWindow.cs` is 1,622 raw lines, App Release passes 3,451 tests / 3
intentional skips, and the complete Release suite passes 7,823 tests / 5
intentional skips. Two fresh-process canonical nine-stop soaks pass with
graceful exits and the unchanged residency/performance limits.
**Behavior rule:** L proves and records the shipped ownership state. It may
correct documentation, test/gate coverage, or a defect demonstrated by the
audit, but it does not add gameplay, alter retail behavior, loosen a gate, or
perform the separately deferred `GameEntity` aggregation.
## 1. Outcome
Close the eight-slice thin-`GameWindow` campaign only after every exit
criterion has current evidence:
- the host is a native construction/callback shell with no AC gameplay
algorithm, entity scan, packet builder, DAT landblock builder,
animation-part composer, or draw-graph body;
- update, render, input, session, framebuffer, and shutdown callbacks are
narrow typed handoffs;
- startup and shutdown have one ordered, transactional, retryable ownership
graph without a service locator or callback facade into the host;
- extracted owners have focused tests and symmetric teardown;
- the Release build/test, connected lifecycle/reconnect, canonical soak, and
framebuffer gates preserve the accepted behavior;
- architecture, roadmap, milestones, issues, divergence bookkeeping, durable
memory, and both session instruction files agree with the code.
Line count is reported as a structural signal, not substituted for those
ownership requirements.
## 2. Completion audit
### 2.1 Host boundary and dependency direction
Measure raw lines, fields, methods, and the remaining callback bodies from the
actual source. Run the Slice 8 boundary/source-shape suites and inspect every
remaining `GameWindow` method. Search for prohibited network packet assembly,
entity/world scans, DAT build loops, part-transform composition, render graph
bodies, direct environment reads, and anonymous device subscriptions. Any
substantial body or callback into the host reopens the responsible checkpoint;
it is not waived because the file is below the target size.
Verify the composed owners and their tests cover:
- ordered partial-load rollback and terminal session start;
- exact frame-pair publication and one update/render handoff;
- logical callback cutoff before session retirement and physical detach;
- hard session/GPU shutdown barriers, soft physical-cleanup reporting,
retry/no-replay progress, and native-window-last release;
- deferred checkpoint acknowledgement, same-frame post-diagnostics capture,
FIFO order, cancellation, and shutdown.
### 2.2 Three corrected-diff review passes
Review the complete Slice 8 diff and final source in three independent passes:
1. **Behavior/retail boundary:** no input, session, update/render, UI, camera,
settings, teleport, or gameplay behavior was silently redesigned; every
retained adaptation remains registered.
2. **Architecture/ownership:** no duplicate canonical state, broad runtime bag,
window back-reference, leaked subscription, conflicting disposer, or layer
inversion; all ownership transfer and teardown paths are symmetric.
3. **Adversarial failure:** partial acquisition at every phase, callback
concurrency, repeated/reentrant close, failed detach, session/GPU barrier
failure, deferred render failure, checkpoint cancellation, and process/GUID
reuse cannot strand or replay work.
Confirmed findings are fixed at their owner and all affected gates are rerun.
## 3. Automated and connected gates
Run, in order:
1. focused Slice 8 composition, callback, settings, frame, lifetime,
automation, and source-boundary tests;
2. App Release;
3. `dotnet build AcDream.slnx -c Release --no-restore`;
4. the complete Release suite;
5. `tools/run-connected-world-lifecycle-gate.ps1 -SkipBuild`, requiring the
capped six-stop lifecycle, exact semantic checkpoints, five capped PNGs,
code-zero authoritative graceful close, and immediate uncapped fresh-process
reconnect with its sixth PNG;
6. consume the two Checkpoint-K fresh-process nine-stop reports, requiring
exact nine-name/sequence/process identity, zero pending teardown/retirement,
staging and warmup work, no unconfounded canonical owner growth, unchanged
process/update/allocation limits, and graceful exit;
7. compare the six current lifecycle/reconnect PNGs with the accepted Slice 7
`9d7df1bf` baseline. If the old generated artifacts are absent, reconstruct
them from that exact commit in an isolated disposable worktree rather than
inventing a new baseline. Accept only live weather/particle timing,
authoritative vitals, and sub-frame camera-settling differences; geometry,
UI/paperdoll layering, private viewports, alpha/depth, and world reveal must
remain equivalent.
Generated logs and the isolated baseline worktree are evidence only and remain
untracked. The user's unrelated `TransitionTypes.cs` edit remains untouched.
## 4. Reconciliation and handoff
After all nonvisual evidence is green:
- mark K and the automated portion of L in the parent ledger;
- close #232 with both canonical soak reports and preserve #225, #153, #116,
#228, TS-50/TS-51/TS-53, and other carried work at their true scope;
- update `code-structure.md`, the architecture, roadmap, milestones,
`AGENTS.md`, `CLAUDE.md`, and `memory/project_gamewindow_decomposition.md`
with the same counts, commits, gates, next-work boundary, and do-not-retry
lessons;
- audit the retail divergence register and add/remove no row unless runtime
behavior actually changed;
- commit the evidence/doc closeout as a bisectable unit and rerun final
source/status checks.
The sole remaining pause is the user's connected visual matrix: first login
and radar; movement/mouse/resize/focus/combat; the shared inventory/skills/
spellbook panel; outdoor/building/dungeon/portal/paperdoll/particle/alpha
presentation; graceful close and fresh reconnect. Only that confirmation marks
L and the overall structural campaign complete.
## 5. Automated closeout evidence
- The final host is 1,622 raw lines. Source-boundary inspection finds no AC
algorithm, packet builder, entity scan, render graph, animation-part
composer, or stored `GameWindow` back-reference in an extracted owner.
- All 293 focused Slice 8 ownership, composition, callback, frame, lifetime,
automation, and source-boundary tests pass. App Release passes 3,451 tests / 3
intentional skips; the complete Release suite passes 7,823 / 5. The build's
17 test-project warnings are the unchanged #228 set.
- The connected lifecycle/reconnect gate passed in 314.4 seconds with graceful
capped and uncapped exits. Its uncapped reconnect reached 174.74 FPS / 5.72
ms at the final checkpoint. All six PNG artifacts are valid; the only route
warning is the expected 25 world-edge empty-landblock misses.
- Both Checkpoint-K nine-stop reports passed in about 403 seconds, produced the
exact names/sequences/process identity, and found no unconfounded canonical
owner growth under the unchanged residency/performance limits.
- The six current lifecycle PNGs were compared against artifacts rebuilt from
exact accepted Slice 7 commit `9d7df1bf` in an isolated worktree. The five
deterministic destinations preserve geometry, UI/paperdoll layering,
private viewports, depth/alpha, and world reveal. Facility Hub was nearly
pixel-identical (mean absolute RGB delta 0.91/255); the Aerlinthe captures
were 3.42 and 3.87/255. Holtburg and reconnect differences are live
entities, particles, vitals, and sub-frame camera timing. Capped login used a
different server-saved location and was checked only for complete rendering
and presentation layering.
- Three corrected-diff reviews are clean: behavior/retail boundary,
architecture/ownership, and adversarial lifecycle/failure handling. The
divergence audit found no new runtime behavior in K or L and requires no
register change.
The generated reports, screenshots, and disposable baseline worktree are
untracked evidence. The user's unrelated `TransitionTypes.cs` edit was not
modified or staged.

View file

@ -1,792 +0,0 @@
# GameWindow Slice 8 — composition and lifecycle shell
**Status:** Automated implementation and closeout complete 2026-07-22; final
user visual matrix pending.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 8.
**Baseline:** `96f8bfcf`; `GameWindow.cs` is 4,666 raw lines, 196 fields, and
70 methods. The Release suite passes 7,341 tests / 5 fixture or environment
skips. The connected lifecycle/reconnect and synchronized nine-stop soak gates
pass.
**Behavior rule:** This is the final behavior-preserving ownership slice. It
must not change accepted input priority, session/reset behavior, update/render
order, world presentation, quality settings, or retail gameplay behavior. It
may fix resource and callback lifetime defects proven by this slice's ownership
audit.
## Progress ledger
- [x] A — freeze construction/callback/shutdown order and delete proven dead or
test-facade residue.
- [x] B — make native-window callback intake an explicit reversible owner and
define host-quiescence failure semantics.
- [x] C — extract live-session host/reset/binding callbacks and verify the
embedded skill formula against named retail.
- [x] D — extract retail world-environment/day/weather behavior.
- [x] E — extract two-phase raw pointer/camera input, focus, and framebuffer
resize behind symmetric named subscriptions.
- [x] F — extract the sole gameplay input-action router plus focused combat and
diagnostic command owners.
- [x] G — extract two-phase persisted settings/display/quality ownership.
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
explicit single ownership and transfer seams.
- [x] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. Production and failure tests now
invoke the same pipeline and carry exact typed prior results. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
- [x] J — move the exact retryable shutdown manifest to a focused lifetime
owner and prove all partial-load/reentrant/retry paths. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md`](2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md).
- [x] K — in separate #232 commits, add canonical owner snapshots to every soak
checkpoint without weakening the process-memory guard. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-k-canonical-soak.md`](2026-07-22-gamewindow-slice-8-checkpoint-k-canonical-soak.md).
- [ ] L — corrected-diff reviews, focused and full Release gates, connected
lifecycle/soak, framebuffer comparison, documentation, and memory are
complete; only the final user visual handoff remains. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-l-closeout.md`](2026-07-22-gamewindow-slice-8-checkpoint-l-closeout.md).
Checkpoint K leaves `GameWindow.cs` at 1,622 raw lines. The focused lifetime
owner holds the typed shutdown roots, exact 18-stage dependency manifest,
retry/no-replay state, structured soft-cleanup failures, hard-barrier fallback,
and native-window-last release. Deferred same-frame checkpoint capture and two
fresh-process nine-stop routes close #232 without weakening the residency
guard. App Release passes 3,451 tests / 3 skips and the complete Release suite
passes 7,823 / 5 skips. All automated Checkpoint-L gates are complete.
Each checked checkpoint lands as a bisectable commit. A checkpoint is not done
while a new class delegates a substantial body back into `GameWindow`, stores
`GameWindow`, or replaces 196 fields with one runtime/service-locator bag.
## 1. Outcome and non-goals
At slice exit `GameWindow` is the native construction shell:
```text
GameWindow
├── creates Window + GL + input context
├── invokes ordered composition functions
├── forwards tiny Silk callbacks to typed owners
├── starts LiveSessionController last
└── asks GameWindowLifetime to close synchronously
```
The shell may retain injected Core/UI game-state roots whose replacement would
be the separately deferred `GameEntity`/client-state aggregation migration. It
must not retain AC algorithms, entity scans, session binding/reset bodies,
settings algorithms, the input switch, anonymous device subscriptions, or the
shutdown operation manifest.
This slice does **not**:
- alter the frozen twelve-phase update graph or ten-phase render graph;
- port a new retail host loop or retire TS-53;
- change input bindings, mouse feel, focus/reset semantics, camera formulas,
resize/DPI behavior, frame pacing, VSync, MSAA, quality, draw distance, or
resource budgets;
- change live-session authority, packet parsing, reset order, the 35-second
graceful F653 confirmation, reconnect, or transport teardown;
- change UI layout, magic, portal presentation, selection, world rendering, or
gameplay behavior;
- fold full `GameEntity` aggregation, headless mode, Linux support, or GPU
particle work into this campaign;
- loosen the soak's working/private-memory threshold.
## 2. Frozen host behavior
No fresh retail research is required for the host boundaries. One audited
exception exists: the embedded skill-formula helper currently cites ACE only,
so Checkpoint C must complete the named-retail pseudocode/cross-reference/test
workflow before moving or claiming that formula.
### 2.1 Startup and callbacks
Preserve this exact order:
1. before `Window.Create`, load persisted Display and resolve VSync/MSAA because
these are native-context attributes;
2. create the window and bind `DisplayFramePacingController` immediately;
3. register Load, Update, main Render, pacing Render, Closing, FocusChanged,
Move pacing, StateChanged pacing, and FramebufferResize in that order;
4. in `OnLoad`, acquire GL and input from the native window;
5. compose camera/DAT/effects/audio, persisted settings, optional devtools,
world/render resources, retained UI, live presentation, streaming, session,
teleport, then update/render roots;
6. call `LiveSessionController.Start` last, including when live mode or
credentials are absent, because its disabled path still performs the
canonical reset.
Main Render remains registered before pacing so the wait occurs after all frame
work and before Silk's swap. Direct raw MouseMove remains ordered after
SilkKeyboard/MouseSource + InputDispatcher subscriptions and before retained
`UiHost` device wiring.
### 2.2 Input, focus, and resize
Preserve the exact input-action priority:
1. pointer press/release transitions;
2. scroll;
3. combat press/release transitions;
4. Press/DoubleClick gate;
5. retained UI semantic actions;
6. selection interactions;
7. pressed movement/autorun;
8. the remaining UI/debug/player/combat/escape commands.
Mouse capture remains three distinct operations:
- focus loss and camera-mode exit use lifecycle end and may publish a final
movement update;
- session reset uses reset semantics and cannot send into the ending session;
- process close retires the session first, then releases presentation without
an active session.
FramebufferResize continues to ignore non-positive sizes, then updates GL
viewport, `ViewportAspectState`, camera aspect, and the forced devtools layout
reset. It must not mutate retained `UiRoot` size; the next UI draw does that.
### 2.3 Session and reset
`LiveSessionController` remains the only session lifecycle owner. The extracted
host adapter may hold typed binding/reset collaborators but no second session,
generation, identity, GUID, routing, or command map.
Preserve `LiveSessionResetManifest` exactly: projection/capture/UI first;
equipped projection before the canonical live runtime; live-runtime convergence
before identity reset; pending effects/hooks/presentation last. The AC skill
formula currently embedded in the window moves only after Checkpoint C adds its
missing named-retail citation and conformance tests (or records the proven
remaining adaptation).
### 2.4 Update and render
`OnUpdate` remains profiler scope plus one immutable
`UpdateFrameOrchestrator.Tick`. `OnRender` takes the existing one window-size
snapshot and calls one `RenderFrameOrchestrator.Render`. PView's
`FramebufferSize` source remains distinct from the logical Window.Size used by
the frame/UI/portal/screenshot path. TS-33 and TS-53 remain registered.
### 2.5 Shutdown
Closing synchronously completes the retryable transaction while the GL context
is current. Direct `Dispose` is the constructed-never-run, Run-failure, and
retry fallback. The native window is disposed only after successful completion
or the explicitly reported last-resort incomplete fallback described below.
The frozen dependency order is:
1. set a no-throw host-quiescence gate and converge live-session shutdown as
independent all-attempted **hard** operations; this guarantees F653 and
transport teardown before dependents;
2. attempt physical detachment of all window/Silk/UI ingress as explicitly
reportable **soft** operations: retry them, record any persistent failure,
but do not block now-safe owner/GPU teardown after quiescence;
3. withdraw **both** update and render frame graphs through their shared slot;
4. retire session/UI dependents;
5. clear the live runtime while teardown callbacks remain alive;
6. retire live dependents;
7. wait for submitted GPU work;
8. retire private/render frontends;
9. retire shared textures;
10. retire the mesh adapter;
11. retire remaining render borrowers, including terrain renderer;
12. retire separately owned borrowed assets, including terrain atlas and sky
shader, only after their respective renderer stage converges;
13. retire remaining render owners and frame-flight ownership;
14. close DAT mappings;
15. dispose the already-detached input context;
16. dispose GL last;
17. return to `GameWindow.Dispose`, which disposes the native window.
The logical quiescence gate makes callbacks inert even when a physical event
unsubscribe reports failure. `ResourceShutdownTransaction` therefore gains an
explicit non-blocking/reportable operation policy for physical detach only.
Those failures remain in the final result and are never swallowed, but they do
not strand unrelated resources. Session convergence and GPU drain remain hard
barriers. Operations within a stage remain independent and all-attempted. Later
stages remain protected until the current stage converges. Successful operations are
never replayed on retry or reentrant completion.
Silk's `Closing` event is not cancellable. `OnClosing` therefore performs the
first synchronous completion attempt and reports an incomplete transaction
without pretending success. The outer `Dispose` retries the same retained
transaction. If a persistent native/driver failure still prevents convergence,
the native window/context is disposed as the explicit last-resort safety net
and the incomplete result is logged with the blocked stage; it is never
reported as a clean shutdown. Tests pin this fallback instead of assuming a
close-cancellation mechanism Silk does not provide.
The lifetime state is explicit: `Active`, `RetryableIncomplete`, clean
`Complete`, `CompleteWithCleanupFailures`, or terminal `AbandonedIncomplete`.
Persistent soft-detach failure followed by otherwise complete teardown produces
`CompleteWithCleanupFailures`; repeated Dispose is inert and returns the same
immutable non-clean report. Hard-barrier retry is allowed only while the native
context is retained. Last-resort native disposal moves to
`AbandonedIncomplete`; every later Dispose is inert and reports the retained
blocked-stage result without touching GL, callbacks, or the already-destroyed
window.
## 3. Architecture and interfaces
### 3.1 Native window intake
Add a focused reversible binding equivalent to:
```csharp
internal sealed class SilkWindowCallbackBinding : IDisposable
{
public static SilkWindowCallbackBinding Create(
IWindow window,
WindowCallbackTargets targets,
DisplayFramePacingController pacing);
public void Attach();
}
```
`WindowCallbackTargets` is a fixed typed set for Load, Update, Render, Closing,
FocusChanged, and FramebufferResize. It is not a general callback list. The
binding is published into its lifetime slot after `Create` and before `Attach`
begins, so even an attach failure plus rollback failure retains a cleanup owner.
Attach treats the current edge as possibly acquired before calling a custom
event add accessor and rolls back in reverse order if registration fails.
The explicit lifecycle state monitor is released around every external event
accessor; external cleanup waits for Attaching/Detaching to converge, while a
gate-owned reentrant cleanup reports typed deferred completion instead of
deadlocking or falsely succeeding. Dispose detaches in reverse registration
order, joins concurrent callers, is idempotent/reentrant-safe, and makes later
window events inert. `GameWindow` clears the retained slot only after terminal
disposal. The separate pacing Render callback remains ordered after main Render.
Every callback enters the shared no-throw host-quiescence monitor, so external
shutdown drains an admitted callback, Closing can stop reentrantly, and a failed
physical unsubscribe cannot re-enter a retired owner.
### 3.2 Input owners
Use two focused owners:
- `CameraPointerInputController` owns named raw MouseMove subscriptions,
pointer position, camera-mode cursor transitions, focus loss, typed scroll
operations, and the existing mode-specific sensitivities. Construction is
deliberately two-phase: `AttachRaw` runs after dispatcher/source wiring and
before retained `UiHost` wiring; `BindGameplayFrame` fills a focused deferred
slot after `GameplayInputFrameController` exists. It consumes canonical
`LocalPlayerModeState`, `ChaseCameraInputState`, input capture, and camera
owners.
- `GameplayInputActionRouter` is the **only** gameplay subscriber to
`InputDispatcher.Fired` and owns the frozen priority graph above. Its pointer
and scroll edges call typed operations on `CameraPointerInputController`; the
pointer owner does not independently subscribe to `Fired`. The router calls existing typed
retained-UI, selection, movement, player-mode, interaction, combat-command,
and diagnostic-command owners.
The window binding, router Fired subscription, pointer raw MouseMove,
Combat/Camera events, dispatcher-to-source links, source-to-Silk links, and
retained-UI-to-Silk links must all be named and reversible. Shutdown's first
stage deactivates them without disposing the input context or the UI owners
needed by session reset. `UiHost` therefore exposes a separate idempotent input
deactivation seam; its later full Dispose owns windows and rendering. Do not
replace these edges with anonymous lambdas retained by a generic subscription
bag.
Every wrapper on those edges—including Silk sources, dispatcher, raw pointer,
retained UiHost, and optional devtools input—checks the same quiescence gate (or
performs its no-throw logical deactivate in stage 1). No device callback can
re-enter during the potentially 35-second live-session close while physical
event removal waits for the following soft-detach stage.
Every multi-event Attach is transactional: if the Nth subscription fails, the
already-attached prefix is removed in reverse order before the exception
escapes. This applies to native window, dispatcher/source/Silk, raw pointer,
Combat/Camera, retained `UiHost`, and optional devtools input bindings.
### 3.3 Session and world environment
- A focused App session host owns the current selection/entered/reset/binding
factories around `LiveSessionLifecycleHost`, `LiveSessionEventRouter`, and
`LiveSessionResetManifest`. It resolves the current session through
`LiveSessionController`; it does not mirror it.
- `WorldEnvironmentController` owns current loaded sky/day state,
`AdminEnvirons`, provider refresh, WorldTime synchronization, and Weather
changes. The existing retail day-group/weather algorithm moves verbatim.
- A named skill-credit resolver owns the formula formerly nested in the session
binding only after Checkpoint C verifies it against named retail; until then
the current ACE-only interpretation is not overclaimed as retail-faithful.
### 3.4 Settings and diagnostics
`RuntimeSettingsController` owns SettingsStore, active toon key, persisted
display/audio/gameplay/chat/character values, quality reapply, and UI
lock/FPS/combat preferences. It is constructed before `Window.Create` and is
the single source for one immutable startup snapshot and Display/VSync/MSAA.
After GL/input sources/input dispatcher/camera/audio exist—but still before
devtools, the world render dispatcher, terrain, retained UI, or streaming—it
applies window/pacing/audio values and resolves the final quality snapshot.
Later factories consume that resolved snapshot. A fixed
typed `RuntimeSettingsTargets` implementation is late-bound only to support
future runtime changes; binding it does not replay startup display or quality
transitions. The controller exposes typed state/commands to existing consumers;
it does not own those renderers, UI trees, or session state.
`RuntimeDiagnosticCommandController` owns command routing for collision wires,
time/weather, sensitivity, and nearby-world dumps. State remains solely in its
canonical owner: time/weather operations call `WorldEnvironmentController`,
sensitivity calls `CameraPointerInputController.AdjustSensitivity`, and world
queries use canonical runtime views. It creates no duplicate state or entity
cache. Diagnostic algorithms do not remain in the window merely because their
input actions originate there.
`FramebufferResizeController` is a separate GL-aware typed target. It owns the
frozen viewport → `ViewportAspectState` → camera aspect → devtools layout-reset
sequence and is not folded into the pointer owner.
### 3.5 Frame-root slots
A focused `GameFrameGraphSlot` is the sole publication point for the current
`UpdateFrameOrchestrator` and `RenderFrameOrchestrator`. The two Silk stubs
resolve through this slot; `GameWindowLifetime` withdraws it after session
convergence and before borrowed owners retire. Clearing a lifetime snapshot is
not withdrawal while a direct window field can still invoke the graph.
### 3.6 Ordered composition
`OnLoad` invokes the narrow production platform phase that owns
`GL.GetApi(_window)` and `_window.CreateInput()`, then invokes small composition
functions at these existing boundaries:
1. host input/camera;
2. content/effects/audio;
3. persisted settings/devtools;
4. world/render resources;
5. interactions/retained UI;
6. live presentation/landblock publishers;
7. streaming/session/local player/teleport;
8. update/render roots;
9. session start.
Each function receives only typed dependencies and may return a small immutable
result needed by the next function. There is no stored mega-context. Stable
owners are copied to locals before retained delegates are constructed so those
delegates cannot capture `GameWindow` accidentally. Construction cycles use
the existing focused deferred slots; they do not use callbacks into the window.
The production ordering lives in a fixed `GameWindowCompositionPipeline` with
typed phase interfaces/factories. Tests invoke that same pipeline with fake
platform/acquisition phases; no test-only clone or source-text assertion stands
in for partial-load execution. A narrow production
`GameWindowPlatformAcquisition` phase is invoked by `OnLoad` and alone calls
`GL.GetApi(window)` and `window.CreateInput()` through injectable factories. It
publishes GL to the lifetime before attempting input, then publishes input
before later phases. Tests execute this exact method with fake factories,
including GL success followed by input failure. Each concrete phase uses a
transactional acquisition scope so failure after any internal acquisition
unwinds the exact prefix.
Every acquired disposable is published to the lifetime owner immediately. A
composer that cannot publish incrementally must unwind its own partial prefix
before throwing. The portal-tunnel transfer remains explicit: publish fallback,
prepare GL resources, transfer to `LocalPlayerTeleportController`, then clear
the fallback before frame graphs are published.
Retained UI uses one `RetailUiRuntimeLease` retained by `GameWindowLifetime`
from Host construction through final teardown. The lease initially owns
`UiHost`, retains a partially constructed `RetailUiRuntime` before Initialize,
and exposes the runtime as a borrower only after Mount succeeds. Constructor
failure leaves Host in the lease; Initialize/cleanup failure leaves the exact
partial runtime in the lease for later retry; success makes lease disposal call
runtime disposal. There is no fallback-to-runtime ownership gap and never two
independent host disposers.
### 3.7 Lifetime owner
`GameWindowLifetime` owns the one `ResourceShutdownTransaction` and typed
teardown-only roots. It does not expose runtime service lookup or gameplay
operations. It exists before `Window.Create`, so window/bootstrap acquisitions
can publish their ownership immediately. Its transaction preserves §2.5 and
fixes these audited gaps:
- update and render orchestrators are both withdrawn;
- `TerrainAtlas` residency/textures are explicitly released after submitted
work and before GL;
- the dedicated sky `Shader` is explicitly disposed;
- a published `UiHost` is disposed even if `RetailUiRuntime.Mount` fails;
- window, dispatcher, pointer, combat, camera, and UI-root callbacks detach
before the owners they target.
## 4. Detailed checkpoint execution
### A — freeze and prune
- Add structural freeze tests for the current startup/session-start, input,
resize, update/render, and shutdown boundaries; later checkpoints replace
these with functional owner tests rather than treating source checks as final
acceptance.
- Extend `ResourceShutdownTransactionTests` for multiple same-stage failures,
failure then explicit retry, empty stages, and reentrant completion.
- Delete only proven dead/test-facade residue: `_capturedMouse`, obsolete
`_streamingRadius`, unused snap constants, `IsPlayerGuid`, `IsDoorName`, and
test-only forwarding helpers. Tests call canonical owners directly.
Result: the exact native attributes/callbacks, input subscription priority,
frame-root/session-start boundary, framebuffer behavior, shutdown stages, and
native-window-last edge are frozen. Dead duplicate state and two test-only
window facades are removed. Three corrected-diff reviews are clean; 56 focused
tests and the complete App suite (2,991 pass / 3 intentional skips) pass.
### B — native window binding and host quiescence
- Implement exact attach/reverse-detach/rollback and the no-throw quiescence
gate.
- Replace direct `Run` event wiring with one owned binding while preserving the
two Render handlers and their order.
- Add post-detach silence, failure-after-Nth-attach, repeated/reentrant Dispose,
callback-during-detach, and persistent physical-detach tests.
Result: one fixed typed binding now owns the exact nine Silk edges, including
the two ordered Render callbacks. Create/publish/Attach and the explicit
lifecycle state preserve cleanup ownership across partial event-accessor
failure, rollback failure, concurrent or reentrant shutdown, and condition
wakeups. The shared host gate drains admitted callbacks and makes failed
physical detach logically inert. Three corrected-diff review loops are clean;
54 focused tests, the App suite (3,027 pass / 3 intentional skips), and the full
Release suite (7,386 pass / 5 intentional skips) pass. No connected gate was
required because this checkpoint changes ownership only and preserves the
frozen callback behavior.
### C — live-session host and skill formula
- Extract selection/entry/reset/binding factories around the existing canonical
session, router, command, and reset owners.
- Before moving the formula, write pseudocode for
`SkillFormula::Calculate @ 0x00591960`, cross-check the DAT field semantics
against ACE plus a second reference, then add conformance tests. If current
behavior differs, register it rather than silently calling the ACE-only
formula retail-faithful inside a structural commit.
- Keep `LiveSessionController.Start` last; both disabled-live and
missing-credential starts must execute the reset path.
Result: `LiveSessionHost` now owns reset-plan construction, exact selection and
entered-world ordering, and create→attach route factories while resolving all
session/command/in-world state through the sole `LiveSessionController`.
Partially attached routes and every individual subscription edge retain failed
cleanup work; successful edges are never replayed, and reset/new generations
remain blocked until teardown converges. Named-retail research corrected the
former ACE-only skill shortcut to exact unsigned `SkillFormula::Calculate @
0x00591960` semantics. Three corrected-diff review loops are clean; focused
session/formula/ledger tests, the App suite (3,048 pass / 3 intentional skips),
Core.Net (548 pass), the Release build, and the full suite (7,408 pass / 5
intentional skips) pass. No connected gate was required for this ownership
checkpoint; the final connected lifecycle gate remains Checkpoint L.
### D — world environment
- Move loaded sky/day state, `RefreshSkyForCurrentDay`, `AdminEnvirons`,
WorldTime synchronization, provider swaps, and Weather changes verbatim into
`WorldEnvironmentController`.
- Preserve existing named-retail citations and registered sound adaptation;
diagnostics call typed commands rather than owning time/weather state.
Result: `WorldEnvironmentController` is now the sole owner of the clock,
loaded sky descriptor, selected day group, Weather state, server time sync,
AdminEnvirons bridge, and time/weather debug cycles. `GameWindow` preserves its
public readonly clock/weather aliases but only composes the owner into render
and session seams. Initialization is explicitly one-shot, and missing GameTime
restores the documented fallback origin instead of inheriting process-global
state. Named-oracle review corrected the day picker to
`SkyDesc::CalcPresentDayGroup @ 0x00500E10` and AdminEnvirons to
`CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20`; TS-54/TS-55 now register
the carried centered-audio and complete fog/ambient/radar gaps. Three
corrected-diff reviews are clean; 17 focused tests, the App suite (3,059 pass /
3 intentional skips), the warning-free Release build, and the full suite
(7,419 pass / 5 intentional skips) pass. No connected gate was required for
this behavior-preserving ownership checkpoint.
### E — pointer, focus, and framebuffer resize
- Make SilkKeyboard/Mouse sources and dispatcher links reversible, then attach
the raw pointer owner in the existing pre-UiHost order.
- Late-bind the gameplay-frame slot without resubscribing or reordering input.
- Extract camera mode, lifecycle focus loss, scroll, and sensitivity state.
- Extract the independent framebuffer resize target and pin logical Window.Size
versus FramebufferSize behavior.
- Require rollback after every Nth event add and silence between every shutdown
stage for raw/retained/devtools device events.
Result: `CameraPointerInputController` now owns raw move, camera-mode cursor,
focus-loss, scroll, and per-mode sensitivity policy; its gameplay-frame edge is
late-bound without resubscription. Silk keyboard/mouse sources,
`InputDispatcher`, retained UI bindings, and the devtools
`QuiescentInputContext` are publish-before-attach, terminal after disposal,
transactional on every side-effecting event add, and retry only the physical
edges that remain pending. Mouse-only hosts retain camera/cursor behavior.
`FramebufferResizeController` is the sole physical framebuffer publisher and
seeds viewport/aspect/camera state from `FramebufferSize`, while logical UI and
frame dimensions remain `Window.Size`. Shutdown now performs no-throw logical
input cutoff, retires the live session, then removes physical callbacks; the
post-session mouse-look release therefore cannot send a final movement packet,
and a bad Silk remove accessor cannot block F653/transport teardown. TS-56
records the carried non-retail camera input scalars. Three corrected-diff review
loops are clean; 66 focused App tests, 9 dispatcher-lifetime tests, 5 Core input
integration tests, the warning-free production Release build, and the complete
Release suite (7,477 pass / 5 intentional skips) pass. `GameWindow` is 4,266
raw lines / 194 fields / 65 methods. No connected gate was required for this
behavior-preserving ownership checkpoint; Checkpoint L retains the final
connected lifecycle and visual gates.
### F — gameplay action and command routing
- Move the frozen action-priority graph into the sole Fired subscriber.
- Extract combat mode command behavior and diagnostic command routing to typed
owners; retained UI, selection, movement, player mode, and item interaction
remain canonical.
- Make Combat, Camera, dispatcher, and retained-root subscriptions named,
transactional, and symmetrically detached.
Result: `GameplayInputActionRouter` is the sole gameplay subscriber to
`InputDispatcher.Fired` and preserves the frozen pointer → scroll → combat →
Press/DoubleClick gate → retained UI → selection → movement → command
priority. Focused command owners now route combat, diagnostics, window/player
mode, item-target mode, and Escape without calling feature bodies on
`GameWindow`. `RetainedUiGameplayBinding` owns the outside-UI item-drop edge;
the combat/diagnostic deferred slots release their targets before session
retirement, and the retained binding detaches transactionally afterward.
Named-retail review
also corrected `ToggleCombatMode @ 0x0056C8C0`: active combat short-circuits to
peace without an equipment lookup, while an incompatible Held object prints
the exact retail notice and sends no mode request. Three corrected-diff review
loops are clean; 54 focused App tests, 20 Core combat tests, the App suite
(3,155 pass / 3 intentional skips), the Release build, and the complete suite
(7,524 pass / 5 intentional skips) pass. `GameWindow` is 4,057 raw lines / 198
fields / 54 methods.
### G — two-phase runtime settings
- Construct `RuntimeSettingsController` before Window.Create and source startup
Display/VSync/MSAA from it.
- After GL/camera/audio and before devtools/world, apply startup
display/pacing/audio and resolve the immutable quality snapshot consumed by
downstream factories.
- Late-bind typed runtime targets without replaying startup transitions, then
move runtime display/quality changes, frame-rate/UI-lock/combat preferences,
and toon-scoped state.
- Preserve restart-required quality behavior and test transitions/previews
without requiring GL.
**Implementation result:** one pre-window `RuntimeSettingsController` now owns
the concrete settings store, the immutable startup snapshot, current Display /
Audio / Gameplay / Chat / Character values, the active toon, resolved quality,
and the optional Settings draft. Startup applies pacing, window state, saved
FOV, and audio in order exactly once; a failed later substep retries without
replaying a successful earlier substep. Complete runtime targets bind only
after their borrowers exist and binding performs no replay. Display saves keep
the persistence → live window → snapshot → quality order; quality applies
alpha-to-coverage → anisotropy → render range → streaming → completion budget.
Combat preference changes merge only their three fields into a live draft, so
unrelated unsaved Gameplay edits survive. Saved FOV now applies without
devtools, correcting the old accidental SettingsVM gate. `/framerate` retains
the named-retail live toggle/notice and keeps acdream's shipped cross-launch
persistence as documented AP-121. UI-lock convergence requires target,
persistence, and Settings baseline publication; a failed substep keeps the
same-value command retryable. `GameWindow` is 3,663 raw lines / 162 fields / 37
methods. Thirty-five focused App settings/boundary tests, all 43
`SettingsVMTests`, the UI Abstractions Release suite (534 pass), and the App
Release suite (3,183 pass / 3 intentional skips) pass. All three independent
corrected-diff reviews are clean; the production App Release build has zero
warnings/errors, and the complete Release suite passes 7,553 tests / 5
intentional skips (17 existing test-project warnings remain tracked by #228).
### H — explicit single resource ownership and frame slots
- Publish TerrainAtlas and the dedicated sky Shader as separate lifetime roots;
renderers remain borrowers and retire before the sole atlas/shader disposal.
- Publish `RetailUiRuntimeLease` before Host/Mount acquisition; add separate
idempotent input deactivation and test constructor failure, Initialize failure
with successful cleanup, transient cleanup followed by lifetime retry,
persistent cleanup followed by terminal abandonment, and exactly one Host
disposal/no unowned Host on every path.
- Publish update/render roots only through `GameFrameGraphSlot` and withdraw the
slot after session convergence.
- Pin portal fallback/transfer unchanged and test every normal/partial prefix
for no leak or double delete.
- Make `Shader` construction and `TerrainAtlas.Build` internally exception-safe;
phase scopes cannot recover GL names created inside a factory that throws.
Inject GL compile/link/texture-allocation failure after every internal name
and prove exact rollback/residency release.
Checkpoint H is complete. `GameRenderResourceLifetime` is the sole terrain-
atlas and dedicated-sky-shader owner; renderers borrow and retire before those
roots. `RetailUiRuntimeLease` retains the Host/runtime chain through partial
construction, input cutoff, retryable disposal, and explicit terminal
abandonment. `GameFrameGraphSlot` publishes update/render roots atomically, and
the portal tunnel now uses one prepare-aware fallback/transfer transaction.
Shader, terrain, text-renderer, buffer/VAO, bindless, and texture-binding
construction/mutation paths use always-on checked GL commit boundaries. A
failed cleanup retains the exact name/handle/binding obligation for retry, and
successful substeps never replay. `GameWindow` is 3,689 raw lines / 162 fields
/ 37 methods. The focused ownership gate passes 61 tests, the App Release suite
passes 3,236 tests / 3 intentional skips, and all three independent final
corrected-diff reviews are clean. The complete Release suite passes 7,606 tests
/ 5 intentional skips; the 17 existing test-project warnings remain tracked by
#228.
### I — ordered production composition
- Split the 2,289-line `OnLoad` body at §3.6 boundaries using the fixed typed
production pipeline, small immutable phase results, and no stored mega-context.
- Remove retained GameWindow captures via stable locals, typed adapters, and
existing deferred slots.
- Run the same pipeline with fake acquisition phases and inject failure after
platform, GL, input, DAT/effects, render resources, retained UI, live/session,
teleport transfer, and frame-root publication. Each concrete phase also
proves transactional rollback of its internal acquisition prefix.
Result: all nine production phases now run through the same executable pipeline
as the prefix/failure oracle. Platform and settings results are explicit typed
inputs wherever consumed; phases reject mismatched instances, all long-lived
owners publish to focused lifetime slots, the frame pair publishes atomically,
and session start is terminal. `GameWindow.cs` is 1,945 raw lines. Three
corrected-diff passes are clean; App Release passes 3,431 tests / 3 skips and
the complete Release suite passes 7,803 tests / 5 skips with the existing 17
#228 warnings. No connected gate was required for the final structural
correction; Checkpoint L retains the connected lifecycle and soak gates.
### J — lifetime cutover and shutdown failure policy
- Move the teardown-only stage records and shutdown manifest out of GameWindow.
- Co-stage no-throw quiescence and live-session convergence as hard operations;
retry/report physical detach as non-blocking operations, withdraw both frame
graphs next, then preserve the remaining frozen order.
- Cover constructed-never-run, Load-never-fired, normal WM_CLOSE, WM_CLOSE then
Dispose, direct/repeated/reentrant Dispose, transient/persistent failures,
session deferred disposal, optional-devtools absence, GPU-drain failure, and
every partial-load prefix.
- Verify acquired owners dispose exactly once, unacquired owners never dispose,
completed operations never replay, and native-window fallback is last and
explicitly reported incomplete when a non-cancellable close cannot converge.
- Verify persistent physical-detach failure still retires downstream owners and
is reported, while persistent session/GPU barriers still protect dependents;
after native fallback the terminal abandoned state makes repeated Dispose
inert.
Result: `GameWindowLifetime` owns the typed root snapshot, 18-stage manifest,
transaction progress, terminal report, and native window. Clean, soft-failure,
hard-failure, native-failure, retry, reentrant, and never-run paths are pinned.
The UI host remains explicitly rooted through failed physical detach until the
native edge is gone; successful finalization releases the completed lifetime
graph, while abandonment retains it. No shutdown stage remains in `GameWindow`,
which is now 1,625 raw lines. App Release passes 3,441 tests / 3 skips and the
complete Release suite passes 7,813 / 5 skips.
### K — canonical soak checkpoints (#232, separate commit)
- Extract `WorldLifecycleResourceSnapshotSource` from the window and reuse the
existing render diagnostics source where fields overlap.
- Change `checkpoint <name>` from an immediate retained-UI-time capture to a
FIFO request with an acknowledgement token. The script runner treats that
token as a command barrier: it executes no later command until the token is
resolved. After private presentation returns and
`RenderFrameDiagnosticsController.Publish` has published the current frame,
a post-diagnostics checkpoint phase drains requests using that same immutable
`RenderFrameOutcome` plus one sample from every canonical source. This avoids
previous-frame diagnostics mixed with current-frame owner counts.
- Mark the token Succeeded only after serialization/write completes. Deferred
capture/write failure marks it Failed, and the runner stops with that exact
error on its next tick. Preserve request sequence/name across the deferred
edge, drain independent producers in FIFO order, and cancel pending tokens
with an explicit shutdown result.
- Store one token against the active script-command index. Repeated pending
ticks poll that token and never enqueue again—even when a render failed before
the post-diagnostics drain. Clear it only after Success, Failure, or
Cancellation is consumed; one command therefore produces one request, write,
and sequence increment.
- Add exactly nine ordered named `checkpoint` commands, parse the JSONL into the
soak report, and assert checkpoint identity/order.
- Gate pending teardown, retirement, staging, and warmup at zero; compare exact
canonical owner/cache counts and bytes at Caul return → Caul plateau; label
authoritative entity/animation population changes as workload warnings.
- Preserve process working/private memory as a separately labelled secondary
residency guard with unchanged thresholds. Require two fresh-process clean
route runs before closing #232.
Result: deferred acknowledged checkpoints now drain after current-frame render
diagnostics, persist the exact frame outcome plus canonical resource owners,
and block the script until both artifacts commit. Seventy-four focused tests,
App Release, the full Release suite, and two 403-second fresh-process routes
pass. Every route produced the exact nine checkpoints, zero pending/staged/
warmup work, no unconfounded canonical owner growth, graceful exit, and the
unchanged process/performance limits. Commits: `bca41487`, `6c5e0604`.
### L — final closeout
- Run three independent corrected-diff reviews: retail conformance,
architecture/integration, and adversarial lifecycle/failure analysis.
- Resolve every confirmed finding and re-review until clean.
- Run focused App/Core/UI tests, Release build, and the complete Release suite.
- Run connected lifecycle/reconnect and two fresh-process synchronized
nine-stop soaks; compare stable framebuffer checkpoints with Slice 7.
- Audit every active divergence row that still points at `GameWindow`; retarget
ownership paths only. Keep TS-53 and other unresolved mechanisms open.
- Update architecture, milestones, roadmap, issues, AGENTS/CLAUDE, and durable
memory with final metrics and exact gates.
Automated result: 293 focused tests and the 7,823/5 complete Release suite
pass. The 314.4-second capped/reconnect lifecycle route passes with six valid
PNGs and graceful exits. Its current deterministic frames preserve Slice 7
commit `9d7df1bf` geometry, UI/paperdoll layering, private viewports,
depth/alpha, and reveal; live entity/particle/vitals/camera timing account for
the accepted differences. All three final corrected-diff reviews are clean.
The user's connected visual matrix is the sole remaining acceptance item.
## 5. Automated acceptance
- exact window callback attach order, reverse detach, rollback, reentrancy, and
post-detach silence;
- transactional rollback after every Nth add for all multi-event bindings, plus
retained/devtools/raw device silence between every shutdown stage;
- exact input subscription/priority order and focus/reset/close semantics;
- exact resize order and non-positive-size rejection;
- disabled-live and missing-credentials startup still perform reset; live
startup remains last;
- PView still reads FramebufferSize while RenderFrameInput, retained UI,
portal/private presentation, and screenshots share one Window.Size snapshot;
- exact session reset trace and current-session borrowing;
- partial-load rollback at every composition checkpoint;
- TerrainAtlas, sky Shader, UiHost, frame graphs, GPU-flight, DAT, input, GL,
and native window have explicit tested lifetimes; terrain/sky renderers remain
borrowers and cannot also dispose the separately owned atlas/shader;
- throwing Shader/TerrainAtlas leaf factories release every internal GL name
and bindless-residency prefix;
- shutdown retry/all-attempted/no-replay semantics remain intact;
- persistent non-cancellable close fallback names the blocked stage and never
reports a clean shutdown;
- completed teardown with soft-detach failures reports immutable
`CompleteWithCleanupFailures`, and repeated Dispose is inert;
- no anonymous MouseMove subscription and no substantial Silk callback body;
- no stored `GameWindow`, broad service locator, or feature-owner callback
facade into the window; the fixed native `WindowCallbackTargets` binding is
the intentional host-boundary exception;
- `OnLoad` is ordered composition, `OnUpdate`/`OnRender` remain one handoff,
and `OnClosing`/`Dispose` delegate to one lifetime owner;
- all canonical soak snapshots are post-diagnostics same-frame atomic,
round-trip, preserve request/name order across the deferred edge and nine-name
order across two fresh processes, and name the exact owner/property on growth;
- checkpoint acknowledgement blocks following script commands, propagates
deferred write failure, and reports shutdown cancellation;
- repeated pending ticks enqueue exactly once and cannot duplicate a checkpoint
after a failed/delayed render;
- complete Release suite, lifecycle gate, soak gate, and framebuffer comparison
pass.
## 6. Final visual handoff
The only user pause is one connected visual smoke gate after all automation is
green:
- first login world bootstrap and radar;
- movement, mouse look/orbit, resize, focus loss/return, combat toggle;
- inventory/skills/spellbook shared panel and retained UI input;
- outdoor, building, dungeon, portal/recall, paperdoll, particles, alpha;
- graceful close and fresh-process reconnect.
After that pass the GameWindow structural campaign is complete. The roadmap
returns to the carried M3 magic/spell-bar/spellbook/component-book and final
two-client portal-out/materialization visual gates before new M4 feature work.

View file

@ -1,764 +0,0 @@
# World interaction completion — pre-M4 program
**Status:** Slice 1 user-accepted 2026-07-23. Slice 2 Use-hand selection,
zero-useability, carried direct-use, AutoWear correction, and the requested
shared item-cooldown follow-up passed their connected gates. Slice 3's first
connected gate exposed an incorrect IdentifyResponse flag table, a non-retail
shared-panel mount, and its missing inscription transaction. All three are
corrected and live-confirmed. The creature-page follow-up now has retail's
ordered stat rows and animated private preview. The follow-up item report and
authored 310 x 400 layout correction are implemented and user-accepted.
Favorite-spell press-time selection and right-click local SpellPanel
examination are implemented and user-accepted. The follow-up maps
component-disabled ACE characters to the modern scarab/prismatic formula,
resolves formula icons by their DAT icon DIDs, installs those icons as each
authored template root's own foreground image, and migrates stale examination
dimensions once to the authored 310 x 400 extent. The final connected
assessment gate passed on 2026-07-24. Slice 4 equipped-child world picking
(with the Opus F1 wielded-pickup-legality correction) passed its connected
visual gate on Coldeve and was user-accepted 2026-07-29. Slices 14 are
complete; resume at Slice 5, vendor browsing.
**Milestone:** M4 prerequisite/preamble.
**Architecture:** retained gameplay UI over shared selection, object, and
interaction state. `GameWindow` remains a composition/callback shell.
## Outcome
Close the remaining retail interaction surfaces before the larger M4
quest/emote/character-creation bodies begin:
1. Favorite spell bars expose their DAT-authored overflow arrows and scroll
through every server-persisted favorite.
2. The status bar's hand and magnifying-glass controls invoke the same Use and
Assess commands as their keyboard paths.
3. Assessing a creature, player, NPC, or object opens its retail information in
the independent movable/resizable retail floaty examination window.
4. World picking can resolve visible equipped children, such as a character's
wielded weapon, while selection markers remain anchored to the picked child.
5. Vendor use opens the authored vendor surface, publishes its inventory, and
supports retail selection/browsing.
6. Vendor buy/sell transactions, quantities, pending-state ownership, and
authoritative inventory reconciliation complete the loop.
The program reuses the existing retained-window host, `SelectionState`,
`ClientObjectTable`, interaction transaction owner, and server-authoritative
inventory updates. It does not create parallel panel positions, item tables,
selection state, or optimistic inventory outcomes.
## Ordered slices
| Slice | Deliverable | Principal owner |
|---|---|---|
| 1 | Spell-bar overflow arrows | `SpellcastingUiController` + generic retained scrollbar/list |
| 2 | Status Use/Assess commands | focused status-bar controller binding to the existing action router |
| 3 | Assessment information panel | retained controller in an independent floaty examination window |
| 4 | Equipped-child world picking | pure world-query/picking policy plus presentation anchor |
| 5 | Vendor browse lifecycle | vendor session/controller plus authored panel |
| 6 | Vendor transactions | server-authoritative buy/sell command and reconciliation owner |
Each slice begins with named-retail research, produces pseudocode and
conformance tests, updates the divergence register if required, and lands as a
separate bisectable commit. A visual gate follows each UI-bearing slice.
Slice 1 now imports both 23-pixel arrow buttons, their rollover/pressed media,
and HideDisabled from the real combat fixture; places their artwork by the
authored leading/trailing positions; shares the list's single pixel-scroll
model; advances one 32-pixel cell per press; and exposes only actively selected
spells. Passive object/endowment refresh preserves a manual offset. The mixed
610/800-pixel DAT anchor chain is solved to a fixed 747-pixel combat root,
producing exactly 18 visible 32-pixel favorite cells (nine numbered plus nine
unnumbered) without consuming the overflow. The initial 57-test/full-suite gate passed;
the corrective 104-test focus set, 3,472 App tests / 3 skips, Release solution
build, and 7,845 complete-solution tests / 5 skips pass. The corrected
18-cell bar and both overflow directions passed the connected gate.
Slice 2 now drives the toolbar hand from canonical `SelectionState` changes and
live `ClientObjectTable` updates. The pure Core predicate ports
`gmToolbarUI::HandleSelectionChanged`: combat-use items, armor/clothing/jewelry,
and `ItemUses` values without `USEABLE_NO` remain active. Clicking still enters
the existing `ItemInteractionController` command path, so weapons use the
server-confirmed AutoWield transaction and targeted tools enter the existing
use-on-target cursor. Empty selection and explicitly unusable spell components
are ghosted and ignore clicks. Empty-selection ghosting is the user's explicit
choice over retail's generic `TARGET_MODE_USE` entry and is registered as
AP-122. The 14-test Core interaction focus, 49-test toolbar focus, Release
solution build, 3,474 App tests / 3 skips, and 7,848 complete-solution tests /
5 skips pass.
The connected selection-state gate passed. Its follow-up exposed three deeper
shared-policy/delivery defects, now corrected from named retail plus the
matching binary. `ItemUses::IsUseable @ 0x004FCCC0` tests only `USEABLE_NO`;
reset/absent value zero is usable. `ItemHolder::UseObject @ 0x00588A80` sends a
Use event immediately for both owned and world objects; acdream had
incorrectly routed the packet through a local approach lookup, where
Blackmoor's Favor has no spatial entity and was silently cancelled. Every
ordinary Use now sends once and ACE owns any authoritative MoveTo chain.
AD-27 remains only for client-side pickup completion. AutoWear applies
`CPlayerSystem::AutoWearIsLegal @ 0x0055EF40` through the same double-click and
toolbar-hand path, resolves the overlapping worn object from the
retail-ordered equipment projection, and emits the exact system line
`You must remove your <item> to wear that`. Research:
[`../research/2026-07-23-retail-item-use-and-autowear-pseudocode.md`](../research/2026-07-23-retail-item-use-and-autowear-pseudocode.md).
The focused 25-test Core and 85-test App sets pass, as do the warning-free
Release solution build, 3,476 App tests / 3 skips, and 7,857 complete-solution
tests / 5 skips. The carried-use delivery correction adds an end-to-end App
pin from Favor activation through wire dispatch and authoritative UseDone busy
release. The Release build retains the 17 tracked test-project warnings;
3,477 App tests / 3 skips and 7,858 complete-solution tests / 5 skips pass.
The user confirmed Blackmoor's Favor now activates correctly.
Before Assess, the user requested the adjacent retail item-cooldown
presentation. `PublicWeenieDesc` now preserves the optional shared cooldown id
and duration, assessed property updates reach the same object fields, and
`CEnchantmentRegistry::OnCooldown @ 0x005943C0` plus
`UIElement_UIItem::UpdateCooldownDisplay @ 0x004E1E20` are ported through the
canonical Core registry and one pure display projection. Every retained item
list shares one controller and the exact
ten DAT-authored radial sprites `0x060067CF..0x060067D8`; items with the same
group display the same server-authored cooldown. Research:
[`../research/2026-07-23-retail-item-cooldown-pseudocode.md`](../research/2026-07-23-retail-item-cooldown-pseudocode.md).
The focused parser/Core/UI/production-DAT tests, warning-free Release solution
build, 3,482 App tests / 3 skips, and 7,875 complete-solution tests / 5 skips
pass. AP-123 records only the retained toolkit's procedural-child adaptation;
the visible assets, ordering, timing, and shared-group behavior are exact.
The user accepted the live cooldown presentation 2026-07-23.
Slice 3 preserves the existing toolbar/keyboard Assess command and ports the
retail response owner around it. One shared UI-busy reference covers the
latest pending GUID, stale replies are rejected, the accepted reply becomes
the current appraisal, and combat-time creature/player refreshes occur every
0.75 seconds only while the examination window is visible. Complete
`IdentifyObjectResponse` parsing now uses ACE/retail's exact flag values and
positional order, includes the three-word HookProfile, and rejects truncated
gated payloads rather than continuing from a corrupt cursor. The first live
monster gate found that `0x0100` had been mislabeled WeaponProfile; the parser
dropped every creature response before response acceptance, leaving the one
busy reference held. A literal ACE `0x0100` creature fixture now protects that
packet-to-controller path.
LayoutDesc `0x2100006B`, root `0x100005F2`, supplies the complete 310 x 400
floaty chrome, title, item/creature/character subviews, close control, and
authored scrollbars. `gmFloatyExaminationUI` is an independent top-level
window, so Inventory/Skills/Spellbook no longer replace it or inherit its
geometry. The imported multiline inscription field now ports
`ItemExamineUI`'s public/hook inscribability, inventory ownership and
same-scribe permission rules, placeholder/signature presentation, exact
failure notices, focus commit behavior, and CP-1252 `SetInscription (0x00BF)`
transaction. Basic item and player reports are live. The creature page now
resolves its type through retail EnumMapper `0x2200000E`, preserves the
authored Character/Level header, creates the exact nine stat rows from
template `0x10000166`, and renders a fixed-heading animated clone through a
private viewport using retail's bounding-box camera and distant light.
The visual-gate correction also ports the separate `0x10000335`
damage/critical/resistance rating list, places authored row chrome behind the
animated preview and text in front, adds the balanced row inset, and follows
the current selection automatically while the examination window is visible.
Item-object preview, specialized item/character detail regions, and exact
creature appraisal font-state selection remain the narrowed AP-110 residual.
The item report now retains `PublicWeenieDesc` hook identity, applies
appraisal-only Value/Burden unknowns, suppresses mounted-hook sentinel
capacities, preserves retail line/paragraph boundaries, and selects the
authored white/green/red item font-color entries. Research:
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
The focused parser/router/request/controller/fixture tests pass, as do the
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
tests / 3 skips, and 7,913 complete-solution tests / 5 skips.
The creature presentation follow-up adds real-DAT row-template and EnumMapper
fixtures plus pure conformance coverage for row order/formatting, enchantment
semantics, failed assessments, stable clone identity, live animated mesh
updates, hydrated mesh bounds, and retail camera fitting. The warning-free App code,
Release solution build, 3,506 App tests / 3 skips, and 7,923 complete-solution
tests / 5 skips pass. The connected visual result remains the closeout gate.
The rating/layering/selection-follow correction adds 18 focused green tests,
passes the Release solution build with the 17 warnings already tracked by
#228, 3,510 App tests / 3 skips, and 7,927 complete-solution tests / 5 skips.
Its corrected connected visual result remains the closeout gate.
The item-report/layout correction restores the authored 310 x 400 examination
size for the connected profile, leaves retail's ordinary resize range intact,
keeps row chrome at its LayoutDesc origin while insetting only foreground
creature text, and starts generated item prose at the top of its authored
scroll surface. `ItemAppraisalTextFormatter` now owns the decomp-ordered item
projection outside `GameWindow` and outside the examination controller. It
ports common weapon damage ranges/speed/range/ammunition, armor protection
bands, defense/caster modifiers, workmanship, ratings, wield/use/activation
requirements, item XP/capacity/lock/mana/uses/creator/rare data, cooldown and
imbued special properties, and both short spell lists and full DAT spell
descriptions. The conformance follow-up replaces public-value fallbacks with
retail's appraisal presence semantics, restores hook/capacity/lock behavior,
and carries each `AddItemInfo` fragment's paragraph and font-color index into
the retained text shaper. AP-110 now names only the remaining specialized,
player-dependent, DAT-display-name, creature-font-state, and object-preview gaps.
Focused conformance fixtures cover the geometry layering plus melee, launcher,
armor, spell, and special-property reports. The Release solution build passes
with the 17 warnings already tracked by #228, 3,514 App tests / 3 skips, and
7,931 complete-solution tests / 5 skips. The connected visual result remains
the historical pre-acceptance checkpoint; the final gate passed 2026-07-24.
The item-format conformance correction adds literal hook-tail cursor fixtures,
wire/session/object-table propagation tests, the exact Black Phyntos Hive
report, capacity/page/lock presence cases, structured paragraph checks, and
LayoutDesc color-palette/enchantment-style coverage. The isolated Release
solution build passes with the same 17 tracked warnings; 3,522 App tests / 3
skips and 7,942 complete-solution tests / 5 skips pass while the previously
launched client retains the normal Release output lock. The corrected
connected item visual was accepted 2026-07-24.
The exhaustive item-report correction replaces the remaining generic/numeric
approximations with the complete named-retail dispatch. Equipment sets use the
literal EoR table; ratings, tinkering/salvage averages, coverage, failed weapon
unknowns, level restrictions, all three item-XP curves, activation heritage,
healer/ordinary boost behavior, rare timers, and magic `~ Name: Description`
rows now preserve retail wording and ordering. Description construction ports
lifespan prose, workmanship/material/gem decoration and portal/PK restriction
bits. `RetailAppraisalNameResolver` follows retail's master
`EnumIDMap -> sub-enum 1 DualEnumIDMap` material chain and shares the installed
creature mapper for slayers and wield requirements; a production-DAT test pins
Ruby and Ghost. AP-110 is narrowed to item preview and the projections that
need live player/localization state (effective shield, cooldown remaining, and
augmentation-cost `StringInfo`), plus character and creature-font residuals.
The focused item formatter suite passes 21/21, App Release passes 3,531 tests /
3 skips, and the complete Release solution passes 7,952 tests / 5 skips. The
connected item-report comparison passed on 2026-07-24.
The material-title and section-boundary correction carries
`PublicWeenieDesc.MaterialType` from CreateObject through the canonical object
table and resolves `ACCWeenieObject::GetObjectName(NAME_APPROPRIATE)` through
the installed DAT material map. Examination titles now produce names such as
`Reed Shark Hide Steel Toed Boots` without duplicating a material already
present in the authored base name. Empty `AddItemInfo` calls are retained as
real report fragments, restoring retail's intentional blank rows after
workmanship, before armor level, around rating/special-property blocks, and at
the later use/item-level boundaries. Focused wire, projection, object-table,
title, and boots-layout fixtures protect the full path. With the subsequent
right-click and press-time retained-item input ports, the Release solution
build and 3,548 App tests / 3 skips plus 7,979 complete-solution tests /
5 skips pass.
The world right-click follow-up ports
`UIElement_SmartBoxWrapper::MouseUp @ 0x004E5820` and the
`sr_Examine` branch of `RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`.
The configurable `SelectRight` binding now completes on release, cancels when
pointer travel crosses the retail-observed three-pixel drag threshold, and
routes through the existing world picker, lighting pulse, canonical
`SelectionState`, and appraisal request owner. Empty world space remains a
no-op, right-drag camera orbit does not appraise its release point, and the
independent configurable `SelectionExamine` action now reaches the same
request/target-mode path.
The retained follow-up ports the separate
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` branch: an occupied
backpack, side-bag, loot, paperdoll-slot, or physical toolbar cell now selects
its item and enters that same appraisal owner on a completed right-click.
Right-button movement cancels the click and can never begin an item drag.
The input-latency follow-up ports `UIElement_ListBox::MouseDown @ 0x0046E3A0`
and the physical left-click branch of
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`. Canonical selection
and the retained green frame update during left-button down, before the
three-pixel drag threshold. Target mode is offered first. Opening, using,
equipping, looting, and shortcut activation remain completed-click or
double-click actions and are suppressed when target mode consumed the press.
Favorite spells now use the parallel non-weenie path:
`gmSpellcastingUI::ListenToElementMessage @ 0x004C7AB0` selects the favorite
on left press, while the spell branch of
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` opens the authored
SpellPanel locally on right-click. Spell IDs never enter `SelectionState` or
the status-bar magnifier path, and no Appraise/busy transaction is invented.
The view projects exact spell fields and the current appropriate formula
through authored component template `0x1000032E`. Its corrective pass uses
each component descriptor's icon DID rather than its inventory WCID, applies
the modern scarab/prismatic formula when ACE disables component enforcement
(IA-21), and introduces per-window authored-geometry revisions so an obsolete
saved examination height resets once without discarding position or future
user resizing. The corrected formula/icons/foreground stack and authored
extent were accepted on 2026-07-24. The 61-test focused App
gate passed before this correction. The corrected focused tests, Release build
with #228's 17 tracked test warnings, 3,555 App tests / 3 skips, and 7,986
complete-solution tests / 5 skips pass.
## Slice 1 — spell-bar overflow arrows
### Retail oracle
The authored Magic combat layout contains, inside every favorite-tab group:
- horizontal scrollbar `0x100000B5`, 685×36 at the 800-pixel design width;
- decrement button `0x10000071`, 23×36;
- increment button `0x10000072`, 23×36;
- item list `0x100000B6`, inset by 23 pixels on both sides and 32 pixels high.
The list references the scrollbar through `UIElement_Scrollable` property
`0x71`. The scrollbar references increment/decrement buttons through
properties `0x77`/`0x78`, and property `0x79` enables HideDisabled. There is no
track or thumb in this specific control: the two authored arrows are the whole
visible scrollbar.
Named retail references and executable pseudocode are recorded in
[`../research/2026-07-23-retail-spellbar-overflow-pseudocode.md`](../research/2026-07-23-retail-spellbar-overflow-pseudocode.md).
### Implementation plan
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
scrollbars both import the referenced decrement/increment children,
including their authored positions, dimensions, Normal/rollover/pressed
media, and HideDisabled property.
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
extents for rendering, hit-testing, track geometry, and dragging. Preserve
the existing 16-pixel default for layouts whose button children are absent.
3. Reproduce retail disabled presentation: a model without overflow rejects
pointer input; when HideDisabled is authored, the scrollbar also draws
nothing and does not claim hit tests.
4. In `SpellcastingUiController`, bind each group's scrollbar to its
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
pixel offset per favorite tab.
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
shortcut, or code scrolls that item into view; passive state refresh does
not re-expose it.
6. Carry the combat root's mixed-parent raw-edge policies through the complete
imported tree and solve for the retail 18-cell favorite viewport; keep that
HUD capacity fixed across desktop resizes.
7. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
controller binding, and selection exposure with focused App tests.
8. Run the App Release suite, solution Release build, and complete Release
suite. Then update the roadmap/memory and request the connected visual gate:
place more favorites than fit, scroll both directions, change tabs, and
verify arrows disappear on a non-overflowing tab.
### Invariants
- DAT supplies the controls and their artwork; no new spell-bar texture or
overlay is invented.
- The scrollbar and list share one `UiScrollable`; there is no second offset.
- One arrow press moves one 32-pixel favorite cell, matching
`UIElement_ListBox::InqScrollDelta`.
- Hidden disabled arrows cannot intercept combat-page dragging or clicks.
- Existing stack, combat-power, chat, inventory, spellbook, and external
container scrollbars retain their current behavior.
- No substantial feature body enters `GameWindow`.
## Slice 2 — status Use/Assess commands
### Use-hand implementation
1. `ItemInteractionPolicy.IsToolbarUseEnabled` is the pure named-retail
selection predicate.
2. `ItemInteractionController.IsToolbarUseEnabled` adapts the selected live
object without triggering a request or consuming the use throttle.
3. `ToolbarController` subscribes to canonical `SelectionState.Changed` and
selected-object add/update/remove notices, then sets the imported button's
normal or ghosted state through `UiButton.Enabled`.
4. Enabled clicks retain the one existing activation path:
equipment enters `AutoWieldController`, ordinary use enters the normal Use
request owner, and targeted items enter `UseItemOnTarget`, which already
owns the retail target cursor.
### Slice 2 closeout
- The shared item-cooldown visual gate passed.
- `gmToolbarUI::ListenToElementMessage @ 0x004BEE90` confirms the existing
magnifying-glass path was already correct: assess the selected GUID
immediately, otherwise enter one-shot Examine target mode.
- Slice 3 owns the response lifetime and retained examination presentation;
Slice 2 adds no duplicate command path.
## Slice 3 — assessment information panel
### Implemented ownership
1. `AppraiseInfoParser` owns the complete positional response payload,
including HookProfile and strict truncation failure.
2. `ItemInteractionController` owns the pending/current GUID pair and the
balanced shared busy reference.
3. `AppraisalUiController` owns response acceptance, item/creature/character
subview selection, report projection, scroll preservation, first-response
visibility, and visible-combat refresh.
4. `RetailUiRuntime` imports and registers retail's
`gmFloatyExaminationUI` as its own top-level window. It deliberately does
not enter `RetailPanelUiController`; Inventory, Skills, Spellbook, and
effects keep their shared main-panel geometry while Examination coexists.
5. Network workers deliver immutable parsed data through the existing
session router; retained state changes remain on the update thread.
6. `AppraisalUiController` owns inscription presentation and optimistic field
state; `WorldSession` owns the exact `0x00BF` GameAction send. Authoritative
inventory/appraisal data remains the source on the next response.
### Connected gate
- Select an item and click the magnifying glass (or press Assess): a separate
floaty window should show its name, available properties, descriptions,
inscription/signature, retail chrome, and working scrollbars.
- Assess a monster, NPC, and player: the correct creature/character page
should appear and the busy cursor should clear.
- On a monster, verify the animated creature is centered behind the exact
Strength/Endurance/Coordination/Quickness/Focus/Self/Health/Stamina/Mana
order, with its creature type and level in the authored header. Assess
differently sized monsters to verify the retail bounding-box fit.
- In combat, leave a creature assessment open long enough to observe a health
refresh; closing the examination window must stop refreshes.
- Keep Examination open while opening/moving Inventory or Skills and confirm
both windows coexist with independent rectangles.
- On the combat spell bar, press and hold a different favorite: its selection
and name should update before release. Right-click a favorite: the same
examination floaty should show the SpellPanel with name, school, mana,
duration, range, description, and component formula. The toolbar magnifier
must not treat the selected spell as an object.
- Assess an owned inscribable weapon. Edit and clear its inscription by
clicking elsewhere, then reassess it. An item authored by another player is
read-only and reports the exact retail permission line when clicked.
## Slice 4 — equipped-child world picking
**Status:** USER-ACCEPTED 2026-07-29 — the two-client visual gate passed on
Coldeve ("child world picking works"). Owner
shape per the program table held: pure world-query/picking policy plus
presentation anchor. No wire, physics, renderer, or
`EquippedChildRenderController` changes. `LiveEntityRuntime` gained scoped
`TryGetAttachedProjectedRecord` / `TryGetPickEligibleRecord` predicates;
`TryGetInteractionEligibleRecord` and the `_visible` set are untouched, so
radar, auto-target, and `CombatAttackTargetSource` remain wielded-item free
(regression-asserted). `WorldSelectionQuery` takes the composed child root as
an injected `Func<uint, Matrix4x4?>` beside the selection-sphere hook, wired in
`LivePresentationComposition` from `EntityEffectPoseRegistry.TryGetRootPose`.
The own-wielded `sr_Use` gate (`0x004E5BE9`) ships through the new
`IWorldSelectionQuery.IsWieldedByPlayer`. Gates: App tests 3,951/3 skips,
complete Release solution 9,783/5 skips, connected world-lifecycle gate
`RESULT=PASS`.
**Correction 2026-07-29 (Opus review finding F1, HIGH).** Making a remote's
wielded weapon selectable made the pickup chain reachable end to end for the
first time, and acdream had never ported
`ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0`'s arm at
`0x005872B7` — `!ACCWeenieObject::IsOwnedByPlayer(item) && item->pwd._location
!= 0` — so `SelectionPickUp` on another character's weapon installed a real
approach and a wire request the server rejects. That arm now ships, with
retail's own notice (`0x007e2228`) and retail's placement ahead of every other
`AttemptToPlaceInContainer @ 0x00588140` stage: one local message, no movement,
no request. The player's own wielded item is `IsOwnedByPlayer`, so it passes
the arm and takes retail's `PositionState.WIELDED` route —
`ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680` records
`IR_PUT_IN_CONTAINER`, not `IR_PICK_UP` — dispatching its container transfer
immediately with no approach. `TryGetApproach` now refuses attached children
outright for the same reason, so no approach can anchor on a wielder's root.
Picking, selection, examination, and the marker anchor are unchanged. The
slice therefore introduces no divergence, contrary to what `f6db964f`'s message
claimed; no register row is owed. Gates: App tests 3,960/3 skips, complete
Release solution 9,792/5 skips, connected world-lifecycle gate `RESULT=PASS`.
### The retail mechanism
Retail picking is render-coupled, not a scene-graph ray walk. A click arms a
per-frame selection cursor (`UIElement_SmartBoxWrapper::MouseDown @
0x004E5700` sets a `SearchReason` — sr_Select/sr_Examine/sr_Use/sr_TargetedUse,
acclient.h:6789 — then `SmartBox::find_object @ 0x00451C60` sets
`Render::set_selection_cursor @ 0x0054B750`). During the frame,
`Render::update_viewpoint @ 0x0054CDD0` builds `selection_ray` via
`Render::pick_ray @ 0x0054B610`, and EVERY drawn part accumulates hits in
`Render::GfxObjUnderSelectionRay @ 0x0054C740`: drawing-sphere test, then
per-polygon tests when enabled, keeping the closest — with the polygon winner
outranking a sphere-only winner (`GetMouseSelectionObjectID @ 0x0054C950`,
read at `SmartBox::DrawNoBlit @ 0x00454C20`).
**The child-vs-parent answer:** each hit records `part->physobj->id`
(`CPhysicsPart::get_physobj_id @ 0x0050D490`), and a part is only a candidate
when `part->physobj->id != 0` (`CPhysicsPart::Draw @ 0x0050D7A0`). Equipped
children are first-class `CPhysicsObj`s with their own ids and part arrays
(`CPhysicsObj::add_child @ 0x0050F870` via `CSetup::GetHoldingLocation @
0x005213F0`; `CPhysicsObj::UpdateChild @ 0x00512D50` composes
`Frame::combine(parent_part_frame, hold_frame)` into the child's own
`m_position` every frame). So a click on a wielded weapon returns THE WEAPON'S
GUID — there is no parent redirection in the path, and no ethereal or
wielded-specific gate: the only candidacy rule is "drawn part with a nonzero
physobj id".
Post-pick (`RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`): the id must exist in
the weenie table; selection is set to the picked id itself
(`ACCWeenieObject::SetSelectedObject @ 0x0058C2E0`); the click flash
(`CPhysicsObj::SetLighting @ 0x00511A80`) is non-recursive — it lights that
object's own part array ONLY, so clicking a weapon flashes the weapon and
clicking a creature does not flash its weapon; the vivid brackets
(`VividTargetIndicator @ 0x004F5CE0`) derive from the selected object's own
selection sphere at its own position, which for a child IS the hand frame.
`sr_Use` on an object whose `_wielderID == player_id` is suppressed
(0x004E5BE9) while selection still happens; `sr_Examine` examines the child id
directly. `PositionState.WIELDED` is distinct from `IN_CONTAINER`
(acclient.h:6802), so container suppression never hides a wielded selection.
### The gap in acdream (the picker is already right)
Equipped children are already live entities with their own `ServerGuid`
(`EquippedChildRenderController.TryRealize`, :448-617) and every draw path
already publishes their selection parts under that guid
(`RetailSelectionScene.AddVisiblePart`, which only skips `serverGuid == 0`).
`RetailWorldPicker.Pick` therefore already returns the weapon as the polygon
winner. The failure is entirely downstream: `WorldSelectionQuery.PickAt`
(:137-154) requires `TryGetInteractionEligibleRecord`, whose `_visible` set
admits `LiveEntityProjectionKind.World` only (`LiveEntityRuntime.cs:1183`
excludes Pending/Attached/Hidden by design), so the winning hit is discarded
and the click reports nothing. Retail would have succeeded. Marker anchoring
has the twin problem: `ResolveVividTargetInfo` (:262-286) gates on the
World-only `TryGetSpatiallyProjectedRecord`, and `TryGetSelectionSphere`
(:293-318) anchors at `entity.Position/Rotation`, which for an attached child
is deliberately the PARENT's root pose (`ApplyParentWorldPose`, :651-658) —
brackets at the wielder's feet. The child's true composed root
(`pose.RootLocal * parentWorld`, the exact `Frame::combine` equivalent) is
already published per frame to `EntityEffectPoseRegistry` (`PublishChildPose`,
:632-644; `TryGetRootPose` :195) and is what the vfx anchors already use.
### Slice plan
1. **Pick eligibility for attached projections.** Add a scoped
`LiveEntityRuntime.TryGetPickEligibleRecord(serverGuid, localEntityId)`
accepting `World` (today's semantics) OR `Attached` (with the same
`IsSpatiallyProjected` + `WorldEntity.Id == localEntityId` staleness
recheck). Consume it in `WorldSelectionQuery.PickAt`, the lighting-pulse
identity paths, and `TryGetInteractionTarget`. **Do NOT widen
`TryGetInteractionEligibleRecord`/`_visible`** — it feeds radar,
auto-target, sticky/MoveTo establishment and `CombatAttackTargetSource`,
none of which retail lets wielded items enter (retail's radar has no
wielded blips).
2. **Marker + sphere anchor.** Branch `ResolveVividTargetInfo` onto the new
predicate, and for Attached records transform the Setup selection sphere by
`EntityEffectPoseRegistry.TryGetRootPose(localId)` (injected as a
`Func<uint, Matrix4x4?>` beside the existing selection-sphere hook) instead
of the parent-derived `entity.Position/Rotation`.
3. **Own-wielded Use gate.** In the use-immediately path, skip the Activate
enqueue when the picked object's `WielderId == playerGuid` (selection and
flash still occur) — the 0x004E5BE9 parity. If deferred, it owes an AP row.
4. **Files:** `LiveEntityRuntime.cs` (predicate), `WorldSelectionQuery.cs`,
`SelectionInteractionController.cs`, plus composition wiring for the
root-pose hook. Untouched: `RetailWorldPicker`, `RetailSelectionScene`,
`WbDrawDispatcher`, `EquippedChildRenderController`.
5. **Conformance tests** (harnesses exist in
`tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs` and
`Rendering/RetailSelectionSceneTests.cs`): child part closest → resolves to
the CHILD guid; stale/withdrawn child record → null, never the parent;
marker uses the pose-registry root, not the parent root; marker suppressed
for the player's own wielded child, shown for a remote's; lighting pulse
lights the child identity only; double-click Use suppressed on own wielded.
6. **Visual gate (user, two-client):** click a remote character's wielded
weapon — selection names the weapon, the flash lights only the weapon, the
vivid brackets track the weapon through the wielder's animation (hand, not
feet), right-click opens Slice 3's examination window on the weapon, radar
shows no weapon blip, and double-clicking your OWN weapon does not fire a
Use.
### Notes
- Slice 3 dependency verified: `SelectionState.Select` stores any nonzero guid
and `RequestAppraisal` has no eligibility gate, so the examination window
works on a picked child unmodified once the pick resolves.
- Divergence register: this slice ADDS no row — it removes an undocumented
deviation (Attached exclusion from pick eligibility versus retail's
part-id pick).
- Existing architectural divergence, unchanged by this slice: retail re-arms
the pick every frame for hover/tooltips (`sr_MouseOver`); acdream picks on
demand per click against the last published frame, with an identity recheck.
## Slice 5 — vendor browse lifecycle (contract authored 2026-08-08)
**Research foundation:**
`docs/research/2026-08-08-slice5-vendor-browse-research.md` (all wire,
retail-symbol, and seam citations live there — this contract only records
DECISIONS and ordered work). Browse only; every buy/sell/accept concern is
Slice 6 (see the research doc's §D fence).
### Decisions on the research doc's eight open questions
1. `VendorState` lives in `AcDream.Core.Items`, a sibling of
`ExternalContainerState`.
2. The shared `PublicWeenieDesc`-body parser IS extracted from
`CreateObject.TryParse` FIRST, as its own behavior-preserving commit
(5.0). Existing CreateObject wire tests must pass unchanged; the
extraction adds no parsing behavior.
3. `ShopSystem::BuyPrice`/`SellPrice` (0x006B6120/0x006B6180,
byte-identical to ACE's `GetBuyCost`/`GetSellCost`) are ported NOW as
pure Core functions with golden-value conformance tests — the browse
list shows retail-correct prices from day one.
4. No request-correlation token in Slice 5: the panel always opens on the
browse/Buy tab. Slice 6 adds the sell-initiated correlation.
5. `VendorProfile::InqAcceptability` (which player items the vendor would
accept) is deferred to Slice 6 with the sell UI it gates.
6. Category/type filter tabs are IN SCOPE for retail parity. The
implementer's D0 reads `VendorItemsUI::AddTypeFilter` /
`ListContainsType` (around 0x004C05C0/0x004C0D90) into a pseudocode
note before any UI work; if that read reveals a mechanism too large
for this slice, STOP and report (fallback — flat list + register row —
requires explicit approval, not implementer discretion).
7. The vendor panel's top-level LayoutDesc id is NOT yet known: the UI
piece budgets a LayoutImporter discovery pass (the exact process that
found the examination window's 0x2100006B), cross-checked by the two
known tab-control ids (0x100000B9 Buy / 0x100000BB Sell) resolving
under the candidate root.
8. AP-110 is narrowed in the SAME COMMIT that lands the panel: "vendor"
leaves the absent-panels list; whatever sub-scope remains absent after
this slice gets its own precise row.
### Ordered work (each lands separately, bisectable)
- **5.0** — extract the shared `PublicWeenieDesc`-body parser
(behavior-preserving; wire tests unchanged; no vendor code).
- **5.1**`ApproachVendor` (GameEvent 0x0062) inbound parser:
`VendorProfile` + the full-desc item list, against the research doc's
byte-verified field table; Core.Net tests with golden byte fixtures.
- **5.2**`VendorState` in Core.Items + the BuyPrice/SellPrice pure
port + conformance tests.
- **5.3** — Runtime ownership: `RuntimeInventoryState` owns the vendor
session per the J4.2 pattern (generation-gated, torn down on
reset/portal/logout); the 0x0062 route opens it; close is CLIENT-LOCAL
(nothing sent on the wire) via the retail distance-watcher semantics;
`ItemInteractionController._activeVendorId` /
`ItemInteractionPolicy.ActiveVendorId` finally receive the real id.
- **5.4** — the authored vendor panel: layout-id discovery, LayoutDesc
import via the Slice-3 examination-window pattern (foreground stacking,
authored extent), browse list reusing Slice-1's retained list/scrollbar
+ DAT icon resolution, category tabs per the D0 read, prices via 5.2.
- **5.5** — register narrowing (decision 8) rides the 5.4 landing commit.
### Trap list (binding)
Do not touch: the J5.2 strict use gate's semantics (the vendor open rides
the EXISTING use transaction — no second gate, per the J4.5 invariant);
`CreateObject.TryParse` behavior (5.0 is extraction only); anything in the
Slice 6 fence (no buy/sell wire, no currency mutation, no
InqAcceptability). New event handling follows the newest existing
GameEvent handler's registration pattern, not a bespoke route.
### Gates
Per landing: build + full suite green (clean-room before each landing
commit). Slice gate (user, connected, ~3 min): approach a Holtburg
vendor, use them, the authored panel opens on the browse tab with
retail-correct items/icons/prices; category tabs filter; walking out of
range closes the panel by itself; nothing is purchasable anywhere.
## Slice 6 — vendor transactions, buy arc (contract authored 2026-08-08; user-pulled forward)
**Research:** `docs/research/2026-08-08-slice6-vendor-transactions-research.md`.
User direction: "I cant buy anything... Fix that first." Root cause of all
four reported symptoms: `VendorUiController` never touches the shared
`SelectionState`/`StackSplitQuantityState` owners every other panel uses.
### Decisions
1. **Shop items materialize into `ClientObjectTable`** while the session is
open (retail creates real CWeenieObjects from the vendor list — Slice 5
research §A.2) and are REMOVED on session close/replace/reset. The
implementer verifies retail's removal site (gmVendorUI::CloseVendor
family) and mirrors its lifecycle. This dissolves F7c's blocker:
`ExamineItemRequested` gets wired in this slice.
2. **Vendor selection is the GLOBAL selection**: a new vendor change source
on the canonical `SelectionState`; row-click selects through it; the
status bar and the existing byte-faithful `StackSplitQuantityState`
slider follow automatically (the split-size mask helper from 5.4's F2
feeds the vendor-owned seeding exactly as gmToolbarUI does at
pc:198635-198790).
3. **Buy = retail's Buy button**: immediate single-item purchase
(gmVendorUI::BuySingleItem, pc:201661) of the selected item with the
slider-chosen quantity for stacks. Outbound `0x005F`: vendorGuid,
count, per-item (i32 amount, u32 guid), TRAILING u32
alternateCurrencyId — the real client sends it (CM_Vendor::Event_Buy,
pc:689288) even though ACE's reader ignores it; we port the real
client. The request rides the EXISTING J5.2 one-request-at-a-time
reservation and completes on `UseDone` (0x01C7) — already the wired
completion signal; no second gate.
4. **Reconciliation is the existing inbound machinery**: money property
updates, inventory CreateObject, and the ApproachVendor refresh
(VendorState.Refreshed) all flow through landed handlers — the slice
VERIFIES the loop end-to-end rather than adding an owner.
5. **No double-click-to-buy**: retail has no such mechanism (confirmed
against the full named table). We match retail. If the user wants it
as a deliberate modernization it needs their explicit call + an AP row.
6. **Deferred, still AP-161**: the Add button / Buying-tab staging list
and everything Sell (0x0060 — researched, next arc).
### Ordered work (one implementer, bisectable commits)
- **6.1** shop-item materialization + removal lifecycle + examine wiring.
- **6.2** the selection coupling (source, row-click, split seeding) —
status bar + slider light up.
- **6.3** the 0x005F builder (golden-byte tests incl. the trailing dword),
Buy-button wiring, gate/UseDone completion, and the verified
reconciliation round-trip. Register: AP-161 narrowed in the landing.
### Gate (user, connected)
Select a stacked item → it shows in the status bar with the slider; pick
a quantity; Buy → coins drop by the displayed price, the stack lands in
the pack, the shop refreshes; a single-item buy works; insufficient funds
fails cleanly; the session still closes on walk-away/portal with the
materialized items removed.
## Slice 6b/6c — vendor completion (contract authored 2026-08-08)
**Research:** `docs/research/2026-08-08-slice6b-vendor-completion-research.md`.
Closes the user's seven-finding gate batch (dropdown polish landed at
`33b45ee5`). Ordered chunks, one implementer:
1. **Move-to-use (Q2):** wire the existing client-predicted approach
primitive (`PlayerInteractionMovementSink.BeginApproach`,
`MovementType.MoveToObject` — today Pickup-only) onto `RequestUse`, so
using a vendor (or anything) beyond range walks the player in first,
retail's shape. No new movement machinery.
2. **Buy staging (Q3):** `AddToBuyList` semantics per the research
trace — Add stages the selection + slider quantity into the Buying
tab's list (rendered count/price), Buy on that tab sends ONE batched
0x005F with every staged entry, the two removal shapes + Clear, and
retail's X-close confirm dialog when staging is non-empty (the current
unconditional hide stays correct only for empty staging).
3. **Selling (Q4):** the Selling tab's list is THE drop target — accept
pack-item drops via the `ExternalContainerController` drag-handler
pattern, filter through `InqAcceptability` (all four rejection reasons
with retail's exact strings), staged sell list, batched 0x0060, and
the existing reconciliation machinery.
4. **Status bar (Q5) — user evidence is the axiom:** the code-reading
says our chain already matches retail, but the user's live session
says the stack count/value/split-bar presentation is absent for a
vendor selection. Reproduce in a UI-level test FIRST (drive the real
SelectedObjectController mount with a vendor selection); fix what the
reproduction reveals; if it genuinely cannot reproduce, STOP and
report with the test as evidence for a live-probe session.
5. **Pack order (Q1):** code-verified correct (wire placement position →
front insert). NO change; the gate re-checks it live and #352 gets
filed only if it reproduces.
Register: same-commit rows for any deviation; AP-161 narrows again as
staging/sell land (its remaining scope should shrink to nothing or to
precisely what stays absent).
**Gate (user):** click a vendor from afar → walk-in + open; stage two
different items with quantities → Buy All → one transaction, coins/items
correct; drag a sellable item onto the Selling tab → stages → Sell →
coins up, item gone; an InqAcceptability-rejected item shows retail's
refusal; X with a staged list → confirm dialog; stacked selection shows
count/value/split-bar in the toolbar; bought items land at the front of
the pack.
## PROGRAM CLOSEOUT — 2026-08-08: all six slices COMPLETE, user-accepted
Slices 5 and 6 closed together after the vendor arc's final gates. The
complete retail vendor experience is live and user-verified end to end:
walk-to-use from afar (the never-animated-target physics-host resolver +
the cylinder-gap range watcher), the authored panel with the scrollable
category dropdown (arrow-cap, downward, left-aligned), retail cost
sentences with live purse repaint on every money change, per-unit and
whole-stack pricing per the split-size mask, the MaxStackSize quantity
slider with the right-justified count entry and two-line name wrap in
the toolbar, staged buying (accumulate + 5000 cap + shop-row decrement +
the four pre-send guards + the batched 0x005F + the X-close confirm),
selling (Selling-tab drop target with drag-over auto-switch,
InqAcceptability with verbatim rejection strings, BF_RETAINED, batched
0x0060), double-click-to-buy (AP-171, user-approved modernization),
prepend pack ordering (the cross-queue placement replay), and
materialized shop objects with ownership-checked lifecycle + examine.
Four adversarial Opus reviews found 34 defects before the user saw them;
the user's connected gates found eleven more that only live sessions
expose; two latent client-wide crashers (#348 cursor-handle exhaustion,
#350 render-ledger overflow) were exposed, root-caused, and fixed along
the way. Landed across `e45c95b0..af1a1ef9`. Deferred with issues/rows:
#352 (range-watcher cylinder unit test), AP-166's pending-sell
highlight, AP-167 (SellSingleItem's non-empty-container branch), AP-168's
shop-stock half, Buying/Selling staging polish beyond the landed scope.
This closes the pre-M4 world-interaction completion program.

File diff suppressed because it is too large Load diff

View file

@ -1,167 +0,0 @@
# Modern Runtime Slice B — EnvCell geometry deduplication and full bake
**Date:** 2026-07-24
**Status:** Complete 2026-07-24; full installed-DAT gate passed
**Parent:** [`2026-07-24-modern-runtime-architecture.md`](2026-07-24-modern-runtime-architecture.md), Slice B
**Prior implementation:** [`../superpowers/plans/2026-07-05-mp1b-pak-and-bake.md`](../superpowers/plans/2026-07-05-mp1b-pak-and-bake.md)
## Outcome
Complete the existing MP1b bake without changing runtime behavior. Every
EnvCell file ID remains an independently addressable pak key, while cells with
the same collision-resistant geometry identity share one serialized blob and one
physical byte range. The client does not consume the pak in this slice.
The full gate must replace the failed 865 GB / 52 minute per-cell bake with a
bounded artifact whose size and time scale with unique geometry, not with all
729,888 cell instances.
## Fixed contracts
1. `DatCollection` remains the only DAT reader.
2. `MeshExtractor` remains the only mesh/texture interpreter used by both the
live path and the bake.
3. Geometry identity is one deterministic hash of the complete typed tuple:
`environmentId`, `cellStructure`, surface count, then the ordered surface
IDs. App and Bake call the same Core implementation.
4. The pure identity helper lives in
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity`. It uses FNV-1a in a
dedicated high-bit namespace and retains bit 33 for compatibility with
existing synthetic-geometry diagnostics. WorldBuilder's original
`hash = hash * 31 + value` calculation remains as a conformance helper only.
5. Every EnvCell TOC key is still
`PakKey.Compose(PakAssetType.EnvCellMesh, fileId)`.
6. Aliases share `offset`, `length`, and `crc32`. The pak format and reader do
not gain an indirection table or a second lookup.
7. The shared payload carries the runtime geometry ID in
`ObjectMeshData.ObjectId`, matching the current live
`PrepareCellStructMeshData(geometryId, ...)` path.
8. A geometry-ID collision whose full source tuple differs fails loudly. It
may never alias silently.
9. Blob and TOC order remain deterministic across worker counts.
10. The destination pak is replaced only after a complete temporary artifact
opens and validates. Cancellation or failure preserves the prior file and
deletes the temporary file.
11. The pak itself and raw bake logs are machine artifacts and are not
committed. A compact, path-free bake report is committed.
**Full-DAT correction (2026-07-24):** the first guarded full-catalog run proved
that the original fixed contract was impossible. Installed retail DAT cells
`0x00030175` (`environment=0x277`, structure `0`, surfaces `0x013B,0x0034`)
and `0x01BC0105` (`environment=0x276`, structure `0`, surfaces
`0x04FC,0x0034`) both produce WorldBuilder ID `0x00000002020E8C13` despite
having visibly different geometry. The difference is algebraic:
one environment step contributes `31³`, exactly canceled by the first-surface
difference `0x04FC - 0x013B = 31²`. The collision guard stopped the bake before
writing an artifact. B1 was therefore corrected at the root: the old polynomial
is retained and tested as historical evidence, while runtime and bake now share
the stronger namespaced identity. The full-catalog collision gate remains
mandatory.
## Implementation checkpoints
### B1 — One geometry identity seam
- Extract one pure identity calculation from the App transaction builder into
`AcDream.Core.Rendering.Wb.EnvCellGeometryIdentity`.
- Retain the existing App entry points as delegates so production behavior and
callers do not change.
- Add Core conformance tests for empty surfaces, ordered surfaces, the
dedicated namespace, App/Bake shared values, and the installed-DAT collision
in the legacy WorldBuilder polynomial.
**Gate:** existing App geometry-ID tests and new Core tests pass with the exact
same values.
### B2 — Pak blob aliases
- Make `PakWriter` retain each written key's immutable TOC receipt.
- Add `AddAlias(aliasKey, existingKey)`, rejecting a duplicate alias key,
missing source key, alias after finish, and accidental self/duplicate use.
- Emit a normal TOC row with the alias key and the source receipt's physical
range and CRC.
- Keep the reader unchanged except for test-only receipt inspection.
**Gate:** primary and alias deserialize field-for-field equal, have identical
offset/length/CRC, use one physical blob, preserve sorted TOC order, and isolate
corruption consistently.
### B3 — Deduplicated bake catalog
- Enumerate EnvCell file IDs exactly as today.
- Read each EnvCell header once and produce:
- a sorted `fileId -> geometryId` alias stream;
- one full source descriptor per unique geometry ID.
- Validate equal geometry IDs against the complete tuple
`(environmentId, cellStructure, ordered surfaces)`.
- Extract only the unique descriptors through
`MeshExtractor.PrepareCellStructMeshData(geometryId, ...)`.
- Write the first sorted cell key as the physical blob and every remaining
cell key as an alias.
- Keep GfxObj, Setup, and side-staged particle-preload assets in the same
deterministic writer transaction.
- Report enumerated cells, valid aliases, unique geometries, dedup ratio,
physical blobs, failures, wall time, output bytes, and peak working set.
**Gate:** a filtered real-DAT fixture containing duplicate cells produces more
EnvCell keys than physical EnvCell blobs, and every key reads the same payload
as a live unique-geometry extraction.
### B4 — Transactional output and compatibility
- Bake beside the destination under a unique temporary filename.
- Pass cancellation through enumeration and parallel extraction.
- Open the completed temporary pak and validate its header, TOC count, source
DAT iterations, bake-tool version, and every TOC range before replacement.
- Atomically replace the destination on the same volume.
- Add stale-version, corruption, cancellation, partial-write, existing-target
preservation, and temporary-file cleanup tests.
**Gate:** no failure path damages or replaces a previously valid pak.
### B5 — Full-scale gate
- Run Release build and the focused Content/Bake suites.
- Run two filtered bakes with different thread counts and require byte-identical
output.
- Run the complete unfiltered bake against the installed retail DAT files.
- Open the result, validate every TOC entry, sample aliases across multiple
landblocks, and run the existing live-vs-pak field equivalence suite.
- Record counts, dedup ratio, failures, elapsed time, peak working set, and
artifact size in `docs/research/`.
- Delete the generated pak after validation unless it is intentionally retained
outside the repository for the later Slice C cutover.
**Exit:** the full bake completes in practical time and disk space, all
equivalence and determinism gates are green, duplicate EnvCells demonstrably
share physical offsets, and no runtime code consumes the pak.
## Completion record
All checkpoints B1-B5 are complete. The full installed-DAT bake produced
751,141 addressable keys backed by 38,370 physical blobs with zero failures.
The 729,888 EnvCell keys resolve to 17,117 unique geometries and 712,771 aliases
(42.6×). The validated package is 28,192.4 MiB and publishes atomically in
81.4 seconds on the reference machine. A cross-landblock eight-key alias fixture
is byte-identical with one versus eight workers. Full evidence:
[`../research/2026-07-24-slice-b-full-bake-report.md`](../research/2026-07-24-slice-b-full-bake-report.md).
No runtime code reads the pak in this slice. Slice C remains blocked by the
physical-local Slice A baseline gate. Release build and the complete solution
suite pass (8,059 passed, five skipped, zero failed); generated pak artifacts
were deleted after validation.
## Verification
Run:
```powershell
dotnet test tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj -c Release
dotnet test tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj -c Release
dotnet build AcDream.slnx -c Release
dotnet test AcDream.slnx -c Release
```
Then run the full bake from the Release binary, capture its compact report, and
validate the output with `PakReader`. No visual gate is required because this
slice changes no runtime path.

View file

@ -1,347 +0,0 @@
# Modern Runtime Slice C — prepared-asset cutover
**Date:** 2026-07-24
**Status:** complete — automated, connected, performance, and user visual
gates passed 2026-07-24
**Checkpoint ledger:**
- C1 typed prepared-source boundary — `c42f93b3`
- C2 production renderer injection — `f05afc07`
- C3 activation probes and lookup precedence — `230a7df4`; automated gate
green
- C4 prepared-source diagnostics — `b1ad4b7c`
- C4 connected equivalence and performance closeout —
[`../research/2026-07-24-slice-c-prepared-asset-cutover-report.md`](../research/2026-07-24-slice-c-prepared-asset-cutover-report.md)
**Parent:** [`2026-07-24-modern-runtime-architecture.md`](2026-07-24-modern-runtime-architecture.md),
Slice C / MP1c
**Authoritative before-state:**
[`../research/2026-07-24-slice-a-physical-local-baselines.md`](../research/2026-07-24-slice-a-physical-local-baselines.md)
at source and binary commit
`49f3a48ee63b82d00731b0f91497930925279c86`.
## 1. Outcome
Production world-mesh streaming reads immutable `ObjectMeshData` from the
validated `acdream.pak`; it no longer reconstructs Setup, GfxObj, EnvCell,
Surface, texture, and particle dependency graphs from DAT on four background
workers during gameplay.
DAT remains the retail source of truth for simulation, UI, animation, dynamic
appearance composition, physics, audio, and effects. The cutover changes when
static render payloads are prepared, not their bytes, pixels, ownership, or
gameplay meaning.
The slice is complete only when:
- the package is a required production input with exact DAT-iteration and bake
version validation;
- ordinary GfxObj and deduplicated EnvCell requests use typed pak keys;
- missing, corrupt, canceled, and successful reads remain distinct;
- production streaming has no implicit live-DAT extraction fallback;
- Setup activation uses an explicit typed presence check before parsing DAT;
- Portal DAT still wins when an ID exists in both Portal and HighRes;
- cancellation and unexpected exceptions are never swallowed;
- installed-DAT byte/field equivalence and the connected visual/lifetime gates
pass;
- portal allocation, exception count, p99, and repeated-Caul memory materially
improve against the committed physical baseline.
## 2. Fixed architecture
### 2.1 Content-layer contract
Add a BCL/Content-only `IPreparedAssetSource` with a typed request and typed
result:
```csharp
enum PreparedAssetReadStatus
{
Loaded,
Missing,
Corrupt,
}
readonly record struct PreparedAssetRequest(
PakAssetType Type,
uint SourceFileId,
ulong RuntimeObjectId,
PreparedEnvCellSchema? EnvCell);
readonly record struct PreparedAssetReadResult(
PreparedAssetReadStatus Status,
ObjectMeshData? Data);
interface IPreparedAssetSource : IDisposable
{
PreparedAssetPresence Probe(PakAssetType type, uint sourceFileId);
PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken);
PreparedAssetSourceStats Stats { get; }
}
```
`CancellationToken` cancellation throws `OperationCanceledException`; it is
not converted into a missing asset. `Loaded` always carries data and
Missing/Corrupt never do.
The existing `PakKey` already persists the asset-type byte in each TOC key.
Therefore Slice C does not invent a second type table or bump the binary
format. A TOC-only probe binary-searches the exact
`(PakAssetType, SourceFileId)` key without reading, CRC-checking, copying, or
deserializing the blob. This is the explicit positive/negative/type metadata.
`PakPreparedAssetSource` owns one `PakReader`. It:
- validates format, bake-tool version, and all four DAT iterations at open;
- exposes TOC-only presence;
- maps PakReader's missing/corrupt/loaded outcomes without ambiguity;
- validates the deserialized payload's logical type and runtime identity;
- never falls through to DAT extraction.
An explicit `DatPreparedAssetSource` is retained only for bake/equivalence and
UI Studio tooling. It uses `MeshExtractor` and is never selected by
`GameWindow`.
### 2.2 Request identity
The renderer's internal runtime ID and the package lookup ID are not always
the same:
| Request | Pak key | `ObjectMeshData.ObjectId` |
|---|---|---|
| ordinary world/live mesh | `GfxObjMesh + GfxObj DID` | GfxObj DID |
| Setup tooling request | `SetupMesh + Setup DID` | Setup DID |
| EnvCell shell | `EnvCellMesh + source Cell DID` | deduplicated geometry ID |
`EnvCellShellPlacement.CellId` is therefore carried into
`EnvCellMeshPreparationScheduler` and retained beside the existing geometry
schema. Cancellation/re-arm reuses that exact descriptor; it never derives a
fake DAT ID from the synthetic geometry hash.
Aliases in the pak make every source Cell DID resolve to the one shared
physical geometry blob. Runtime validates that the blob's `ObjectId` equals
the scheduler's deduplicated geometry ID before publishing it.
### 2.3 Ownership and thread model
- `ContentEffectsAudioComposition` opens DAT first, then the prepared source,
and publishes both through typed owners.
- `GameWindow` stores only the additional lifetime root and passes the source
through `ContentEffectsAudioResult`.
- `WorldRenderComposition` injects the source into `WbMeshAdapter`.
- `ObjectMeshManager`'s existing four workers read immutable prepared
payloads. They retain the current request deduplication, LIFO locality,
cancellation, CPU cache, staging high-water, ownership, and retry rules.
- Pak reads and deserialization remain worker-only. GL upload remains
render-thread-only.
- Shutdown disposes `WbMeshAdapter` and joins its workers before unmapping the
pak, then disposes DAT. Construction rollback uses the reverse order.
- No worker, source, or result stores a window reference or service locator.
### 2.4 Setup activation and non-allocating DAT lookup
`WorldEntity.SourceGfxObjOrSetupId` can be either a GfxObj or Setup. The current
`SequencerFactory` and `ResolveActivation` try to parse every value as Setup;
the physical baseline records 4,659 `ArgumentOutOfRangeException` events on one
uncapped route.
Both paths first probe `SetupMesh + id` in the prepared source:
- absent: return the existing no-Setup/no-script result without a DAT call;
- present: parse the valid Setup through the bounded DAT object cache;
- corrupt package entry: renderer load fails loudly; activation does not guess;
- cancellation: rethrow;
- known corrupt DAT data (`InvalidDataException`, `EndOfStreamException`, and
the exact verified DatReaderWriter malformed-record exception): diagnose and
return no activation;
- unexpected exceptions: diagnose and rethrow.
The exception list is evidence-driven from installed fixtures and tests; a
blanket `catch` is forbidden.
`IDatReaderWriter.TryResolvePreferred` is added as a non-allocating lookup.
The concrete adapter checks Portal first, then HighRes, Language, and Cell.
That encodes the existing load-bearing Portal-wins behavior directly.
`ResolveId()` remains for compatibility tooling, but production mesh
extraction and bounds code no longer use
`ResolveId().ToList().OrderByDescending(...)`.
### 2.5 Texture identity
Pak payloads contain default-palette texture bytes. Dynamic appearance
composition continues through the existing owner-scoped `TextureCache`.
Default and palette-overlaid pixels retain distinct cache keys; the prepared
source never inserts default bytes into the dynamic composite cache.
## 3. Checkpoints
### C1 — typed prepared-source boundary
Implement:
- typed request/presence/read-result/stat records;
- PakReader TOC-only probe and status-preserving read;
- `PakPreparedAssetSource` compatibility validation;
- explicit `DatPreparedAssetSource` for tooling;
- `RuntimeOptions.PreparedAssetPath`, defaulting to
`<DatDir>/acdream.pak`, with `ACDREAM_PAK_PATH` as the sole override;
- Content tests for absent/present/corrupt/canceled/type-mismatched reads,
alias identity, iteration mismatch, bake-version mismatch, and disposal.
Gate:
- Content and Bake tests green;
- current pak round-trip/equivalence tests remain green;
- no App/runtime behavior changed yet.
Commit as one boundary unit.
### C2 — production renderer injection
Implement:
- prepared-source acquisition/publication/teardown in composition;
- `WorldRenderComposition` and `WbMeshAdapter` injection;
- `ObjectMeshManager` worker reads through `IPreparedAssetSource`;
- source Cell DID propagation for EnvCell aliases;
- exact `TranslucencyKind` persisted per prepared texture batch, removing the
render-thread `GfxObjMesh.Build` metadata reconstruction (bake-tool identity
advances to version 3; the pak container remains format version 1);
- exact terminal-failure and cancellation semantics;
- current CPU cache, staging, upload, retry, ownership, and shutdown behavior
retained;
- UI Studio explicitly opts into the live-DAT tooling source;
- remove production worker calls to `MeshExtractor`.
Focused tests:
- GfxObj request key and identity;
- EnvCell alias key plus geometry identity;
- cached/re-armed/canceled generation;
- missing and corrupt entries become terminal preparation failures without
retry storms;
- source disposed only after workers and mesh adapter;
- production composition cannot construct without a valid prepared source;
- tooling source remains explicit.
Gate:
- App Release build/tests green;
- source audit finds no production `MeshExtractor.Prepare*` call from the
streaming workers;
- missing pak and incompatible pak fail at startup with actionable messages.
Commit as one integration unit.
### C3 — activation probes and lookup precedence
Implement:
- Setup-presence gate shared by `SequencerFactory` and `ResolveActivation`;
- specific corrupt-data exception handling and diagnostics;
- cancellation and unexpected-exception propagation;
- non-allocating Portal-first `TryResolvePreferred`;
- remove the old LINQ lookup in `MeshExtractor`, recursive part collection,
and remaining bounds helper;
- explicit default-palette versus palette-overlay cache-identity tests.
Focused tests:
- GfxObj entity never enters Setup parser;
- valid Setup parses once then hits bounded cache;
- corrupt Setup is diagnosed once;
- unexpected exception is visible;
- cancellation propagates;
- dual-DAT fixture chooses Portal;
- missing ID allocates no result collection;
- drudge, robed-player, and palette-dyed-armor seam fixtures preserve fields
and pixels.
Gate:
- no `ResolveActivation` bare catch;
- zero `ArgumentOutOfRangeException` Setup-probe storm on connected route;
- full Release suite green.
Commit as one semantic-cleanup unit.
### C4 — bake, connected equivalence, and performance closeout
1. Re-bake installed DATs atomically to the production path with the current
tool.
2. Validate the full artifact, key counts, aliases, CRC contract, iterations,
and exact source/binary provenance.
3. Run focused byte/field equivalence and seam fixtures.
4. Run the capped and uncapped physical nine-stop routes plus pinned dense
Arwic with the same reference configuration.
5. Compare fixed-camera screenshots under the committed tolerance/mask rule.
6. Compare per-portal p99/allocation, process allocation/GC, exception count,
cache counters, prepared-source probes/reads/loaded/missing/corrupt totals,
and repeated-Caul ownership/memory to Slice A.
7. Run Release build and the complete Release suite.
8. Update the parent plan, roadmap, WorldBuilder inventory, divergence
register if and only if a real deviation changed, and durable memory.
Acceptance:
- every route reaches every destination and disconnects gracefully;
- no world, animation, particle, effect, texture, mesh, landblock, or GPU
ownership regression;
- fixed-camera screenshots pass;
- no invalid Setup exception storm;
- no production DAT mesh extraction;
- portal single-frame allocation and p99 materially improve;
- process allocation/GC materially improve;
- repeated-Caul memory passes the existing assertion without weakening it.
If pixels or behavior differ, the slice is not complete even when performance
improves. If timing improves but heap reservation still fails, keep the failure
open and attribute it before considering Slice C closed.
### C4 result — completed 2026-07-24
The format-1/bake-tool-3 production package validated with 751,141 keys and
the exact installed DAT iterations. Installed-DAT seam/equivalence tests, all
three connected physical-display routes, canonical lifetime assertions, and
the complete Release suite passed. The user accepted the prepared path's
geometry, materials, textures, and world presentation.
Against the committed Slice A physical baseline:
- capped CPU p95/p99 improved 15.8%/16.9%, GPU p95/p99 17.4%/20.0%;
- uncapped CPU p95/p99 improved 13.8%/17.0%, GPU p95/p99 22.5%/20.6%;
- dense-Arwic CPU/GPU p99 improved 12.0%/16.8%;
- process allocation fell 54.9% capped, 41.8% uncapped, and 61.9% dense;
- GC pause time fell 52.8%, 48.4%, and 54.1%;
- the invalid typed-Setup exception count fell to zero in every route;
- the largest frame-thread allocation fell from roughly 203 MiB to 46.8 MiB
capped and 39.9 MiB uncapped;
- both warm repeated-Caul memory comparisons passed the unchanged assertion.
Windows reports clean memory-mapped package pages in working set, so total
working set is higher even though private memory and ownership remain bounded.
That is file cache, not retained world state. Exact measurements and the
artifact policy are recorded in the
[Slice C report](../research/2026-07-24-slice-c-prepared-asset-cutover-report.md).
## 4. Review checklist per checkpoint
Because this task is running without delegated reviewers, the primary engineer
performs three explicit read-only passes over each complete checkpoint diff:
1. **Contract/conformance:** request identity, Portal precedence, package
compatibility, byte/field/pixel equivalence.
2. **Architecture/lifetime:** dependency direction, worker/render-thread
ownership, cancellation, construction rollback, shutdown order, no
production fallback.
3. **Adversarial:** corrupt/truncated package, missing key, stale/canceled
request, alias mismatch, callback failure, exception visibility, repeated
portal/GUID/landblock churn.
Every confirmed finding is fixed and the full diff is re-read before the
checkpoint commit.

View file

@ -1,300 +0,0 @@
# Modern Runtime Slice D — Typed Asset Handles and Unified Residency
**Status:** complete — D1 through D5 passed 2026-07-24
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
**Baseline:** Slice C closeout commit `a564c4b7`
**Behavior contract:** no visual-quality or retail-behavior change
## 1. Outcome
Slice D makes runtime memory finite, attributable, and generation-safe without
replacing the specialized caches that already work.
One `ResidencyManager` owns policy, logical asset identity, generations,
leases, budgets, and the aggregate diagnostic snapshot. Existing renderer and
content owners continue to own their physical storage:
- `ObjectMeshManager` owns object render data, mesh ranges, texture atlases,
prepared CPU mesh entries, and staged uploads.
- `GlobalMeshBuffer` owns vertex/index arenas and fence-delayed range reuse.
- `CompositeTextureArrayCache` owns pooled composite-array layers and arrays.
- `StandaloneBindlessTextureCache` owns standalone particle arrays.
- `GpuFrameFlightController` and `GpuRetirementLedger` own fence-delayed
physical release.
- `RetailAnimationLoader` owns parsed animation residence.
- Deferred-alpha producers own their reusable CPU scratch arrays.
The manager never stores or deletes an OpenGL name. It observes immutable
facts, makes bounded policy decisions during the update/resource-maintenance
phase, and issues logical trim requests. The physical owner executes any GL
release during its existing render-thread maintenance phase.
## 2. Current-owner inventory
| Domain | Existing policy | Physical owner | Slice D action |
|---|---|---|---|
| Object render data | 1 GiB estimated GPU / 50 unowned objects, per-frame bounded reclamation | `ObjectMeshManager` | Preserve; source limits from typed budgets; report logical/non-arena/arena/retiring bytes separately |
| Prepared mesh CPU cache | 100 entries / 128 MiB LRU | `CpuMeshUploadCache` | Preserve; source limits from typed budgets; report hits, misses, evictions, count, bytes |
| Mesh staging | 256 claims / 128 MiB, generation checked | `MeshUploadStagingQueue` | Preserve; source limits from typed budgets; report queued and claimed bytes separately |
| Global mesh arenas | 384 MiB vertex, 128 MiB index, 896 MiB maximum physical overlap; shrink hysteresis | `GlobalMeshBuffer` | Preserve; expose live, free, largest-free, staging, retired, and fragmentation facts |
| Composite texture arrays | 64 MiB unowned / 128 MiB physical, throttled logical eviction and fence release | `CompositeTextureArrayCache` | Preserve; source limits from typed budgets; report resident/unowned/retiring/fragmentation |
| Standalone particle arrays | 32 MiB / 256 unowned, one eviction per frame | `StandaloneBindlessTextureCache` | Preserve; source limits from typed budgets; report owned/unowned/retiring bytes |
| Prepared package | one immutable memory map; payload arrays are charged to CPU cache/staging | `PakPreparedAssetSource` | Report mapped virtual bytes separately; do not mislabel the 27.5 GiB address map as committed residence |
| Parsed animations | unbounded dictionary | `RetailAnimationLoader` | Add byte-and-count bounded concurrent LRU; a live sequencer keeps its own reference after cache eviction |
| Decoded audio waves | 32 MiB LRU | `DatSoundCache` | Preserve and report; sound-table metadata is finite DAT identity metadata |
| Deferred-alpha scratch | grow-only lists/arrays at three submission seams | producer owners | Add capacity-budgeted retention with immediate correctness-preserving growth and policy-driven shrink |
| Mesh bounds memo | not present in current source | none | Close stale issue #243; do not add a replacement |
The process-wide `GpuMemoryTracker` remains an independent physical-allocation
cross-check. It is not a policy owner because it intentionally covers shaders,
framebuffers, UI textures, and other allocations outside Slice D's cache set.
## 3. Typed identity
```csharp
readonly record struct AssetHandle<TAsset>(uint Index, ushort Generation);
readonly record struct OwnerToken(uint Index, ushort Generation);
readonly record struct AssetLease<TAsset>(
AssetHandle<TAsset> Handle,
OwnerToken Owner);
```
Rules:
1. Index zero and generation zero are invalid.
2. A slot generation increments before an index is reused.
3. A stale handle cannot touch, transition, lease, release, or retire its
replacement generation.
4. Owner tokens are allocated and retired independently from assets. Retiring
an owner releases exactly that owner's leases.
5. Repeated acquisition of the same `(asset, owner)` is idempotent.
6. Callers never derive a handle, owner, or generation through an integer cast.
7. Physical cache keys remain domain-specific (`ulong` mesh id,
`CompositeTextureKey`, surface DID). An adapter maps those keys to typed
handles; the manager does not replace their key or storage types.
## 4. State machine
The logical state machine is single-writer. Terminal failure states preserve
diagnostic identity until a new generation is requested.
| Current | Event | Next | Required accounting/action |
|---|---|---|---|
| `Absent` | request | `Requested` | allocate/current generation; record priority and request frame |
| `Requested` | prepare succeeds | `Prepared` | publish actual CPU prepared/decoded bytes |
| `Requested` | cancel | `Cancelled` | clear transient bytes; never publish a completion |
| `Requested` | missing | `Missing` | retain negative result only |
| `Requested` | corrupt | `Corrupt` | retain diagnostic result; do not retry silently |
| `Requested` | failure | `Failed` | retain typed failure; do not swallow |
| `Prepared` | queue upload | `UploadPending` | move or add staging/GPU-requested charges |
| `Prepared` | release with no owners | `Retiring` | enqueue logical eviction |
| `UploadPending` | upload succeeds | `Resident` | clear requested/staging charge; publish exact resident charge |
| `UploadPending` | retryable rollback completes | `Prepared` | clear GPU-requested charge; retain CPU data |
| `UploadPending` | cancel/no owners | `Retiring` | invalidate generation; stale completion is ignored |
| `UploadPending` | corrupt/failure | `Corrupt`/`Failed` | clear transient charges; retain diagnostic state |
| `Resident` | acquire/touch | `Resident` | update owner set, priority, frame, and rebuild cost |
| `Resident` | last owner releases | `Resident` | becomes eviction-eligible; physical cache may retain it |
| `Resident` | evict | `Retiring` | invalidate logical availability before physical release |
| `Retiring` | fence release completes | `Absent` | clear retiring bytes and recycle slot with next generation |
| any non-retiring state | session/world generation invalidates | `Retiring` or terminal cancel | reject later stale work |
| terminal failure state | explicit new request | `Requested` on a new generation | prior completion cannot revive the new request |
Illegal transitions throw in tests and emit a release diagnostic in
production composition; they are never silently coerced.
## 5. Threading and phase ownership
The graphical client currently advances update and render on one native window
thread. Slice D nevertheless fixes the contract at the future host boundary:
### Shared/worker side
- Decode and preparation workers may only enqueue immutable
`ResidencyObservation` records.
- Workers never mutate the manager ledger, budgets, owners, GL resources, or
render cache collections.
- The observation queue is bounded by active asset generations. Repeated
observations for one `(handle, kind)` coalesce last-writer before drain.
- Cancellation and corrupt/failure completion carry the exact asset generation.
### Update/resource-maintenance phase
- `ResidencyManager` is single-writer.
- At most one manager drain and policy pass runs per host tick.
- It applies observations, updates the lease/state ledger, captures domain
facts, and emits bounded `ResidencyTrimRequest` commands.
- Eviction order is: zero owners first, lowest priority first, oldest use
generation/frame first, lowest rebuild cost first, then stable handle index.
- Manager code never waits on decode, file I/O, a GPU fence, or a lock acquired
by the render path.
### Render-resource phase
- Specialized owners consume trim requests without calling back into the
manager while holding their internal mutation locks.
- Logical removal occurs before publication of a physical-release request.
- OpenGL non-residency/deletion and arena range return remain render-thread
only and fence delayed through the existing retirement owners.
- Physical completion is observed on the following manager drain. A stale
completion with the wrong handle generation is rejected.
No render-thread code blocks waiting for the manager, and the manager never
sees a raw GL name or bindless handle.
## 6. Accounting model
Every domain reports the following independent values:
- `LogicalBytes`: immutable metadata and logical entry overhead where measured.
- `CpuPreparedBytes`: parsed/prepared payload retained for reuse.
- `DecodedBytes`: canonical decoded pixels/audio/animation graphs.
- `PinnedBytes`: CPU data that cannot currently be evicted due to a live lease.
- `StagingBytes`: queued or claimed upload payload.
- `GpuRequestedBytes`: physical allocation requested but not yet published.
- `GpuResidentBytes`: currently drawable physical allocation.
- `RetiringBytes`: logically dead physical allocation awaiting a fence/retry.
- `MappedVirtualBytes`: immutable package address space, not committed RAM.
- `BudgetBytes`: the policy ceiling for the relevant charge.
- `CapacityBytes`, `UsedBytes`, `LargestFreeBytes`: allocator facts used to
calculate fragmentation without pretending free capacity is a leak.
Aggregate totals never add `MappedVirtualBytes` to committed CPU residence and
never add an arena's logical allocation bytes to its physical capacity twice.
## 7. Budgets and defaults
`RuntimeOptions` owns environment parsing once. `ResidencyBudgetOptions`
contains byte/count ceilings with the current production values as defaults:
- object mesh physical: 1 GiB;
- object mesh unowned entries: 50;
- prepared mesh CPU: 128 MiB / 100;
- staging: 128 MiB / 256;
- composite arrays: 128 MiB physical / 64 MiB unowned;
- standalone particle arrays: 32 MiB / 256;
- decoded animations: 64 MiB / 512;
- decoded audio: 32 MiB;
- deferred-alpha retained CPU scratch: 16 MiB aggregate with per-owner floors.
Environment overrides are diagnostic/startup inputs and reject zero, negative,
overflowing, or malformed values by falling back to the documented default.
The settings quality preset may select a complete budget profile later; Slice D
first exposes the typed runtime target so a settings change is atomic rather
than mutating individual caches independently. Defaults preserve today's
visual radius and cache behavior.
Multi-session retuning is evidence driven. This slice records actual working
sets under normal, dense, and forced-pressure routes before changing production
defaults below their current values.
## 8. Delivery checkpoints
### D1 — Contract and logical ledger
- Add typed handles, owner tokens, leases, priorities, states, observations,
trim requests, budgets, and aggregate snapshots.
- Implement the transition table and generation-safe owner release.
- Test stale completions, duplicate acquire, owner reuse, asset-slot reuse,
cancellation, corrupt/failure states, and illegal transitions.
**Complete 2026-07-24.** The typed ledger, generation checks, bounded
worker-observation journal, startup budget profile, and transition conformance
tests landed without moving physical GL ownership into the policy layer.
### D2 — Existing-owner adapters
- Parameterize current mesh, staging, composite, and standalone budgets.
- Add exact snapshot facts to each owner.
- Register them with one manager without changing their physical storage or
fence protocol.
- Cross-check manager GPU totals with `GpuMemoryTracker`, allowing explicitly
enumerated non-cache resources.
**Complete 2026-07-24.** Production composition passes one immutable budget
profile into the existing mesh and texture owners and registers exact snapshot
sources for object geometry/atlases, prepared meshes, staging, the global mesh
arena, composite arrays, standalone particle arrays, and the prepared package
mapping. Removed object atlases and standalone textures retain retiring-byte
ownership until their accepted frame-fence release actually completes.
Lifecycle artifacts now publish both cache-attributed GPU bytes and the signed
`GpuTrackerMinusResidencyBytes` remainder; that remainder deliberately covers
enumerated non-cache allocations such as terrain, shaders, UI textures,
framebuffers, and dynamic draw buffers rather than being mislabeled as cache
residence.
### D3 — Missing policies
- Bound `RetailAnimationLoader` by count and estimated retained bytes.
- Bring deferred-alpha retained scratch behind a bounded capacity owner.
- Close stale #243 after proving no current bounds cache exists.
**Complete 2026-07-24.** `RetailAnimationLoader` now uses a concurrent
in-flight load gate feeding a byte-and-count bounded LRU. Cache eviction drops
only the cache reference, so active sequencers retain valid immutable
animations; duplicate DID requests coalesce while unrelated reads remain
parallel. The shared retail alpha path now partitions its 16 MiB startup budget
between the queue, object dispatcher, and particle renderer. Each physical
owner grows immediately when a frame requires it, reports exact retained
backing capacity, and shrinks only after repeated severe under-use. This bounds
one-frame density spikes without changing draw order, particle content, or
visible quality. Issue #240 is closed. Issue #243 is closed as a stale audit
finding: no `_boundsCache`, bounds memo, or cited insertion site exists in the
current source, so no replacement cache was invented.
### D4 — Diagnostics and forced pressure
- Publish aggregate and per-domain budgets/occupancy/fragmentation through the
lifecycle diagnostic artifact.
- Add deterministic forced-pressure tests that exceed every existing ceiling,
drain fence release, and prove convergence.
- Assert manager and physical-owner accounting return to zero at teardown.
**Complete 2026-07-24.** The lifecycle artifact now serializes validated
per-domain rows plus aggregate entry/owner, budget, allocator
capacity/usage/fragmentation, traffic, committed CPU, mapped-address-space, and
physical GPU totals. The final omitted bounded owner, decoded audio, now
receives its startup budget through `RuntimeOptions` and reports decoded bytes
and hit/miss/eviction traffic beside animations and the render caches. A
deterministic all-domain pressure matrix exceeds every reported ceiling and
proves zero-charge convergence. The focused physical-owner suites separately
exercise prepared-mesh/staging rejection, composite and standalone texture
eviction, fence-delayed release, global-arena range retirement, animation/audio
LRU pressure, and alpha-scratch convergence. Release build and the complete
8,111-test / 5-skip solution gate are green.
### D5 — Connected gate
- Run capped, uncapped, and dense connected routes against Slice C.
- Run repeated same-location portal loops and compare first/second/third visit.
- Require no missing texture/mesh, no stale-generation release, no monotonic
owner/resource growth, and graceful shutdown.
- Preserve fixed-camera screenshots and obtain the user's visual gate if the
connected route changes any visible frame.
**Complete 2026-07-24.** The exact D4 source/binary commit `1853a57c` passed
the capped and uncapped physical-local nine-stop routes and pinned dense Arwic,
with zero failures and graceful transport shutdown in all three processes.
Cache-attributed residence was non-monotonic: the third Caul checkpoint fell
from the second visit's 50.3/355.0 MiB committed-CPU/physical-GPU residence to
38.5/335.1 MiB. Parsed animations stopped at 55 entries, alpha scratch stayed
below 16 MiB, and staged/retiring bytes were zero at every checkpoint. The
harness's fixed-camera captures passed without a visible-frame change. Exact
methodology and measurements:
[`../research/2026-07-24-slice-d-unified-residency-report.md`](../research/2026-07-24-slice-d-unified-residency-report.md).
## 9. Acceptance
Slice D closes only when:
1. All state/ownership/generation tests pass.
2. Forced-pressure tests prove the configured ceilings, not merely a route
that happens to stay below them.
3. The third same-location visit plateaus within the documented tolerance.
4. Repeated portal loops do not grow logical owners, staged work, resident GPU
resources, retiring resources, or parsed animation residence.
5. The complete Release build/test suite is green.
6. The architecture, roadmap, issues, inventory, and durable memory agree with
the implementation.
7. Connected screenshots retain Slice C's visual output.
All seven conditions passed 2026-07-24.

View file

@ -1,484 +0,0 @@
# Modern Runtime Slice E — Cost-budgeted streaming and retirement
**Status:** complete — E0/E1/E2/E3/E4/E5/E6 landed and gated 2026-07-24
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
**Baseline:** Slice D closeout commit `66690805`
**Behavior contract:** no visual-quality or retail-behavior reduction
## 1. Outcome
Slice E removes whole-window portal publication and retirement transactions
from a single update frame. It preserves the existing worker, canonical
`GpuWorldState`, presentation owners, retry receipts, world-reveal barrier,
and renderer resource owners.
The change is scheduling and lifecycle architecture:
- one immutable `StreamingWorkBudget` defines real per-frame limits;
- one single-writer scheduler admits and advances explicit work stages;
- destination-critical work receives reserved capacity across portal frames;
- old generations become unavailable and quiescent immediately;
- expensive owner/resource teardown resumes from exact cursors;
- publication advances through exact owner receipts under time, byte, entity,
upload, and retirement-operation limits;
- the existing `WorldRevealCoordinator` remains the only reveal predicate.
No distance, texture, particle, cell, collision, or presentation quality is
reduced.
## 2. Retail boundary
Retail teardown is synchronous:
- `CLandBlock::destroy_static_objects @ 0x0052FA50`;
- `CLandBlock::Destroy @ 0x0052FAA0`;
- `CObjectMaint::DestroyObjects @ 0x00508C30`.
Retail portal/cell blocking freezes object maintenance, physics, landscape,
game time, and ambient sound while continuing scene/UI/event work:
`SmartBox::UseTime @ 0x00455410`.
Acdream's async adaptation may defer physical cleanup, but may not leave a
retired owner active. Full translation:
[`../research/2026-07-24-retail-streaming-retirement-pseudocode.md`](../research/2026-07-24-retail-streaming-retirement-pseudocode.md).
## 3. Existing-owner inventory
| Concern | Current owner | Current behavior | Slice E action |
|---|---|---|---|
| Worker inbox/outbox | `LandblockStreamer` | unbounded channels; near jobs prioritized | retain worker; expose bounded, allocation-free single-consumer drain and backlog facts |
| Completion apply | `StreamingController._deferredApply` | retained `List`; count-budgeted loads; unload and priority ring bypass | replace with explicit generation/priority FIFO stage queues and exact retained-byte accounting |
| Full reload | `BeginFullWindowRetirement` | snapshots and begins every resident retirement synchronously | retain stable snapshot/cursor and advance admissions under budget |
| Origin recenter | `OriginRecenterRetirement` | has a cursor but drains it to completion in one call | make the cursor the canonical budgeted admission receipt |
| Logical/spatial detach | `GpuWorldState.DetachLandblock/DetachNearLayer` | exact immediate bucket withdrawal; captures entity receipt | preserve; add destination-generation quiesce before cursored detaches |
| Retirement ledger | `LandblockRetirementCoordinator` | retryable per-stage/per-entity state, but advances a complete ticket on begin/advance | expose one bounded atomic step at a time; stable FIFO |
| Retirement effects | `LandblockPresentationRetirementOwner` | lights, translucency, plugin, terrain, physics, cells/buildings | preserve order; each entity/stage consumes measured budget |
| Publication ledger | `LandblockPresentationPipeline` | retryable stage booleans, but `Advance` runs the complete transaction | convert booleans/cursors into resumable atomic publication steps |
| Render publication | `LandblockRenderPublisher` | terrain/visibility/AABB/buildings/EnvCells whole stages | retain receipts; meter GPU bytes and split iterable registries where needed |
| Physics publication | `LandblockPhysicsPublisher` | rebuilds terrain/cells/buildings/statics/reflood in one call | cursor cell/building/static preparation and commit at safe mutation boundaries |
| Static presentation | `LandblockStaticPresentationPublisher` | allocates dictionaries/sorts arrays, then loops prior/plugin entities | prepare stable ordered arrays once; cursor cleanup/light/plugin work |
| Mesh upload | `ObjectMeshManager`/`WbMeshAdapter` | existing per-frame renderer owner/budgets | report exact upload bytes/operations to the streaming meter; never move GL ownership |
| Reveal | `WorldRevealCoordinator` + `WorldRevealReadinessBarrier` | one correct login/portal predicate | add destination generation/readiness stage facts; never duplicate predicate |
| Portal wait cue | `PortalTunnelPresentation` | authored tunnel and centered wait notice port exists | hold tunnel past five seconds and publish diagnostic; never early reveal |
| Audio | `OpenAlAudioEngine`/`AudioHookSink` | one-shot owner is not retained in source slot | tag 3-D voices by owner; stop old-generation owners at quiesce |
## 4. Work model
### 4.1 Immutable budget
The App-layer type is equivalent to:
```csharp
public readonly record struct StreamingWorkBudget(
TimeSpan MaxUpdateTime,
int MaxCompletionAdmissions,
long MaxAdoptedCpuBytes,
int MaxEntityOperations,
long MaxGpuUploadBytes,
int MaxGlRetireOperations,
float DestinationReserveFraction);
```
All fields are positive and validated as one profile. The initial High/default
profile is evidence-tuned, not a reduction in quality:
- 2.0 ms update-thread streaming work;
- 64 completion admissions;
- 8 MiB retained CPU payload adoption;
- 256 entity operations;
- 8 MiB requested GPU upload;
- 64 logical GL-retirement admissions;
- 75% reserved for destination-critical work while a reveal is pending.
These are initial scheduling ceilings, not content ceilings. Work continues on
later frames until exact convergence. Connected E gates may retune the profile
as one atomic change.
The compatibility `MaxCompletionsPerFrame` setting maps to complete Low/Medium/
High/Ultra work profiles during migration. It does not remain an independent
hidden second throttle.
### 4.2 Meter
`StreamingWorkMeter` is stack/frame scoped and single-thread owned. It records:
- start/deadline timestamp;
- admitted results and retained CPU bytes;
- entity operations;
- GPU bytes requested;
- GL retirement operations;
- stage completions, yields, overruns, failures, and oldest-work age.
Reservation is checked before an atomic operation using its conservative known
cost. Elapsed time is checked after every atomic operation. A first operation
may run when a dimension is otherwise empty so progress cannot deadlock; any
single-operation overrun is named in diagnostics and becomes a required split
site.
### 4.3 Cost estimation
`LandblockStreamResultCost` is computed once from immutable payloads:
- terrain vertex/index bytes;
- entity count;
- mesh-reference count;
- EnvCell visibility/shell count;
- physics cell/building/GfxObj counts;
- prepared payload arrays retained by the completion.
It never uses process working set as a scheduling unit and never counts the
memory-mapped package's address space as adopted bytes.
## 5. Queue and ordering contract
One scheduler owns queues by `(generation, priority, stage)`.
Priorities:
1. destination collision/required Near scene;
2. visible Near;
3. ordinary Near;
4. Far/speculative;
5. retirement cleanup after immediate quiesce.
Rules:
- FIFO is exact within one generation/priority.
- Generation supersession cancels stale unpublished work and releases its
retained-byte charge.
- Destination reserve applies only while the canonical reveal generation is
pending.
- Unreserved capacity is work-conserving: any queue may use it.
- At least one noncritical/retirement operation is admitted when capacity
remains, preventing starvation during a slow destination.
- Failed stages retain their exact receipt and queue position.
- Callback reentrancy appends work; it never mutates the active cursor.
- `_deferredApply`, `MaxDrainIterations`, the eager priority-radius bypass, and
count-only `MaxCompletionsPerFrame` execution disappear at cutover.
## 6. Immediate quiesce versus deferred release
At a hard destination-generation edge:
1. The old world generation is marked unavailable to normal world drawing,
picking, radar, status targeting, and collision queries.
2. Object/static animation, PhysicsScript, particle emission, and ambient/
owner-tagged audio for that generation are frozen or silenced.
3. The portal tunnel/UI/event path continues, matching retail's
`blocking_for_cells` branch.
4. Stable resident landblock IDs are captured once.
5. Per-landblock spatial detach and owner/resource release advance from cursors.
Quiesce is idempotent and generation-scoped. It does not destroy live server
identity, replay Hidden/UnHide, or clear the replacement generation.
For ordinary Near→Far demotion outside a hard portal edge, the exact landblock
detach remains the immediate visibility/collision boundary; only its expensive
owner cleanup is deferred.
## 7. Publication contract
One accepted completion progresses through:
```text
Admitted
-> PreparedReceipt
-> RenderPrefix
-> PhysicsBase
-> RenderCellsBuildings
-> StaticEntityPresentationAndCollision
-> SpatialCommit
-> RenderPinsAndEnvCellReplay
-> LiveProjectionRecovery
-> Complete
```
Each transition owns a retry receipt and known cost. Mutation order remains the
existing retail-rooted order. A stage cannot publish partial externally visible
state unless its receipt describes and resumes that exact partial prefix.
Destination reveal consumes only `Complete`/renderer-upload facts belonging to
the active destination generation. Far-ring work may continue after reveal.
## 8. Threading
- Worker threads build immutable `LandblockBuild`/prepared payloads and enqueue
immutable completions only.
- The update/render thread is the sole scheduler, meter, world-state,
publication, and retirement writer.
- GL upload/non-residency/deletion remains with renderer resource owners.
- No update operation waits for the worker, a GPU fence, file I/O, or a lock
held by render work.
- Back-pressure never blocks the update thread. Worker-side bounded queues may
wait/cancel; update-side admission is non-blocking.
## 9. Delivery checkpoints
### E0 — Oracle, inventory, and contract
- Record named-retail teardown/blocking pseudocode.
- Inventory every current publication/retirement owner and bypass.
- Define budget dimensions, atomic work, queue ordering, quiesce, retries,
threading, diagnostics, and acceptance.
**Complete 2026-07-24.**
### E1 — Typed budgets, meter, cost model, and diagnostics
- Add validated `StreamingWorkBudgetOptions` to `RuntimeOptions`.
- Add pure `StreamingWorkMeter` and deterministic injected-clock tests.
- Add exact immutable completion-cost estimation.
- Publish per-stage backlog, bytes, oldest age, work/yield/overrun facts through
lifecycle artifacts.
- Preserve current execution while measuring the would-yield decisions.
**Complete 2026-07-24.** `RuntimeOptions` now owns one validated scheduling
profile (time, admissions, retained CPU bytes, entity operations, requested GPU
bytes, GL retire operations, and destination reserve). The single-thread meter
has a deterministic clock seam, first-operation progress rule, explicit
yield/oversize/failure/overrun facts, and no effect on legacy execution at this
checkpoint. Immutable completion payloads receive deterministic charges for
their exact array data and logical retained entries; this is deliberately not
misreported as CLR heap size. Lifecycle JSON now includes last-frame work,
deferred retained bytes/age, and pending publication/retirement counts.
Validation: complete App suite 3,643 passed / 3 skipped; complete solution
8,124 passed / 5 skipped; Release solution build green. Existing priority,
retry, stale-generation, and deferred-compaction tests remain green.
### E2 — Explicit admission queues
- Replace `_deferredApply` with stable generation/priority queues.
- Bound result admission by count and CPU bytes.
- Remove `MaxDrainIterations`.
- Preserve exact retry position and stale-generation rejection.
- Keep current publication atomic until E4, but execute only work admitted by
the typed meter.
**Complete 2026-07-24.** Production now consumes the worker channel through
one allocation-free `TryPeek`/`TryRead` source, prices the exact immutable
result before adoption, and admits it through the typed completion-count and
retained-CPU dimensions. Accepted work lives in reusable stable FIFOs for
destination, control, unload, Near, and Far classes. FIFO order is exact inside
each class, a blocked publication head does not reorder its tail, callback
appends cannot invalidate an active receipt, and exact-reference compaction
cannot conflate equal records or GUID/landblock reuse.
`_deferredApply`, `MaxDrainIterations`, direct priority hunting, and the unload
budget bypass are gone. Destination and unload work win queue order but consume
the same execution meter. One indivisible publication may make progress after
admission work, but that privilege is granted only once per frame and every
oversize/overrun remains named. Stale generations consume a bounded admission
without retaining their payload. The old completion-count quality setting now
selects a complete scaled time/count/byte/operation profile rather than acting
as a hidden second throttle.
Validation: 240 focused App streaming tests and 58 focused Core streamer/
controller tests passed; the complete App suite passed 3,656 / 3 skipped, the
complete solution passed 8,138 / 5 skipped, and the Release solution build was
clean. Physical connected and visual gates are intentionally deferred until
E6; the active desktop was RDP and is not valid performance evidence.
### E3 — Quiesce and budgeted retirement
- Add destination-generation world quiesce.
- Tag/stop owner audio at the immediate edge.
- Make full-window and recenter retirement admission cursor/time/entity bounded.
- Make each retirement ticket advance by bounded atomic entity/stage work.
- Prove no old generation ticks, emits, collides, targets, or renders while its
retained memory converges.
**Complete 2026-07-24.** `WorldGenerationQuiescence` now publishes the hard
login/portal edge through one generation-scoped availability state owned by
`WorldRevealCoordinator`. The edge withdraws world drawing, picking/radar
spatial queries, target selection, object/static animation, PhysicsScript time,
particle/effect progression, liveness, spatial reconciliation, and owner-tagged
3-D audio while network/event/command, portal presentation, UI, streaming, and
destination readiness continue. A superseding reveal remains continuously
quiescent and only its exact generation can reopen the world; canonical live
records and readiness state remain retained, so the mechanism does not
synthesize Hidden/UnHide or rebuild server identity.
Full-window and shared-origin retirement are now retained frame transactions.
Generation invalidation, worker-inbox clearing, accepted-payload release,
region clearing, stable resident capture, and landblock detachment all resume
from exact cursors through the frame's single `StreamingWorkMeter`.
`LandblockRetirementCoordinator` preserves stable FIFO order and advances one
entity or one owner stage per reservation across scripts, classifications,
lighting, translucency, plugin projection, terrain, physics, cell visibility,
building registries, and environment cells. Failures retry the exact unfinished
entity/stage without replaying committed work. A landblock detach remains one
named indivisible spatial boundary; its complete entity count is charged before
the operation.
Validation: 51 focused reveal/quiescence/retirement tests passed; the complete
App suite passed 3,666 / 3 skipped; the complete solution passed 8,148 / 5
skipped; Release solution build green. The physical connected performance and
visual gate remains deferred to E6 because the active desktop is RDP and is not
valid GPU/FPS evidence.
### E4 — Cursor-budgeted publication
- Move stable ordered arrays/cost facts into retained receipts.
- Cursor static cleanup, static light/collision/plugin publication, physics
cell/building work, and environment-cell publication at safe boundaries.
- Feed GPU requested-byte and retirement-operation facts from renderer owners.
- Prove every failure resumes the exact unfinished operation without replay.
**Complete 2026-07-24.** One accepted completion now remains at the exact queue
head while `LandblockPresentationPipeline` advances retained render, physics,
and static receipts through the frame's single meter. Stable entity/Gfx/building
and prior-owner arrays are captured once. Terrain upload bytes are charged at
the render owner; physics cells/buildings, static collision/light/plugin work,
prior static teardown, shadow reflood, building-registry construction, and
EnvCell shell construction advance one bounded operation at a time. Building
and EnvCell replacements are prepared off-side and become visible only when
complete. Failures retain the unfinished cursor and never replay a committed
prefix. Settings/native callbacks can defer policy changes but cannot drain
publication outside `StreamingController.Tick`.
The final `GpuWorldState.MutationBatch` remains one deliberate observer-atomic
operation: bucket mutation, render-id ownership, activation, and the outer
visibility notification cannot span frames without exposing a partial spatial
identity. It is time-metered and named for E6 overrun evidence; all iterable
owner work now occurs before it.
Validation: focused render/building/publication/controller tests passed; the
complete App suite passed 3,669 / 3 skipped; the complete Release solution
passed 8,152 / 5 skipped; Release solution build green. Physical connected
performance and visual evidence remains an E6 gate.
### E5 — Destination reservation and reveal generation
- Replace eager `PriorityRadius` execution with destination reservations.
- Join scheduler destination generation to the existing reveal coordinator.
- Keep the tunnel animating and surface the retail wait cue/diagnostic beyond
five seconds.
- Remove the last count-only execution/bypass paths.
**Complete 2026-07-24.** `WorldRevealCoordinator` now owns one exact
generation/cell/radius reservation from login or portal begin through complete
or cancel. `StreamingController` no longer exposes a mutable priority
landblock/radius pair: accepted work records the reveal generation that
classified it, stale completion cannot consume a replacement generation's
reserved lane, and stale teardown cannot clear the replacement reservation.
The frame meter records destination and non-destination use independently and,
while a reveal is active, protects the configured destination share across
wall time, completion admissions, adopted CPU bytes, entity operations,
requested GPU bytes, and GL-retirement admissions. Destination work still
uses the same global ceilings and the existing first-indivisible-operation
progress diagnostic; it does not regain a budget bypass.
The former ten-second forced materialization path is deleted. An incomplete
destination remains in the authored portal scene until the canonical render,
composite, and collision predicate is true. After five seconds the retained
gameplay UI displays retail's centered
`"In Portal Space - Please Wait..."` notice and lifecycle telemetry records the
cue once; the tunnel continues its DAT animation and repeats the notice on the
retail camera-rotation cadence. Login completion now ends its reveal
reservation/quiescence at the same auto-entry edge that exposes the ready
world.
Validation: 91 focused reservation/reveal/teleport/UI tests passed; the
complete App suite passed 3,674 / 3 skipped; the complete Release solution
passed 8,158 / 5 skipped; Release solution build green with zero warnings.
The physical-local routes and final performance/resource assertions remain
E6, not E5.
### E6 — Gates and closeout
- Deterministic pressure, reentrancy, failure, cancellation, stale-generation,
GUID reuse, dungeon, outdoor, and session-reset suites.
- Release build and full solution tests.
- Physical-local capped/uncapped nine-stop and pinned dense-Arwic routes.
- Compare portal p99 and maximum single-frame allocation to Slice A and Slice C.
- Require zero `viewport-before-ready`, zero stranded old generation, staged
upload, collision, effect, audio, or GPU owner.
- Fixed-camera screenshots and subjective visual gate only if pixels change.
- Update architecture, inventory, issues, roadmap, divergence register, and
durable memory.
**Complete 2026-07-24.** Deterministic budget, pressure, cancellation,
stale-generation, retry, callback-reentrancy, GUID-reuse, quiescence, and
resource-ownership gates pass. The Release solution builds and the complete
suite passes 8,164 tests with 5 intentional skips.
The exact `91e82c3c6850fcedbf20322b2ecf4fdcd11a2b2a` Release binary passed
capped and uncapped nine-stop routes plus pinned dense Arwic on the physical
local AMD display. Every destination materialized only after the canonical
render/composite/collision predicate, every client exited gracefully, and
every canonical checkpoint ended with zero pending publication, retirement,
worker/destination/class backlog, staged upload, composite warmup, residency
staging/requested-GPU, or retiring bytes.
The gate found and corrected two connected-only ownership defects. CPU
mesh-cache hits now stage only for a live exact owner, preventing evicted work
from recreating stale uploads. Loaded spatial residency is now distinct from
world availability, allowing quiesced destination live objects to prepare
their render projection without becoming drawable, collidable, pickable,
targetable, audible, or simulated before reveal.
Relative to Slice A, capped/uncapped CPU p99 improved 34.7%/30.4% and largest
frame allocation fell 84.7%/82.2%. Relative to Slice C, CPU p99 improved
21.4%/16.1% and largest frame allocation fell 33.5%/9.7%. Full evidence:
[`../research/2026-07-24-slice-e-cost-budgeted-streaming-report.md`](../research/2026-07-24-slice-e-cost-budgeted-streaming-report.md).
### Post-closeout portal regression correction — 2026-07-25
The first retained-scene visual pass exposed four scheduling/ownership defects
that the original E6 route did not isolate:
- a 25x25 shared-origin window admitted 625 detach operations over many
frames even though no intermediate old spatial subset was observable;
- destination publication could sit behind an unrelated retirement receipt,
small-mesh upload count, or composite upload count while most byte budget
remained unused;
- private paperdoll/appraisal meshes borrowed world ownership and the
paperdoll discarded its private object during temporary player
unavailability;
- ACE could retain an expired GUID in `KnownObjects`, omit CreateObject on
revisit, and leave doors, signs, portals, and NPCs permanently absent.
The correction atomically swaps the old spatial generation into exact deferred
receipts, advances only the destination's same-key receipt out of order, and
orders destination worker/publication work first. A reveal-generation render
profile raises only the small-object mesh count from 8 to 64 and composite
count from 16 to 64; all 8 MiB byte/array/buffer/mipmap ceilings remain
unchanged. Ordinary work still advances when no destination completion exists.
Private viewports now own independent mesh leases and the paperdoll survives
temporary SmartBox absence. `DormantLiveEntityStore` retains only cold accepted
spawn data after active teardown so an ACE revisit can hydrate through the
ordinary generation-safe transaction.
The world-availability edge now ends at retail's portal/world viewport swap;
the one-second `WorldFadeIn` protocol tail still sends LoginComplete and closes
teleport state later. The user confirmed faster portal transit, continuous
paperdoll presentation, and persistent server-spawned objects after repeated
portal travel. Full evidence:
[`../research/2026-07-25-portal-regression-closeout.md`](../research/2026-07-25-portal-regression-closeout.md).
## 10. Acceptance
Slice E closes only when:
1. No priority or unload path bypasses all work budgets.
2. No frame admits another operation after a configured count/byte/entity
dimension is exhausted. The documented first-indivisible-operation rule is
the sole oversize path and remains named.
3. Every wall-time overrun names one indivisible operation and the scheduler
admits no later operation after its deadline. Because the meter uses elapsed
wall time on a non-real-time OS, thread preemption can make an otherwise
bounded atomic operation observe more than 2 ms; the physical gate therefore
also requires materially improved route/portal p99 and no repeatable
algorithmic tail.
4. Old generations become unavailable, tick-frozen, and audio-silent at the
hard transition edge.
5. Retirement/publication retries resume exact cursors without replay.
6. Deferred retained CPU bytes stay within their queue budget.
7. Reveal is possible only for the active destination generation and the
existing readiness predicate.
8. Portal-window p99 and largest frame allocation materially improve over the
post-Slice-A physical baseline.
9. Complete connected routes end with zero pending publication, retirement,
upload, effect, collision, audio, and stale-generation owners.
10. Visual output/range/quality and retail gameplay behavior are unchanged.

View file

@ -1,788 +0,0 @@
# Modern Runtime Slices F/G — Incremental Render Scene and Delta Submission
**Date:** 2026-07-24
**Status:** approved 2026-07-24; Slices F and G complete
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
**Baseline:** Slice E closeout commits `91e82c3c` and `e7d9d6fa`
**Behavior contract:** no change to pixels, view distance, particles, PView
visibility, clipping, draw order, selection, lighting, animation, or
retail-faithful gameplay
## 0. Execution ledger
| Unit | State | Evidence / rollback |
|---|---|---|
| F0a — partition-input referee | complete | Diagnostic-only `CurrentRenderSceneOracle`; no production draw decision changed. |
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
| F1 — scene types and contained adapter | complete | `Arch 2.1.0` is pinned in App only behind acdream contracts. Five narrow archetypes, generation/incarnation/sequence gates, borrowed-query invalidation, memory accounting, deterministic digesting, and update-thread enforcement pass 11 focused tests. Release gate: 3,701 App tests / 3 skips and 8,185 complete-solution tests / 5 skips. |
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
| F5 | complete | Exact `81e2f1a5` passed capped and uncapped nine-stop routes plus uncapped dense Arwic. Across the final checkpoints the referee completed 1,663 + 1,677 + 81 comparisons with zero mismatches, zero pending deltas, zero rejected operations, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-24-slice-f5-render-scene-shadow-gate.md`. |
| G0 — borrowed frame product | complete | Two reusable arenas publish exact generation/frame-stamped borrowed views for scene candidates, cell ranges, transforms, classification, light sets, selection, PView/clip references, counts, and source digest. Release/reuse invalidates every copied view; abort never publishes; frame N may build while N-1 is borrowed; the warm path allocates zero bytes. Eight focused tests pass inside the 3,743-App / 8,227-solution Release gate. |
| G1 — scene-query replacement | complete | Exact `e346f8bb` passed capped and uncapped nine-stop routes plus uncapped dense Arwic: 29,392 + 29,025 + 1,522 same-frame candidate comparisons, zero mismatches, equal counts/digests at every checkpoint, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g1-scene-query-candidate-gate.md`. |
| G2 — packed dispatcher input | complete | Exact `f9829d5f` passed capped and uncapped nine-stop routes plus dense Arwic. Candidate order, packed input, classified output, and selection each completed 16,879 + 17,282 + 770 comparisons with zero mismatch; journal/rejection counts were zero and shutdown graceful. Evidence: `docs/research/2026-07-25-slice-g2-packed-dispatcher-gate.md`. |
| G3 — retained classification/storage | complete | Exact `6a026c5a` passed capped and uncapped nine-stop routes plus dense Arwic with 17,069 + 16,827 + 757 exact comparisons in every channel, zero mismatch, warm cross-frame reuse, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g3-retained-classification-gate.md`. |
| G4 — production cutover | complete | Exact cutover `ef1d263337997bb030eadb7b8e71d73dc659907a` makes the retained frame product the production entity source at the existing five retail PView stages. Exact `03b10183` passed capped/uncapped nine-stop and uncapped dense-Arwic automation with 46,599 total product comparisons and zero mismatch. After the independent portal/private-view/revisit corrections, the user accepted the connected retained-path visual matrix on 2026-07-25: server objects and paperdoll survive travel, distant use works, `/ls` and spell recalls both materialize through the retail purple haze, and neither character pop nor recall tail remains. Evidence: `docs/research/2026-07-25-slice-g4-production-cutover-automated-gate.md` and `docs/research/2026-07-25-portal-regression-closeout.md`. |
| G5 — old-path retirement and production closeout | complete | Exact `10ccce3f` removes `InteriorEntityPartition` from normal production: meshes and attached particles consume retained route ranges, while the legacy partition remains only for standalone tests and explicit diagnostic/oracle probes. Exact `14fbe92b` passes the seven-checkpoint lifecycle/reconnect gate after hardening the optional monitor-refresh boundary. The user visual gate, capped/uncapped/dense correctness routes, ordinary 519.7-FPS production profile, 3,826 App tests / 3 skips, and 8,335 complete-solution tests / 5 skips pass. The measured 22.34 KiB/frame remainder is assigned to Slice I1. Evidence: `docs/research/2026-07-25-slice-g5-production-profile.md`. |
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
non-drawing diagnostic infrastructure and therefore is not a visual-cutover
rollback unit. Each later commit that changes the production draw source is
listed in section 10 before it is offered for a connected gate.
## 1. Decision record
### 1.1 Why this work has a separate approval gate
Slices AE optimized measurement, prepared content, residency, streaming, and
retirement without replacing the renderer's scene model. Slices F/G resume the
MP3 work that the user explicitly deferred on 2026-07-05:
- Slice F introduces a second, render-only projection in non-drawing shadow
mode.
- Slice G makes that projection the renderer's production input and removes the
old whole-world enumeration/partition path.
This is a materially larger regression surface than changing an isolated
scheduler or cache. A wrong cell bucket, incarnation, transform, material
variant, or draw-order classification can produce the historical
stairs/doorway/player-vanish bug classes while still compiling and appearing
faster. The approval gate therefore remains binding.
Approval had to be explicit and recorded as:
> Approved — continue with all slices.
The user supplied that approval on 2026-07-24 after reviewing the post-E
evidence and rewrite risk. Runtime work therefore proceeds from the exact
pre-F/G anchor `e7d9d6fa`.
### 1.2 Post-Slice-E evidence
Physical-local Release captures on exact binary
`91e82c3c6850fcedbf20322b2ecf4fdcd11a2b2a` produced:
| Capture | CPU p50 / p95 / p99 | GPU p50 / p95 / p99 |
|---|---:|---:|
| Uncapped nine-stop | 1.216 / 4.216 / 5.450 ms | 0.725 / 2.486 / 2.666 ms |
| Dense Arwic uncapped | 2.652 / 4.153 / 5.736 ms | 1.576 / 2.504 / 2.662 ms |
Dense Arwic exercised 15,530 entities and 111 animated entities. Its CPU
median is already below the provisional 3.33 ms / 300 FPS target. Therefore:
- F/G is not needed to claim that one provisional median target.
- CPU p95/p99 still exceed GPU p95/p99, leaving measurable CPU headroom.
- The current path still scales with the resident entity population rather
than only with scene changes and visible buckets.
- The expected win is lower CPU utilization, better dense-scene scaling,
lower tail cost, and a clean future snapshot/headless seam.
- A dramatic average-FPS increase is possible but is not promised.
The go/no-go decision is consequently about accepting architectural rewrite
risk for throughput headroom and a cleaner long-term runtime boundary, not
about repairing an existing visual or stability failure.
## 2. Current owner map
| Concern | Current owner | Current cost/contract | F/G treatment |
|---|---|---|---|
| Logical live identity and accepted state | `LiveEntityRuntime` | sole GUID/incarnation authority | unchanged; emits exact projection facts only |
| Spatial landblock membership | `GpuWorldState` | sole loaded/pending bucket authority | unchanged; journals committed render projection deltas |
| Static publication/retirement | `LandblockPresentationPipeline` and retirement coordinator | retained exact Slice-E receipts | append render deltas at existing committed receipt boundaries |
| Live presentation resources | `EntitySpawnAdapter` | exact per-incarnation mesh/composite ownership | unchanged; appearance facts copied into render projection |
| Equipped-child presentation | `EquippedChildRenderController` | final current transform and visibility | copied into equipped projection archetype |
| Static animation | `RetailStaticAnimatingObjectScheduler` | final root transform for active animated statics | active-only synchronization into dynamic transform components |
| Frame camera/root/building facts | `WorldRenderFrameBuilder` | borrowed one-frame facts | retained |
| Retail cell visibility | `PortalVisibilityBuilder` / `RetailPViewRenderer` | retail PView and viewcone oracle | retained without algorithm changes |
| Per-frame entity split | `InteriorEntityPartition` | scans every resident `WorldEntity` | replaced by incremental scene indices in G |
| Visibility walk/classification | `WbDrawDispatcher` | walks entity/mesh pairs and rebuilds instance groups | consumes packed frame candidates and dirty instance ranges in G |
| Alpha order | `RetailAlphaQueue` | shared stable far-to-near ordering | retained |
| Selection | retail selection scene/sink | visible part publication and lighting | same accepted parts and transforms from frame view |
| Lighting | `LightManager` plus dispatcher selection | per-object retail light set | same algorithm over packed bounds |
| GL ownership | `WbMeshAdapter`, texture/residency owners, dispatcher buffers | render-thread only | unchanged |
`GpuWorldState` remains the canonical spatial projection. The new render scene
is a derived, disposable presentation index. It cannot answer gameplay,
network, collision, inventory, interaction, radar, target, or persistence
questions.
## 3. Target boundary
### 3.1 Acdream-owned API
Arch is an implementation detail in `AcDream.App`. No Arch type crosses an
acdream interface, enters Core, or appears in a test fixture outside App.
The boundary is equivalent to:
```csharp
internal interface IRenderScene
{
RenderSceneGeneration Generation { get; }
RenderProjectionCounts Counts { get; }
RenderDeltaApplyResult Apply(ReadOnlySpan<RenderProjectionDelta> deltas);
void SynchronizeDynamicSources(in DynamicProjectionSyncInput input);
RenderSceneDigest BuildDigest(RenderSceneDigestBuffer reuse);
RenderSceneQuery OpenQuery();
void Clear(RenderSceneGeneration replacementGeneration);
}
```
The exact API may split mutation, query, and diagnostics into narrower
interfaces, but these rules are fixed:
- mutation is update-thread only;
- draw receives only a borrowed read/query surface;
- draw cannot call `LiveEntityRuntime`, `GpuWorldState`, or `WorldEntity`
dictionaries;
- Arch entities and `.Value` primitives stay inside the scene implementation;
- all external identities are acdream-owned wrapper types;
- session/world generations are explicit;
- stale incarnation deltas are rejected rather than applied to a reused slot.
### 3.2 Identity
Use distinct wrapper identities:
```text
RenderProjectionId
RenderOwnerIncarnation
RenderSceneGeneration
RenderSpatialBucket
RenderAssetHandle
```
Projection keys are derived presentation identities, not a second gameplay
GUID map:
- static: world generation + canonical landblock + current static entity id;
- live root: current local `WorldEntity.Id` + accepted instance sequence;
- equipped child: parent local id + attachment slot/child local id +
incarnation;
- EnvCell: world generation + cell id.
The scene may keep a source-key-to-projection lookup solely to apply deltas. It
may not expose that lookup as object identity to gameplay consumers.
### 3.3 Component set
Initial packed components:
```text
RenderTransform
PreviousRenderTransform
RenderMeshSet
RenderMaterialVariant
RenderSpatialResidency
RenderWorldBounds
RenderFlags
RenderDegradeState
RenderSortKey
RenderOwnerIncarnation
RenderDirtyMask
```
Archetypes remain narrow:
- outdoor static;
- indoor-cell static;
- live/dynamic root;
- active animated static;
- equipped child;
- translucent/light-bearing variants only where query density justifies them.
Do not attach every component to every projection. Effects, particles, and
lights remain in their existing owners during F/G unless a measured query
benefit and an exact parity fixture justify a packed reference.
### 3.4 Delta journal
One update-thread-owned `RenderProjectionJournal` receives:
```text
Register
UpdateTransform
UpdateAppearance
UpdateFlags
Rebucket
Unregister
ClearGeneration
```
Each delta carries:
- scene generation;
- projection source key;
- exact owner incarnation;
- monotonically increasing journal sequence;
- complete payload for the changed channel.
Rules:
- structural order is exact;
- updates may coalesce only within one host tick, after the most recent
structural edge for the same projection/incarnation;
- register/unregister and generation boundaries never coalesce away;
- unregister of an old incarnation cannot remove its replacement;
- a journal drains fully at the update-to-render boundary;
- any carry-over at frame publication is an invariant failure;
- callback reentrancy appends after the current sequence and cannot invalidate
active iteration;
- no bounded queue or worker thread is introduced in F/G—the accepted host
thread remains the only writer.
### 3.5 Static versus dynamic synchronization
Static projections are change-driven:
- landblock spatial publication registers them once;
- appearance or static-animation membership changes issue explicit deltas;
- rebucket/retirement removes them once.
Dynamic root and equipped-child transforms are synchronized after animation,
physics, remote motion, local-player projection, and attachment transforms
have reached their existing final frame state. The synchronizer walks only
the scene's dynamic/active-animated source registry, compares packed final
facts, and emits updates for changed records. It never scans all resident
statics.
This active-only synchronization is deliberate. Retrofitting presentation
version counters into every simulation setter during F would broaden the
change into gameplay/physics ownership. A later measured slice may replace
the comparison with versioned producers without changing `IRenderScene`.
## 4. Referee before the match
The comparison oracle lands before Arch or shadow storage.
`CurrentRenderSceneOracle` captures the current accepted render input after
PView construction and before dispatcher classification. Its deterministic
digest contains, in stable sort order:
- source/projection identity and incarnation;
- spatial class: outdoor static, cell static, dynamic, equipped;
- canonical landblock and full cell;
- root transform and world bounds;
- mesh ids and part transforms in order;
- material/palette/texture replacement identity;
- draw/ancestor/hidden/translucency/degrade flags;
- active-animation classification;
- PView survivor class and clip-route identity;
- expected selection part ids;
- opaque/alpha classification identity.
The oracle is validated on the current production path first. It must be:
- deterministic across repeated unchanged frames;
- allocation-free when disabled;
- bounded and reusable when enabled;
- independent of dictionary enumeration order;
- continuously sampled during login, portal transit, reveal, movement,
animation, appearance change, and teardown—not only at stable checkpoints.
The oracle does not decide what retail should draw. It records what the
already accepted current path supplies, so F/G cannot silently redefine
parity in favor of the new implementation.
## 5. Slice F — Non-drawing shadow render scene
### F0 — Current-path oracle
1. Add acdream-owned digest/value types and reusable buffers.
2. Capture the current partition and dispatcher candidate identity without
changing draw behavior.
3. Add deterministic unit fixtures for static, dynamic, animated, equipped,
translucent, hidden, duplicate ids across landblocks, and GUID reuse.
4. Extend canonical lifecycle artifacts with digest generation, counts,
mismatch count, journal depth, and scene bytes.
5. Run the current pipeline alone through the canonical route and establish
the referee's own stability.
**Commit gate:** no Arch dependency; identical screenshots; no material
performance regression with comparison disabled.
### F1 — Scene types and contained Arch adapter
1. Verify Arch's current license and selected package API; pin one exact
version in `AcDream.App` only.
2. Add `IRenderScene`, wrapper ids, component structs, query contracts, and
`ArchRenderScene`.
3. Add an architecture test preventing Arch namespaces outside the allowed App
folder and preventing primitive id extraction outside the owner assembly.
4. Add memory accounting for chunks, lookup/index storage, journal buffers,
and retained synchronization sources.
5. Prove create/update/remove/clear and stale-incarnation rejection in pure App
tests.
**Commit gate:** the scene is unbound and non-drawing; full suite green.
**Landed evidence:** the package/API/license decision is recorded in
`docs/research/2026-07-24-arch-render-scene-dependency.md`. Production
composition does not construct `ArchRenderScene`; F1 therefore changes no
runtime path. Source/reflection architecture tests confine all Arch types and
primitive-ID extraction to `AcDream.App/Rendering/Scene/Arch`. Pure App tests
cover every structural/channel operation, generation replacement, stale
incarnation and sequence rejection, borrowed-query invalidation, deterministic
digest order, retained-memory accounting, and cross-thread mutation rejection.
The Release gate is 3,701 App tests / 3 skips and 8,185 complete-solution tests
/ 5 skips.
### F2 — Static projection journal
1. Append Register deltas from the existing final landblock spatial publication
receipt.
2. Append Unregister deltas from the exact retirement/demotion receipt.
3. Preserve owner landblock identity independently of the caller's current
draw landblock—the #119 cache lesson.
4. Mirror indoor cell statics, outdoor statics, scenery, building shells, and
EnvCell projection identity without changing their resource ownership.
5. Make same-landblock rehydrate a reconcile operation: retained identities
update, omitted identities unregister, and new identities register.
6. Clear by exact world generation on login, portal supersession, session
reset, and graceful shutdown.
**Commit gate:** shadow static digest equals the referee over deterministic
publication/retirement permutations.
### F3 — Live and equipped projection journal
1. Register only after the current live record, materialized projection, and
presentation resources have committed.
2. Use exact local id + instance sequence; never server GUID alone.
3. Journal loaded/pending visibility edges without treating rebucket as
logical recreate.
4. Mirror appearance, mesh refs, palette/material variants, draw flags,
ancestor visibility, full cell, and bounds.
5. Mirror equipped children after their current attachment transform update.
6. Unregister exactly once on pickup/parenting/withdraw/delete/session reset,
rejecting callbacks from displaced generations.
**Commit gate:** duplicate CreateObject, reentrant callback, pending-to-loaded,
loaded-to-loaded, hidden, parent/child, delete/recreate, and GUID-reuse tests
have zero leaked or stale projections.
### F4 — Dynamic synchronization and incremental indices
1. Synchronize final current-frame transforms for live roots, active animated
statics, and equipped children.
2. Maintain outdoor-static, per-cell-static, dynamic, translucent, selectable,
light-candidate, and dirty-record indices incrementally.
3. Preserve `InteriorEntityPartition.IsIndoorCellId` semantics.
4. Preserve retail's special dynamic routing:
- outdoor dynamics under an interior root enter the outside stage;
- exit-portal-straddling dynamics may enter both stages;
- out-of-flood indoor dynamics remain members but fail the per-dynamic
viewcone cull.
5. Assert index counts and membership against the current partition every
sampled frame.
**Commit gate:** no whole-static-world synchronization in a steady frame;
unchanged dynamic records emit no update.
### F5 — Continuous shadow comparison
1. Apply the journal and build the shadow digest at the update/render boundary.
2. Compare continuously at a fixed diagnostic cadence and at every canonical
checkpoint.
3. Record the first exact mismatch with both projections and source channel;
never log an unbounded mismatch storm.
4. Run deterministic portal, churn, animation, appearance, hidden, selection,
and teardown tests.
5. Run capped and uncapped nine-stop routes plus dense Arwic on the physical
display.
6. Prove:
- zero mismatches;
- zero stale delta applications;
- zero journal carry-over;
- zero retained old-generation scene state;
- bounded shadow memory;
- no pixel changes because the shadow does not draw.
**Slice F gate:** all evidence above passes before any production consumer is
switched. F may temporarily cost memory and a small amount of CPU in diagnostic
mode; those costs are reported separately and shadow comparison is disabled by
default after its gate.
#### F5 connected-gate correction record
The first physical capped nine-stop route on exact commit `056bbd4e` completed
the harness and graceful shutdown, but the shadow referee rejected the route at
the same-location Sawato revisit. The first mismatch was a missing live root
(`sourceChannel=live`, `field=presence`), while journal pending and rejected
delta counts remained zero. This proved the problem was source-workset
reconciliation rather than a drain failure.
The derived live journal had been synchronizing only records it already
retained. Once a root projection was absent, that loop could not recover it.
The correction sources ordinary world roots from the canonical active spatial
workset already owned by `LiveEntityRuntime`. Attached children deliberately
remain callback-authoritative and are refreshed only while retained, preventing
removed equipment from being resurrected.
The same route also showed that the diagnostic referee was re-hashing immutable
selection polygons and vertices every sampled frame. Geometry fingerprints are
now cached by GfxObj and mesh identity, and all retained projection collection
walks use allocation-free indexed access. Permanent tests cover root recovery,
attachment non-resurrection, cached selection geometry, retained transform
updates, and active animated-static synchronization. The capped, uncapped, and
dense-town routes are all rerun from the corrective commit; no F5 success is
claimed from the rejected `056bbd4e` route.
The next exact capped route on `91463db5` completed all nine stops and shut
down gracefully, but F5 again rejected it. A transition frame reached the
post-render referee with 5,111 pending deltas because the only ordinary drain
was embedded in the spatial reconciler, which is intentionally skipped while
the world is quiesced. Later revisit frames also diverged, but the bounded
first-mismatch recorder correctly retained the earlier journal-ordering fault.
The correction makes scene publication an explicit final update-frame commit
phase after streaming, network, teleport, camera, and every conditional
spatial reconcile. It runs even while world simulation is quiesced. The former
reconciler drain is removed, leaving one ordinary update/render-boundary
publication; session reset retains its separate ordered teardown drain. The
next capped redrive must prove both zero pending deltas and expose or clear any
remaining projection mismatch before uncapped evidence begins.
The exact `9fab1feb` capped route proved the final commit boundary: no pending
or rejected delta mismatch recurred. At Sawato revisit the referee then
preserved the remaining fault as
`sourceChannel=live id=projection:03000000000F4279 field=presence`.
Category counts showed two equipped children missing while every ordinary root
and static projection matched. The equipped controller intentionally removes
child presentation during portal transit without discarding the accepted
parent relation; destination recomposition publishes `ProjectionPoseReady`,
not a second logical `EntityReady`. The live journal incorrectly required the
child to be already retained before accepting that callback. The correction
treats the equipped owner's composed-pose callback as the authoritative
attached-child presence/update edge. A permanent test proves a current
attachment removed for presentation is registered again on its first
destination pose, while passive synchronization still cannot resurrect a
removed attachment. The exact corrected commit must repeat the capped route
before uncapped and dense-town gates.
The exact `bb1f4a64` capped route showed that destination-pose recovery covers
attachments which are recomposed, but Sawato's retained population has seven
children whose accepted relation is not replayed on revisit. The first missing
identity changed with that population (`projection:03000000000F4286`);
baseline had 43 roots + 8 children and revisit had the same 43 roots but only
1 retained child. Existing rendering still presents those children through
the rehydrated spatial entity set.
The lifecycle correction is to distinguish spatial withdrawal from logical
unregistration. `ProjectionRemoved` means an equipped child left cell
presentation; it now retains the exact scene identity with `Draw` inactive.
The existing projection-visibility edge can reactivate that record on
same-location rehydration, and a destination pose can still refresh it.
`OnResourceUnregister` remains the only logical teardown edge. Tests now pin
inactive retention, passive non-reactivation, exact reactivation, duplicate
removal, and final logical teardown. Missing-projection diagnostics also carry
the expected class, server GUID, local ID, and parent cell for any further
failure. Repeat capped F5 on the exact corrective commit before advancing.
The exact corrective commit `81e2f1a5` passed the complete physical gate:
- capped nine-stop route `connected-r6-soak-20260724-232827`: 1,663/1,663
successful comparisons;
- uncapped nine-stop route `connected-r6-soak-20260724-233826`: 1,677/1,677
successful comparisons;
- uncapped dense-Arwic route `connected-dense-town-20260724-234807`: 81/81
successful comparisons.
Every canonical checkpoint had the shadow enabled, the active projection count
equal to the current-path oracle, no first mismatch, no journal carry-over, and
no rejected delta. All three processes used a Release binary whose embedded
commit exactly matched source and all closed gracefully. This closes Slice F.
The shadow scene remains diagnostic-only and is safe to retain if a later
production cutover is reverted. The pre-cutover runtime rollback anchor remains
`e7d9d6fa`.
## 6. Slice G — Frame product and production cutover
### G0 — Borrowed double-buffered frame product
Add two reusable `RenderFrameArena` instances. Building frame N writes the
arena not borrowed by draw N-1 and publishes a generation-stamped
`RenderFrameView`.
The view contains spans/ranges for:
- visible outdoor static candidates;
- visible per-cell static candidates keyed by PView order;
- dynamic candidates;
- final root/part transforms;
- opaque and alpha classification records;
- per-object light sets;
- selection parts and lighting;
- existing PView/clip products by borrowed reference;
- diagnostic counts and source digest.
Rules:
- no extra frame of latency;
- no render thread is introduced;
- a view is invalid after its arena is reused;
- no consumer retains spans across frame completion;
- abort recycles the incomplete arena without publishing it;
- world generation and frame sequence must match at draw.
**Completed 2026-07-24.** `RenderFrameExchange` owns exactly two retained
arenas. A published view is accessible only through a validated borrow token;
release invalidates every struct copy, and arena reuse advances its epoch.
Publication requires a source digest from the same scene generation, while
borrowing requires the exact expected generation and frame sequence. The
non-published arena alone may be reused, so an aborted build cannot destroy the
last completed product. PView/clip products remain borrowed references and all
variable-size payloads use retained arrays. The 1,000-frame warm regression
allocates zero bytes. The product is deliberately uncomposed until G1 supplies
scene-query data; G0 therefore changes no production draw behavior and is not a
visual rollback unit.
### G1 — Replace whole-world partition with scene queries
1. Query outdoor statics directly.
2. Query each PView-visible cell bucket in the exact existing far-to-near order.
3. Query the dynamic set once, then apply the existing per-dynamic viewcone and
outside-stage/straddle rules.
4. Preserve look-in cells as a distinct landscape-stage source.
5. Preserve particle-owner filtering against the exact mesh survivor set.
6. Compare the new candidate digest with the old partition without drawing
twice.
**Commit gate:** exact candidate parity through indoor/outdoor transitions,
door apertures, look-ins, non-flooded rooms, and portal straddles.
### G2 — Packed dispatcher input
1. Introduce an acdream-owned `RenderInstanceCandidate` consumed by
`WbDrawDispatcher`.
2. Copy every current `WorldEntity` fact the dispatcher actually uses:
- mesh refs and ordered part transforms;
- root transform;
- owner landblock/cell;
- palette and texture replacement identity;
- hidden/draw/ancestor flags;
- scale, bounds, source setup/gfx id;
- selection owner identity;
- translucency/degrade state.
3. Preserve cache key and invalidation semantics, especially owner landblock
identity and incomplete-mesh retry.
4. Keep Wb/texture/selection/light/alpha owners unchanged.
5. Add old-input/new-input instance-set comparison at the dispatcher boundary.
**Completed 2026-07-25.** Exact `f9829d5f` passed capped and uncapped
nine-stop routes plus uncapped dense Arwic. Candidate order, packed mesh-part
input, classified opaque/alpha output, and selection each completed
16,879 + 17,282 + 770 comparisons with zero mismatch. The stronger gate
corrected two referee blind spots: route canonicalization had hidden source
order, and the no-VAO diagnostic path had hashed stale reusable group lists
despite accepting zero instances. `GpuWorldState` now owns explicit traversal
slots plus mutable bucket indices; zero-instance fingerprints describe empty
accepted output. Evidence:
`docs/research/2026-07-25-slice-g2-packed-dispatcher-gate.md`.
**Commit gate:** identical instance keys, model matrices, material identities,
clip slots, light sets, selection parts, alpha records, and draw counts.
### G3 — Dirty instance ranges and reusable command storage
1. Retain persistent instance records keyed by projection + mesh part + batch.
2. Rebuild classification only for Register, Appearance, Material, Mesh,
Degrade, or relevant Flags dirtiness.
3. Update transform/light/selection fields through dirty ranges.
4. Reuse MDI command, batch, sort, and alpha scratch storage.
5. Cache static command templates by scene generation and visible-cell set only
after parity proves the simpler indexed path.
6. Keep resource upload and GL retirement on the accepted render thread.
7. Do not add GPU culling, GPU particles, or render-worker jobs in G.
**Commit gate:** warm unchanged frames rebuild no static classification and
allocate no core scene/dispatcher memory.
**Completed 2026-07-25.** Exact `6a026c5a` passed capped and uncapped
nine-stop routes plus uncapped dense Arwic. Candidate order, packed input,
classified output, and selection completed 17,069 + 16,827 + 757 continuous
comparisons with zero mismatch. Dense Arwic's final warm frame reused all
3,828 unchanged visible classifications, rebuilt zero static projections, and
kept 410 active animated classifications on the live path.
The first development pass exposed a reveal-edge ownership gap: inbound state
continues while portal reveal blocks ordinary simulation, but the derived
render projection had also been blocked. A narrow hidden-frame projection sync
now follows inbound network/command mutation without advancing effects,
particles, attachments, lights, animation, or physics. Dirty state is
acknowledged only after every packed comparison succeeds. Deterministic tests
also pin generation/incarnation replacement, incomplete resources, retirement,
and 1,000 warm reuse frames with zero thread allocation. Evidence:
`docs/research/2026-07-25-slice-g3-retained-classification-gate.md`.
### G4 — Cutover
1. Run old and new input construction in compare-only mode; draw only the old
path.
2. Fix every mismatch at its source.
3. Switch the single production draw input to `RenderFrameView`.
4. Run instance-set and fixed-camera image comparisons.
5. Obtain the user's connected visual confirmation.
6. Delete:
- production `InteriorEntityPartition` use;
- full `LandblockEntries` walk from normal world draw;
- transition flags and dual-input compare code;
- obsolete static classification rebuild paths.
7. Keep only reusable oracle tooling behind diagnostics/tests.
There is no permanent runtime fallback. A failed gate reverts the cutover
commit; it does not ship a hidden old/new switch.
**Automated gate completed 2026-07-25.** Exact gated source `03b10183` passed
capped and uncapped nine-stop routes plus uncapped dense Arwic. The retained
product completed 20,502 + 24,463 + 1,634 continuous comparisons with zero
mismatch. Every canonical reveal was ready with zero pending composite warmup,
the scene journal was empty, source and binary hashes matched, and every
process closed gracefully. The gate corrected whole-retained-world composite
warmup discovery by sourcing the exact published 3×3 destination neighborhood;
that correction is draw-path independent. Fixed-camera artifacts are captured.
The first connected retained-path gate exposed independent portal scheduling,
private-viewport ownership, and ACE revisit-liveness defects. Their root-cause
correction is recorded in
`docs/research/2026-07-25-portal-regression-closeout.md`. The user's subsequent
pass occurred while exact cutover `ef1d2633` was temporarily reverted by
`2c848d41`, so it proves those corrections but not the retained draw source.
`20f9fadb` reapplies the cutover over the accepted corrections. The user
accepted that connected retained path on 2026-07-25, including stable objects
and paperdoll through travel, distant interaction, and the corrected `/ls` and
spell-recall materialization. G4 is complete. G5 removes the old production
partition route and runs the production-only matrix.
### G5 — Physical performance and lifecycle gate
Run the exact Slice-E reference matrix:
- capped physical-local nine-stop;
- uncapped physical-local nine-stop;
- pinned dense-Arwic uncapped with stationary turn;
- fresh login, portal/dungeon, same-location revisit, world edge;
- appearance, hidden/unhide, equipped-child changes;
- graceful logout/reconnect;
- rapid generation replacement and GUID reuse.
Report, without changing thresholds:
- CPU/GPU p50/p95/p99/p99.9 and maximum;
- update, PView, scene query, frame build, dispatcher, alpha, and GL stages;
- frame-thread and process allocation;
- scene/chunk/index/journal bytes;
- managed heap, LOH/POH fragmentation, GC pauses;
- tracked CPU/GPU/staging/retiring bytes;
- instance/draw/triangle counts;
- screenshot and instance-set comparison.
## 7. Acceptance matrix
### 7.1 Correctness
- Exact static/live/equipped projection count and digest.
- Exact PView cell order and viewcone behavior.
- Exact outside-stage and portal-straddling dynamic behavior.
- Exact opaque/clip-map/alpha classification and stable alpha order.
- Exact root and part transforms with no one-frame lag.
- Exact hidden, NoDraw, ancestor visibility, and translucency behavior.
- Exact material/palette/texture replacement identity.
- Exact light sets and selection-part placement.
- Exact incarnation behavior across stale updates and GUID reuse.
- Zero replay of logical create/default script/effect work on rebucket.
- Zero draw calls into gameplay/spatial dictionaries.
### 7.2 Resource and lifecycle
- Journal empty at every frame publication.
- No old world generation in scene after replacement converges.
- Scene memory plateaus on third location visit.
- No stale Arch entity handle after removal/reuse.
- No extra mesh/texture/composite owner.
- No unbalanced reference, staged upload, dynamic light, effect, or alpha
record.
- Graceful shutdown drains scene and GL retirement exactly once.
### 7.3 Performance
The program goals remain:
- dense uncapped p50 at or below 3.33 ms, or a newly attributed blocker;
- traversal p99 at or below 16.67 ms;
- no post-warm frame above 33.3 ms without an attributed indivisible/OS event;
- steady update p95 at or below 2 ms;
- core render-scene/dispatcher allocation p50 of zero;
- whole-client steady allocation initially at or below 4 KiB/frame;
- no known exception-as-control-flow site.
Because Slice E already satisfies the dense median target, G must additionally
show one of:
- a statistically meaningful CPU p95/p99 reduction;
- lower CPU utilization at equal uncapped throughput;
- materially improved resident-entity scaling;
- or a documented result that the removed scan was no longer dominant and the
next measured bottleneck belongs to Slice H.
No visual-quality reduction is an acceptable way to pass.
## 8. Commit and review sequence
Each sub-slice is one or more bisectable commits, but a commit may not mix
shadow storage, production cutover, and old-path deletion.
For every commit:
1. inspect the complete diff for authority leakage, stale identity, lifecycle
asymmetry, and allocations;
2. run focused App tests;
3. run `dotnet build AcDream.slnx -c Release`;
4. run the complete Release solution tests before a sub-slice gate;
5. update architecture, roadmap, issues, divergence register, and memory in the
same commit when their truth changes;
6. introduce no retail-divergence row—F/G is performance-only and any visible
difference is a bug.
No substantial feature body enters `GameWindow`. Composition changes live in
`FrameRootComposition`; scene, journal, oracle, synchronization, and frame
product owners live under focused `AcDream.App/Rendering` namespaces.
## 9. Stop/rollback conditions
Stop the current sub-slice and fix the root cause when:
- the shadow digest differs;
- source identity requires a second authoritative GUID map;
- a draw consumer must call back into `LiveEntityRuntime` or `GpuWorldState`;
- delta order cannot represent a reentrant lifecycle edge;
- the new path needs a timer, suppression flag, or retry workaround;
- a screenshot/instance difference cannot be explained;
- the frame product adds a frame of latency;
- memory fails to plateau;
- F/G performance is worse outside explicitly enabled shadow comparison.
The rollback unit is the failing sub-slice commit. Do not retain a permanent
legacy fallback, dual draw, or user-facing renderer switch.
## 10. Approval and rollback state
This plan is approved and active.
- Pre-F/G runtime rollback anchor: `e7d9d6fa`.
- Exact first G4 production cutover:
`ef1d263337997bb030eadb7b8e71d73dc659907a`.
- If tomorrow's visual gate fails, surgically run
`git revert ef1d263337997bb030eadb7b8e71d73dc659907a`. This preserves the
proven F/G referee, journal, frame-product, and retained-classification
commits through G3. Do not reset the branch and do not revert `6a026c5a`.
- Every production cutover commit is added here and to
`claude-memory/project_render_pipeline_digest.md`.
- Shadow/referee commits precede the production switch and need not be reverted
for a visual mismatch unless they independently change runtime behavior.
- A failed visual gate reverts the exact cutover commit(s); it does not use
`git reset`, discard later evidence, or enable a permanent fallback.

View file

@ -1,230 +0,0 @@
# Modern runtime Slice H execution plan
**Status:** COMPLETE (2026-07-25)
**Parent plan:** `2026-07-24-modern-runtime-architecture.md`, Slice H
**Prerequisite state at execution:** Slice G4 production cutover was
automated-gated; its user visual gate later passed on 2026-07-25.
## 1. Safety boundary
Slice H removes work which scales with presentation rate. It does not change
retail gameplay rules, visible-cell authority, light membership, packet
ordering, acknowledgement timing, heartbeat placement, or UI presentation.
Slice H did not delete or alter the G4 old-route referee while its visual gate
was pending. That gate has since passed; G5 owns the final deletion boundary.
If the G4 visual result is wrong, revert only:
```text
git revert ef1d263337997bb030eadb7b8e71d73dc659907a
```
Do not reset history and do not revert G3. The portal-warmup corrections
`129dd77d` and `03b10183` are independent of the retained draw-source cutover.
## 2. H-a — retained UI, live attachments, diagnostics, and frame scratch
### H-a1 — cached shaped text — COMPLETE
1. Add a small retained text-layout cache keyed by source revision/value plus
width, padding, color, and font identity.
2. Convert appraisal item, inscription, spell, creature, character-info, and
active-effect detail providers so expensive report construction and
wrapping occur only after source or layout changes.
3. Preserve `UiText.LinesProvider` as the rendering seam. Dynamic one-line
widgets remain dynamic; this change is limited to report/wrapped content.
4. Ensure selection, appraisal response, component count, enchantment,
character-property, font, and resize changes invalidate the right cache.
5. Add tests proving stable draws return the same line collection while source
and geometry changes rebuild it.
Gate: the existing appraisal/character/effect behavior tests remain unchanged,
and repeated stable provider polls allocate no report/wrap objects.
Landed evidence:
[`../research/2026-07-25-slice-h-a1-ui-text-cache.md`](../research/2026-07-25-slice-h-a1-ui-text-cache.md).
### H-a2 — visible cooldown participants — COMPLETE
1. Keep the shared retail heartbeat and one cooldown result per represented
item.
2. Index mounted `UiItemList` participants once at bind time.
3. At heartbeat, visit only lists whose complete ancestor chain is visible.
Hidden panels keep no stale visual state: their slots query zero while
hidden and are refreshed on the first visible heartbeat.
4. Keep future-slot support and shared-group behavior.
Gate: hidden inventory, equipment, external-container, vendor, and spell-bar
lists are not scanned; reopening immediately displays the authoritative
cooldown step.
Landed evidence:
[`../research/2026-07-25-slice-h-a2-cooldown-scope.md`](../research/2026-07-25-slice-h-a2-cooldown-scope.md).
### H-a3 — equipped-child transition work — COMPLETE
1. Preserve both frame ordering points: pre-network presentation and
post-network spatial reconciliation have different authority boundaries.
2. Split transition retry from pose recomposition.
3. Run the complete parent-first pose walk once after animation.
4. In the post-network phase, recompose only attachments dirtied by accepted
root/relation/appearance changes.
5. Replace mutation-safe dictionary `ToArray` snapshots with retained key
scratch buffers. Callback reentrancy still observes the same transition
generation checks.
Gate: equip, unparent, reparent, pose loss, rollback, delete, GUID reuse, and
subtree-withdrawal suites preserve exact event and resource counts.
Landed evidence:
[`../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md`](../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md).
### H-a4 — diagnostics and frame scratch — COMPLETE
1. Reuse one synchronous `RetailPViewFrameInput` owned by
`WorldSceneRenderer`; reset every field before each draw.
2. Expose stable borrowed landblock entry/bounds views instead of creating
iterator state in each frame.
3. Consumer-gate optional shadow enumeration. Preserve landblock visibility
counting because the normal title and lifecycle checkpoints are active
consumers of `lb V/T`; neutralizing it would be a visible/automation
behavior change.
4. Do not rework `InteriorEntityPartition` while the G4 referee remains.
Issue #241 closes with G5 old-route deletion, not by editing the fallback.
5. Profile capped and uncapped production paths and retain evidence.
Gate: identical framebuffer/scene digest, no per-frame PView input allocation,
no iterator allocation for stable landblock views, and no shadow traversal
with all optional consumers disabled.
Landed evidence:
[`../research/2026-07-25-slice-h-a4-frame-scratch.md`](../research/2026-07-25-slice-h-a4-frame-scratch.md).
## 3. H-b — exact light top-k — COMPLETE
Retail anchors:
- `CEnvCell::add_dynamic_lights` at `0x0052D410`
- `Render::insert_light` at `0x0054D1B0`
- `Render::add_dynamic_light` at `0x0054D420`
- global caps at `0x0081EC94` / `0x0081EC98`
Execution:
1. Preserve the resident `_all` registry and existing visible-cell filter.
2. Preserve dynamics-first ordering and squared distance from the player.
3. Replace only the over-cap complete sort with a bounded exact top-k
selection.
4. Define tie-breaking from the existing comparator so the selected set and
final submission order are identical, including equal-distance fixtures.
5. Differential-test randomized candidate sets and the captured 463-light Town
Network fixture against the old full-sort oracle.
6. Run the fixed-camera screenshot comparison and connected light gate.
Gate: identical selected IDs and submission order for every fixture, identical
Town Network screenshot, reduced overflow CPU/allocation, and AP-85 retired
only after the visual evidence passes.
Landed evidence:
[`../research/2026-07-25-slice-h-b-light-top-k-report.md`](../research/2026-07-25-slice-h-b-light-top-k-report.md).
The deterministic differential fixtures preserve exact accepted output; the
463-light diagnostic microbenchmark reduced selection CPU time by 29.1% with
zero warmed allocations. AP-85 remains open because its retail dual-pool
behavior is outside this performance-only unit.
## 4. H-c — ordered, allocation-conscious network I/O
H-c is executed as three independently reversible units:
1. **H-c1 — pooled receive owner — COMPLETE.** One cancellable async socket
receive, a retained full-size edge buffer, right-sized pooled FIFO
datagrams, unchanged blocking handshake cadence, and direct span sends.
Evidence:
[`../research/2026-07-25-slice-h-c1-pooled-receive-owner.md`](../research/2026-07-25-slice-h-c1-pooled-receive-owner.md).
2. **H-c2 — borrowed decode — COMPLETE.** Parse and verify headers, optional
fields, and fragments as borrowed views. Copy only multi-fragment state
that must survive the current datagram. Evidence:
[`../research/2026-07-25-slice-h-c2-borrowed-packet-decode.md`](../research/2026-07-25-slice-h-c2-borrowed-packet-decode.md).
3. **H-c3 — direct outbound framing — COMPLETE.** Write packet and fragment
framing into caller storage and remove intermediate payload arrays.
Evidence:
[`../research/2026-07-25-slice-h-c3-direct-outbound-framing.md`](../research/2026-07-25-slice-h-c3-direct-outbound-framing.md).
Retail/transport invariants:
- one outstanding receive;
- kernel datagram arrival order remains inbound queue order;
- one acknowledgement decision per accepted packet;
- `LinkStatusHolder::OnHeartbeat` at `0x004113D0` remains at the same
last-heard point;
- `PumpOnce` handshake pacing remains unchanged;
- fragmented message assembly and sequence gates remain authoritative.
Execution:
1. Capture allocation and ordering baselines for login, idle, motion, portal,
appraisal, container, and combat traffic.
2. Introduce a single cancellable socket receive owner using reusable/pool
storage.
3. Decode packet headers and fragments from spans/borrowed storage.
4. Copy only payloads whose lifetime crosses the receive/dispatch boundary;
pooled receive memory is never returned while referenced.
5. Replace outbound temporary arrays with span/list segment writes where the
socket API permits.
6. Prove cancellation, remote close, malformed packet, receive fault, fragment
reassembly, queue backpressure, and reconnect cleanup.
7. Run connected lifecycle, reconnect, portal, interaction, and soak gates.
Gate: identical decoded event/ack traces, no receive-thread death, no pooled
buffer lifetime violation, and materially reduced bytes per packet.
## 5. Commit and evidence order
Each numbered unit lands as a bisectable commit after focused tests, App
Release, complete Release, and documentation reconciliation:
1. H-a1 cached shaped text.
2. H-a2 cooldown visibility.
3. H-a3 attachment retry/recomposition.
4. H-a4 diagnostics/frame scratch.
5. H-b exact light top-k.
6. H-c network receive/decode/send allocations.
7. Slice-H connected measurements and closeout.
Raw captures stay under `.test-out/` or `logs/`; reviewed summaries and compact
fixtures are committed under `docs/research/` or the matching test project.
## 6. Deferred boundaries
- Physics `Transition` pooling (#237) remains Slice I and requires reset-
completeness/identity tests first.
- G5 old-route/referee deletion waits for the user's G4 visual acceptance.
- Static publication indexing (#242) is not silently folded into H; it is
measured after H-a and scheduled only if still material.
- Slices IL start only after Slice H is measured and documented, but no
additional authorization is required.
## 7. Closeout
All H-a, H-b, and H-c implementation units are complete. The Release build
and complete solution pass 8,292 tests with 5 skips. The connected
login/portal/dungeon/revisit/graceful-disconnect/fresh-process-reconnect gate
passed on commit `41c1a593927fcf71ef83b4825c659a4970e9b858` with all seven
checkpoints ready, no invariant or transport failure, and both sessions
closing through the normal logout path.
The connected automation deliberately constructs the retained G4 current-path
oracle, so its scene-sized per-frame allocation is comparison-instrumentation
cost and not representative of production. A separate normal uncapped Release
session, with neither lifecycle automation nor the oracle composed, stabilized
at 25.6 KiB/frame and roughly 500 presented frames/second under RDP. Allocation
stack attribution identifies the remaining dominant production owners as
`Transition` query scratch (Slice I) and the retained current partition/referee
path (G5). The H-owned packet decode and recurring outbound framing paths
independently allocate zero bytes when warm. The program-wide 4 KiB/frame
target therefore remains an end-of-G5/Slice-I acceptance condition rather than
being misreported as achieved by H alone.
Closeout evidence:
[`../research/2026-07-25-slice-h-closeout.md`](../research/2026-07-25-slice-h-closeout.md).

View file

@ -1,371 +0,0 @@
# Modern runtime Slice I — flat collision assets and zero-allocation physics
**Status:** I0I7 COMPLETE — retail oracle, zero-allocation transition
scratch, deterministic immutable collision records, complete prepared
package, exact flat traversal, dual publication, flat-authoritative production
cutover, parsed-graph removal, connected correctness/lifetime acceptance, and
documentation closeout
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice I
**Purpose:** remove parsed DAT object graphs and steady collision allocations
without changing one retail collision decision, float, comparison, or traversal
order.
## 1. Non-negotiable boundaries
1. Retail collision math is frozen. This slice changes storage and scratch
ownership only.
2. Every float read from DAT is copied bit-for-bit. The bake performs no
normalization, quantization, coordinate conversion, `double` round-trip, or
plane reconstruction.
3. Positive/negative child order, polygon order within every leaf, early
returns, epsilon comparisons, and contact-selection tie behavior remain
exact.
4. The current object-graph traversal remains an executable oracle until the
flat traversal passes mass differential and connected gates.
5. Physics `Transition` reuse lands only after reset-completeness,
cross-mover-identity, failure, and reentrancy tests prove that no state can
leak between resolves.
6. No GL/App dependency enters Core. Flat collision records live in
`AcDream.Core`; serialization and package access live in `AcDream.Content`;
the bake tool produces them offline.
7. The active G4 render cutover is independent. Its exact visual rollback
remains:
```text
git revert ef1d263337997bb030eadb7b8e71d73dc659907a
```
## 2. Target data architecture
The runtime collision representation is immutable, array-backed, and addressed
by integer index:
```text
FlatCollisionAsset
├── FlatPhysicsBsp
│ ├── FlatBspNode[] child indices; -1 = no child
│ ├── int[] leaf polygon-index stream
│ ├── FlatPolygon[] plane, cull mode, id, vertex range
│ └── Vector3[] verbatim polygon vertices
├── FlatCellContainmentBsp split planes + positive/negative indices
├── FlatSetupCollision cyl/sphere records + verbatim dimensions
└── EnvCellTopology portals, visible cells, seen-outside
```
Node order is deterministic pre-order. The flattener records the original
positive child before the negative child and retains each source leaf's
polygon list order. Traversal follows indices using the same recursive
control flow as the current retail port; “flat” does not mean a new collision
algorithm.
Per-EnvCell world transform remains publication state, not baked geometry.
CellStruct collision geometry can therefore alias by exact source identity;
cell-specific portals, visibility, `seen_outside`, and placement remain a
separate small payload/publication record.
## 3. Execution slices
### I0 — oracle inventory and fixed evidence
1. Grep named retail before touching traversal:
- `BSPTREE::find_collisions`
- `BSPNODE::find_walkable`
- `BSPLEAF::find_walkable`
- `BSPNODE::sphere_intersects_*`
- `BSPNODE::point_inside_cell_bsp`
- `CTransition::find_transitional_position`
- `CTransition::init_path`
- `CPhysicsObj::transition`
2. Cross-reference ACE and the existing acdream pseudocode/trajectory notes.
3. Write one collision-layout pseudocode note that separates:
source storage, traversal order, mutable query scratch, and returned
side-effects.
4. Inventory every production call into `BSPQuery`, every field read from
`PhysicsBSPNode`, `CellBSPNode`, `ResolvedPolygon`, `CellPhysics`,
`GfxObjPhysics`, and `SetupPhysics`, and every raw DAT graph retained by
`LandblockBuild`/`PhysicsDatBundle`.
5. Capture representative installed-DAT fixtures:
- outdoor/no-BSP;
- Facility Hub and cottage indoor cells;
- staircase/ramp and cellar-lip cases;
- a multipart door/building shell;
- projectile/thin-wall geometry;
- cells with asymmetric/null BSP children;
- leaves with multiple polygons and equal-distance candidates.
6. Record allocation baselines for player, remote, projectile, and camera
resolve loops.
Gate: source inventory is complete, fixtures reproduce through the current
graph path, and no behavior code changed.
### I1 — reusable transition and query scratch
1. Add explicit `ResetForReuse` methods to `ObjectInfo`, `CollisionInfo`,
`SpherePath`, and `Transition`.
2. Reset every scalar, nullable, property backing field, diagnostic counter,
collision GUID list, sphere element, walkable reference, backup value, and
transient flag. Retain only storage identity whose contents are completely
reset.
3. Give each `PhysicsEngine` one owned transition lease. The normal
update-thread path rents, initializes, resolves, snapshots the value-only
result, and returns in `finally`.
4. Make reentrancy explicit and tested. Never hand one mutable transition to
two concurrent/nested resolves and never silently share session scratch.
5. Reuse walkable vertex arrays only when their exact logical length matches;
allocate only on a true size change. No larger-capacity array may leak stale
vertices into polygon math.
6. Convert short-lived `BSPQuery.CollisionSphere` reference objects to
value/ref scratch while preserving each mutation/copy boundary.
7. Add structural reflection tests that poison every resettable member, reset,
and compare its complete value graph with a fresh instance while asserting
retained buffer identities.
8. Differential-run fresh-transition and reused-transition engines across
success, collision, failure, placement, grounded, airborne, sliding,
step-up/down, two-sphere, projectile, and camera cases.
Gate: bit-identical `ResolveResult` and body side effects, no state leakage
when alternating hostile fixtures between different mover IDs, and zero
steady transition/query-scratch allocation. **PASSED 2026-07-25:** the expanded
118-test graph oracle, structural poison/reset suite, hostile fresh-vs-reused
differential, Core/solution Release gates, and all five measured profiles pass.
Issue #237 is closed. Evidence:
[`../research/2026-07-25-slice-i1-transition-scratch.md`](../research/2026-07-25-slice-i1-transition-scratch.md).
### I2 — immutable flat collision schema and flattener
1. Add Core-only flat record types with explicit index/range contracts.
2. Flatten physics and containment BSPs iteratively during preparation while
emitting deterministic pre-order node arrays.
3. Copy planes, spheres, vertices, setup dimensions, and collision metadata
verbatim. Tests compare every float via `SingleToInt32Bits`.
4. Resolve each leaf polygon ID to a direct polygon index at preparation time.
Missing IDs are corruption, not a runtime fallback.
5. Validate:
- child indices/ranges;
- acyclic/reachable tree shape;
- polygon and vertex ranges;
- source-vs-flat node, child, polygon, and float identity;
- deterministic output independent of dictionary enumeration.
6. Keep source graph and flat asset together only in test/shadow fixtures.
Gate: round-trip structural equality over synthetic edge cases and installed
DAT samples; no traversal cutover yet. **PASSED 2026-07-25:** 13 focused
structural/installed-DAT tests, the 131-test expanded Slice I oracle, complete
Core/solution Release gates, exact float-bit comparisons, and corruption
tripwires pass. The graph route remains the only executable traversal.
Evidence:
[`../research/2026-07-25-slice-i2-flat-collision-schema.md`](../research/2026-07-25-slice-i2-flat-collision-schema.md).
### I3 — package serialization, bake, and prepared collision source
1. Append new `PakAssetType` values; never renumber the existing mesh values.
2. Bump `CurrentBakeToolVersion` because a complete production package now
includes collision assets.
3. Add strict little-endian serializers with checked counts/ranges, exact float
bits, cancellation, CRC coverage, corruption rejection, and trailing-byte
rejection.
4. Add a typed `IPreparedCollisionSource` over the existing memory-mapped pak.
It shares package/catalog lifetime but does not overload the render-only
`ObjectMeshData` API.
5. Bake:
- GfxObj physics BSP + polygons + visual bounds;
- Setup cylinder/sphere/dimension records;
- CellStruct physics BSP + containment BSP + physics/portal polygons;
- EnvCell topology records.
6. Alias only byte-identical immutable collision payloads. Cell-specific
topology and world placement cannot be aliased merely because geometry is.
7. Extend full-bake determinism, two-thread equivalence, corruption,
cancellation, publish-transaction, and catalog-completeness tests.
Gate: two independent bakes are byte-identical; every collision key required
by the canonical route is present; a failed/cancelled bake cannot replace the
last good pak. **PASSED 2026-07-25:** two complete 29,908,271,024-byte bakes
with different worker counts produced the same SHA-256 and 2,232,170 typed
keys with zero failures. Strict serialization/corruption/cancellation gates,
representative installed-package reads, Release build, and 8,371 solution
tests / 5 skips pass. Production traversal remains graph-only. Evidence:
[`../research/2026-07-25-slice-i3-prepared-collision-package.md`](../research/2026-07-25-slice-i3-prepared-collision-package.md).
### I4 — flat traversal shadow implementation
1. Port each current entry point line-for-line against integer-index nodes:
- point-in-cell;
- sphere/cell overlap;
- walkable search;
- six-path moving collision dispatcher;
- static sphere/polygon overlap;
- time-of-impact overlap.
2. Share only polygon-level math that is storage-independent. Do not
“simplify” recursion, branch ordering, or early returns.
3. Preserve mutation semantics for valid-position spheres, hit polygon, path,
contact plane, slide normal, walk interpolation, and transition state.
4. Add a differential harness that executes old and flat queries from cloned
transition/body inputs and compares:
- bool/enum result;
- all output vectors/planes by float bits;
- selected polygon ID;
- path/collision/object side effects;
- cell membership and final `ResolveResult`.
5. Sweep a large installed-DAT sample with randomized points, spheres,
movement vectors, insert modes, orientations, scales, and boundary values.
6. Require explicit fixtures for null children, on-plane equality, radius
equality, equal candidate distances, multi-polygon leaf order, and deep
trees.
Gate: zero differential mismatch. Any mismatch blocks cutover; no tolerance
band is permitted for deterministic scalar results.
**PASSED 2026-07-25:** the integer-indexed shadow ports every current
containment, overlap, walkable, and six-path moving-collision entry point.
The 13-test referee completed 10,000 randomized static/swept comparisons,
5,000 walkable comparisons, 7,500 installed-DAT comparisons across three
representative cells, all six dispatcher paths, and 1,200 complete resolver
frames with exact state and float-bit equality. Ten thousand warmed flat
iterations allocated zero managed bytes. Production remains graph-authoritative
until I5/I6. Release build and 8,384 solution tests / 5 skips pass. Evidence:
[`../research/2026-07-25-slice-i4-flat-bsp-differential.md`](../research/2026-07-25-slice-i4-flat-bsp-differential.md).
### I5 — dual publication and connected shadow gate
1. Extend collision preparation so a `LandblockBuild` carries immutable flat
assets and small placement/topology records rather than reparsing them on
the update thread.
2. During the shadow phase, publish both current and flat views under one
landblock generation/receipt.
3. Keep the current graph path authoritative. Run sampled flat queries from
cloned inputs and emit a deterministic mismatch artifact without affecting
gameplay.
4. Verify load, pending-to-loaded, demotion, rehydrate, same-location revisit,
removal, cancellation, and session reset release both views exactly once.
5. Run connected login, indoor/outdoor portal churn, doors, ramps, jumping,
projectiles, camera collision, and graceful reconnect.
Gate: zero shadow mismatch and no retained collision owner after landblock or
session teardown.
**Complete 2026-07-25.** Near-tier builds carry one immutable prepared
collision closure; streamed and live objects publish graph and flat assets
strictly, with no graph-only fallback. Cancellation, publication, demotion,
rehydration, revisit, adjacent-landblock isolation, removal, and session
teardown gates pass. The complete Release solution passes 8,396 tests / 5
skips. The connected lifecycle/reconnect and nine-stop routes sampled 14,064
queries with exact state/float-bit equality, zero mismatch, zero flat fault,
zero mismatch artifact, equal graph/flat cell residency at every stable
checkpoint, and graceful exits. Evidence:
[`../research/2026-07-25-slice-i5-dual-collision-shadow.md`](../research/2026-07-25-slice-i5-dual-collision-shadow.md).
### I6 — production cutover and graph removal
1. Flip canonical `PhysicsDataCache` records and every `BSPQuery` production
call to flat assets in one bisectable commit.
2. Keep the graph route as an automated referee for the first cutover gate,
with an exact revert command recorded in this plan and project memory.
3. After automated and user correctness acceptance, remove:
- production `PhysicsBSPTree`/`CellBSPTree` retention;
- production polygon dictionaries and vertex DBObj graphs;
- raw collision graphs from streaming build/publication payloads;
- obsolete graph adapters and referee code.
4. Live-DAT tooling may parse source graphs only to produce/compare flat
assets; gameplay production never falls back from a missing/corrupt prepared
collision asset.
Gate: trajectories and collision fixtures remain bit-identical, package-only
startup succeeds, and retained-memory accounting shows no parsed collision
DBObj graph after stable world reveal.
**Production cutover landed 2026-07-25 at
`068a06518dd710ca5e7f166754cbb7789ca1ed0f`.** Gameplay is now
flat-authoritative, package publication is strict, and the parsed graph runs
only as a sampled referee. The exact rollback for this cutover is:
```text
git revert 068a06518dd710ca5e7f166754cbb7789ca1ed0f
```
Do not revert I3I5 package/publication work. The pre-visual automated gate
passes 8,402 Release tests / 5 skips and a strict dense-Arwic connected run
with 46,309/46,309 exact graph-referee matches, zero mismatch/fault/artifact,
and graceful shutdown. Production-like uncapped dense Arwic (15,530 entities,
no automation oracle) sustained roughly 190200 FPS with stable CPU p50
~5.0 ms / p95 ~5.97.3 ms and GPU p95 ~2.1 ms. Evidence:
[`../research/2026-07-25-slice-i6-flat-production-cutover.md`](../research/2026-07-25-slice-i6-flat-production-cutover.md).
The user accepted the connected collision gate on 2026-07-25. Production
parsed-graph removal then landed at
`82f8d4f82e24ad85042ac3e25c4f6464bebce758`. The production
`PhysicsDataCache`, stable `GpuWorldState`, near-build payloads, and live
publication path now retain only immutable flat assets. Graph construction
survives solely in explicit tests, bake/equivalence tools, and source-oracle
constructors. The exact rollback for graph removal is:
```text
git revert 82f8d4f82e24ad85042ac3e25c4f6464bebce758
```
Do not revert I3I6 unless evidence implicates the prepared package or
flat-authoritative traversal itself.
### I7 — allocation, soak, and documentation closeout
1. Measure capped and uncapped resolve allocation separately for player,
remotes, projectiles, and camera.
2. Run the canonical connected lifecycle/reconnect route and the nine-stop R6
portal soak.
3. Repeat indoor door/ramp/cellar, jump/fall, combat/projectile, and camera-wall
gates.
4. Under RDP, accept correctness and lifecycle evidence only; defer absolute
GPU/frame-time acceptance to a physical-display run.
5. Update architecture, WorldBuilder inventory, milestones, roadmap, issue
ledger, divergence register, and both physics/render memory digests.
Final gate:
- full Release build and solution tests;
- zero deterministic old-vs-flat mismatch;
- zero steady transition/query-scratch allocation;
- no raw collision graph in production stable state;
- connected portal/reconnect/interaction routes pass;
- exact cutover rollback recorded before user verification.
**Completed 2026-07-25.** Release build and the complete solution pass
8,413 tests / 5 skips. The focused collision/door/doorway/cellar/ramp/jump/
projectile/allocation gate passes 154 Core tests plus 47 App camera/projectile
tests; player, grounded publication, remote, projectile, and camera resolves
remain 0 B/resolve.
The exact-binary connected lifecycle/reconnect route and canonical nine-stop
R6 soak both pass with graceful exits. Every stable checkpoint reports
`0/0/0` retained parsed GfxObj/Setup/cell graphs while flat GfxObj/Setup/cell
residency remains populated. Movement, jump, combat, projectile, portal,
dungeon, revisit, reconnect, publication, retirement, and teardown checks
pass. The user accepted collision behavior over RDP. Absolute post-removal
GPU/frame-time and ordinary-process memory comparison remains a physical-
display measurement, not a Slice-I correctness blocker. Evidence:
[`../research/2026-07-25-slice-i7-closeout.md`](../research/2026-07-25-slice-i7-closeout.md).
**Corrective gate 2026-07-26.** Later connected house/dungeon testing exposed
one I7 consumer omission: `CEnvCell::find_transit_cells` still required the
deleted parsed portal-polygon dictionary, even though the prepared topology
and polygon table retained the exact same plane. The production consumer now
uses that direct prepared index. Synthetic graph-free tests and the captured
Holtburg cottage camera path prove identical graph/prepared results; no
package rebake or parsed-graph restoration is needed. Evidence:
[`../research/2026-07-26-prepared-indoor-transit-regression.md`](../research/2026-07-26-prepared-indoor-transit-regression.md).
## 4. Commit order
Each unit is independently buildable and reversible:
1. `docs(physics): pin Slice I collision oracle and fixtures`
2. `perf(physics): reuse reset-complete transition scratch`
3. `feat(physics): define deterministic flat collision assets`
4. `feat(content): bake and read flat collision assets`
5. `feat(physics): add differential flat BSP traversal`
6. `feat(streaming): shadow-publish flat collision assets`
7. `perf(physics): cut production traversal to flat assets`
8. `refactor(physics): remove parsed collision graphs`
9. `docs(physics): close Slice I evidence and roadmap`
The exact production-cutover commit and its `git revert <sha>` command are
added to this plan and `claude-memory/project_physics_collision_digest.md`
immediately when step 7 lands, before any visual/correctness gate.

View file

@ -1,465 +0,0 @@
# Modern runtime Slice J — presentation-independent runtime
**Status:** COMPLETE 2026-07-27
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice J
**Authorization:** the user approved Slices FL, including the otherwise-frozen
Slice J gameplay-owner moves, on 2026-07-24
**Purpose:** move the existing authoritative client kernel into a
presentation-independent assembly without creating a second world, changing
retail behavior, or making the graphical host a special case.
Slice J is a lifetime-group campaign, not one large move. Each sub-slice keeps
the graphical client on the same owner instance, establishes host parity, and
deletes the replaced App production path before the next group moves.
## 1. Fixed invariants
1. `RuntimeEntityDirectory` is the sole server-GUID/incarnation/local-ID owner.
`LiveEntityRuntime` is the exact-key App projection host. There is never a
second GUID map or synchronized gameplay world.
2. Runtime types do not reference `AcDream.App`, either UI assembly, Silk.NET,
OpenAL, Arch, ImGui, a native window, or an OpenGL resource.
3. App projection state is keyed by runtime-issued local entity identity plus
incarnation. It may not recover or mirror server GUID ownership.
4. The accepted update order, packet ordering, timestamp gates, outbound
cadence, and retail-shaped gameplay algorithms do not change during the
extraction.
5. Contracts expose borrowed/immutable views, typed commands, and ordered
deltas. They do not expose App render components or deep per-frame object
graphs.
6. All clocks, queues, random sources, diagnostics identity, plugin behavior,
and mutable caches that can differ between sessions are instance-scoped.
7. Immutable prepared content may be shared. Session state and mutable query
scratch may not.
8. Every new owner participates in the structural teardown protocol in parent
plan §9.2.1 through acquisition leases and exact acknowledgements.
9. A headless construction path is proved after every coherent move. “No
window” means no App, Silk, OpenAL, retained UI, particle, or render assembly
is loaded—not a hidden graphical process.
10. Slice I is closed: production collision is flat-authoritative and retains
no parsed collision graph. J1 contracts adapt that final canonical owner
shape; they may not reintroduce a graph fallback or mirrored collision
authority.
## 2. Target dependency direction
```text
AcDream.App ───────────────► AcDream.Runtime
AcDream.Headless (Slice K) ─► AcDream.Runtime
├──► AcDream.Core.Net
├──► AcDream.Core
├──► AcDream.Content
└──► AcDream.Plugin.Abstractions
AcDream.Runtime -X► AcDream.App / AcDream.UI.* / Silk.NET / OpenAL / Arch
```
Runtime-emitted entity events contain incarnation, transform, flags, and
property deltas only. An App-side projection adapter translates them into
`IRenderScene` journal operations. Render vocabulary never enters Runtime.
## 3. Teardown transaction
The exact order is:
1. cancel scheduler/session generations and make commands inert;
2. detach producers, drain accepted ordered events, and poison queues;
3. withdraw and acknowledge graphical presentation;
4. retire session residence and GPU-backed owners through existing fences;
5. dispose the process-owned immutable `ContentStore` only after every runtime
and host has completed.
Completed stages never replay. Failure retains the exact remaining suffix.
Logout, reset, mid-portal disconnect, reconnect replacement, construction
rollback, and native close all traverse the same transaction.
## 4. Execution slices
### J0 — boundary, plan, and dependency enforcement
**Completed 2026-07-25.**
- Add `AcDream.Runtime` and `AcDream.Runtime.Tests`.
- Permit references only to Core, Core.Net, Content, and Plugin.Abstractions.
- Add App → Runtime as the future host dependency; Runtime never references
App.
- Add direct-reference, dependency-closure, source-project, and assembly-load
guards that fail if a presentation/backend dependency enters Runtime.
- Add only an assembly boundary marker; do not introduce a facade
`GameRuntime`, mirror state, or move behavior in J0.
- Pin this lifetime-group plan and parent-plan teardown protocol.
Gate: Release build and full tests pass; the Runtime test process loads no App,
UI, Silk, OpenAL, Arch, or ImGui assembly; graphical behavior is byte-for-byte
unchanged because no runtime path changed.
Evidence: four focused dependency tests pass, the Release solution builds with
zero errors, and the complete Release suite passes 8,406 tests with five
pre-existing skips. Exact rollback:
```text
git revert b632672e5ccabfb44c551e08f1c411ab2669c44a
```
### J1 — read, command, event, clock, and lifecycle contracts
**Completed 2026-07-25 at `854d9e9c`.**
- Define `IGameRuntimeView`, typed command interfaces, ordered event/delta
records, instance clock, lifecycle states, generation tokens, and teardown
acknowledgements.
- Build App adapters over the current owners first. Adapters borrow; they do
not copy mutable collections or own lifetime.
- Add a normalized parity trace of accepted inbound events, runtime commands,
state revisions, and lifecycle edges.
- Prove press-time graphical input reaches the same current owners through the
command contract with no extra frame of latency.
Gate: adapter and direct-host traces are identical over deterministic session,
entity, inventory, chat, movement, and portal fixtures; no canonical owner has
moved yet.
Result:
- `AcDream.Runtime` owns immutable borrowed-view contracts, typed commands,
ordered deltas, an instance clock, generation tokens, lifecycle snapshots,
and retryable teardown acknowledgements.
- Focused App adapters borrow the current canonical owners. They create no
second GUID map, state store, command queue, or frame boundary.
- Startup plus press-time selection, movement, and combat input now traverse
the typed command seam synchronously and still mutate the exact same owners.
- Runtime tests pass 13/13, App tests pass 3,838 with three skips, and the
complete Release solution passes 8,424 tests with five skips.
- The exact-binary connected gate at
`logs/connected-world-gate-20260725-190953/report.json` passed six capped
world checkpoints plus a fresh uncapped reconnect. Both processes exited
gracefully with no failures.
Exact rollback:
```text
git revert 854d9e9cd13092bd5aaa3cf025d73eeb4600e9f8
```
### J2 — session lifetime and ordered transport group
**Completed 2026-07-25 at `75930787`.**
**Detailed execution plan:**
[`2026-07-25-modern-runtime-slice-j2.md`](2026-07-25-modern-runtime-slice-j2.md).
- Remove App presentation dependencies from `LiveSessionController`,
`LiveSessionHost`, lifecycle host, event router, and command router.
- Move the same owner instances into Runtime with Core.Net transport and
instance-scoped clocks/queues.
- Preserve bind-before-connect, publish-after-EnterWorld, exact generation
rechecks, receive ordering, ack placement, graceful F653/disconnect, and
retryable replacement.
- Let App provide only endpoint credentials, presentation bindings, and
graphical lifecycle acknowledgements.
Gate: connected login/logout/reconnect and malformed/reentrant lifecycle suites
produce the same packet/order trace; no-window construction can connect and
disconnect without loading App or a backend.
Result: Runtime now owns the one `WorldSession` generation, ordered inbound
route, connect/enter/tick/replace transaction, and exact retryable teardown
acknowledgement. App keeps only connection-option conversion, graphical
callbacks, and one borrowed inertable UI command projection. The Release build,
79 Runtime tests, 3,776 App tests / 3 skips, 8,428 complete tests / 5 skips,
and the exact-binary seven-checkpoint connected gate pass with graceful exits.
Evidence:
[`../research/2026-07-25-slice-j2-session-lifetime-closeout.md`](../research/2026-07-25-slice-j2-session-lifetime-closeout.md).
Exact rollback:
```text
git revert 75930787741db40a83eab8663e4464dce8d687ba
```
### J3 — canonical identity, properties, and object-table group
**Detailed execution plan:**
[`2026-07-25-modern-runtime-slice-j3.md`](2026-07-25-modern-runtime-slice-j3.md).
**Completed 2026-07-26.** Runtime owns accepted entity
wire state and the only canonical identity/incarnation/local-ID directory.
App's `LiveEntityProjectionStore` and every materialized presentation/spatial
workset are keyed by exact `RuntimeEntityKey`; the temporary GUID-shaped
compatibility view is deleted. `RuntimeEntityObjectLifetime` now owns the
exact entity directory and live `ClientObjectTable`; App, routing, interaction,
and UI borrow both. Exact J3.5 binary `ce3ac310` publishes one committed
per-generation Runtime entity/object delta stream, issues identity before App
hydration, and supplies direct allocation-free views to graphical and
no-window hosts. App reconstructs neither entity nor inventory state/events.
J3.6 at `119b7c11` makes every canonical commit, callback, teardown receipt,
reset, and direct disposal failure-safe and proves the full owner ledger
converges to zero. Its 8,484 Release tests / 5 skips, exact seven-checkpoint
lifecycle/reconnect gate, and canonical nine-stop route pass. Evidence:
[`../research/2026-07-26-slice-j3-6-lifetime-closeout.md`](../research/2026-07-26-slice-j3-6-lifetime-closeout.md).
- Strip renderer, particle, Wb, and streaming components from the canonical
record into an App projection store keyed only by local identity/incarnation.
- Move `LiveEntityRuntime`, accepted spawn/state/property timestamps,
`ClientObjectTable`, container/inventory indices, vitals, enchantments,
spell state, target/combat state, and their exact teardown into Runtime.
- Emit ordered create/update/rebucket/hidden/withdraw/delete deltas.
- Keep DAT-backed visual hydration, render registrations, effects, lights,
paperdoll, selection markers, and radar projections in App.
Gate: GUID reuse, equal-generation updates, parent/child identity,
inventory/container mutations, Hidden, delete/recreate, and portal teardown
match current traces; App has no server-GUID ownership.
### J4 — chat, inventory, magic, and gameplay-state services
**Completed 2026-07-26.**
**Detailed execution plan:**
[`2026-07-26-modern-runtime-slice-j4.md`](2026-07-26-modern-runtime-slice-j4.md).
- Move canonical chat history/channel state, inventory transactions, spell and
component state, enchantments, vitals, skills/attributes, and gameplay
cooldown state by coherent owner groups.
- Keep retained panel layout, text shaping, icons, paperdoll rendering, and
status-bar presentation in App.
- Route plugins and future bots through the same typed command/event surface
as graphical UI.
Gate: deterministic command/response traces and graphical ViewModel revisions
match; no Runtime type references UI abstractions.
J4.1 completed at `c9d25ade`: Runtime now owns the exact chat transcript,
reply/retell targets, negotiated Turbine rooms, friends, and squelch state.
Graphical UI, devtools, command routing, plugins/current-runtime projections,
and no-window consumers borrow that one graph. Its 152 Runtime tests, 3,765
App tests / 3 skips, 8,494 complete Release tests / 5 skips, and exact-binary
seven-checkpoint lifecycle/reconnect gate pass. J4.2 completed at `011efbea`:
Runtime owns external-container, item-mana, shortcut/component, shared-busy,
use-reservation, and one-request-at-a-time state while borrowing J3's exact
object table. Its 157 Runtime tests, 3,770 App tests / 3 skips, 8,515 complete
Release tests / 5 skips, and exact-binary seven-checkpoint
lifecycle/reconnect gate pass. J4.3 completed at `d02a12ce`:
`RuntimeCharacterState` owns the coupled spellbook and local player sheet;
content, routing, retained UI, reset, and shutdown borrow it, and the duplicate
desired-component snapshot is gone. Its 162 Runtime tests, 3,772 App tests / 3
skips, 8,522 complete Release tests / 5 skips, and exact-binary
seven-checkpoint lifecycle/reconnect gate pass. J4.4 completed at `dcb61efb`:
Runtime owns character options and server run/jump projections, exposes exact
borrowed J4 gameplay views, and routes retained UI plus future no-window hosts
through one synchronous generation-gated state-command seam. Its 169 Runtime
tests, 3,777 App tests / 3 skips, 8,534 complete Release tests / 5 skips, and
exact-binary seven-checkpoint lifecycle/reconnect gate pass. J4.5 completed at
`89e6b207`: Runtime owns the one shortcut manager, graphical command effects
flow through the exact Runtime inventory/character owners, the item-use UI
cannot construct a second transaction gate, and the combined failure-safe
ownership ledger converges. Its 175 Runtime tests, 3,780 App tests / 3 skips,
8,544 complete Release tests / 5 skips, exact-binary lifecycle/reconnect gate,
and canonical nine-stop route pass. Evidence:
[`../research/2026-07-26-slice-j4-5-gameplay-state-closeout.md`](../research/2026-07-26-slice-j4-5-gameplay-state-closeout.md).
Detailed execution begins with a source-ownership inventory. Move one coherent
lifetime group at a time in this order:
1. chat history/channel/command state;
2. inventory transaction and gameplay item-use state that is not already
canonical in `RuntimeEntityObjectLifetime`;
3. learned spells, formulas/components, enchantments, and cast/cooldown state;
4. vitals, skills, attributes, advancement, and character-option state;
5. plugin/bot borrowed views and typed commands over those same owners;
6. delete every replaced App owner/adapter, then run the combined no-window,
graphical parity, teardown, Release, lifecycle/reconnect, and nine-stop
gates.
Panel trees, text layout, icons, 3-D paperdolls, status bars, and animation/VFX
remain App presentation. The inventory object collection already moved in J3;
J4 must borrow it rather than create another inventory model.
### J5 — movement, physics, interaction, and combat group
**Completed 2026-07-26.** Detailed execution began with a source/owner inventory
and a named-retail ordering matrix. Each coherent move must preserve the
accepted connected interaction, facing, projectile, and collision behavior.
The executable sub-slice ledger, presentation boundary, acceptance gates, and
rollback discipline live in
[`2026-07-26-modern-runtime-slice-j5.md`](2026-07-26-modern-runtime-slice-j5.md).
J5.1 completed at `b298f99f`. `RuntimeActionState` owns the one exact
selection, combat, and temporary target-mode graph. Program, plugins, retained
UI, session routing, typed commands/views, and shutdown borrow its children;
the App interaction-state class and private controller fallback are gone.
Its 182 Runtime tests, 3,779 App tests / 3 skips, 8,550 complete Release tests /
5 skips, and exact-binary seven-checkpoint lifecycle/reconnect gate pass.
J5.2 completed at `f5f7b417`. Runtime now owns the use throttle and exact
use/appraisal/pickup transaction identities, typed ordered interaction FIFO,
and post-arrival pickup token while borrowing J4's sole busy/request gate.
App retains picking, movement/transport, lighting, toasts, drag/drop, and
pending-slot presentation. Its 197 Runtime tests, 3,775 App tests / 3 skips,
8,561 complete Release tests / 5 skips, and exact-binary seven-checkpoint
lifecycle/reconnect gate pass. J5.3 completed at `20df9d15`: Runtime owns the
exact attack, target, combat-mode, and spell-cast intent children while App
bars, input adapters, content queries, and transport borrow them. Its 223
Runtime tests, 3,754 App tests / 3 skips, 8,566 complete Release tests / 5
skips, and exact-binary seven-checkpoint lifecycle/reconnect gate pass. J5.4
completed at `aa3f4a60`: Runtime owns the exact local movement controller,
construction seam, autorun latch, typed view, and outbound MTS/jump/AP cadence;
App input and direct commands borrow the same owner. Its 303 Runtime tests,
3,716 App tests / 3 skips, 8,575 complete Release tests / 5 skips,
exact-binary lifecycle/reconnect gate, and canonical nine-stop movement route
pass. J5.5 completed at `7e6033d0`: Runtime owns the per-session engine/cache/
scratch/shadow/collision/body/host/remote/workset graph; App publishes
prepared collision and projects committed snapshots. Its 314 Runtime tests,
3,718 App tests / 3 skips, 8,588 complete Release tests / 5 skips,
exact-binary lifecycle/reconnect gate, and canonical nine-stop collision/
movement route pass. J5.6 completed at `2aee3356`: Runtime owns the canonical
projectile component, exact-key workset, prediction/correction state, and
retail projectile simulation; App resolves immutable DAT shape data and
projects committed frames only. Its 317 Runtime tests, 3,718 App tests / 3
skips, 47 focused Core projectile tests, 8,591 complete Release tests / 5
skips, exact arrow/bolt/spell evidence, lifecycle/reconnect gate, and canonical
nine-stop route pass. J5.7 completed at `cdee7a4b`: Runtime owns accepted
remote-body/vector activation, final simulation retirement, terminal
physics/shadow/workset cleanup, and one combined simulation ownership ledger.
Its Runtime-only movement/use/combat/cast/projectile host proves no-window
parity and terminal convergence after projection failure. Its 323 Runtime
tests, 3,717 App tests / 3 skips, 8,596 complete Release tests / 5 skips,
exact-binary lifecycle/reconnect gate, and canonical nine-stop route pass.
Evidence:
[`../research/2026-07-26-slice-j5-1-canonical-action-state.md`](../research/2026-07-26-slice-j5-1-canonical-action-state.md)
and
[`../research/2026-07-26-slice-j5-2-interaction-transactions.md`](../research/2026-07-26-slice-j5-2-interaction-transactions.md)
and
[`../research/2026-07-26-slice-j5-3-combat-magic-intent.md`](../research/2026-07-26-slice-j5-3-combat-magic-intent.md)
and
[`../research/2026-07-26-slice-j5-4-local-movement-ownership.md`](../research/2026-07-26-slice-j5-4-local-movement-ownership.md)
and
[`../research/2026-07-26-slice-j5-5-physics-remote-ownership.md`](../research/2026-07-26-slice-j5-5-physics-remote-ownership.md)
and
[`../research/2026-07-26-slice-j5-6-projectile-ownership.md`](../research/2026-07-26-slice-j5-6-projectile-ownership.md)
and
[`../research/2026-07-26-slice-j5-7-simulation-ownership-closeout.md`](../research/2026-07-26-slice-j5-7-simulation-ownership-closeout.md).
- Move presentation-free movement interpretation, authoritative/predicted
physics state, selection identity, approach/use transactions, combat intent,
projectiles, and outbound movement cadence.
- Replace input and camera dependencies with typed commands and runtime views.
- Keep camera, mouse look, selection markers/highlight, combat/spell bars,
animated pose composition, sounds, and particles in App.
- Share immutable flat collision content while retaining per-session transition
scratch.
Gate: bit-identical collision/trajectory fixtures, target-facing and
auto-approach traces, outbound packet timing, combat/magic commands, and
graphical feel gates remain unchanged.
### J6 — world, portal, environment, and projection handshake
**J6 complete 2026-07-26.** The instance-scoped Runtime
environment owner landed at `902076c0`. The canonical reveal generation and
typed readiness owner landed at `a6860d55` plus `acb845d8`; App's duplicate
lifecycle owner is deleted and the exact connected lifecycle/reconnect gate
passes with zero reveal invariants. Canonical F751/Position correlation and
exact generation/sequence/cell placement handshake landed at `6a063a27`; the
App transit coordinator and accepted-destination mirror are deleted. Exact
typed host acknowledgement and retryable graphical-resource receipts landed at
`18d17d8b`; Runtime now retains every outstanding host obligation through
failure, re-entrancy, cancellation, supersession, and reset. Its 365 Runtime
tests, 3,716 App tests / 3 skips, 8,642-test / 5-skip complete Release suite,
and exact-binary lifecycle/reconnect route pass. Evidence:
[`../research/2026-07-26-slice-j6-4-host-acknowledgement.md`](../research/2026-07-26-slice-j6-4-host-acknowledgement.md).
Detailed owner inventory, retail ordering, host-acknowledgement contract,
execution ledger, adversarial matrix, and current plan:
[`2026-07-26-modern-runtime-slice-j6.md`](2026-07-26-modern-runtime-slice-j6.md).
- Move authoritative teleport/session cell identity, world clock, weather
state, and reveal-generation state required by gameplay.
- Keep streaming publication, terrain/EnvCell rendering, portal tunnel,
materialization VFX, sky drawing, audio, and UI in App.
- Define the destination-ready/withdrawal acknowledgements between Runtime and
the graphical or headless host without making visual readiness gameplay
authority.
Gate: fresh login, repeated recall/portal, world edge, dungeon, reconnect, and
mid-portal disconnect traces match; the graphical screenshots remain accepted.
### J7 — one `GameRuntime` composition root
J7.1 completed at `96ddd165`; J7.2/J7.3 completed at
`ce41efb9e5938f79a7580476b949d172f52a1e69`. Production now owns one
`GameRuntime`, App borrows its children, and shutdown is one ordered root
transaction. All focused, complete Release, exact-binary lifecycle/reconnect,
and canonical nine-stop automated gates pass. The user accepted radar,
paperdoll, near/distant Use, combat facing, and both recall paths on
2026-07-27; J7 is closed. Detailed evidence:
[`2026-07-26-modern-runtime-slice-j7.md`](2026-07-26-modern-runtime-slice-j7.md)
and
[`../research/2026-07-26-slice-j7-game-runtime-root-cutover.md`](../research/2026-07-26-slice-j7-game-runtime-root-cutover.md).
- Compose all moved lifetime groups into one instance-scoped `GameRuntime`.
- Make `GameWindow`/App own exactly one Runtime instance plus presentation
adapters.
- Delete temporary direct-owner adapters and every superseded App production
path.
- Register all Runtime and App projection owners with structural acquisition
leases and the exact teardown transaction.
Gate: graphical connected lifecycle/resource routes pass with no mirrored
state, no extra update latency, and no performance regression.
Exact J7.2/J7.3 rollback:
```text
git revert ce41efb9e5938f79a7580476b949d172f52a1e69
```
### J8 — no-window integration and Slice J closeout
**Detailed execution plan:**
[`2026-07-26-modern-runtime-slice-j8.md`](2026-07-26-modern-runtime-slice-j8.md).
Completed at `a9a822f2` after J7's 2026-07-27 physical-display acceptance.
- Build a transport/content test host that constructs Runtime directly.
- Connect, enter world, receive chat/inventory/world updates, move, use,
fight/cast through deterministic fixtures, portal, log out, reconnect, and
tear down without loading App/Silk/OpenAL/UI.
- Add cancellation, failure injection, same-process multiple-instance
isolation, credential-safe diagnostics, and zero-presentation-allocation
gates.
- Update architecture, milestone, roadmap, issue/divergence ledgers, and
durable memory.
Final gate: Release build/full tests, connected graphical route, no-window
integration, resource teardown, packet/order/timing parity, and the user visual
matrix all pass. Slice K may then build the Linux multi-session host without
another gameplay extraction.
J8 passes 395 Runtime tests, 3,731 App tests / 3 skips, 8,696 complete-solution
tests / 5 skips, the exact-binary lifecycle/reconnect route, and the canonical
nine-stop route. Graphical and direct hosts share one `GameRuntime`, one
retryable canonical-generation reset transaction, and the same gameplay-owner
graph. Exact rollback:
```text
git revert a9a822f206cc6021bc099988144488f3bd7c397b
```
## 5. Commit order
1. `arch(runtime): establish presentation-independent boundary`
2. `feat(runtime): define borrowed views commands and ordered events`
3. `refactor(runtime): move session lifetime and ordered transport`
4. `refactor(runtime): move canonical entity and property ownership`
5. `refactor(runtime): move gameplay state services`
6. `refactor(runtime): move movement physics interaction and combat`
7. `refactor(runtime): separate world authority from presentation`
8. `refactor(runtime): compose one graphical game runtime`
9. `test(runtime): close no-window parity and teardown`
Each commit is independently buildable and retains an exact rollback command
in this plan before its connected gate.
J5.7 exact rollback:
```text
git revert cdee7a4b49addb5e1500753f6a885f7c899bd0f0
```

View file

@ -1,203 +0,0 @@
# Modern runtime Slice J2 — session lifetime and ordered transport
**Status:** COMPLETE 2026-07-25
**Parent:** `2026-07-25-modern-runtime-slice-j.md`, J2
**Production base:** `854d9e9cd13092bd5aaa3cf025d73eeb4600e9f8`
**Production commit:** `75930787741db40a83eab8663e4464dce8d687ba`
**J1 documentation:** `f84624b1e9b83f928debdbe65a5a1576b999ee34`
## 1. Objective
Move the one canonical live-session lifetime, transport generation, ordered
inbound subscription owner, and route activation/teardown transaction from
`AcDream.App` into `AcDream.Runtime`.
The move is structural. It must preserve the exact current connect, character
selection, EnterWorld, packet dispatch, command activation, tick, reconnect,
logout, and retryable cleanup order. App remains the graphical host and
supplies credentials, retained-UI/domain callbacks, the graphical command
projection, and presentation reset acknowledgements.
## 2. Fixed ownership after J2
`AcDream.Runtime` owns:
- the exact `WorldSession` generation and its connect/enter/tick/dispose
transaction;
- generation validation and live-session lifecycle state;
- the serial route lifecycle: attach inbound before Connect, activate outbound
only after EnterWorld, make commands inert before detaching inbound;
- the retryable teardown suffix and exact acknowledgement stages;
- the complete ordered inbound subscription set and all Core/Core.Net event
wiring;
- connection options with no dependency on App `RuntimeOptions`;
- the production endpoint/session operations and deterministic operations
seam used by no-window tests.
`AcDream.App` owns:
- conversion from `RuntimeOptions` into immutable Runtime connection options;
- graphical reset, identity, layout, toolbar, settings, dialog, and
presentation callbacks;
- the `ICommandBus` projection needed by the current UI until gameplay command
groups move in J4/J5;
- graphical source adapters for existing App interfaces while those consumer
groups await their own J3J6 migrations.
The App adapters borrow the Runtime session and command route. They do not
dispose the transport, track another generation, retain a second active route,
or mirror in-world state.
## 3. Type and file migration
### J2.1 — Runtime lifetime contracts
Add under `src/AcDream.Runtime/Session/`:
- `LiveSessionConnectOptions`
- `RuntimeLiveSessionController`
- `RuntimeLiveSessionHost`
- `RuntimeLiveSessionBinding`
- `IRuntimeLiveSessionLifecycleHost`
- `IRuntimeLiveSessionOperations`
- `IRuntimeLiveSessionEventRoute`
- `IRuntimeLiveSessionCommandRoute`
- the Runtime-only lifecycle host and retryable route rollback owner
The host implements the J1 `IRuntimeSessionCommands` surface directly. Start,
Reconnect, and Stop are generation-gated at that owner; the J1 App command
adapter no longer translates a second session result or invents teardown
stages.
`RuntimeLiveSessionController` exposes only borrowed state:
`CurrentSession`, `IsInWorld`, `Generation`, and `Tick`. It carries no UI
command bus and implements no App interface.
### J2.2 — Ordered inbound route move
Move the existing `LiveSessionEventRouter` and
`LiveSessionSubscriptionSet` into Runtime without changing subscription order:
1. object/property table wiring;
2. combat-state wiring;
3. entity and environment messages;
4. inventory/item state;
5. character/spell state;
6. social/chat state.
The existing accepting gate, reverse-order retryable unsubscribe, construction
rollback, and reentrant-cleanup rejection remain line-for-line. The route
bindings remain delegates over current owners for J2; their owner groups move
in J3/J4.
### J2.3 — Graphical host adapter
Replace the App lifetime implementation with focused adapters:
- a session source bridge for existing App `IsInWorld`, `CurrentSession`,
frame-tick, and retained-UI seams;
- a graphical route factory that constructs the moved Runtime event router and
the current App command projection;
- a single active-command projection whose retained UI reference becomes
inert before route detachment;
- graphical lifecycle callbacks for reset, selection, EnterWorld, connecting,
and connected presentation.
Delete the superseded App controller, lifecycle-host logic, subscription
owner, and route rollback implementation in the same commit. Do not keep
delegating compatibility copies of lifetime state.
### J2.4 — Test ownership
Move controller, host, lifecycle, event-router, and subscription tests into
`AcDream.Runtime.Tests`. Preserve every existing call-order, duplicate start,
reentrant reconnect/stop/dispose, rollback-failure, stale generation, malformed
packet, and retryable teardown case.
Keep graphical command-projection and App bridge tests in
`AcDream.App.Tests`. Add assertions that:
- the bridge has no generation or in-world backing fields;
- the command projection has at most one borrowed active route;
- stale command references are inert after replacement;
- press-time graphical input still reaches the same owner synchronously.
Add a Runtime-only construction test that creates the production lifetime
graph with deterministic transport operations, enters and exits a session,
and verifies that App, UI, Silk.NET, OpenAL, Arch, and ImGui assemblies are
not loaded.
## 4. Exact behavioral order
Start:
1. advance generation;
2. drain any retired teardown suffix;
3. reset the supplied host when required;
4. validate enabled state and credentials;
5. resolve endpoint and create the one `WorldSession`;
6. construct and attach all inbound routes;
7. publish connecting state;
8. Connect and receive the character list;
9. publish connected state;
10. select the first available character and publish identity;
11. EnterWorld;
12. activate the outbound command route;
13. mark in-world and publish entered-world presentation.
Stop/replacement:
1. advance generation and reject further old-generation commands;
2. make the active graphical command route inert;
3. detach inbound subscriptions in reverse order;
4. dispose `WorldSession`, preserving its graceful F653/disconnect behavior;
5. detach the exact host/session pair;
6. reset domain and presentation state;
7. acknowledge only the completed stages.
If a stage throws, the exact remaining suffix is retained. A retry never
replays a completed stage.
## 5. Acceptance
Focused:
- Runtime lifetime and event-route suites all pass after moving from App;
- App graphical bridge and command projection suites pass;
- J1 normalized traces are identical before and after the move;
- source/dependency guards find no Runtime presentation/backend dependency;
- Runtime-only session construction and teardown load no forbidden assembly.
Complete:
- `dotnet build AcDream.slnx -c Release`;
- full Release solution tests;
- connected capped login/six-checkpoint/logout plus fresh uncapped reconnect
on the exact binary;
- both processes exit gracefully;
- no packet/order, reveal, route, teardown, or resource-convergence failure.
J2 changes no pixels, input feel, or retail algorithm. A separate user visual
pause is therefore not required unless the connected gate exposes a
presentation symptom.
## 6. Result and rollback
All focused and complete gates passed: 79 Runtime tests, 3,776 App tests with
three skips, 8,428 complete Release tests with five skips, and the exact-binary
seven-checkpoint connected lifecycle/reconnect route. Both processes exited
gracefully; every checkpoint had zero render-shadow mismatch and zero pending
render delta. Evidence:
[`../research/2026-07-25-slice-j2-session-lifetime-closeout.md`](../research/2026-07-25-slice-j2-session-lifetime-closeout.md).
Exact J2 rollback:
```text
git revert 75930787741db40a83eab8663e4464dce8d687ba
```
J1 remains independently reversible:
```text
git revert 854d9e9cd13092bd5aaa3cf025d73eeb4600e9f8
```

View file

@ -1,270 +0,0 @@
# Modern runtime J3.4 — canonical object-table ownership
**Status:** COMPLETE
**Parent:** `2026-07-25-modern-runtime-slice-j3.md`
**Required production base:** `e937cc36df39cf6ea1eaba2f9c0243d1929da702`
**Prior closeout:** `docs/research/2026-07-25-slice-j3-3-exact-projection-store.md`
**Production commit:** `5ef8b5371d8990f0380acd939e71cd711289d429`
**Closeout:** `docs/research/2026-07-26-slice-j3-4-canonical-object-lifetime.md`
## Objective
Make one presentation-independent Runtime lifetime-group root own both:
- the canonical `RuntimeEntityDirectory`; and
- the existing canonical `ClientObjectTable`.
App, retained UI, interaction controllers, and the graphical projection host
must borrow those exact same instances. There must be no copied inventory
model, second object table, asynchronous delivery seam, extra input frame, or
change to retail event order.
This slice moves ownership and transaction boundaries, not the
`ClientObjectTable` implementation out of `AcDream.Core`. The type is already
presentation-free and has extensive Core conformance coverage.
## Current ownership inventory
At the required base:
- `LiveEntityRuntime` constructs its own `RuntimeEntityDirectory`.
- `GameWindow` constructs a public readonly `ClientObjectTable Objects`.
- App composition records pass that table to retained UI, selection,
interaction, magic, combat, paperdoll, appraisal, cooldown, and diagnostics.
- Runtime `LiveSessionEventRouter` already applies property, stack, inventory,
player-description, container, and appraisal traffic directly to the
borrowed table through `ObjectTableWiring`.
- App `LiveEntityHydrationController` applies CreateObject to the table only
after canonical Runtime create acceptance and revalidates
`CreateIntegrationVersion`.
- App `LiveEntityDeletionController` applies logical DeleteObject before the
exact App projection teardown suffix.
- App `LiveSessionResetManifest` clears the object table before clearing live
graphical projections.
- Optimistic moves, confirmations, rollback, stack updates, container
replacement, and every table event are synchronous on the current thread.
Retail behavior and citations already live in:
- `src/AcDream.Core/Items/ClientObjectTable.cs`;
- `src/AcDream.Core.Net/ObjectTableWiring.cs`;
- `docs/research/2026-07-13-retail-give-item-pseudocode.md`;
- the named-retail symbols cited beside `ACCObjectMaint::CreateObject`,
`ACCObjectMaint::DeleteObject`, and
`ACCWeenieObject::ServerSaysMoveItem`.
J3.4 does not reinterpret those algorithms.
## Fixed target
Add a presentation-free Runtime owner equivalent to:
```csharp
public sealed class RuntimeEntityObjectLifetime
{
public RuntimeEntityDirectory Entities { get; }
public ClientObjectTable Objects { get; }
}
```
The concrete name may change, but these invariants do not:
1. The owner constructs each canonical instance exactly once.
2. `LiveEntityRuntime` receives `Entities`; it may not construct a directory.
3. Runtime session routing receives `Objects` from the same owner.
4. App projection, UI, and interaction composition receive borrowed references
only.
5. Accepted CreateObject/DeleteObject object-table mutation is implemented by
the Runtime group and revalidates the exact canonical record/version.
6. Reset preserves the current order: object-table clear before graphical live
projection teardown, with completed stages never replayed.
7. Studio/sample-data object tables remain independent fixture owners and are
not part of the live session.
## Execution
### 1. Pin parity traces and construction identity
- Capture normalized traces for:
- initial player description and equipment manifest;
- item CreateObject add/update;
- same-incarnation refresh;
- new-generation replacement;
- property Int/Int64/String/Float/DataID updates;
- stack-size update;
- optimistic move, authoritative confirm, and rejection rollback;
- wield/unwield;
- ViewContents replacement and close;
- logical delete, GUID reuse, and session clear.
- Add a construction-identity test proving the router, App projection host,
retained UI source, and interaction source all receive one table instance.
- Preserve current synchronous event order in the golden trace.
### 2. Introduce the Runtime lifetime-group root
- Add the owner under `src/AcDream.Runtime/Entities/`.
- Construct `RuntimeEntityDirectory` and `ClientObjectTable` there.
- Expose borrowed properties only; no collection mirror or event repeater.
- Add Runtime-only tests that instantiate it without loading App, UI, Silk,
OpenAL, Arch, or ImGui assemblies.
- Keep mutable state instance-scoped so two roots can hold conflicting GUIDs
and inventory contents without interference.
### 3. Inject canonical identity into the App projection host
- Change `LiveEntityRuntime` to require the exact
`RuntimeEntityDirectory` instance from the group.
- Remove its internal `new RuntimeEntityDirectory(...)`.
- Preserve the existing local-ID start override for focused wraparound tests
through an explicit fixture/group construction seam, not a second production
allocator.
- Assert that App cannot replace the directory after projection construction.
- Preserve J3.3 `RuntimeEntityKey` identity and teardown behavior unchanged.
### 4. Replace the `GameWindow` object-table owner
- Construct the Runtime group before retained UI and live presentation
composition.
- Delete `GameWindow`'s public readonly `ClientObjectTable Objects` field.
- Pass `runtimeGroup.Objects` through the existing typed composition records.
- Keep App controllers as direct synchronous borrowers; do not wrap every read
in a copied DTO or event bus.
- Add source guards rejecting a production App `new ClientObjectTable()` and a
second Runtime entity directory. Exempt Studio/sample-data and tests.
### 5. Move accepted object mutation to the Runtime group
- Move the generation/version-sensitive
`ObjectTableWiring.ApplyEntitySpawn/Delete` transaction boundary out of App.
- Runtime must apply a CreateObject only after the canonical directory accepts
that exact incarnation and while its create-integration version remains
current.
- New-generation replacement must remove the old object generation and publish
the same add/update/removal sequence as today.
- Logical delete must mutate the table before App graphical teardown, exactly
as the current `beforeTeardown` callback does.
- Pickup/parent leave-world must not delete the object-table entry.
- Dormant liveness pruning must retain the existing object-table semantics;
only an authoritative logical delete removes the retained object generation.
- Keep all callbacks synchronous and revalidate after every callback that can
replace, delete, or clear the incarnation.
### 6. Route session and reset ownership
- Bind J2 `LiveSessionEventRouter` directly from the Runtime group rather than
an App-owned table field.
- Keep the current pre-Connect subscription order.
- Keep local-player property projection to `LocalPlayerState` and the same
table instance until J4 moves the coherent player/gameplay state group.
- Replace the App reset field with a Runtime-group object-table reset
acknowledgement at the same manifest position.
- Failure injection must retain the unfinished reset suffix and never clear a
replacement session's table.
### 7. Delete superseded ownership seams
- Remove public ownership comments and aliases that describe the table as a
GameWindow state owner.
- Remove any App construction helper whose only purpose was to allocate the
live table.
- Keep direct table parameters on UI/controllers for now: they are explicit
borrowed dependencies and J4 will move higher-level inventory transactions.
- Update architecture, plans, durable memory, and exact rollback in the same
closeout commit.
## Adversarial cases
- Reentrant `ObjectAdded` deletes or replaces the same GUID.
- Reentrant `ObjectMoved` starts another optimistic move.
- Delete arrives during CreateObject callbacks.
- Same GUID with a new `INSTANCE_TS` while old projection teardown is pending.
- Unknown-object authoritative move publishes the same nullable item event.
- Equal, stale, and wrapped object/physics generations.
- Multiple in-flight optimistic moves followed by confirm/reject in either
order.
- Container replacement while item events mutate the same container.
- Reset during object-table event dispatch.
- Failed graphical teardown after canonical object removal.
- Two Runtime roots use identical GUIDs with different contents.
- No-window root executes the full entity/object lifecycle without loading App
or backend assemblies.
## Mandatory gates
Run in this order:
1. Runtime entity/object lifetime tests.
2. Core `ClientObjectTable*` and `ObjectTableWiring*` tests.
3. Focused App hydration, deletion, reset, inventory, interaction, paperdoll,
magic, cooldown, and runtime-adapter tests.
4. All Runtime tests.
5. All App tests.
6. `dotnet build AcDream.slnx -c Release`.
7. `dotnet test AcDream.slnx -c Release --no-build`.
8. Exact-binary connected lifecycle/reconnect gate.
9. At every connected checkpoint: zero render-shadow mismatches, zero pending
deltas, graceful process exit, and object/projection teardown convergence.
A user visual pause is not required unless an automated or connected gate
exposes an inventory, paperdoll, selection, or interaction symptom.
## Acceptance
- One Runtime lifetime-group root owns the exact entity directory and object
table.
- `GameWindow` owns neither instance directly.
- App and Runtime routing borrow the same object table.
- Runtime remains the only GUID/incarnation/local-ID authority.
- Create/delete/property/container ordering matches the pinned trace.
- Optimistic move/rollback and press-time UI remain synchronous.
- No App/UI/backend dependency enters Runtime.
- No mirror, queue, frame delay, or steady-state traversal allocation is added.
- Runtime-only entity/object lifecycle and multiple-instance isolation pass.
- Complete Release and exact connected gates pass on one committed binary.
## Rollback
The exact J3.4 rollback is:
```text
git revert 5ef8b5371d8990f0380acd939e71cd711289d429
```
J3.3 remains independently reversible; do not use its rollback for a J3.4
failure.
## Result
`RuntimeEntityObjectLifetime` now constructs and owns the one
`RuntimeEntityDirectory` and one live `ClientObjectTable`. `GameWindow`,
session routing, retained UI, interaction, and the graphical projection host
borrow those exact instances.
Accepted CreateObject mutation revalidates the exact canonical record and
`CreateIntegrationVersion` before, during, and after synchronous object-table
callbacks. Authoritative DeleteObject acceptance uses an exact, single-use
Runtime token: the active identity is retired first and retained-object
removal then commits before App resource teardown. Dormant/offscreen pruning
does not remove retained object information.
Final gates:
- Runtime: 118 passed;
- App: 3,762 passed / 3 skipped;
- complete Release solution: 8,453 passed / 5 skipped;
- exact-binary connected lifecycle/reconnect:
`logs/connected-world-gate-20260726-055713/report.json`;
- two sessions, seven checkpoints, graceful zero-code exits, zero failures,
zero render-shadow mismatches, zero pending deltas, and zero pending live
teardown/publication/retirement work.
## Final J3 goal
J3.4 is not the end of J3. J3.5 must publish one ordered per-session
entity/object delta stream and make graphical/direct hosts consume the same
committed facts without a second queue. J3.6 then failure-injects and proves
the complete no-window entity/object lifecycle, zero leaked records,
tombstones, projections, container entries, queued deltas, or subscriptions,
plus Release and connected parity. That J3.6 proof is the clean boundary before
J4 moves inventory transactions, vitals, enchantments, magic, and other
gameplay-state services.

View file

@ -1,180 +0,0 @@
# Modern runtime J3.3 — exact-key App projection store
**Status:** COMPLETE 2026-07-25
**Parent:** `2026-07-25-modern-runtime-slice-j3.md`
**Required base:** `f46ddb5cdb1e145752bea49aeb1d62bfe71284d3`
**Production commits:** `420e5eea70fd2c29cf9c8614e6298a1f84d64045`,
then exact-spatial correction
`e937cc36df39cf6ea1eaba2f9c0243d1929da702`
**Closeout:** `docs/research/2026-07-25-slice-j3-3-exact-projection-store.md`
## Objective
Make every materialized App-side live-object owner use the exact
`RuntimeEntityKey` (`Runtime local ID + INSTANCE_TS`) issued by
`RuntimeEntityDirectory`.
Runtime remains the only server-GUID authority. App may carry GUID as immutable
metadata for diagnostics and outbound commands, but no App dictionary may use
GUID to decide which incarnation is current.
No asynchronous projection queue, extra input frame, renderer behavior change,
or local-ID allocation-order change is permitted.
## Fixed model
There are three distinct states:
1. **Canonical-only registration** — Runtime accepted an incarnation, but no
graphical host has claimed a local ID. No materialized projection exists.
2. **Materialized projection** — Runtime has claimed the local ID and App owns
exactly one sidecar under the resulting `RuntimeEntityKey`.
3. **Retryable teardown** — Runtime retains the exact canonical tombstone while
App retains only the unfinished teardown suffix for that exact key. A
registration that failed before local-ID claim has no graphical tombstone.
A small synchronous construction context may carry a canonical record between
registration and materialization. It is not a retained App identity map.
## Execution
### 1. Introduce `LiveEntityProjectionStore`
- Store materialized sidecars in
`Dictionary<RuntimeEntityKey, LiveEntityRecord>`.
- Resolve GUID only by asking `RuntimeEntityDirectory` for the current
canonical record, reading its exact key, then querying the store.
- Reject key reuse, local-ID mismatch, incarnation mismatch, and stale
canonical records.
- Preserve insertion order and allocation-free borrowed enumeration.
- Retain teardown progress under the exact key; use canonical-reference storage
only for the exceptional pre-materialization registration rollback which has
no key.
### 2. Move sidecar creation to local-ID claim
- `RegisterLiveEntity` returns canonical registration facts without allocating
a graphical sidecar.
- `MaterializeLiveEntity` claims the Runtime local ID at the same point as
today, creates the exact-key sidecar, then calls the projection factory.
- Same-incarnation refresh before materialization mutates only Runtime state.
- Same-incarnation refresh after materialization resolves the existing sidecar
through its exact key.
- Factory, resource-registration, and rollback failures retain exactly the
currently unfinished ownership suffix.
### 3. Re-key every presentation workset
Convert the following from GUID/local-ID-only maps to exact-key maps:
- materialized and visible `WorldEntity` projections;
- animation and spatial-animation owners;
- remote-motion and spatial-remote-motion owners;
- projectile and spatial-projectile owners;
- ordinary root object workset;
- effect profile, visibility publication, hydration, and teardown lookups.
Local-ID-only iteration views validate the incarnation through the projection
store before yielding. No stale entry can match a replacement that reused the
same numeric local ID after wraparound.
### 4. Cut consumers over to borrowed queries
- Replace direct GUID dictionary access in selection, radar, combat targeting,
local-player animation/mode, teleport, physics update, diagnostics, and
composition with typed `TryGet...`, exact-key, or borrowed-enumeration
methods.
- Keep synchronous press-time reads; do not publish a copied frame snapshot.
- Preserve server GUID in yielded metadata where outbound commands need it.
- Remove `MaterializedWorldEntities`, `WorldEntities`, and component
dictionaries once all consumers use the focused queries.
### 5. Delete the J3.2 compatibility view
- Delete `ActiveRecordView`.
- Delete `_activeRecords` and every App GUID-keyed live-object/component map.
- Remove the detached `LiveEntityRecord(EntitySpawn)` fixture constructor;
component tests construct exact Runtime identities through a shared fixture.
- Add source guards rejecting:
- App `Dictionary<uint, LiveEntityRecord>`;
- App live-object/component maps keyed by server GUID;
- an App reverse GUID/local-ID map;
- a projection store key other than `RuntimeEntityKey`.
### 6. Failure and parity gate
Exercise:
- create without materialization;
- same-incarnation refresh before and after materialization;
- factory throw before sidecar publication;
- resource register failure with successful and failed rollback;
- loaded/pending rebucket and visibility reentrancy;
- delete/recreate with the same GUID;
- local-ID wraparound and stale-key rejection;
- parent/pickup/Hidden transitions;
- session clear during every projection callback;
- retryable teardown with a live replacement.
Run:
- focused Runtime and App identity/projection tests;
- all App tests;
- Release solution build;
- complete Release tests;
- exact-binary connected lifecycle/reconnect;
- render-scene identity/digest referee at every checkpoint.
## Acceptance
- Runtime owns the only GUID/incarnation/local-ID authority.
- Every materialized App owner is keyed by exact `RuntimeEntityKey`.
- App retains no GUID identity dictionary or reverse local-ID authority.
- A stale key cannot query, update, withdraw, or tear down a replacement.
- Local-ID allocation order is unchanged.
- No new allocation appears in steady-state live-object traversal.
- No callback is delayed to another frame.
- All automated and connected gates pass on the exact commit.
## Completion result
All acceptance conditions pass on final exact binary `e937cc36`.
- `LiveEntityProjectionStore` is the only materialized App sidecar store and is
keyed by exact `RuntimeEntityKey`.
- The temporary `ActiveRecordView`, `_activeRecords`, detached App record
constructor, and broad materialized/visible `WorldEntity` collection
surfaces are deleted. Borrowed exact-key record views remain for hot
traversal and tests.
- Hydration, animation, effects, lights, equipped children, remote teleport,
render journal, liveness, movement observations, teardown, spatial
residence, and visibility transitions carry the exact key.
- `GpuWorldState` retains the exact key through loaded/pending rebucketing,
landblock retirement, origin recentering, quiescence, and reentrant
visibility delivery. DAT-static entities remain outside live Runtime
identity.
- Runtime remains the only current-GUID/incarnation/local-ID authority. App
resolves GUID through Runtime and never decides which incarnation is current.
- Synchronous callbacks, allocation order, render ordering, and frame timing
are unchanged.
Evidence:
- 104 focused App ownership/streaming tests passed.
- 3,761 App tests passed / 3 skipped.
- 8,444 complete Release tests passed / 5 skipped.
- Release solution build passed.
- `logs/connected-world-gate-20260725-221008/report.json` passed on exact commit
`e937cc36`: two sessions, seven checkpoints, two graceful zero-code exits,
zero failures, zero render-shadow mismatches, and zero pending deltas.
Exact rollback, newest commit first:
```text
git revert e937cc36df39cf6ea1eaba2f9c0243d1929da702
git revert 420e5eea70fd2c29cf9c8614e6298a1f84d64045
```
J3.4 is the next clean slice. It moves canonical `ClientObjectTable` ownership
under the Runtime entity/object lifetime group without moving retained UI,
icons, paperdolls, grids, cooldown drawing, or interaction presentation.

View file

@ -1,323 +0,0 @@
# Modern runtime Slice J3 — canonical identity, properties, and object table
**Status:** COMPLETE 2026-07-26
**Parent:** `2026-07-25-modern-runtime-slice-j.md`, J3
**Production base:** `9496c01b`
**J2 production commit:** `75930787741db40a83eab8663e4464dce8d687ba`
## 0. Current checkpoint
| Slice | Status | Commit / evidence |
|---|---|---|
| J3.0 ownership inventory | complete | `b0fecc5f65ed26042c61a1651fb134aeb432c141`; `docs/research/2026-07-25-slice-j3-entity-ownership-inventory.md` |
| J3.1 accepted-wire owners | complete | `f7442d13e9ae7d5b077ffecb1f8a10899ad46edd`; 110 Runtime tests and complete Release parity remain green after J3.2 |
| J3.2 canonical Runtime entity directory | complete | `f46ddb5cdb1e145752bea49aeb1d62bfe71284d3`; 8,441 Release tests / 5 skips and `logs/connected-world-gate-20260725-200749/report.json` pass |
| J3.3 App projection store | complete | `420e5eea70fd2c29cf9c8614e6298a1f84d64045` + `e937cc36df39cf6ea1eaba2f9c0243d1929da702`; 8,444 Release tests / 5 skips and `logs/connected-world-gate-20260725-221008/report.json` pass |
| J3.4 canonical object-table ownership | complete | `5ef8b5371d8990f0380acd939e71cd711289d429`; 118 Runtime tests, 3,762 App tests / 3 skips, 8,453 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-055713/report.json` pass |
| J3.5 ordered deltas and borrowed views | complete | `ce3ac310d92722ffb637e81cb1957458874dd220`; 134 Runtime tests, 3,765 App tests / 3 skips, 8,472 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-064324/report.json` pass |
| J3.6 adversarial hardening and closeout | complete | `119b7c115107245546180b2f5cb3cb44c7d5476a`; 146 Runtime tests, 3,765 App tests / 3 skips, 8,484 complete Release tests / 5 skips, exact lifecycle/reconnect gate, and canonical nine-stop route pass |
J3.5 passes the Release solution build, 134 Runtime tests, 3,765 App tests with
three skips, and 8,472 complete tests with five expected skips. Its exact-
binary seven-checkpoint connected lifecycle/reconnect route also passes with
graceful exits, zero render-shadow mismatches, and zero pending deltas. Runtime
now issues canonical identity before graphical hydration, publishes entity and
inventory commits through one per-generation sequence, and exposes the same
direct borrowed views to graphical and no-window hosts. App no longer
reconstructs those views or events. No frame queue, backend dependency, or
gameplay behavior change was added. The exact rollback for J3.5 is:
```text
git revert ce3ac310d92722ffb637e81cb1957458874dd220
```
J3.6 closes the group under callback failure, re-entrancy, retained teardown,
reset, GUID reuse, and direct disposal. Its complete owner ledger converges to
zero and both exact-binary connected routes pass. Evidence:
[`../research/2026-07-26-slice-j3-6-lifetime-closeout.md`](../research/2026-07-26-slice-j3-6-lifetime-closeout.md).
The exact rollback for J3.6 is:
```text
git revert 119b7c115107245546180b2f5cb3cb44c7d5476a
```
## 1. Objective
Move the one canonical server-object identity/incarnation owner, accepted
CreateObject/property/timestamp state, parent relation state, and
`ClientObjectTable` lifetime into `AcDream.Runtime`.
App becomes a projection host. Its world entity, animation, effect, light,
selection, paperdoll, spatial bucket, and hydration state is keyed by a
Runtime-issued local identity plus exact incarnation. App must not retain a
second server-GUID map or decide whether a packet belongs to the current
incarnation.
This is a structural move. Existing named-retail CreateObject, timestamp,
parenting, pickup, Hidden, delete, and generation-replacement behavior remains
unchanged. No renderer, particle, streaming, GL, or retained-UI type may enter
Runtime.
## 2. Scope reconciliation
The umbrella J3 text overlaps later lifetime groups. This detailed plan is the
authoritative split:
- J3 moves raw accepted entity identity, immutable spawn/property snapshots,
timestamp gates, parent relations, and the canonical client object/container
table.
- J4 moves higher-level inventory transactions, vitals, enchantments,
spell/component state, cooldown services, chat, and their ViewModel-facing
revisions.
- J5 moves movement/physics/interaction/combat authority.
J3 may retain existing Core physics references already stored on the entity
record while separating presentation, but it does not redesign or relocate
their behavior. That coherent move remains J5.
## 3. Fixed ownership after J3
`AcDream.Runtime` owns:
- server GUID to current incarnation;
- exact Runtime local identity allocation and reverse lookup;
- accepted `INSTANCE_TS` and every retail packet-channel timestamp;
- the latest accepted immutable `WorldSession.EntitySpawn`;
- logical create, same-incarnation refresh, pickup, parent, Hidden/state,
delete, generation replacement, tombstone, and session-clear state;
- unresolved/staged/committed parent relations;
- `ClientObjectTable`, containers, placement indices, pending authoritative
property state, and generation-aware removal;
- ordered immutable entity/object deltas;
- retryable logical teardown state and the acknowledgement from an attached
projection host.
`AcDream.App` owns:
- `WorldEntity` and render-scene projection;
- loaded/pending spatial buckets and visibility derived from streaming;
- animation, remote-motion presentation, projectile presentation, effects,
lights, selection geometry, radar presentation, and private paperdolls;
- projection hydration/recovery transaction state;
- render/resource registration and its exact retryable teardown suffix.
App projection keys are:
```text
RuntimeLocalEntityId + INSTANCE_TS
```
Server GUID may be carried as immutable metadata for diagnostics and outbound
commands, but it is not an App lookup authority.
## 4. Runtime contracts
Introduce or finish contracts equivalent to:
```csharp
public readonly record struct RuntimeEntityKey(
uint LocalEntityId,
ushort Incarnation);
public readonly record struct RuntimeEntityDelta(
ulong Sequence,
RuntimeGenerationToken SessionGeneration,
RuntimeEntityKey Entity,
RuntimeEntityDeltaKind Kind,
RuntimeEntitySnapshot Snapshot);
public interface IRuntimeEntityProjectionHost
{
RuntimeProjectionAcknowledgement Register(RuntimeEntitySnapshot entity);
RuntimeProjectionAcknowledgement Update(RuntimeEntityDelta delta);
RuntimeProjectionAcknowledgement Withdraw(RuntimeEntityKey entity);
RuntimeProjectionAcknowledgement TearDown(RuntimeEntityKey entity);
}
```
The concrete names may follow existing project vocabulary, but these
invariants are mandatory:
- Runtime issues identity before App creates a projection.
- Every App callback carries the exact local identity and incarnation.
- Callback acknowledgement is exact and retryable.
- Completed teardown work never replays.
- App cannot query or mutate Runtime by a stale local identity/incarnation.
- Runtime emits deltas only after canonical state commits.
- Callback reentrancy may supersede the outer operation; the outer operation
must revalidate generation/incarnation/operation version before continuing.
## 5. Execution slices
### J3.0 — oracle and ownership inventory
- Pin the current field/method ownership table for `LiveEntityRecord`,
`LiveEntityRuntime`, `InboundPhysicsStateController`,
`ParentAttachmentState`, `ClientObjectTable`, `GpuWorldState`, hydration,
and teardown owners.
- Reuse the existing retail pseudocode and citations; no behavior is being
invented. Add research only where a current branch lacks a cited oracle.
- Capture normalized existing traces for create, same-generation refresh,
parent/unparent, pickup, Hidden, delete/recreate, GUID reuse, container
placement, and session clear.
- Record baseline render-scene identity/digest at deterministic fixtures.
### J3.1 — move narrow accepted-wire owners
- Move `InboundPhysicsStateController`, `ParentAttachmentState`, their result
types, and their complete tests into Runtime.
- Preserve timestamp cursor/order behavior and parent candidate ordering
line-for-line.
- App's existing `LiveEntityRuntime` temporarily composes those Runtime owners;
no second state is introduced.
- Add Runtime dependency/load guards around the moved types.
Gate: all timestamp, malformed, wraparound, parent-generation, and packet-order
tests pass in Runtime; App behavior and complete suite remain unchanged.
### J3.2 — canonical Runtime entity directory
- Extract current GUID/incarnation maps, accepted snapshot state, session
lifetime version, operation versions, tombstones, delete/generation
replacement, and local-ID allocation into one Runtime owner.
- Split `LiveEntityRecord` into canonical Runtime state and an App projection
sidecar. Canonical fields never reference App interfaces or `WorldEntity`.
- Preserve current local-ID allocation order during graphical hydration for
parity; Runtime owns the allocator and claim, while a no-window host may
claim immediately.
- Retain exact reentrant supersession and retryable tombstone behavior.
- Keep physics/movement fields structurally in place where required for J5,
but remove every renderer/effect/hydration reference from the canonical
record.
Gate: the complete existing `LiveEntityRuntimeTests` identity/timestamp/
teardown subset moves to Runtime and produces the same normalized trace.
### J3.3 — App projection store and cutover
- Add one App projection store keyed only by Runtime local identity plus
incarnation.
- Move `WorldEntity`, spatial visibility, hydration/recovery flags, animation,
remote-motion presentation, projectile presentation, effect profiles,
resource flags, and active presentation worksets into that store.
- Convert `GpuWorldState`, hydration, equipped children, render journal,
animation/effect/light owners, selection/radar queries, and teardown to the
exact projection key.
- Replace App's mixed `LiveEntityRuntime` facade with focused Runtime directory
and App projection-store dependencies; delete its GUID maps and mixed record.
- Preserve current register-once, rebucket-only, withdraw-without-destroy,
Hidden, and retryable logical teardown order.
Gate: projection identity/digest and all existing hydration, rebucket,
visibility, effect, animation, projectile, selection, and teardown tests match.
Source guards reject any App server-GUID authority map.
### J3.4 — canonical object-table ownership
**Detailed execution plan:**
[`2026-07-25-modern-runtime-slice-j3-object-table.md`](2026-07-25-modern-runtime-slice-j3-object-table.md).
- Add a Runtime entity/object lifetime-group root that owns the canonical
entity directory and the existing Core `ClientObjectTable`.
- Construct it before either graphical UI or live presentation; both borrow
the same instances.
- Remove the public `ClientObjectTable` owner field from `GameWindow`.
- Route J2 inbound object/property/container updates directly to the Runtime
group.
- Keep UI controllers, item icons, paperdoll, grids, cooldown drawing, and
interaction presentation as App borrowers.
- Preserve synchronous press-time reads and current event ordering; add no
queue or copied inventory model.
Gate: object add/update/remove, placement, container replacement, optimistic
move rollback, stack response, GUID reuse, and session-clear traces match.
### J3.5 — ordered deltas and borrowed views
**Detailed execution plan:**
[`2026-07-26-modern-runtime-slice-j3-delta-stream.md`](2026-07-26-modern-runtime-slice-j3-delta-stream.md).
- Publish one monotonic per-session Runtime entity/object delta stream.
- Make J1 `IGameRuntimeView.Entities` and inventory snapshots read the Runtime
owners directly rather than App adapters.
- Feed App projection mutation from the same committed deltas/acknowledgements;
do not create a second queue or frame delay.
- Remove superseded App entity/object event mirroring and source adapters.
- Assert same-thread graphical input and inbound presentation remain
synchronous.
Gate: direct-host and graphical-host normalized traces are identical, including
sequence, generation, identity, properties, placement, and teardown edges.
### J3.6 — hardening and closeout
- Failure-inject every projection registration/update/withdraw/teardown stage.
- Exercise callback reentrancy, duplicate CreateObject, stale/equal/wrapped
timestamps, delete/recreate with the same GUID, pending-to-loaded movement,
parent events before either object, malformed property packets, and session
reset during callbacks.
- Prove zero leaked canonical records, tombstones, App projections, resources,
container entries, parent candidates, queued deltas, or subscriptions.
- Add a Runtime-only entity/object fixture that performs create, property
updates, parent/container moves, Hidden, delete/recreate, clear, and teardown
without loading presentation/backend assemblies.
- Run Release build, focused suites, complete tests, exact-binary connected
lifecycle/reconnect, and the canonical world route.
- Because J3 changes projection identity plumbing, run the existing automated
render digest/referee at every checkpoint. Pause for user visual verification
only if the referee or connected client exposes a pixel/interaction symptom.
## 6. Commit order
1. `refactor(runtime): move accepted entity wire state`
2. `refactor(runtime): own canonical entity identity and incarnations`
3. `refactor(app): key live projections by runtime identity`
4. `refactor(runtime): own canonical object table`
5. `feat(runtime): publish ordered entity and object deltas`
6. `test(runtime): close J3 entity ownership parity`
Each commit is independently buildable and bisectable. The exact rollback for
each lands in this document before that commit's connected or visual gate.
## 7. Acceptance
J3 is complete only when:
- Runtime owns the only server-GUID/incarnation map and object table;
- App projections are keyed by local identity plus incarnation;
- Runtime has no App/UI/backend dependency;
- App has no authoritative GUID map or accepted timestamp gate;
- no mirrored collection, asynchronous seam, or added input frame exists;
- normalized direct/graphical traces match;
- deterministic render identity/digests match;
- Runtime-only no-window entity/object lifecycle passes;
- Release build and complete tests pass;
- exact-binary connected login/travel/logout/reconnect passes gracefully;
- every teardown owner converges to zero.
## 8. Current rollback chain
```text
git revert 75930787741db40a83eab8663e4464dce8d687ba
git revert 854d9e9cd13092bd5aaa3cf025d73eeb4600e9f8
git revert b632672e5ccabfb44c551e08f1c411ab2669c44a
```
These are J2, J1, and J0 respectively. Do not use them for a J3 sub-slice
failure; record and revert only the exact failing J3 commit.
J3 sub-slices are independently reversible:
```text
git revert ce3ac310d92722ffb637e81cb1957458874dd220
git revert 5ef8b5371d8990f0380acd939e71cd711289d429
git revert e937cc36df39cf6ea1eaba2f9c0243d1929da702
git revert 420e5eea70fd2c29cf9c8614e6298a1f84d64045
git revert f46ddb5cdb1e145752bea49aeb1d62bfe71284d3
git revert f7442d13e9ae7d5b077ffecb1f8a10899ad46edd
```
Revert J3.6 before J3.5. Revert J3.5 before J3.4. Revert J3.4 before J3.3.
Revert J3.3's exact-spatial
correction before its main projection-store commit. Revert J3.3 before J3.2,
and J3.2 before J3.1, when rolling back more than one sub-slice.

View file

@ -1,318 +0,0 @@
# Modern runtime J3.5 — committed entity/object deltas and borrowed views
**Status:** COMPLETE — `ce3ac310d92722ffb637e81cb1957458874dd220`
**Parent:** `2026-07-25-modern-runtime-slice-j3.md`
**Required production base:** `5ef8b5371d8990f0380acd939e71cd711289d429`
**Prior closeout:** `docs/research/2026-07-26-slice-j3-4-canonical-object-lifetime.md`
## Objective
Publish one synchronous, monotonic, generation-stamped stream from the
canonical Runtime entity/object lifetime and make both graphical and
no-window/direct hosts observe those same committed facts.
`IGameRuntimeView.Entities` and `IGameRuntimeView.Inventory` must read the
Runtime-owned `RuntimeEntityDirectory` and `ClientObjectTable` directly.
`CurrentGameRuntimeEventAdapter` must stop reconstructing entity changes from
App projection visibility and inventory changes from an App-owned event seam.
This slice adds no later-frame queue, copied world, inventory mirror, input
delay, renderer dependency, or gameplay behavior change.
## Current seam at the production base
- `RuntimeEntityObjectLifetime` owns the exact directory and object table.
- `CurrentGameRuntimeViewAdapter.EntityView` still reads App
`LiveEntityRuntime`, so a direct host cannot enumerate canonical entities
without constructing the graphical projection host.
- Its inventory view reads the right table but asks `LiveEntityRuntime` to
recover incarnation metadata.
- `CurrentGameRuntimeEventAdapter` owns a separate sequencer and reconstructs:
- entity events only from `ProjectionVisibilityChanged`; and
- inventory events by subscribing directly to object-table callbacks.
- Registered, refreshed, parented, picked-up, state-updated, cellless, and
authoritative-delete canonical edges therefore do not have one complete
Runtime stream.
- Runtime local IDs are still claimed at App materialization rather than at
canonical registration, even though direct and graphical hosts require one
stable cross-host identity.
The J1 adapter was intentionally temporary. J3.5 deletes these superseded
entity/object adapter responsibilities.
## Fixed contracts
### One canonical stream owner
Add one instance-scoped stream under `AcDream.Runtime.Entities`, owned by
`RuntimeEntityObjectLifetime`.
It exposes a narrow subscription equivalent to:
```csharp
public interface IRuntimeEntityObjectObserver
{
void OnEntity(in RuntimeEntityDelta delta);
void OnInventory(in RuntimeInventoryDelta delta);
}
public interface IRuntimeEntityObjectEventSource
{
IDisposable Subscribe(IRuntimeEntityObjectObserver observer);
}
```
Entity and inventory deltas share one sequence owner. There is no entity queue
plus inventory queue and no App repeater. Every stamp carries the exact
`RuntimeGenerationToken` and current Runtime frame number.
Sequence rules:
- sequence starts at one for each live session generation;
- entity and inventory commits consume the same monotonic sequence;
- rejected/stale packets consume no sequence;
- callback reentrancy appends later sequence values in actual commit order;
- session reset publishes its committed withdrawals/removals/clear edge, then
the retired generation is sealed;
- a replacement generation cannot publish through the retired stream context.
### Canonical entity identity and snapshot
Runtime claims a local ID when it creates the canonical active record, before
any graphical projection callback. The ID remains stable for that incarnation
until canonical teardown; graphical hydration failure must not release a
still-active Runtime identity.
The public entity view is canonical and presentation-free:
- server GUID;
- Runtime local ID;
- exact `INSTANCE_TS` incarnation;
- full cell;
- final physics state;
- accepted position/snapshot facts.
Graphical hydration, loaded/pending visibility, and render-resource state do
not become gameplay truth. If retained diagnostic contracts still need
attached-host counts, those are supplied by a separate projection
acknowledgement/diagnostic seam and are not mixed into canonical entity
identity.
### Commit ordering
An entity delta is created only after the exact Runtime record mutation
commits. The graphical projection host receives that same immutable committed
delta synchronously and returns an exact acknowledgement; public observers
then observe the same sequence. There is no second conversion step and no
frame boundary.
Required entity edges:
- `Registered` after active-record/local-ID commit;
- `Updated` after same-incarnation CreateObject, ObjDesc, vector, movement,
accepted position, parent, or equivalent canonical refresh;
- `Rebucketed` after a canonical full-cell change;
- `Hidden` after a committed Hidden state transition;
- `Withdrawn` after pickup/accepted parent/cellless exit-world while the
incarnation remains logically alive;
- `Deleted` after authoritative identity retirement and retained-object
removal acceptance;
- generation replacement orders old `Deleted`/withdrawal before new
`Registered`.
Loaded-to-pending renderer visibility is not a canonical withdrawal and emits
no gameplay entity delta.
Required inventory edges remain the exact object-table callback order:
- `Added`;
- `Updated`;
- `Moved`;
- `Removed` with exact removed generation;
- `Cleared`.
Unknown-object move notices retain the object ID and placement facts rather
than disappearing because no `ClientObject` instance exists.
### Borrowed views
The Runtime lifetime group exposes concrete allocation-free
`IRuntimeEntityView` and `IRuntimeInventoryView` implementations:
- entity enumeration walks `RuntimeEntityDirectory.ActiveRecords`;
- entity lookup resolves directly through the directory;
- inventory enumeration/lookup walks `ClientObjectTable`;
- inventory incarnation comes from the exact Runtime directory or the
object's retained generation, never an App sidecar;
- visitors receive value snapshots and may not retain mutable owners.
`CurrentGameRuntimeViewAdapter` borrows those view objects. It retains only
the still-unmoved J4+ chat/movement/portal/lifecycle composition.
## Execution
### 1. Pin the pre-cutover normalized oracle
Add a focused deterministic fixture covering:
- create, same-incarnation refresh, and generation replacement;
- ObjDesc/property and accepted state/vector/movement/position updates;
- parent, pickup, cellless withdrawal, and re-entry;
- object add/update/move/remove/clear;
- optimistic move, confirm, rejection rollback, and container replacement;
- authoritative delete and delete/recreate with the same GUID;
- reset and next-generation reuse;
- callback reentrancy.
Record both the canonical final state and exact event order. Preserve existing
retail-facing object-table callback order; add missing canonical entity edges
without treating projection visibility as gameplay truth.
### 2. Add the Runtime entity/object stream
- Add the stream owner and narrow observer/source contracts in Runtime.
- Give it one per-generation sequencer and explicit session-generation/frame
context.
- Use copy-on-write subscriber storage so steady dispatch allocates no observer
list and unsubscribe is exact.
- Reject duplicate subscriptions, cross-owner subscriptions, stale
generations, and dispatch after disposal.
- Keep callback execution on the existing update/network-drain thread.
- Revalidate generation/incarnation/operation version after any callback that
can re-enter Runtime.
### 3. Move identity allocation to canonical registration
- Claim the Runtime local ID in the same transaction that installs an active
canonical record.
- Preserve current allocation range and wrap/exhaustion behavior.
- Stop releasing the ID when only App materialization rolls back.
- Release it only when the canonical incarnation reaches terminal teardown.
- Prove pending/unhydrated and direct-host entities still have exact keys.
- Prove GUID reuse gets a new key and stale keys never resolve.
### 4. Publish canonical entity commits
- Centralize accepted canonical mutations behind
`RuntimeEntityObjectLifetime` transaction methods.
- Publish only after state commits and only while the exact operation remains
current.
- Carry the complete immutable accepted snapshot required by a graphical or
direct projection host.
- Replace App's projection-visibility-derived entity event reconstruction.
- Preserve synchronous graphical projection application and exact retryable
teardown acknowledgements.
- Do not emit on renderer rebucket, loaded/pending visibility, hydration
retries, or other presentation-only changes.
### 5. Publish exact object-table commits
- Bind the stream to its owned `ClientObjectTable`.
- Convert callbacks to immutable `RuntimeInventoryItemSnapshot` values with
generation metadata resolved by Runtime.
- Preserve add/update/move/remove/clear order and callback reentrancy.
- Keep optimistic movement and rollback synchronous.
- Ensure an authoritative delete produces the exact entity/object ordering
pinned by J3.4.
- Do not add a second retained inventory collection.
### 6. Replace App entity/object view and event adapters
- Make `CurrentGameRuntimeViewAdapter.Entities` and `.Inventory` return the
Runtime group's borrowed views.
- Remove its `LiveEntityRuntime` dependency for canonical entity/inventory
reads.
- Make `CurrentGameRuntimeEventAdapter` forward the Runtime stream directly.
- Remove its `ProjectionVisibilityChanged` and object-table subscriptions.
- Retain command, lifecycle, chat, movement, and portal adapter work only until
their scheduled J4J6 owner moves.
- Add source guards preventing a new App entity/object mirror, sequencer, or
owner-event reconstruction.
### 7. Direct/graphical parity and teardown
- Build one no-App Runtime fixture and one graphical-host fixture over the
same input script.
- Compare normalized generation, sequence, identity, cell/state, properties,
placement, and terminal edges exactly.
- Verify subscriber removal and Runtime disposal leave zero observers.
- Verify reset seals the old generation and a new session begins at sequence
one without stale delivery.
- Verify graphical callbacks remain same-thread and press-time object reads
remain immediate.
## Adversarial cases
- Observer adds/removes an observer during dispatch.
- Observer creates, deletes, or replaces the same GUID reentrantly.
- Observer clears the session during create or object-table callbacks.
- Graphical projection callback throws after canonical commit.
- Same-incarnation CreateObject arrives while a prior projection teardown is
retained.
- Authoritative delete races a dormant exact-incarnation record.
- Equal, stale, and wrapped timestamps.
- Local-ID wrap and namespace exhaustion.
- Graphical hydration fails before and after resource registration.
- Unknown-object move and removal.
- Container replacement mutates contents from inside an event callback.
- Multiple Runtime groups use identical GUIDs and session-generation values.
- Subscription disposal is replayed and Runtime disposal occurs during a
callback.
## Mandatory gates
Run in this order:
1. Runtime stream, borrowed-view, sequence, reentrancy, and identity tests.
2. Core `ClientObjectTable*` and `ObjectTableWiring*` tests.
3. Focused App Runtime-adapter, hydration, update, rebucket, deletion, reset,
inventory, paperdoll, selection, interaction, and cooldown tests.
4. Direct-host versus graphical-host normalized parity fixture.
5. All Runtime tests.
6. All App tests.
7. `dotnet build AcDream.slnx -c Release`.
8. `dotnet test AcDream.slnx -c Release --no-build`.
9. Exact-binary connected lifecycle/reconnect gate.
10. At every connected checkpoint: zero render-shadow mismatches, zero pending
deltas, graceful process exit, and exact canonical/projection teardown
convergence.
A user visual pause is required only if an automated or connected gate exposes
a visible inventory, selection, radar, paperdoll, or world-object symptom.
## Acceptance
- Runtime owns one per-session ordered entity/object stream.
- Entity and inventory deltas share one generation/sequence order.
- Runtime views read the directory and object table directly.
- Every active canonical entity has a Runtime key before App projection.
- App reconstructs neither entity nor inventory events.
- Projection visibility is not emitted as canonical gameplay withdrawal.
- Graphical and no-window normalized traces are identical.
- No copied collection, queue, extra input frame, or backend dependency exists.
- Reentrant callbacks cannot resurrect or mutate a superseded incarnation.
- Reset/disposal leaves zero observers, queued deltas, and retained stream
state.
- Complete Release and exact connected gates pass on one committed binary.
## Rollback
J3.5 is independently reversible:
```text
git revert ce3ac310d92722ffb637e81cb1957458874dd220
```
Do not revert J3.4 as a shortcut for a J3.5 failure. Automated and connected
evidence is recorded in
`../research/2026-07-26-slice-j3-5-canonical-delta-stream.md`.
## Next boundary
J3.6 executes from
[`2026-07-26-modern-runtime-slice-j3-hardening.md`](2026-07-26-modern-runtime-slice-j3-hardening.md).
It failure-injects the complete canonical entity/object lifecycle and proves
zero records, tombstones, projections, resources, container entries, parent
candidates, subscriptions, or pending deltas after reset and disposal. That
closeout is required before J4 moves chat, inventory transactions, vitals,
enchantments, spells/components, cooldowns, and ViewModel-facing revisions.

View file

@ -1,206 +0,0 @@
# Modern runtime J3.6 — adversarial lifetime hardening and closeout
**Status:** COMPLETE 2026-07-26 at `119b7c115107245546180b2f5cb3cb44c7d5476a`
**Parent:** `2026-07-25-modern-runtime-slice-j3.md`
**Required production base:** `ce3ac310d92722ffb637e81cb1957458874dd220`
**Prior closeout:** `../research/2026-07-26-slice-j3-5-canonical-delta-stream.md`
## Objective
Close the canonical entity/object lifetime group under failure, re-entrancy,
reset, and disposal before J4 moves another gameplay owner.
This slice adds no feature, cross-frame/background queue, retry timer,
renderer fallback, or gameplay behavior. Its synchronous re-entrant dispatch
drain preserves one observer-visible sequence within the committing call. It
proves that Runtime's canonical identity/object owner and App's
exact-key graphical projection either complete each transaction or retain one
explicitly retryable suffix, and that every terminal path converges to zero.
## Fixed boundary
- Runtime remains the sole GUID/incarnation/local-ID, accepted-wire,
parent-relation, object-table, direct-view, and entity/object-event owner.
- App remains an exact-key projection and resource host.
- The server remains authoritative.
- Existing synchronous retail ordering remains unchanged.
- J4 chat, vitals, enchantments, spells/components, cooldowns, and
ViewModel-facing revisions do not enter this slice.
- A failure test may expose a production ownership defect. Fix the owning
transaction at its commit/acknowledgement boundary; do not add a suppression
flag, timer, retry loop, or symptom guard.
## Execution
### 1. Pin the complete ownership ledger
Create one assertion helper that reports every J3 owner:
- active Runtime records;
- teardown Runtime records and claimed local IDs;
- accepted snapshot/timestamp and parent-relation entries;
- active and teardown App projections;
- materialized world entities and graphical resource registrations;
- spatial loaded/pending projection keys;
- animation, remote-motion, projectile, root-object, effect, light, script,
and equipped-child bindings;
- object-table objects, containers, pending moves, and replacement state;
- Runtime stream subscriptions, dispatch failures, and pending work.
The helper must distinguish an intentionally retained retry receipt from a
leak. A stable terminal checkpoint permits neither.
### 2. Failure-inject canonical registration and hydration
Exercise failures and re-entry:
- before and after Runtime active-record/local-ID commit;
- same-incarnation refresh;
- generation replacement while prior App teardown succeeds or fails;
- projection sidecar creation;
- world-entity factory;
- resource registration before commit and after partial commit;
- resource rollback before and after commit;
- animation/physics/effect/relationship/ready publication stages;
- observer replacement, delete, and session clear during each callback.
Prove that a still-active Runtime incarnation never loses its key, a failed
partial owner remains reachable by one exact teardown receipt, and retry
cannot replay create-time resources already committed.
### 3. Failure-inject accepted updates and placement
Cover ObjDesc, motion, vector, state, position, parent, pickup, explicit
rebucket, withdrawal, and child-NoDraw:
- stale/equal/new/wrapped timestamps;
- loaded-to-loaded, loaded-to-pending, pending-to-loaded, and cellless
transitions;
- projection callback failure before and after the graphical mutation;
- observer delete/recreate or same-record newer update;
- parent event before the parent, before the child, and across GUID reuse;
- malformed or missing-object updates.
Assert the shared stream publishes only committed canonical facts, consumes
no sequence for rejected packets, never emits renderer visibility as a
withdrawal, and never applies a displaced callback to a replacement
incarnation.
### 4. Failure-inject object-table transactions
Run the exact object-table behavior through the Runtime stream:
- create/add versus update;
- optimistic container move;
- confirmation;
- rejection rollback;
- wield/container replacement;
- stack/value/property refresh;
- unknown-object move;
- authoritative entity delete;
- dormant/offscreen delete;
- clear and next-generation reuse;
- callback mutation of the same object or containing container.
Verify callback order, exact removed generation, immediate borrowed-view
visibility, and no second retained inventory collection.
### 5. Prove reset, retry, and disposal convergence
Inject a failure at every App teardown stage, then repeatedly invoke the
existing retryable teardown entry until it completes. Exercise:
- ordinary authoritative delete;
- delete/recreate with the same GUID;
- session reset during create, update, object callback, and teardown callback;
- logout;
- transport replacement;
- mid-portal disconnect;
- Runtime and App disposal;
- repeated subscription disposal and disposal from inside dispatch.
At convergence assert the complete ownership ledger is zero, the retired
generation cannot publish, the next generation starts at sequence one, and
later object-table mutations cannot reach disposed observers.
### 6. Runtime-only end-to-end fixture
Without loading App, UI, Silk.NET, OpenAL, Arch, or ImGui:
1. construct Runtime;
2. bind a generation and frame clock;
3. register several conflicting entity/object shapes;
4. apply properties, motion/vector/state/position, parent/container moves,
Hidden, pickup, delete/recreate, and clear;
5. visit direct views after each commit;
6. record the exact shared delta stream;
7. reset and reconnect;
8. dispose.
Run the same normalized input script through the graphical harness and require
exact equality. Assert the Runtime assembly load closure remains backend-free.
### 7. Closeout gates
Run, in order:
1. focused Runtime lifetime/stream failure matrix;
2. Core `ClientObjectTable*` and `ObjectTableWiring*`;
3. focused App hydration, projection, update, parent, rebucket, delete,
session-reset, selection, inventory, and paperdoll suites;
4. Runtime-only versus graphical normalized parity;
5. all Runtime tests;
6. all App tests;
7. `dotnet build AcDream.slnx -c Release`;
8. `dotnet test AcDream.slnx -c Release --no-build`;
9. exact-binary connected lifecycle/reconnect;
10. canonical nine-stop connected world route;
11. documentation, divergence, dependency, and source-ownership audit.
A user visual pause is required only if automated or connected evidence shows
a visible inventory, selection, radar, paperdoll, portal, or world-object
symptom.
## Acceptance
J3 is complete only when:
- every canonical mutation and terminal path has adversarial coverage;
- direct and graphical traces remain exact;
- rejected/stale work consumes no event sequence;
- no callback can mutate a superseded incarnation;
- every partial acquisition remains reachable and retryable;
- every stable reset/disposal checkpoint has zero records, tombstones, keys,
projections, resources, container entries, parent candidates,
subscriptions, and pending deltas;
- Runtime-only construction and lifecycle load no presentation/backend
assembly;
- full Release and both exact-binary connected routes pass;
- architecture, roadmap, milestones, AGENTS/CLAUDE, durable memory, and exact
rollback are synchronized.
## Rollback
J3.6 is independently reversible:
```text
git revert 119b7c115107245546180b2f5cb3cb44c7d5476a
```
J3.5 remains independently reversible with
`git revert ce3ac310d92722ffb637e81cb1957458874dd220`. Never revert J3.4
or J3.3 to mask a later failure.
## Closeout evidence
The complete failure matrix, zero-owner ledger, 8,484-test Release result,
exact-binary lifecycle/reconnect gate, canonical nine-stop route, divergence
audit, and rollback are recorded in
`../research/2026-07-26-slice-j3-6-lifetime-closeout.md`.
## Next boundary
J3 is closed. Execute J4 from
`docs/plans/2026-07-25-modern-runtime-slice-j.md`: move the remaining
presentation-independent gameplay-state owners in coherent lifetime groups,
with direct/graphical parity and zero-mirror deletion after every group.

View file

@ -1,252 +0,0 @@
# Modern runtime Slice J4 — gameplay-state lifetime groups
**Status:** COMPLETE
**Parent:** `2026-07-25-modern-runtime-slice-j.md`, J4
**Production base:** `77c013998b902724d66e487589a4ea18538e1ea6`
**Authorization:** the user approved Slices FL, including Slice J gameplay
owner moves, on 2026-07-24
## Objective
Move the remaining presentation-independent player/gameplay state into
`AcDream.Runtime` without creating a second model, changing retail behavior,
or making the graphical host authoritative.
Runtime must construct and own each mutable Core state object. App, retained
UI, devtools, plugins, and the live-session router borrow those exact
instances. Every moved group retains current synchronous packet/command
ordering and its existing retail-derived algorithms.
## Execution ledger
| Group | Status | Evidence |
|---|---|---|
| J4.1 communication/social | complete | `c9d25ade50c0a5c4f7db5b6cca680e01e35cc18e`; 152 Runtime tests, 3,765 App tests / 3 skips, 8,494 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-075109/report.json` pass |
| J4.2 inventory transactions | complete | `011efbeaa72509b35ea7f4a442e50a2377ae1ea4`; 157 Runtime tests, 3,770 App tests / 3 skips, 8,515 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-082057/report.json` pass |
| J4.3 magic/player sheet | complete | `d02a12ceac54d035797b4c58f2fc2f0ccad9a4d6`; 162 Runtime tests, 3,772 App tests / 3 skips, 8,522 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-083921/report.json` pass |
| J4.4 character projections | complete | `dcb61efb5af6c12bd619369e28cf11bacc37bc73`; 169 Runtime tests, 3,777 App tests / 3 skips, 8,534 complete Release tests / 5 skips, and `logs/connected-world-gate-20260726-091340/report.json` pass |
| J4.5 combined cleanup | complete | `89e6b207f81946b40c38c8dc6597817ad99fc6e0`; 175 Runtime tests, 3,780 App tests / 3 skips, 8,544 complete Release tests / 5 skips, exact-binary lifecycle/reconnect and nine-stop gates |
## Current ownership inventory
The App `GameWindow` currently constructs or roots these
presentation-independent owners:
| Group | Current canonical objects | Current App roots |
|---|---|---|
| Communication/social | `ChatLog`, `TurbineChatState`, `FriendsState`, `SquelchState`; reply/retell targets currently live in `ChatVM` | public `GameWindow` fields plus `ChatVM` subscription |
| Inventory/session transactions | J3's Runtime-owned `ClientObjectTable`; `ExternalContainerState`, `ItemManaState`, shortcut snapshot, desired-component snapshot | `GameWindow`, App snapshot wrappers, and `LiveSessionRuntimeFactory` |
| Magic/effects/player sheet | coupled `Spellbook` + `LocalPlayerState`; learned spells, favorites, filters, desired components, active enchantments, cooldown bucket, attributes, skills, vitals, properties, positions | `GameWindow` fields; App `MagicRuntime` and retained controllers borrow them |
| Character settings/skill motion projection | character options and run/jump controller inputs | App input/composition state |
| Presentation-only state | formatted/layout chat rows, retained panels, text/icons, selected rows, scroll offsets, paperdoll/viewport, effects rendering, cooldown overlays | App/UI; these do **not** move |
`RuntimeEntityObjectLifetime` already owns the inventory object collection and
ordered object deltas. J4 must reuse that owner. It may not introduce a second
inventory list or replay object-table events into another collection.
Selection, approach/use input, movement, combat intent, target state,
projectiles, and outbound motion/cast cadence remain J5. World reveal,
environment, and portal presentation remain J6.
## Fixed invariants
1. One mutable owner instance per state class; adapters and panels borrow.
2. Runtime has no App, UI, Silk, OpenAL, Arch, ImGui, or GL dependency.
3. No asynchronous App seam, extra frame, polling copy, or reconstructed
ViewModel collection.
4. Existing `WorldSession`/`GameEventWiring` registration order and
generation acceptance gates remain unchanged.
5. Session reset preserves the current semantic order. Visible chat history
survives character teardown while reply targets, local identity, negotiated
rooms, friends, and squelch state reset.
6. Immutable DAT-derived catalog data may be installed once. Mutable learned
spells, enchantments, filters, favorites, desired components, vitals,
skills, and cooldown state are session-owned.
7. Every group has a Runtime-only construction/reset/disposal test that loads
no presentation/backend assembly.
8. The replaced App ownership path is deleted in the same sub-slice.
## Execution
### J4.1 — communication and social state
**Completed 2026-07-26 at `c9d25ade`.**
- Add a Runtime owner for `ChatLog`, `TurbineChatState`, `FriendsState`,
`SquelchState`, and reply/retell command targets.
- Move reply/retell target tracking out of `ChatVM`; the VM borrows the same
presentation-independent target state.
- Make `LiveSessionRuntimeFactory`, command routing, current-runtime views,
retained UI, devtools, and plugins borrow the Runtime owner.
- Replace `GameWindow` object fields with read-only aliases to that owner.
- Preserve transcript lifetime and exact per-session reset semantics.
Gate: incoming/outgoing tell tracking, room negotiation/cookie wrap, friend and
squelch updates, command routing, clear, reset/reconnect, graphical/direct
event traces, disposal, and Runtime dependency closure.
Result: `RuntimeCommunicationState` constructs and owns the exact `ChatLog`,
reply/retell targets, Turbine rooms, friends, and squelch database. Session
routing, both UI stacks, plugins/current-runtime views, and command routing
borrow those instances. The canonical chat stream is synchronous,
failure-isolated, and preserves one monotonic sequence under re-entrant
callbacks without a frame/background queue. Exact-binary lifecycle/reconnect
evidence is recorded in
[`../research/2026-07-26-slice-j4-1-communication-state.md`](../research/2026-07-26-slice-j4-1-communication-state.md).
### J4.2 — inventory transaction state
**Completed 2026-07-26 at `011efbea`.**
- Move `ExternalContainerState`, `ItemManaState`, shortcuts, and desired
component snapshots into one Runtime owner.
- Keep the J3 object table as the only item collection and borrow it directly.
- Split presentation/input-only portions from item-use reservations only where
the existing class mixes them; do not move retained drag/drop or panel state.
- Route inbound object/use/mana/shortcut/component updates and bot/plugin views
through the same owner.
Gate: optimistic move/rollback, external-container open/replace/close,
use-completion, mana query, shortcut/component replacement, clear/reconnect,
and zero duplicate collection.
Result: `RuntimeInventoryState` owns external-container, item-mana,
shortcut/component, shared-busy, use-reservation, and one-request-at-a-time
state while borrowing J3's exact object table. App retains only
presentation/input projections. Failure-safe reset and callback delivery
prevent a failed observer or dispatch from stranding the canonical busy gate.
Exact evidence is recorded in
[`../research/2026-07-26-slice-j4-2-inventory-state.md`](../research/2026-07-26-slice-j4-2-inventory-state.md).
### J4.3 — spell, enchantment, vital, attribute, and skill state
**Completed 2026-07-26 at `d02a12ce`.**
- Move construction/lifetime of the coupled `Spellbook` and
`LocalPlayerState` into Runtime.
- Preserve one-time immutable `SpellTable` installation from App content.
- Keep learned spells, favorites, filters, desired components,
enchantments/cooldowns, vitals, attributes, skills, properties, and positions
on those exact owners.
- Retained magic/effects/character panels and App `MagicCatalog` borrow; no
DAT/UI type enters Runtime.
Gate: complete PlayerDescription, incremental updates, spell/enchantment
add/remove, favorite/filter/component changes, cooldowns, vital modifiers,
clear/reconnect, ViewModel revisions, and graphical/direct trace parity.
Result: `RuntimeCharacterState` constructs and owns the exact coupled
`Spellbook` and `LocalPlayerState`. Content installs immutable DAT metadata
through that owner; routing, retained UI, reset, and shutdown borrow it. The
interim desired-component mirror was deleted, leaving the spellbook as the
only mutable component-count owner. Exact evidence is recorded in
[`../research/2026-07-26-slice-j4-3-character-state.md`](../research/2026-07-26-slice-j4-3-character-state.md).
### J4.4 — character settings and player-state projections
**Completed 2026-07-26 at `dcb61efb`.**
- Move remaining presentation-independent character options and skill-derived
state needed by commands/bots.
- Keep controller/body/camera/input application in App/J5.
- Expose borrowed immutable views and synchronous typed commands for all J4
groups.
Gate: character replacement, settings isolation, two Runtime instances in one
process, no static mutable session state, and no backend load.
Result: the Runtime character graph owns both option bitfields and the
server-authoritative run/jump projection; the two former App owners are
deleted. Exact borrowed inventory/character/social views and synchronous
generation-gated state commands are shared by graphical UI and future
no-window hosts. Normalized checkpoints now cover the J4 state. Exact evidence
is recorded in
[`../research/2026-07-26-slice-j4-4-character-projections.md`](../research/2026-07-26-slice-j4-4-character-projections.md).
### J4.5 — combined cleanup and closeout
**Completed 2026-07-26 at `89e6b207`.**
- Delete temporary App owner fields, reset branches, reconstructed runtime
views/events, and compatibility adapters replaced by J4.
- Prove graphical UI and no-window hosts observe the same owner instances,
revisions, command effects, and ordered deltas.
- Failure-inject construction, subscription, callback, reset, and disposal for
every group; require complete owner-ledger convergence.
Run:
1. focused Core and Runtime state tests;
2. UI ViewModel/controller tests;
3. App composition/session/reset/runtime-adapter tests;
4. Runtime dependency/source/assembly-load guards;
5. full Runtime and App suites;
6. Release build and complete Release suite;
7. exact-binary lifecycle/reconnect gate;
8. canonical nine-stop route;
9. documentation, divergence, dependency, and source-ownership audit.
Result: the Runtime inventory graph now owns the one retail 18-slot shortcut
manager, retained toolbar/spell controllers apply command effects through the
canonical Runtime owner, and `ItemInteractionController` can no longer create a
second inventory transaction/busy owner. Exact retail local/send ordering is
pinned for shortcuts, favorites, filters, and desired components. Graphical
and direct no-window command effects match, while a combined failure-safe
ledger proves every J4 owner and subscription converges after reset or terminal
disposal. Exact evidence is recorded in
[`../research/2026-07-26-slice-j4-5-gameplay-state-closeout.md`](../research/2026-07-26-slice-j4-5-gameplay-state-closeout.md).
## Acceptance
J4 is complete only when:
- Runtime constructs and owns every listed presentation-independent object;
- App/UI/plugin/bot consumers borrow exact instances;
- no App mirror, reconstructed collection, or command target remains;
- reset, reconnect, direct disposal, and two-instance isolation pass;
- graphical and Runtime-only normalized traces match;
- Runtime's assembly closure remains backend-free;
- full Release and both exact-binary connected routes pass;
- architecture, roadmap, milestones, AGENTS/CLAUDE, durable memory, evidence,
and exact rollback are synchronized.
## Rollback
Record each J4 production commit before its connected gate. Revert J4 groups in
reverse order only. J3.6 remains independently reversible with:
```text
git revert 119b7c115107245546180b2f5cb3cb44c7d5476a
```
Do not revert J3 to mask a J4 owner/reset failure.
J4.1 is independently reversible with:
```text
git revert c9d25ade50c0a5c4f7db5b6cca680e01e35cc18e
```
J4.2 is independently reversible with:
```text
git revert 011efbeaa72509b35ea7f4a442e50a2377ae1ea4
```
J4.3 is independently reversible with:
```text
git revert d02a12ceac54d035797b4c58f2fc2f0ccad9a4d6
```
J4.4 is independently reversible with:
```text
git revert dcb61efb5af6c12bd619369e28cf11bacc37bc73
```
J4.5 is independently reversible with:
```text
git revert 89e6b207f81946b40c38c8dc6597817ad99fc6e0
```

View file

@ -1,534 +0,0 @@
# Modern runtime Slice J5 — movement, physics, interaction, and combat
**Status:** COMPLETE — J5.1J5.7 closed 2026-07-26
**Parent:** `2026-07-25-modern-runtime-slice-j.md`, J5
**Production base:** `2c67a2c3`
**Authorization:** the user approved Slices FL, including the Slice J
gameplay-owner gates, on 2026-07-24
## Objective
Move the complete presentation-independent action and simulation graph into
`AcDream.Runtime` while preserving retail behavior and the accepted graphical
feel.
At completion:
- Runtime owns selection, interaction mode/transactions, combat and spell-cast
intent, local/remote movement, outbound cadence, per-session physics, and
projectile simulation;
- App converts physical input into typed commands and projects committed state
into cameras, retained UI, animation, sound, particles, markers, and meshes;
- graphical and future no-window hosts borrow the same owners and observe the
same ordered state;
- no render callback, UI element, or camera object is gameplay authority.
## Presentation boundary
### Moves to Runtime
- selected/previous object identity;
- temporary use/examine/use-item-on-target mode;
- appraisal/use/pickup/approach transaction identity;
- combat mode, attack build/request/repeat intent, target identity;
- spell-cast request intent;
- `PlayerMovementController` state, movement interpretation, canonical local
body, object clock, movement managers, and outbound cadence;
- per-session `PhysicsEngine`, transition scratch, shadow/collision gameplay
state;
- remote canonical body/movement state;
- projectile component state and simulation;
- typed views, commands, ordered events, reset, disposal, and ownership
ledgers for all of the above.
### Remains in App
- Silk input sources and configurable key/chord mapping;
- camera orbit, mouse cursor capture, and device-delta filtering;
- world ray construction/picking;
- selection lighting pulse, vivid markers, and selected-object HUD;
- combat/spell bars and retained target-mode cursor images;
- animated part/root pose composition;
- meshes, terrain/EnvCell streaming publication, sounds, particles, lights,
translucency, and debug drawing;
- UI text, toasts, drag/drop visuals, pending-slot mesh, and appraisals'
retained window.
The camera's instant mouse-look mode still emits typed retail turn/sidestep
commands to Runtime. The camera may not write the player heading directly.
## Fixed invariants
1. Retail algorithms and branch/order semantics do not change during the move.
2. One mutable owner exists for every state class.
3. Runtime has no App/UI/backend dependency.
4. `RuntimeEntityRecord` remains canonical entity identity and physics
authority; App `LiveEntityRecord` remains a projection sidecar.
5. `RuntimeInventoryState` remains the only busy/inventory-transaction owner.
6. Core flat collision data is immutable/shareable. Each runtime/session owns
its `PhysicsEngine`, transition scratch, bodies, shadows, interpolation, and
projectile state.
7. All mutation stays on the existing update thread. Network workers enqueue
immutable messages only.
8. App input-to-Runtime commands are synchronous; no extra frame, polling
mirror, or background queue is introduced.
9. App presentation callbacks may re-enter/delete/replace entities. Every
continuation revalidates exact record, incarnation, authority version, and
object-clock epoch before commit.
10. Reset/disposal executes every suffix after an observer failure and refuses
a new session until the complete ownership ledger converges.
11. Every sub-slice is independently buildable, tested, connected-gated where
behavior can change, documented, committed, and given an exact rollback.
## Execution ledger
| Sub-slice | Status | Result/evidence |
|---|---|---|
| J5.0 owner/retail inventory | complete | `docs/research/2026-07-26-slice-j5-owner-and-retail-order-inventory.md` |
| J5.1 canonical action state | complete | `b298f99f913249d299796e6a71f7a9b6b283ac1a`; one Runtime selection/combat/target-mode owner; 182 Runtime tests, 3,779 App tests / 3 skips, 8,550 complete Release tests / 5 skips, exact-binary connected lifecycle/reconnect pass |
| J5.2 interaction transactions | complete | `f5f7b4177f449ebcad9724bfea263ef73ab60255`; one Runtime transaction owner; 197 Runtime tests, 3,775 App tests / 3 skips, 8,561 complete Release tests / 5 skips, exact-binary connected lifecycle/reconnect pass |
| J5.3 combat and magic intent | complete | `20df9d155db50706a42420d60a9b15861cf4bfe7`; exact Runtime attack/mode/target/cast owners; 223 Runtime tests, 3,754 App tests / 3 skips, 8,566 complete Release tests / 5 skips, exact-binary connected lifecycle/reconnect pass |
| J5.4 local movement and outbound cadence | complete | `aa3f4a60f87f2cbebc3d8ecd5e33d779b7fb13c1`; one Runtime movement/controller/cadence owner; 303 Runtime tests, 3,716 App tests / 3 skips, 8,575 complete Release tests / 5 skips, exact-binary lifecycle/reconnect and nine-stop movement gates pass |
| J5.5 per-session physics and remote simulation | complete | `7e6033d0adcd0b572c20e89dd275746fb442d52f`; one Runtime engine/cache/scratch/shadow/body/host/remote/workset owner; 314 Runtime tests, 3,718 App tests / 3 skips, 8,588 complete Release tests / 5 skips, exact-binary lifecycle/reconnect and nine-stop collision/movement gates pass |
| J5.6 projectile runtime | complete | `2aee33569f0268d7bc0f4f52437dfa0b405432a4`; one Runtime projectile component/workset/simulation/correction owner; 317 Runtime tests, 3,718 App tests / 3 skips, 47 focused Core projectile tests, 8,591 complete Release tests / 5 skips, exact arrow/bolt/spell evidence, lifecycle/reconnect pass, and nine-stop route pass |
| J5.7 combined cleanup and closeout | complete | `cdee7a4b49addb5e1500753f6a885f7c899bd0f0`; one combined simulation ledger, Runtime-only fixture parity, exact terminal teardown, 323 Runtime tests, 3,717 App tests / 3 skips, 8,596 complete Release tests / 5 skips, exact-binary lifecycle/reconnect and nine-stop gates |
## J5.1 — canonical action state
Create one Runtime owner for:
- `SelectionState`;
- `CombatState`;
- presentation-free interaction/target-mode state.
Actions:
1. Move interaction-mode types/state from App UI into Runtime gameplay.
2. Make Runtime construct all three exact owners.
3. Remove Program's selection construction and GameWindow's direct combat
construction.
4. Make App composition, session routing, retained UI, plugins, and current
Runtime commands/views borrow the exact Runtime instances.
5. Remove `ItemInteractionController`'s optional private
`InteractionState` fallback; production and tests must pass the exact owner.
6. Add a combined ownership/revision snapshot and failure-safe reset/disposal.
7. Add Runtime views for selection, combat mode/known health, and interaction
mode without exposing mutable collections.
Gate:
- named-retail selection order and deduplication;
- combat Begin/reset defaults;
- target-mode enter/acquire/cancel/reset;
- UI/plugin observer failure isolation;
- two Runtime instances in one process;
- exact graphical/direct owner identity;
- source guard proving App cannot construct a production owner;
- Runtime dependency and assembly-load closure;
- focused Runtime/App tests, Release build, full Release suite, connected
lifecycle/reconnect route.
Completed 2026-07-26 at
`b298f99f913249d299796e6a71f7a9b6b283ac1a`. `RuntimeActionState` now
constructs the exact selection, combat, and temporary interaction-mode
children. Program, plugins, retained UI, session routing, typed commands/views,
and terminal shutdown borrow those exact instances. The App-owned
`InteractionState` and `ItemInteractionController` fallback are deleted.
Failure-safe reset/disposal, two-instance isolation, normalized checkpoints,
dependency closure, and production source guards pass. The exact-binary
connected report is
`logs/connected-world-gate-20260726-104445/report.json`: 7/7 ready
checkpoints, two graceful code-zero exits, zero failures, and only the 25
expected world-edge landblock misses. Evidence:
[`../research/2026-07-26-slice-j5-1-canonical-action-state.md`](../research/2026-07-26-slice-j5-1-canonical-action-state.md).
## J5.2 — interaction transactions
Split App orchestration into:
- App `WorldSelectionQuery`/picker/lighting/drag presentation;
- Runtime interaction transaction owner;
- typed query, movement, inventory-policy, transport, and presentation
acknowledgement seams.
Runtime owns:
- use throttle and source/target identity;
- appraisal request/current identity;
- ordinary use reservation transfer to authoritative UseDone;
- pickup request and exact post-arrival identity/token;
- ordered outbound interaction queue;
- entity-hidden/deleted cancellation;
- reset/disposal convergence.
App retains:
- cursor position and world ray;
- lighting pulse;
- toasts/text rendering;
- retained drag/drop and pending-slot drawing.
Ordering:
- ordinary Use sends immediately, then transfers its busy reservation to
UseDone;
- targeted Use enters mode until a compatible target is acquired;
- pickup's same-frame send drains after local movement output and before
inbound dispatch;
- post-arrival pickup sends once only on natural completion;
- stale/replaced object identities never execute queued work.
Gate:
- current connected near/far door/NPC/corpse use;
- item-on-target, appraisal, pickup, pending-slot accept/failure;
- same-frame W-release plus R;
- delete/GUID reuse, hidden target, cancellation, transport failure, callback
reentrancy, reset/disposal;
- normalized graphical/direct ordered traces and no duplicate busy owner.
Completed 2026-07-26 at
`f5f7b4177f449ebcad9724bfea263ef73ab60255`.
`RuntimeInteractionTransactionState`, owned beneath `RuntimeActionState`, now
owns the retail 200-ms use gate, exact use source/target identity, pending and
current appraisal identity, typed ordered interactions, and the exact
post-arrival pickup token. It borrows—not duplicates—J4's
`InventoryTransactionState`, so ordinary Use transfers one busy reference to
authoritative UseDone and pickup retains the same one-request-at-a-time gate.
App retains world rays, lighting, movement installation, transport, toasts,
drag/drop, and pending-slot presentation. The old App delegate queue and App
transaction fields are deleted.
Identity-bound work is cancelled by exact local incarnation on delete and by
server GUID on Hidden. Re-entrant reset, disposal, transport failure, stale
approach completion, and GUID reuse cannot resurrect a transaction or strand
the busy gate. Focused near/far use, carried-use, appraisal, item-on-target,
pickup, pending-slot, hidden/delete, reset, and callback-order tests pass.
The exact-binary connected report is
`logs/connected-world-gate-20260726-111330/report.json`: 7/7 ready
checkpoints, two graceful code-zero exits, zero failures or invariant
violations, and only the 25 expected world-edge landblock misses. Evidence:
[`../research/2026-07-26-slice-j5-2-interaction-transactions.md`](../research/2026-07-26-slice-j5-2-interaction-transactions.md).
## J5.3 — combat and magic intent
Move:
- `CombatAttackController`'s presentation-free build/request/repeat state;
- combat-mode selection/default policy;
- target death/auto-target identity transitions;
- `SpellCastingController` intent and last-request state.
Replace `InputAction` dependencies with Runtime-specific typed commands.
App maps configured actions and bar/button presses onto those commands and
projects Runtime snapshots.
Keep App-only:
- combat/spell bar widgets;
- camera target tracking;
- animation, sound, particles, and response text layout;
- DAT catalog/icon/formula presentation.
Ordering:
- movement aborts repeat attack at the existing retail input boundary;
- targeted attack/cast turns through the shared movement owner before
release;
- cast stops movement, emits exactly one targeted/untargeted request, then
increments the shared busy reference;
- server attack/cast completion remains authoritative.
Gate:
- power-bar cadence and height selection;
- repeat start/abort/movement cancellation;
- combat-mode equipment defaults;
- 180-degree bow/spell target facing;
- hostile-only auto-target after death;
- self/untargeted/targeted spell requests, component failure, busy completion;
- graphical bar parity and direct typed-command parity.
**Completed 2026-07-26 at
`20df9d155db50706a42420d60a9b15861cf4bfe7`.** `RuntimeActionState`
constructs and owns the exact combat-attack, combat-target, combat-mode, and
spell-cast intent children. Retained attack/spell bars and graphical input
borrow those owners; direct Runtime combat/magic commands drive the same
operations and snapshots without a retained UI. App retains world/content
queries, transport, bars, animation, sound, particles, and response text.
Runtime tests pass 223/223, App tests pass 3,754/3 skips, and the complete
Release suite passes 8,566/5 skips. The exact-binary connected report
`logs/connected-world-gate-20260726-115752/report.json` passes two sessions,
7/7 checkpoints, graceful code-zero exits, and zero failures. Evidence:
[`../research/2026-07-26-slice-j5-3-combat-magic-intent.md`](../research/2026-07-26-slice-j5-3-combat-magic-intent.md).
## J5.4 — local movement and outbound cadence
Move `PlayerMovementController`, its data types, and
`LocalPlayerOutboundController` into Runtime. Replace test-era environment
reads with typed construction options and the Runtime character movement-skill
projection.
App provides:
- current typed movement snapshot from the input dispatcher;
- filtered mouse-look turn adjustment;
- animation root-motion frame and completion acknowledgements;
- optional diagnostic sink.
Runtime owns:
- canonical local `PhysicsBody`, `MotionInterpreter`, `MovementManager`,
`MoveToManager`, `PositionManager`, and object clock;
- local input edge interpretation;
- run lock, jump charge/launch, movement/contact state;
- target-facing movement;
- pre-inbound MTS/jump and post-inbound autonomous-position send cadence;
- complete canonical `Position`, contact-plane, timestamps, and sent baselines.
Gate:
- bit-exact `RawMotionState`, MTS, jump, and autonomous-position fixtures;
- strict cadence/equality boundaries;
- complete quaternion/cell-local frame;
- short-tap animation-owned start;
- run/walk/back/strafe/turn/mouse-look/jump/land;
- portal/focus/reset teardown;
- target-facing and movement-aborts-repeat ordering;
- zero added per-frame allocations after warmup.
**Completed 2026-07-26 at
`aa3f4a60f87f2cbebc3d8ecd5e33d779b7fb13c1`.**
`RuntimeLocalPlayerMovementState` constructs and owns the exact
`PlayerMovementController`, construction-time motion seam, autorun latch,
typed movement view, and terminal ledger. The movement controller and
`LocalPlayerOutboundController` now live in `AcDream.Runtime.Gameplay`.
Graphical input samples physical state and borrows that owner; direct Runtime
commands mutate the same state without an App/input detour. Character
run/jump skill projections enter through typed construction options rather
than process environment reads. Runtime tests pass 303/303, App tests pass
3,716/3 skips, and the complete Release suite passes 8,575/5 skips. The
exact-binary connected lifecycle/reconnect report
`logs/connected-world-gate-20260726-123425/report.json` passes both sessions,
7/7 readiness checkpoints, graceful code-zero exits, and zero failures. The
canonical route
`logs/connected-r6-soak-20260726-124049.report.json` passes all nine
destinations; forward, jump, combat, and authoritative movement-truth
exercises pass at Caul, Holtburg, and the Caul return before graceful
shutdown. Evidence:
[`../research/2026-07-26-slice-j5-4-local-movement-ownership.md`](../research/2026-07-26-slice-j5-4-local-movement-ownership.md).
## J5.5 — per-session physics and remote simulation
Add a Runtime physics owner that constructs one `PhysicsEngine` per runtime
instance. App streaming publishes immutable prepared collision assets through
typed admission/withdrawal acknowledgements.
Move canonical remote simulation state and component indexes from App sidecars
to `RuntimeEntityRecord`/Runtime owners:
- remote body/interpolation/movement-manager state;
- canonical cell/rebucket writes;
- physics-host lookup;
- shadow/collision gameplay membership;
- ordinary-object update workset.
App consumes committed snapshots/events to:
- update `WorldEntity`;
- rebucket render projections;
- publish current root/part/equipped-child poses;
- refresh graphical collision/debug views.
Gate:
- complete flat collision conformance corpus remains bit-identical;
- fresh/reused engine parity and zero-allocation resolve profiles;
- player and remote position/cell/orientation parity;
- crowd de-overlap, doors, stairs, dungeon/cell seams, login placement;
- hidden/pending/rehydration, teleport correction, delete/GUID reuse;
- two concurrent Runtime instances prove scratch/shadow isolation.
**Completed 2026-07-26 at
`7e6033d0adcd0b572c20e89dd275746fb442d52f`.**
`RuntimeEntityObjectLifetime` now constructs one `RuntimePhysicsState`, which
owns the sole production `PhysicsEngine`, data cache/cell graph, transition
scratch, shadow registry, collision admissions, canonical bodies and hosts,
remote components, ordinary/remote worksets, simulation, and full-cell
commits. App publishes prepared collision through generation-scoped receipts,
supplies DAT/animation inputs, and projects immutable committed snapshots; it
does not construct or run a parallel solver. Runtime tests pass 314/314, App
tests pass 3,718/3 skips, and the complete Release suite passes 8,588/5 skips.
The exact-binary lifecycle/reconnect report
`logs/connected-world-gate-20260726-134107/report.json` passes both sessions,
7/7 readiness checkpoints, graceful code-zero exits, and zero failures. The
canonical route
`logs/connected-r6-soak-20260726-134638.report.json` matches source/binary,
passes all nine render/collision-ready destinations and movement exercises,
and exits gracefully with code zero and zero failures. Evidence:
[`../research/2026-07-26-slice-j5-5-physics-remote-ownership.md`](../research/2026-07-26-slice-j5-5-physics-remote-ownership.md).
## J5.6 — projectile runtime
Move projectile component identity, prediction versions, and simulation into
Runtime. Store the canonical component on `RuntimeEntityRecord`, not the App
sidecar.
Runtime uses:
- existing Core `ProjectilePhysicsStepper`;
- canonical body/cell/state/timestamp authority;
- strict prepared collision shapes;
- existing split Begin/Complete prediction with exact-version revalidation.
App acknowledges successful commits by:
- updating/rebucketing the render projection;
- synchronizing graphical shadow representation;
- publishing effect root/pose.
App does not decide collision outcome, impact, damage, effect, or deletion.
Gate:
- existing clamp/gravity/AlignPath/angular/collision/thin-wall fixtures;
- designated-target exclusion semantics;
- loaded/pending/cross-landblock/correction/delete/GUID-reuse;
- 96-owner lifetime/effect stress;
- no duplicated mesh, effect, shadow, body, or script owner;
- arrow, bolt, and representative spell projectile connected route.
## J5.7 — combined cleanup and closeout
Delete:
- temporary App owner fields and construction fallbacks;
- App simulation component dictionaries;
- current-runtime adapters that reconstruct moved snapshots;
- reset/disposal branches replaced by Runtime;
- compatibility aliases after all consumers borrow Runtime.
Add:
- one J5 ownership ledger;
- failure injection at every construction, subscription, command, projection
acknowledgement, reset, and disposal boundary;
- graphical/direct normalized traces;
- Runtime-only movement/use/combat/cast/projectile fixture host;
- source/dependency guards against App/backend leakage and duplicate owners.
Run:
1. focused Core physics/selection/combat tests;
2. Runtime state/command/event/teardown tests;
3. App input/interaction/combat/magic/physics/projectile tests;
4. Release build and complete Release suite;
5. exact-binary lifecycle/reconnect route;
6. canonical nine-stop movement/combat/jump route;
7. connected interaction, casting, projectile, portal, and reconnect route;
8. user graphical feel gate for movement, facing, use, combat/cast bars, and
visible missiles;
9. architecture, divergence, issue, roadmap, AGENTS/CLAUDE, and durable-memory
reconciliation.
**Completed 2026-07-26 at
`cdee7a4b49addb5e1500753f6a885f7c899bd0f0`.**
Runtime now owns accepted CreateObject vector installation, remote-motion
construction/activation, canonical vector commits, final simulation-component
retirement, terminal physics/shadow/workset cleanup, and one combined
entity/object/physics/gameplay ownership ledger. App compatibility views and
the duplicate remote-body initializer are deleted; graphical checkpoints
borrow direct Runtime views. A Runtime-only fixture drives movement, use,
combat, casting, and projectile simulation without loading App or a
presentation/backend assembly, and injected projection failure still converges
the complete ledger.
Runtime tests pass 323/323, App tests pass 3,717/3 skips, the complete Release
suite passes 8,596/5 skips, and the Release solution builds with zero errors.
The exact-binary lifecycle/reconnect report
`logs/connected-world-gate-20260726-160825/report.json` passes all seven
checkpoints and both graceful code-zero exits. The canonical report
`logs/connected-r6-soak-20260726-161406.report.json` matches embedded
binary/source SHA, passes all nine destinations and movement/combat/jump
exercises, converges every streaming backlog to zero, and exits gracefully
with zero failures. Evidence:
[`../research/2026-07-26-slice-j5-7-simulation-ownership-closeout.md`](../research/2026-07-26-slice-j5-7-simulation-ownership-closeout.md).
## Commit and rollback discipline
Each sub-slice lands as one independently reversible production commit followed
by a documentation/evidence checkpoint where needed.
Planned production commit sequence:
1. `refactor(runtime): own canonical action state`
(`b298f99f913249d299796e6a71f7a9b6b283ac1a`)
2. `refactor(runtime): own interaction transactions`
3. `refactor(runtime): own combat and magic intent`
4. `refactor(runtime): own local movement and outbound cadence`
5. `refactor(runtime): own per-session physics simulation`
6. `refactor(runtime): own projectile simulation`
7. `refactor(runtime): close simulation ownership`
Record each full SHA and exact `git revert <sha>` command in this file before
running its connected gate. Revert J5 groups in reverse order. Do not revert
J4 or the flat-collision Slice I cutover to mask a J5 ownership failure.
J5.1 rollback:
```text
git revert b298f99f913249d299796e6a71f7a9b6b283ac1a
```
J5.2 rollback:
```text
git revert f5f7b4177f449ebcad9724bfea263ef73ab60255
```
J5.3 rollback:
```text
git revert 20df9d155db50706a42420d60a9b15861cf4bfe7
```
J5.4 rollback:
```text
git revert aa3f4a60f87f2cbebc3d8ecd5e33d779b7fb13c1
```
J5.5 rollback:
```text
git revert 7e6033d0adcd0b572c20e89dd275746fb442d52f
```
J5.6 rollback:
```text
git revert 2aee33569f0268d7bc0f4f52437dfa0b405432a4
```
J5.7 rollback:
```text
git revert cdee7a4b49addb5e1500753f6a885f7c899bd0f0
```
## Acceptance
J5 is complete only when:
- every listed presentation-free owner is constructed by Runtime;
- App owns presentation only and borrows exact Runtime state;
- graphical and no-window commands produce the same ordered state and packets;
- collision and trajectory fixtures remain bit-identical;
- reset, reconnect, same-process multiple-instance, GUID reuse, and failure
injection converge every owner;
- no extra frame or per-frame allocation regression exists;
- complete Release and connected gates pass;
- the user accepts graphical movement, facing, interaction, combat/casting,
and projectile behavior;
- architecture, roadmap, milestones, issues/divergences, AGENTS/CLAUDE,
evidence, memory, and rollback instructions are synchronized.

View file

@ -1,407 +0,0 @@
# Modern runtime Slice J6 — world, transit, and host handshake
**Status:** J6 COMPLETE
**Parent:** `2026-07-25-modern-runtime-slice-j.md`, J6
**Authorization:** the user approved Slices FL, including gameplay-owner
moves, on 2026-07-24
**Purpose:** move the one authoritative world-environment and local-transit
lifetime into `AcDream.Runtime` while preserving the accepted retail portal
presentation and leaving graphical readiness, streaming, sky drawing, VFX,
audio, and UI in `AcDream.App`.
## 1. Retail ordering oracle
The load and placement boundary is fixed by the named retail client:
- `SmartBox::TeleportPlayer @ 0x00453910`
- `SmartBox::UseTime @ 0x00455410`
- `gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0`
- `CPhysicsObj::enter_world @ 0x00516170`
- `SkyDesc::CalcPresentDayGroup @ 0x00500E10`
- `CPlayerSystem::Handle_Admin__Environs @ 0x0055DE20`
The consolidated pseudocode is already captured in:
- `docs/research/2026-07-15-retail-portal-space-pseudocode.md`
- `docs/research/2026-07-16-portal-completion-pseudocode.md`
- `docs/research/2026-07-24-retail-streaming-retirement-pseudocode.md`
- `docs/research/2026-04-23-daygroup-selection.md`
The load-bearing order is:
```text
fresh F751 notification
establish one teleport generation
matching accepted Position
establish one exact destination
CellManager finishes destination load
host acknowledges exact generation + destination readiness
SmartBox::TeleportPlayer / CPhysicsObj::enter_world
commit position and destination cell
resume destination simulation
gmSmartBoxUI::EndTeleportAnimation
continue private tunnel presentation
TunnelFadeOut -> WorldFadeIn
reveal normal world viewport
WorldFadeIn tail ends
send LoginComplete and complete the generation
```
The graphical host may delay the readiness acknowledgement until render,
composite-texture, and collision publication have converged. It does not own
the teleport generation, destination identity, materialization state, or
simulation availability. A headless host supplies the same acknowledgement
contract using its own required domains.
## 2. Baseline owner inventory (before J6)
### 2.1 Authoritative state that J6 removed from App
| Baseline App owner | State | Final J6 owner |
|---|---|---|
| `WorldEnvironmentController` | server-synced world clock, selected day group, weather, AdminEnvirons override, debug overrides | `RuntimeWorldEnvironmentState` |
| static `DerethDateTime.OriginOffsetTicks` | mutable Region calendar origin shared by every process session | instance `DerethCalendar` owned by each environment |
| `TeleportTransitCoordinator<T>` | F751 freshness, Position correlation, accepted-destination history | `RuntimeWorldTransitState` |
| `WorldRevealLifecycleTelemetry` | reveal generation, destination, readiness, materialized/completed/cancelled/world-visible state | `RuntimeWorldTransitState` |
| `WorldGenerationAvailabilityState` | whether gameplay/world simulation may advance | borrowed projection of `RuntimeWorldTransitState` |
| `CurrentGameRuntimeViewAdapter.PortalView` | reconstructs Runtime portal state from App lifecycle state | direct borrowed Runtime view |
### 2.2 App state that must remain App
| Owner | Reason |
|---|---|
| `WorldRevealReadinessBarrier` | joins renderer, texture-upload, and collision-publication facts |
| `StreamingController` / destination reservation | graphical streaming and publication |
| `WorldRevealRenderResourceScheduler` | GPU upload priority |
| `PortalTunnelPresentation`, `TeleportAnimSequencer`, `TeleportViewPlaneController` | private graphical presentation |
| `WorldGenerationQuiescence` effects | clears graphical selection and suspends/resumes world audio |
| `LoadedSkyDesc` / `DayGroupData` projection | raw DAT-backed sky presentation and renderer input |
| terrain, EnvCell, sky, VFX, audio, UI owners | presentation |
### 2.3 State that already has the correct Runtime owner
- Current player full-cell and position: `RuntimeLocalPlayerMovementState`.
- Accepted entity position/timestamp state: `RuntimeEntityDirectory`.
- Canonical entity identity and incarnation: `RuntimeEntityObjectLifetime`.
- Physics placement/cell membership: `RuntimePhysicsState`.
- Network session generation and ordered transport: `LiveSessionController`.
J6 must borrow these owners. It may not add a second current-cell field, entity
map, movement body, session generation, or packet queue.
## 3. Target Runtime contracts
### 3.1 Environment
```csharp
public sealed class RuntimeWorldEnvironmentState
{
WorldTimeService WorldTime { get; }
WeatherSystem Weather { get; }
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
void Initialize(RuntimeWorldEnvironmentDefinition definition);
void SynchronizeFromServer(double ticks);
RuntimeEnvironmentEffect ApplyAdminEnvirons(uint changeType);
void RefreshDayGroup();
void ResetSession();
}
```
The immutable definition contains only presentation-independent values:
calendar origin, sky tick rate, and ordered day-group descriptors with the
Core `SkyStateProvider`. Runtime owns the selected index and weather state.
App retains the raw `LoadedSkyDesc` and resolves Runtime's active index to the
matching `DayGroupData` for drawing.
`WorldTimeService` receives an instance `DerethCalendar` and an injectable
`TimeProvider`; production no longer mutates process-global calendar origin or
reads wall time through a hidden static dependency.
### 3.2 Transit/reveal
```csharp
public readonly record struct RuntimeTeleportDestination(...);
public readonly record struct RuntimeDestinationReadiness(
long Generation,
uint DestinationCell,
bool IsUnhydratable,
bool RenderReady,
bool CompositeReady,
bool CollisionReady);
public sealed class RuntimeWorldTransitState : IRuntimePortalView
{
bool QueueTeleportStart(ushort sequence);
RuntimeDestinationOfferResult OfferDestination(...);
bool ActivatePending(out RuntimeTeleportDestination destination);
long BeginReveal(RuntimePortalKind kind, uint destinationCell);
bool AcknowledgeDestinationReadiness(in RuntimeDestinationReadiness ack);
bool AcknowledgeMaterialized(long generation, uint destinationCell);
bool AcknowledgeWorldViewportVisible(long generation);
bool Complete(long generation);
bool Cancel(long generation);
void ResetSession();
}
```
All mutators validate the exact active generation and destination. A stale
acknowledgement cannot open a newer destination. Readiness is an immutable
host acknowledgement; Runtime owns the resulting lifecycle state.
The snapshot separately exposes:
- destination readiness;
- simulation availability;
- materialization;
- normal-world viewport observation;
- completion/cancellation.
This preserves retail's two release edges:
1. materialization resumes destination simulation;
2. the later tunnel/world swap releases graphical destination reservations.
### 3.3 App adapters
`WorldRevealCoordinator` becomes a graphical host adapter:
- it asks Runtime to begin the generation;
- begins exact-generation streaming and render-resource reservations;
- evaluates App readiness and acknowledges the exact generation/cell;
- projects Runtime simulation-availability edges to selection/audio;
- releases graphical reservations only at the viewport edge;
- owns no lifecycle snapshot or generation counter.
`LocalPlayerTeleportController` remains the portal presentation orchestrator.
It borrows `RuntimeWorldTransitState` for F751/Position correlation and
lifecycle facts, while retaining only translated render-space placement,
camera/view-plane state, tunnel animation, and App callback reentrancy guards.
## 4. Execution ledger
### J6.0 — owner proof and detailed plan
- Re-read the named retail functions and existing consolidated pseudocode.
- Inventory every producer, consumer, reset edge, and checkpoint.
- Pin the target contracts, no-mirror rules, sub-slices, gates, and rollback
discipline in this document.
Gate: documentation/source audit proves every mutable field has exactly one
planned owner and no accepted presentation edge changes.
### J6.1 — instance-scoped environment owner
**Complete 2026-07-26 at `902076c0`.**
- Add `DerethCalendar` and make `WorldTimeService` consume its exact instance.
- Add `RuntimeWorldEnvironmentState` and immutable environment definitions.
- Reduce App's `WorldEnvironmentController` to DAT conversion plus
`IWorldSceneSkyStateSource` projection.
- Route TimeSync, AdminEnvirons, diagnostics, render weather, and sky lookup
through the one Runtime state.
- Delete production mutation of `DerethDateTime.OriginOffsetTicks`.
- Add two-instance isolation, day-group equality, TimeSync, debug override,
AdminEnvirons, reset, and teardown tests.
Gate: Core/Runtime/App focused tests, Release build, full tests, exact
day-group fixtures, and no Runtime dependency violation.
Result: Runtime now owns the one instance-scoped calendar, synchronized world
clock, weather state, selected day group, AdminEnvirons state, and debug
overrides. App only translates immutable DAT sky definitions and projects the
selected Runtime state into the renderer. The 332 Runtime tests, 3,718 App
tests / 3 skips, 8,611 complete Release tests / 5 skips, and exact-binary
connected lifecycle/reconnect route pass. Both connected clients used
`902076c0a42079596a8a273e9be402776cabf4b0`, completed all seven checkpoints,
and exited gracefully with code zero. Evidence:
[`../research/2026-07-26-slice-j6-1-environment-ownership.md`](../research/2026-07-26-slice-j6-1-environment-ownership.md).
### J6.2 — canonical reveal generation and typed readiness
**Complete 2026-07-26 at `a6860d55` plus connected correction
`acb845d8`.**
- Add `RuntimeWorldTransitState`, immutable readiness acknowledgement, and
direct `IRuntimePortalView`.
- Move lifecycle telemetry and generation state from App to Runtime.
- Make `WorldRevealCoordinator` a strict App host adapter.
- Replace App's mutable availability state with a read-only Runtime projection;
retain selection/audio side effects as exact edge observers.
- Reject stale generation, wrong destination, reordered, duplicate, and
post-cancel acknowledgements.
- Preserve unhydratable destination behavior and the five-second retail wait
cue without a timeout or forced reveal.
Gate: login, portal, supersession, stale acknowledgement, callback failure,
session reset, and exact two-release-edge fixtures pass.
Result: Runtime owns the sole monotonic reveal generation, destination,
typed readiness latch, materialization/simulation edge, viewport observation,
completion, cancellation, wait cue, and portal materialization count. App's
former lifecycle owner is deleted; `WorldRevealCoordinator` is a graphical
host adapter and `WorldGenerationAvailabilityState` is a read-only Runtime
projection. The first connected run correctly exposed that post-acceptance
render-readiness samples may regress while the accepted atomic edge must
remain latched; `acb845d8` ignores those later samples without mutating
Runtime truth or reporting false invariant failures.
Runtime tests pass 349/349, App tests pass 3,716/3 skips, and the complete
Release suite passes 8,626/5 skips. Exact-binary report
`logs/connected-world-gate-20260726-171724/report.json` records six capped
checkpoints and one uncapped fresh-process reconnect, graceful code-zero exits,
zero failures, zero reveal invariant failures, and only the expected
world-edge warning. Evidence:
[`../research/2026-07-26-slice-j6-2-reveal-ownership.md`](../research/2026-07-26-slice-j6-2-reveal-ownership.md).
### J6.3 — canonical F751/Position transit and placement handshake
**Complete 2026-07-26 at `6a063a27`.**
- Move `TeleportTransitCoordinator` state into the Runtime transit owner.
- Convert accepted network destination data once into
`RuntimeTeleportDestination`.
- Keep render-space translation/recenter and camera placement in App.
- Validate placement and materialization against the exact Runtime generation,
sequence, and destination cell.
- Derive current session cell from the existing Runtime movement/entity owner;
add no parallel current-cell field.
- Reset/cancel transit through Runtime's structural teardown.
- Delete the App transit coordinator and App portal-view reconstruction.
Gate: Position-before-F751, F751-before-Position, duplicate/stale/wrapped
sequence, superseding teleport, same-landblock, cross-landblock, dungeon,
mid-portal reset, and reconnect traces match.
`RuntimeWorldTransitState` now owns wrap-safe F751 history, pending/active
teleport sequence, both accepted packet orders, the exact destination, and
generation/sequence/cell placement and materialization validation. The former
App transit coordinator and accepted-destination mirror are deleted; App
retains only graphical portal and placement work.
Runtime tests pass 360/360, the focused App transit/reveal suite passes 34/34,
App tests pass 3,710/3 skips, and the complete Release suite passes 8,631/5
skips. Exact-binary report
`logs/connected-world-gate-20260726-175333/report.json` records six capped
checkpoints and one uncapped fresh-process reconnect, graceful code-zero exits,
zero failures, zero reveal invariant failures, and only the expected world-edge
warning. Evidence:
[`../research/2026-07-26-slice-j6-3-teleport-correlation.md`](../research/2026-07-26-slice-j6-3-teleport-correlation.md).
### J6.4 — projection acknowledgement and owner cleanup
**Complete 2026-07-26 at `18d17d8b`.**
- Make App presentation registration/withdrawal use typed generation-scoped
acknowledgements.
- Ensure simulation release, viewport reveal, reservation release, and final
completion each occur once and retain retryable suffixes after a callback
failure.
- Remove superseded App lifecycle/generation state and compatibility types.
- Extend normalized checkpoints and ownership ledgers with environment,
transit, readiness, and pending-host-ack counts.
- Add direct/no-window host fixtures proving Runtime can complete login,
portal, cancellation, and reset without loading App or a backend.
Gate: source/dependency guards find no App authority or Runtime presentation
dependency; every failure-injection ledger converges to zero.
Result: Runtime owns an exact host-projection token and typed remaining-stage
ledger for each reveal generation/destination. App retains only graphical
resource receipts attached to that token. Registration, simulation release,
reservation release, terminal completion, cancellation, supersession, and
reset preserve an exact retryable suffix through callback failure and
re-entrancy. Normalized checkpoints expose environment and transit ownership,
including pending host acknowledgements. Direct Runtime fixtures complete
login, portal, cancellation, and reset without loading App or a backend.
### J6.5 — connected and documentation closeout
**Complete 2026-07-26.**
- Run focused Core, Runtime, and App projects.
- Run `dotnet build AcDream.slnx -c Release`.
- Run the complete Release test suite.
- Rebuild the exact binary.
- Run fresh login, repeated recall/portal, world edge, dungeon, graceful
disconnect, reconnect, and mid-portal disconnect routes.
- Compare the accepted portal screenshots and presentation trace.
- Update architecture, code structure, milestone, roadmap, parent plan,
AGENTS/CLAUDE, divergence ledger, and durable memory.
Runtime tests pass 365/365, App tests pass 3,716/3 skips, and the complete
Release suite passes 8,642/5 skips. Exact-binary report
`logs/connected-world-gate-20260726-182805/report.json` records six capped
checkpoints and one uncapped fresh-process reconnect, graceful code-zero exits,
zero failures, and zero environment/transit/host ownership debt at every
stable checkpoint. Evidence:
[`../research/2026-07-26-slice-j6-4-host-acknowledgement.md`](../research/2026-07-26-slice-j6-4-host-acknowledgement.md).
J6 is complete only when automated and connected evidence proves:
- one Runtime environment owner;
- one Runtime transit/reveal owner;
- no stale destination acknowledgement can reveal a different generation;
- no App portal-state reconstruction remains;
- no portal timing, paperdoll, object-lifetime, VFX-tail, or world-void
regression;
- teardown and reconnect converge with no pending host acknowledgement.
## 5. Mandatory adversarial matrix
- two Runtime instances with different Region origins and server clocks;
- TimeSync while a debug override is active;
- day/year rollover and day-group reselection;
- every AdminEnvirons fog and sound value, plus unknown values;
- Position before F751 and F751 before Position;
- duplicate, old, equal, and `ushort`-wrapped teleport sequences;
- readiness for the wrong generation or destination;
- ready then superseded, materialized then cancelled, viewport ack before
readiness, and duplicate completion;
- destination recenter still pending;
- indoor render ready without EnvCell collision;
- outdoor collision ready without the full render neighborhood;
- unhydratable authoritative destination;
- callback reentrancy at begin, readiness, placement, viewport, and complete;
- disconnect during every portal animation state;
- same-process disconnect/reconnect and two simultaneous Runtime instances;
- graphical host withdrawal failure followed by retry;
- zero App/UI/Silk/OpenAL/Arch assemblies in direct Runtime tests.
## 6. Commit and rollback discipline
Each coherent production sub-slice lands separately after its focused,
Release, full-suite, dependency, and teardown gates. The exact production
commit and matching `git revert <sha>` are recorded here, in the parent plan,
the closeout evidence, architecture, and durable memory before moving on.
J6.1 exact rollback:
```text
git revert 902076c0a42079596a8a273e9be402776cabf4b0
```
J6.2 exact rollback, newest first:
```text
git revert acb845d812574206da1693fb379fec8a6f175fcb
git revert a6860d5563ae845a2cbb916abacf2b4aee515cdb
```
J6.3 exact rollback:
```text
git revert 6a063a27d4c805cd166d5a487afc290c34c400fd
```
J6.4 exact rollback:
```text
git revert 18d17d8bb17ed9ff3ed01da8273e2948abe6398b
```
Documentation-only research/plan commits are not behavior rollback points.
No workaround, timer, forced-ready path, duplicated owner, or alternate
headless behavior is permitted.

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