Plan P1 iteration and P3 rendering milestones
Native and manual checks / native (ubuntu-24.04) (push) Failing after 32s
Native and manual checks / manual (push) Successful in 27s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Native and manual checks / native (ubuntu-24.04) (push) Failing after 32s
Native and manual checks / manual (push) Successful in 27s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# P1 gameplay iteration design
|
||||
|
||||
This design closes [PLAN P1](../../../PLAN.md#p1-lua-и-скорость-итераций) for Linux and Windows desktop 2D/3D authoring. It improves the path from changing gameplay or a scene to seeing the result in the Player. C++ remains the compiled gameplay language; Lua remains an optional module with development reload. MCP controls the Editor and authoring/build services, not the live Player world.
|
||||
|
||||
The Editor's dark visual treatment should be consistent with [the iteration console reference](../../design/p1-iteration-console-reference.png) and [the new-project reference](../../design/p1-project-template-reference.png). These images are visual references only. The behavior, data contracts, platform paths, accessibility and available actions in this specification and `PLAN.md` are the source of truth. The Windows path shown in the project image is illustrative; the real chooser must use each platform's native paths.
|
||||
|
||||
## Existing baseline and scope
|
||||
|
||||
The optional Lua VM, LuaLS annotations, component schemas, Inspector fields, atomic development reload and rollback already exist. `BuildService` already serializes GUI/MCP builds, uses incremental CMake/Ninja native trees separated by Debug and Release, publishes immutable successful generations, and preserves the last good generation after failure. The authoring service atomically journals every transaction under `.faset/recovery` and refuses to save over an externally changed scene. A disposable workflow script measures one changed C++ build and first Player frame. These remain the foundation, not new work to replace.
|
||||
|
||||
P1 adds verifiable reuse of unchanged schema/build packages, useful compiler diagnostics, convenient new-project choices and code navigation, autosave of named scenes, and repeatable iteration measurements. It also closes the recorded Lua validation gap on Windows and in a graphical Release export. P1 does not add C++ hot reload, dynamic gameplay loading, Lua state preservation across reload, Player-world MCP access, a built-in code editor, or broad performance guarantees. Dynamic gameplay loading is reconsidered only if measured native link time is a material bottleneck.
|
||||
|
||||
## User workflows
|
||||
|
||||
### C++ edit, build and Play
|
||||
|
||||
The developer edits `Scripts/Gameplay.cpp` or a header, presses **Build**, and sees a named job with phase, progress, elapsed time and diagnostics. A compiler error shows severity, a short message, project-relative file and one-based line/column when the tool supplies them. Selecting it opens the source at that location in the configured external editor. Raw compiler output remains available. A failed build leaves the prior Player/schema generation intact and marks Inspector metadata stale. An unchanged second build reports a cache hit and reuses the same validated generation. **Play** still captures the current authoring scene, invokes the build service, then launches an isolated Player; editing after capture does not silently change that snapshot.
|
||||
|
||||
### Lua edit and reload
|
||||
|
||||
A Lua project declares scripts in `project.faset.json` and can start with no project C++ files. Script edits mark schema metadata stale when declarations change; **Refresh Lua** validates schemas without recompiling unrelated C++ gameplay. The running development Player watches or explicitly reloads scripts; successful reload resets its world/script state, while an invalid edit preserves the old running version. The Editor and MCP surface parseable diagnostics with source locations where available. A packaged Release Player includes only the declared Lua snapshot and needs neither `slangc`, LuaLS nor the Editor.
|
||||
|
||||
### New project and source navigation
|
||||
|
||||
The new-project chooser offers four clear combinations: C++ 2D, C++ 3D, Lua 2D and Lua 3D. The existing CLI `--new NAME --dimension 2|3` and two-argument `BuildService::scaffold(name, dimension)` retain their C++ scaffold behavior for compatibility. The chooser and an explicit `--language cpp|lua` select a runnable template that adds a minimal start scene and documented starter behavior for the chosen dimension. Template creation never overwrites existing user files and rejects nonempty conflicting destinations. Lua starters include the module declaration and language-server setup without a C++ gameplay stub. The source browser opens C++, headers and Lua under `Scripts`; diagnostic rows open the exact project source line/column when supported by the configured editor.
|
||||
|
||||
`faset_source_open` accepts a project-relative path and optional one-based line/column, validates that the target is a regular `.cpp`, `.hpp`, `.h` or `.lua` file beneath `Scripts`, and launches an argv array without a shell. It rejects traversal, symlinks escaping the project, directories and executable files. The existing `faset_script_open` Lua-only command remains a compatibility alias. Editor configuration supports `{file}`, `{line}`, `{column}` and `{project}` tokens. The default Zed invocation uses its `path:line:column` syntax; custom commands can substitute the tokens. Missing editor executables yield a concise actionable error and never make a build fail.
|
||||
|
||||
### Autosave
|
||||
|
||||
Autosave is enabled by default for named, dirty scene documents while a persistent Editor session is running, including headless MCP sessions. It waits 2 seconds after the last document revision change and saves through `AuthoringService::save`; rapid edits coalesce. A project-level `editor.autosave` Boolean in `project.faset.json` controls it, with a visible toggle in Project settings and a read-only `faset_autosave_status` command. Changing the toggle takes effect immediately in the current session and persists for the next open. `Ctrl+S` and `faset_document_save` remain immediate explicit saves. An unnamed scene is never assigned an implicit path: the UI says **Save As required**, and the existing recovery journal protects its edits.
|
||||
|
||||
Autosave carries the document revision it observed into an optional `expected_revision` on the save API. A concurrent edit makes that save fail with `revision.conflict`, after which the newer revision is scheduled normally. Existing `disk_hash` comparison still rejects external modifications; autosave never silently overwrites them. A failed autosave leaves the document dirty and its journal intact, shows a persistent conflict/error with Save As or reload guidance, and does not log the same failure every frame. Another edit or an explicit retry permits a new attempt. Saving creates no Undo operation; Undo/Redo remain valid after autosave. Scene snapshots already captured for Play/export remain immutable. The recovery journal remains active even when autosave is disabled or before its timer expires.
|
||||
|
||||
## Build cache and schema correctness
|
||||
|
||||
The native build always runs CMake configure and Ninja/selected generator build. Those tools own source/header dependency analysis; a shortcut based only on `Session::source_signature()` is unsafe because that signature covers project `Scripts` but not engine sources, shaders, CMake recipes or toolchain changes. A no-op native build should do no compile or link work, but the UI must not call it a native cache hit merely because the later schema/package stage was reused.
|
||||
|
||||
After a successful native build, `BuildService` computes a versioned package key from the selected configuration, normalized build recipe and configure arguments, `CMakeCache.txt` and toolchain identity, complete project `Scripts` content snapshot, Lua declaration/fingerprint, Player and SchemaExporter hashes, all required SPIR-V/reflection hashes, and copied runtime-library hashes. The toolchain identity includes resolved compiler/CMake/Slang executable identity or content hash; changing a toolchain in place must force the native tree to be reconfigured/rebuilt or make the service refuse reuse. Asset source files are not gameplay-build inputs.
|
||||
|
||||
If the key matches the last successful immutable build, and every required file passes the stored manifest hash and schema validation, the service returns that generation with explicit `schema_cache_hit=true` and `generation_reused=true`. It skips SchemaExporter and package copying. A missing, malformed or corrupt cached file is never returned as a hit; the service attempts a fresh candidate or fails while retaining the prior pointer. A changed key runs SchemaExporter against the same captured Lua source snapshot, validates the complete schema, stages all files, rechecks the project source snapshot, then atomically publishes the new generation and pointer. Do not publish a mixed snapshot if any `Scripts` source changes during the build. Debug and Release have distinct keys and native trees. Export always revalidates referenced assets and packages their current generations even if gameplay build reuse succeeds; a stale asset must block export until reimport.
|
||||
|
||||
`JobStatus` and `faset_job` expose the reused-generation flag, schema hit flag, elapsed phase times and final build fingerprint. Existing `result.directory`, `result.player`, `result.schema` and the last-good pointer remain compatible. Cache results must be understandable in the GUI and MCP without parsing logs.
|
||||
|
||||
## Diagnostics and UI behavior
|
||||
|
||||
The build service keeps its bounded raw log and adds a structured `diagnostics` array: `{severity, phase, message, file?, line?, column?, code?}`. The parser handles Clang/clang-cl and Lua source-location formats on Linux and Windows, including drive letters and Unicode paths. Project files are normalized to project-relative paths; outside-project diagnostics remain visible as text but cannot be passed to `faset_source_open`. ANSI escape sequences do not contaminate messages. Unrecognized output stays in the raw log; nonzero exit without a parsed error still creates a generic job failure rather than an empty error panel. Multiline notes remain associated with the triggering error where practical.
|
||||
|
||||
The Console shows current and recent build jobs, counts by severity, phase, elapsed time, the first actionable error and expandable raw output. Selecting a diagnostic invokes the same `faset_source_open` command available to MCP. Disabled source actions explain why a location cannot be opened. The status bar distinguishes **Saved**, **Pending autosave**, **Saving**, **Save conflict**, **Save failed** and **Save As required**. Controls remain keyboard reachable, compact, high contrast and consistent with the Editor's dark theme. Layout and copy may evolve from the image reference as the actual controls are implemented.
|
||||
|
||||
## Measurement and validation
|
||||
|
||||
`tools/measure_workflows.py` retains disposable projects and machine-readable raw results. It records cold configure/build, warm unchanged build, changed C++ source/header build, build failure/recovery, Play-to-first-rendered-frame, Lua edit-to-successful-reload, and repeated Editor event-to-visible-state latency. Run at least five repetitions after a stated warm-up for warm/changed cases; retain each sample plus median and nearest-rank p95. Record exact revision and dirty state, OS, CPU, RAM, GPU/driver, compiler, CMake, Slang, build configuration, project/source hashes, scene size and whether validation/readback were enabled. Keep cold-start samples separate from warm samples; label offscreen first frame and windowed first presented frame separately using the Player's existing `startup_ms.main_to_first_frame` and `presentation_mode` profile fields. Include a larger generated scene/content case alongside the checked-in 2D/3D examples. Measurements are observations, not flaky CI timing gates.
|
||||
|
||||
For the documented Linux reference host and exact two sample scenes, track the existing budgets: measured frame p95 ≤ 4 ms, GPU and readback p95 ≤ 1 ms each, simulation and snapshot p95 ≤ 0.5 ms each, explicit live Vulkan allocations ≤ 20 MiB, and startup from `main()` ≤ 500 ms. Run a 3,000-frame warm resource-lifecycle sample to check for growth. Report any missed budget honestly and investigate; do not claim the limits for other machines or content. Record a separate available Windows baseline, distinguishing software Vulkan from physical GPU. If physical Windows hardware is unavailable, mark that coverage unverified rather than blocking the functional Windows CI result.
|
||||
|
||||
The Lua module must pass Linux and Windows native CPU suites; the watched/explicit reload integration must run under an available Vulkan device on both CI platforms; and a Lua-only Release export must validate and render after relocation with source project paths unavailable. Both Linux and Windows CI report exact test outcomes. Update the English Manual with C++/Lua iteration recipes, template selection, source editor setup, cache indicators, autosave/conflict recovery, and diagnostic navigation. Update `PLAN.md`, `docs/IMPLEMENTATION.md` and a dated validation dossier only after checks and measured evidence exist.
|
||||
|
||||
## Acceptance matrix
|
||||
|
||||
| Area | Required evidence |
|
||||
| --- | --- |
|
||||
| Lua | Existing lifecycle/safety/CLI suites pass on Linux and Windows; watched/explicit reload and relocated Lua-only Release export render on available Vulkan implementations. |
|
||||
| Cache | Second unchanged build reuses the same verified schema/package generation and does not invoke SchemaExporter; header, Lua, recipe/toolchain, shader or runtime output changes invalidate appropriately; corrupt entries never become hits. |
|
||||
| Rollback | Failed compile/schema/copy and source-race cases leave the previous successful pointer and Inspector schema available but stale; export still rejects stale assets. |
|
||||
| Diagnostics | Clang, clang-cl, Lua, Unicode path, drive-letter and unparseable-output fixtures pass; real C++ compile failure produces a navigable diagnostic and raw log. |
|
||||
| Templates/navigation | All four new-project combinations launch or validate, old CLI/scaffold defaults still work, existing files are not overwritten, source-open rejects traversal and opens project code at the requested position. |
|
||||
| Autosave | Named scene saves after idle, rapid edits coalesce, unnamed scene remains in recovery, disk/revision conflicts never overwrite, Undo and Play snapshot remain stable; GUI and MCP show status. |
|
||||
| Iteration | Raw repeated cold/warm/changed/Play/Lua/UI samples and environment metadata are committed; claims are limited to measured scenes/hosts; reference budgets and any misses are shown. |
|
||||
| Documentation | Strict MkDocs build and compiled tutorial tests pass; Manual, `PLAN.md` and validation dossier describe observed behavior and known coverage gaps. |
|
||||
|
||||
## Delivery sequence
|
||||
|
||||
Deliver safe cache keys and source snapshots first, then diagnostics/navigation, templates and autosave as independently testable slices. Finish with cross-platform Lua/export validation, repeated measurements, documentation and independent review. Each slice uses a failing contract test before implementation and a focused passing suite before its commit. The final P1 status is only marked complete when the acceptance matrix has linked evidence; unavailable physical Windows GPU or benchmark hardware is recorded as a coverage limit, not silently treated as passed.
|
||||
@@ -0,0 +1,51 @@
|
||||
# P3 lighting and shadows design
|
||||
|
||||
This design implements the lighting and shadow portion of [PLAN P3](../../../PLAN.md#p3-освещение-тени-и-temporal-reconstruction) for desktop 3D games. Temporal reconstruction has a separate design and implementation plan. The P2 Direct, GPU frustum, and GPU occlusion paths must shade the same scene from the same immutable snapshot. Ordered sprites, editor UI, and 2D presentation remain unlit unless a later 2D-lighting project explicitly changes them.
|
||||
|
||||
## Existing behavior and target
|
||||
|
||||
`Snapshot` currently carries one `light_direction`; `SceneView::build` overwrites it for each `faset.light`, without using the authored color or intensity. `fragmentMain` evaluates one hard-coded directional BRDF and samples one 1024² D32 shadow map. Its shadow matrix covers a fixed world-origin orthographic box. The shadow pass renders all `cast_shadow` meshes through CPU-transformed geometry even when P2 GPU visibility is selected; it is independent of the camera visibility decision. The compiled `gpuShadowMain` is not used by the current pass. These facts make multiple authored lights, large camera movement, and explicit shadow budgets impossible without changing the scene and shader contracts.
|
||||
|
||||
P3 supplies one sun plus point and spot lights, camera-fitted cascaded sun shadows, a bounded local shadow atlas, and independent per-shadow-view caster selection. Authored values reach exported Player games and the editor preview through the existing scene schema. A scene with no light component retains the previous default sun appearance. Lighting is accumulated in linear space before the current tone mapping; the legacy directional factor is preserved for the default so existing unlit UI and basic rendering tests remain meaningful. Artist-facing intensity is unitless in this stage; photometric units and IES profiles are outside P3.
|
||||
|
||||
## Scene contract and validation
|
||||
|
||||
Keep `faset.light` at builtin schema version 1 and add optional fields with defaults: `kind` (`directional`, `point`, `spot`, default `directional`), `enabled` (true), `color` (white), `intensity` (1, nonnegative), `range` (10, positive, for local lights), `inner_angle` (0.35 radians), `outer_angle` (0.70 radians, strictly below π/2), `casts_shadow` (true), and `shadow_priority` (integer 0). Existing fields and component IDs remain valid. Cross-field validation requires `0 <= inner_angle <= outer_angle`; local range, color channels, intensity, transforms, and cone directions must be finite. Invalid authored values return a diagnostic with entity and field rather than nonfinite GPU data. Missing new fields use the schema defaults.
|
||||
|
||||
`Snapshot` retains `light_direction` for existing direct-render clients. Add an optional `SunLight`, a vector of `LocalLight`, and an optional explicit `CameraFrustum` containing unjittered view/projection matrices, near/far distances, and projection kind. `SceneView` populates these from the authoring scene. Each light carries the stable entity/component identity, transformed position or normalized direction, color, intensity, range, cone angles, and shadow options. The first enabled directional light by stable ID is the sun; extra directionals produce a visible diagnostic until a later multi-sun design exists. If there is no authored directional light, the renderer synthesizes the legacy sun from `Snapshot::light_direction`. Local lights are ordered by stable ID to prevent reordering from changing atlas allocation or results. Point attenuation tends smoothly to zero at `range`; a spot multiplies it by a smooth inner-to-outer cone factor. The shader handles zero distance and invalid normals without NaN output.
|
||||
|
||||
Explicit camera frustum data is required for four cascades. If a low-level caller supplies only the legacy `view_projection`, the renderer uses one bounded legacy-compatible sun shadow view and reports `effective_sun_cascades = 1`; it never silently claims CSM. 2D sprite-only snapshots do not incur shadow work. The future temporal stage may jitter the main raster projection, but the shadow planner consumes the unjittered `CameraFrustum` exclusively.
|
||||
|
||||
## Graphics shader ABI and ownership
|
||||
|
||||
Preserve material descriptor **set 0** and its four bindings: sampled sun depth image at binding 0 (formerly the single shadow map), shadow sampler at 1, color image at 2, color sampler at 3. Add frame lighting descriptor **set 1** with a `StructuredBuffer` of a versioned, 16-byte-lane lighting header and local-light records plus one sampled 2D local depth atlas. Both the Direct pipeline and P2 GPU graphics pipeline bind this frame set; their fragment stage remains `fragmentMain` in `shaders/baseline.slang`. Move the P2 GPU vertex scene bindings (instance, visible-ID, view buffers) from set 1 to **set 2** in `shaders/gpu_scene.slang`, the Vulkan graphics layout, and the exact reflection validator. P2 cull and HZB compute bindings stay in their current set 0. Baseline graphics push constants remain 96 bytes and P2 graphics push constants remain 112 bytes, under the Vulkan 1.3 guaranteed 128-byte minimum. The shadow vertex entry can continue to read a per-view matrix through the existing push block, so no new shader entry is required for atlas rasterization.
|
||||
|
||||
Host and Slang structs have named offsets, strides, static assertions, and reflection checks. Keep the SPIR-V + normalized Faset reflection package as the runtime boundary; `slangc` remains a build dependency, not a Player dependency. Existing shader-reload behavior must preserve working pipelines when an incompatible or invalid bundle is offered. Update the package validator and export checks whenever an entry or layout changes. Lighting buffers are renderer-owned per frame and never expose Vulkan types to authoring, gameplay, or MCP.
|
||||
|
||||
## Sun cascades and atlas
|
||||
|
||||
Use four practical-split cascades between the camera near plane and `min(camera far, 80 world units)` with λ = 0.5. Bound each receiver frustum slice by a stable square extent derived from its enclosing sphere in sun-light space; snap the XY projection center to its effective texel grid so small camera movement does not shimmer. The sun atlas is one 2048² D32 image with four 1024² tiles, including a two-texel guard inside each tile. Its C++/shader metadata contains each light VP, split end, atlas scale/offset, effective UV clamp, and bias. Select the cascade by unjittered view depth and blend over the final 5% of a split to avoid a hard line. Three-by-three PCF samples clamp to that tile's guarded interior. Atlas rendering clears each tile independently and samples it only after the whole atlas transitions from depth attachment to depth read-only. The split/fitting, atlas, blend, PCF guard, and depth-bias considerations follow Microsoft's [CSM technical article](https://learn.microsoft.com/en-us/windows/win32/dxtecharts/cascaded-shadow-maps) and [shadow depth-map guidance](https://github.com/MicrosoftDocs/win32/blob/docs/desktop-src/DxTechArts/common-techniques-to-improve-shadow-depth-maps.md); Faset's actual implementation remains Vulkan-native.
|
||||
|
||||
The receiver frustum determines a cascade's XY footprint, but casters upstream of the camera slice still matter. Build shadow-view visibility from world-space caster bounds, never the main camera's P2 visible IDs. Extend the light-space depth interval conservatively over all cast-shadow bounds overlapping the cascade's XY footprint, then test candidates against that resulting view. Do not truncate this interval by the main camera far plane. Shadow geometry uses source LOD 0 initially, independent of the main camera's selected mesh LOD; a measured shadow-LOD policy can follow later.
|
||||
|
||||
## Local shadows and budgets
|
||||
|
||||
The local atlas is a separate 2048² D32 image with sixteen 512² guarded tiles. A spot light uses one perspective shadow view; a point light uses six 90° face views and receives all six tiles or none. A shadow tile stores its owner stable ID/face and generation. Rank candidate shadowed lights by authored `shadow_priority`, then projected influence, then stable ID; keep allocations stable while their owners remain eligible. Lower-priority lights that do not fit still illuminate the scene without shadowing, with an explicit reason/counter. No partial point-light shadow cubemap is allowed.
|
||||
|
||||
At most 128 local lights are submitted to the shader per view; select them deterministically by priority/influence if more exist and report the count of omitted lights. At most 16 local shadow faces and 4096 caster draws per frame are scheduled. Shadow views are atomic for draw budgeting: if all casters for a view do not fit, skip that view and treat its contribution as unshadowed. Sun cascades are considered nearest first; local views follow by priority. The policy exposes requested and effective counts. If the nearest cascade itself exceeds the draw budget, it becomes unshadowed rather than emitting an incomplete shadow. No skipped tile is sampled. Atlas allocation is capped at 32 MiB of D32 image payload for both atlases, excluding Vulkan alignment/driver overhead; actual allocation bytes remain visible in `FrameStats`.
|
||||
|
||||
Check D32 sampled/depth-attachment support, 2048² image extent, relevant sampled-image/storage-buffer limits, and allocation results before enabling the corresponding shadow feature. If the 2048² profile is unavailable, try a 1024² profile with four 512² sun tiles and sixteen 256² local tiles. If neither profile is available, keep direct lighting and explicitly report shadows unavailable. A failed optional atlas must not silently disable local illumination or crash a scene that previously rendered. Maintain explicit Vulkan transitions and barriers; the current `RenderGraph` validates ordering but does not synthesize barriers. One `ShadowAtlases` graph pass may loop over tile rendering instances because graph pass names must be unique.
|
||||
|
||||
Every scheduled view records why it was redrawn (first use, light/camera/caster change, atlas reassignment, or conservative every-frame update), its caster count, tile, and GPU time. Caching is optional in this stage: reporting an every-frame update is preferable to reusing stale depth. If caching is introduced, an uncertain caster revision forces a redraw. Camera cuts, resolution changes, light deletion, and slot reuse invalidate relevant assignments and histories.
|
||||
|
||||
## Forward+ measurement gate and diagnostics
|
||||
|
||||
Start with a correct bounded loop over local lights in the fragment shader. Benchmark fixed 1080p scenes with 0, 4, 16, 32, 64, and 128 visible local lights in Release, with shadows separately disabled and enabled. Retain raw per-frame CSV, driver/GPU/commit/configuration, warm-up and three repeated runs, median and p95 of main raster, shadow passes, full GPU command span, and capture/readback CPU time. The current full framebuffer readback is synchronous, so its CPU cost is not attributed to light shading. If 32 or more lights add at least 1.0 ms median to main raster or at least 15% of the light-free GPU frame on the Linux reference GPU, add tiled Forward+; otherwise record why the simple path remains default and keep the measurement harness for later hardware. A conservative, depth-free 16×16 screen-tile list built from projected light volumes is sufficient for the first measured optimization; it does not require a depth prepass before the existing forward pass. No light may disappear on list overflow: the fragment path checks an overflow flag and scans the full submitted-light list for that tile. Measure the sum of list construction and raster, not the raster pass alone, before enabling tiled mode by default.
|
||||
|
||||
`FrameStats`, Player diagnostics, and the editor debug overlay expose requested/effective sun cascades, visible and omitted local lights, scheduled/dropped shadow faces and reasons, atlas occupancy and allocated bytes, caster draws, shadow GPU time, and the active lighting path. Counters remain diagnostic-only; they do not require GPU readback for scheduling. Shadow atlas thumbnails may be added to developer diagnostics but are not a prerequisite for functional lighting.
|
||||
|
||||
## Acceptance and integration boundaries
|
||||
|
||||
CPU tests cover schema defaults/invalid fields, stable extraction from reordered entities, shadow-view math, cascade split/texel snap, offscreen casters, deterministic atlas allocation and face/draw capacity, point-light all-or-none behavior, and fallback capability policy. GPU image tests cover point distance, spot cone, color/intensity, sun cascade boundaries, caster movement, camera translation/cut/resize, point seams and atlas tile isolation, overflow as unshadowed light, and Direct vs P2 frustum/occlusion equivalence. Shader reflection, hot reload, exported Player shader contents, and Vulkan validation are exercised on Linux physical GPU and pinned Linux SwiftShader; Windows native build, GPU-labeled CI tests, and relocated exported Player examples run on pinned Windows SwiftShader. A physical Windows GPU is reported only if actually tested. The manual documents light properties, budgets, expected fallbacks, and script/editor examples. Acceptance records commit, hardware/driver, raw benchmark data, tests, known limitations, and links from `PLAN.md` without claiming universal speedups.
|
||||
|
||||
Temporal reconstruction owns previous transforms, velocity targets, jitter, history rejection, TAA, and upscaling. Lighting owns only stable unjittered shadow views and direct-light data. The shared files `renderer.hpp`, `renderer.cpp`, and `baseline.slang` need sequenced integration or an agreed ABI commit before concurrent feature work; temporal code must not repurpose the lighting descriptors or shadow camera metadata.
|
||||
@@ -0,0 +1,59 @@
|
||||
# P3 temporal reconstruction design
|
||||
|
||||
This design implements the temporal half of [PLAN P3](../../../PLAN.md#p3-освещение-тени-и-temporal-reconstruction) for Linux and Windows desktop games. The outcome is a selectable, observable 1:1 TAA path followed by a lower-internal-resolution temporal upscaler. The existing Direct renderer without temporal processing remains the reference and the default. P3 lighting and shadow work has its own design and can change the scene shader's lighting inputs without changing the temporal pass contract.
|
||||
|
||||
Temporal renderer and shader integration starts **after** the lighting plan's Task 2 checkpoint commit, `Share typed multi-light shading across Direct and GPU paths`. That commit establishes material descriptor set 0, frame-lighting set 1, and GPU graphics scene set 2; GPU cull/HZB compute descriptors remain in set 0. Temporal graphics variants consume the same material and lighting sets as `fragmentMain`, add prior-transform reads only from the GPU scene set 2, and leave the 96-byte Direct and 112-byte GPU graphics push constants intact. Temporal resolve/composite use their own checked pipeline layouts. The shared reflection and hot-reload validator must accept the combined ABI, not reconstruct the pre-lighting set 1 layout.
|
||||
|
||||
## Boundaries and modes
|
||||
|
||||
`RendererConfig` selects `TemporalMode::Off`, `TAA`, or `Upscale`; `Upscale` also specifies an internal render scale in `[0.5, 1)`. TAA uses scale `1`. A runtime setter lets Editor and Player select the same modes without changing scene authoring files. Requested and effective modes are recorded separately: unsupported compute/format/extent capabilities produce an explicit `Off` fallback and a readable reason. Invalid scale is rejected as configuration input, rather than silently clamped. An exported Player contains cooked SPIR-V and reflection; Slang is not needed at runtime. Temporal processing is renderer-side and adds no live-game MCP endpoint.
|
||||
|
||||
The output extent remains `Renderer::width()/height()`, the swapchain/capture extent and the pixel coordinate system of UI. At scale below one, opaque and world-space raster targets use an internal extent, with integer rounding made deterministic. `Snapshot::scene_rect` remains in output pixels; it is converted once to the internal viewport. Camera aspect and projection remain those of the output scene rectangle. Shadow map dimensions and light-space matrices never depend on render scale. HZB dimensions, viewport metadata, culling projection, and post-cull depth refer to the actual internal scene target.
|
||||
|
||||
## Frame data and history ownership
|
||||
|
||||
The caller supplies an **unjittered** `Snapshot::view_projection` and `projection`. Picking, gizmos, and authoring coordinates continue using them. The renderer chooses a deterministic Halton(2,3) subpixel offset, applies it only to the scene raster clip transform, and retains both current and previous jittered view-projection matrices. The offset is measured in internal scene pixels and enters clip space as `clip.xy += 2 * jitter / internalViewportSize * clip.w`; the sign is verified by an image/motion test for the Vulkan viewport convention. Culling and HZB projections must match the jittered geometry they test, with conservative edge handling. `Snapshot::camera_cut` is authoritative; a large discontinuity in camera position/orientation also rejects history when a caller fails to mark a teleport.
|
||||
|
||||
`InstanceTracker` already records `previous_model`, world bounds, a stable slot/generation, mesh identity and rendered-frame completion. That state becomes available to both Direct and GPU paths. HZB history and temporal color history have **independent** validity decisions; absence of HZB or a Direct visibility mode cannot invalidate an otherwise valid TAA history. Mesh/LOD replacement, reused slot, an anonymous draw, camera cut and changed view make that instance's velocity invalid. GPU `InstanceRecord` gains a previous model matrix while keeping the existing stable slot/generation lanes. The `metadata.x` low bit remains the HZB eligibility bit and a distinct bit marks temporal previous-transform eligibility; culling tests only the HZB bit. Dense candidate indices remain frame-local addresses, not temporal identities.
|
||||
|
||||
Temporal color/depth history is renderer-owned and double-buffered at **output resolution**. Each completed render commits one frame. A pending failed render must not promote history. The current active view may reuse history only if the previous frame has the same `view_id`, output/internal extents, scene rectangle, unjittered projection, temporal mode/scale, and compatible shader generation, and is not a cut. Switching among views safely resets history; retaining multiple cached histories is not required. Resize, shader reload, mode or scale change, and camera discontinuity reset it explicitly. The first valid frame uses current color only. The reset reason and whether a previous history was actually used are visible in `FrameStats` and Editor diagnostics.
|
||||
|
||||
## Motion and reconstruction signal
|
||||
|
||||
Both opaque mesh paths write the same per-pixel motion contract:
|
||||
|
||||
- Direct path: the CPU-transformed vertex carries current clip position, prior clip position computed from `InstanceUpdate::previous_model` and prior jittered view-projection, plus a validity flag. The existing Off vertex format/shader remains a compatible path.
|
||||
- GPU path: the vertex shader reads current/previous model matrices from the instance record and current/previous jittered view-projections from the view record. It emits current and prior clip coordinates plus validity. Both Main and Post raster use this contract.
|
||||
- The scene fragment writes display-UV motion (`currentUV - previousUV`), expected depth in the previous projection, and validity/reactive state to a sampled floating-point MRT. A missing prior transform, invalid clip `w`, nonfinite projection, or non-opaque/reactive fragment marks motion invalid. UI never contributes motion. UVs are local to the scene rectangle so internal and output extents map consistently.
|
||||
|
||||
The scene image remains the current renderer's display-referred LDR shading for this stage. History uses a higher-precision floating-point color image so repeated accumulation does not quantize to 8 bits; the final composite writes the existing `R8G8B8A8_UNORM` output exactly once. There is no exposure adaptation or HDR claim in this temporal implementation. P3 lighting may later move scene shading to linear HDR behind the same `sceneColor/depth/velocity -> resolvedColor` boundary.
|
||||
|
||||
The signal checklist is informed by [AMD's FSR 2 integration guide](https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/master/README.md#input-resources): it describes current color, depth, motion and reactive data, and explicit reset on camera cuts. Faset implements its own resolver and does not link FSR 2. Faset's **current-minus-previous local UV** motion sign and jitter-inclusive convention are its own contract; an FSR 2 adapter would have to convert to that API's documented motion and jitter conventions. [NVIDIA's Adaptive TAA paper](https://research.nvidia.com/publication/2018-08_adaptive-temporal-antialiasing) identifies blur and ghosting as temporal failure modes, which motivates the moving-image acceptance tests here; its adaptive ray-tracing algorithm is not part of this design.
|
||||
|
||||
The 1:1 TAA resolve samples current color/depth/velocity, dilates motion from the nearest depth in a 3×3 neighborhood at silhouettes, and computes `previousUV = currentUV - motion`. It rejects history when view history or fragment motion is invalid, coordinates leave the previous scene rectangle, or previous expected depth disagrees with sampled history depth beyond a depth-aware tolerance. Surviving history color is clipped to a current-color 3×3 neighborhood and blended with a bounded weight reduced by motion and reactive content. A newly exposed background pixel must use current color immediately; it may accumulate normally on following frames. The resolve writes color and depth to the next output-resolution history image. Sky/clear pixels use current color when reliable reprojection is unavailable.
|
||||
|
||||
`Upscale` reuses this validated resolve but samples current color/depth/velocity from a lower internal extent. It reconstructs the current signal at output-pixel positions, keeps output-resolution history, and uses the same rejection/dilation and first-frame spatial fallback. Switching render scale resets history. TAA must pass its moving-image tests before upscaling is enabled or described as complete; this path is a small, measured temporal upscaler, not a promise of Unreal TSR quality.
|
||||
|
||||
## Render graph and scene/UI split
|
||||
|
||||
The temporal path adds internal `sceneColor`, `sceneDepth`, and `sceneVelocity` targets while retaining `color` as the full-resolution final image used by Readback and Presentation. Existing Off rendering and its one-target shader remain available. The temporal graph orders passes as follows:
|
||||
|
||||
1. ShadowMap and optional P2 MainCull.
|
||||
2. Main scene raster clears scene color/depth/velocity and renders opaque meshes. Optional P2 BuildCurrentHZB and PostCull run from this scene depth, then PostRaster loads all three scene attachments and finishes deferred opaque meshes.
|
||||
3. World transparency and sprites render against the same internal scene depth before resolve. Pixels they blend into color mark their velocity/reactive value invalid, so stale opaque history cannot leak through moving translucent content. They do not acquire synthetic rigid-mesh motion. The existing ordering and depth behavior are retained.
|
||||
4. TemporalResolve reads the completed scene image/depth/velocity and previous history, writes the next color/depth history. TemporalComposite maps that resolved scene to the full-size `color` target.
|
||||
5. A separate output-resolution UI pass renders quads, text, gizmo overlays and Editor chrome on `color`, without jitter or TAA. Readback and Presentation then consume `color` as before.
|
||||
|
||||
Only the temporal-enabled branch needs new scene targets and a second color attachment in opaque pipelines. A one-target UI pipeline remains valid. `RenderGraph` validates ordering by names; Vulkan layout transitions and compute→sampled/attachment barriers are recorded explicitly. The renderer's single queue, fence-per-frame model makes history lifetime simple but its synchronous capture/readback cost remains part of full-frame timings. The present 12-slot GPU timestamp pool must grow enough to time every new pass instead of silently omitting the end of the graph.
|
||||
|
||||
## Shader package, controls and fallback
|
||||
|
||||
Temporal vertex/fragment, resolve and composite entry points have validated reflection, SPIR-V hash, descriptor types and record strides. Adding `previousModel` changes the GPU instance stride; the C++ static assertion, Slang record, reflection validator and GPU tests change together. Baseline Off shaders retain their existing entry points and layout fingerprints. Shader reload replaces a **complete** compatible temporal pipeline set atomically or preserves the working set; successful reload invalidates color history. BuildService copies every temporal `.spv` and `.reflection.json` into Linux/Windows exports and validates a relocated Player without Slang installed.
|
||||
|
||||
Editor diagnostics expose mode, scale, effective mode/fallback, internal extent, jitter, history accepted/reset reason, reject statistics when requested, GPU temporal pass time and memory. Player profiling records these same fields per frame. GPU readback for per-pixel diagnostics is optional and never required for the normal resolve. If temporal resources cannot be created on a supported Vulkan 1.3 device, the actual mode is `Off`, the frame still renders by the existing path, and the reason is reported. A failed temporal shader reload keeps the prior pipelines and does not corrupt history or change the visible mode.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Deterministic headless GPU sequences run for Direct, GPU frustum and GPU occlusion where supported. They cover static thin diagonals/wires at subpixel positions, slow camera pan, a moving rigid object, a doorway disocclusion, an explicit camera cut and unmarked teleport, projection/view/scene-rectangle/resize/mode/scale changes, anonymous and mesh-replaced instances, world transparency, sprites and pixel-exact UI. Tests compare current frames to `Off` and a high-resolution spatial reference, with fixed tolerance and documented image metrics rather than requiring bit-identical output across drivers. They assert zero Vulkan validation errors where the layer is present, no reappearance of old color after cut/reveal, correct actual mode and reset reason, and no UI softening. A clean fallback is exercised on a deliberately unsupported capability fixture.
|
||||
|
||||
Run CPU policy tests, reflection/package/tamper tests, Linux validation on physical GPU and software Vulkan, Windows software Vulkan CI and native builds, and relocated 2D/3D Player captures. Record the exact revision, driver/GPU, build mode, internal/output resolutions, temporal and full-frame GPU/CPU costs, memory, image differences, and limitations. These results gate any claim that TAA/upscaling improves image quality or frame time in a particular scene.
|
||||
Reference in New Issue
Block a user