Document P3 lighting and provisional acceptance

This commit is contained in:
Emil
2026-09-24 03:02:44 +03:00
parent b191ae0bed
commit 2dfff62f8c
10 changed files with 354 additions and 45 deletions
+21 -1
View File
@@ -16,7 +16,27 @@ The panel reports the previous completed frame: renderer wall time, GPU timestam
In **GPU occlusion** mode, enable **Show HZB** to inspect the current grayscale depth pyramid. The **Mip** slider selects a pyramid level; the preview starts at mip 3 to keep its readback small. A larger mip number shows coarser depth. The preview reads the HZB only while the panel and toggle are open, and only once per completed frame or mip change. Switching it off or closing the panel releases the preview; its GPU texture retires when the next frame begins. Opening diagnostics also enables readback of GPU visibility counters, which is disabled again when the panel closes. Disable the HZB preview for performance comparisons: its diagnostic copy and texture upload add GPU and CPU work. **Freeze counters** does not freeze the HZB image.
The Vulkan backend emits `VK_EXT_debug_utils` labels for `ShadowMap`, `ForwardAndUI`, `Readback`, and, when presenting, `Presentation`. A graphics capture tool that supports this extension can identify those command-buffer regions. Labels remain available without the Khronos validation layer when the extension is exposed; unsupported systems continue rendering and report labels unavailable. A submitted-label count confirms calls were emitted, not that an external capture tool was tested.
The **Lighting and shadows** section reports the actual local lights submitted
and omitted, requested/effective sun cascades, requested/rasterized local faces,
allocated local tiles, and shadow caster draws against the 4096-draw limit.
Dropped-face counters distinguish a full atlas, caster budget, and unavailable
atlas; `point` counts faces dropped as a complete six-face group. A light whose
shadow faces are dropped still illuminates without a shadow. Atlas memory is the
live explicit Vulkan allocation size for the separate sun and local atlases.
When GPU timestamps are available, the panel shows sun and local shadow pass
durations. A zero duration after a disabled sun or sprite-only frame confirms
that no sun shadow raster ran. The lighting path names the algorithm actually
used, so compare it with a benchmark's requested mode before interpreting costs.
See [Lighting](lighting.md) for the 128-light and 16-tile limits.
The Vulkan backend emits `VK_EXT_debug_utils` labels for `SunShadowAtlas`,
`LocalShadowAtlas`, `ForwardAndUI`, `Readback`, and, when presenting,
`Presentation`. A fallback frame can have no shadow-raster label. A graphics
capture tool that supports this extension can identify the command-buffer
regions. Labels remain available without the Khronos validation layer when
the extension is exposed; unsupported systems continue rendering and report
labels unavailable. A submitted-label count confirms calls were emitted, not
that an external capture tool was tested.
This module is disabled by default and is linked only to the graphical Editor and its dedicated test when enabled. Player and exported games do not link ImGui. No overlay control changes authoring documents, gameplay state or export settings.
+87 -37
View File
@@ -1,37 +1,82 @@
# Add lights to a 3D scene
# Light a 3D scene
Add a **Light** component to a scene entity. The entity's transform places a point
or spot light; its rotation aims a spot light along local negative Z. A directional
light uses the entity's orientation. Light colors and intensity contribute to the
mesh's linear PBR illumination before tone mapping. Sprites and UI retain their
unlit tint.
Select an entity in the **Scene** tree, choose **+ Add Component** in the
**Inspector**, and add **Light**. Its Transform places a point or spot light. A
spot light points along the entity's local negative Z axis; a directional light
uses the entity's orientation. Lights affect 3D meshes in linear PBR shading
before tone mapping. Sprites and Editor UI remain unlit.
The version-1 `faset.light` component has three `kind` values:
| Kind | Position and direction | Useful fields |
| Light kind | Coverage | Shadow cost |
| --- | --- | --- |
| `directional` | Direction from the entity transform | `color`, `intensity`, `casts_shadow` |
| `point` | Position from the entity transform; illuminates every direction | `color`, `intensity`, `range` |
| `spot` | Position and local negative-Z direction | `color`, `intensity`, `range`, `inner_angle`, `outer_angle` |
| `directional` | A sun-like direction, independent of position | Up to four cascade tiles |
| `point` | All directions within `range` | Six local-atlas tiles, assigned together |
| `spot` | A cone within `range` | One local-atlas tile |
Angles are radians. A spot's inner angle must not exceed its outer angle. Intensity
must be nonnegative and range positive. `enabled: false` keeps the component in the
scene without contributing light. The `shadow_priority` integer is reserved for the
bounded local-shadow scheduler; it does not change brightness.
The Light component's fields are:
In the current rendering checkpoint, one enabled directional light can cast the
existing single-map shadow. Point and spot lights illuminate meshes but do not yet
cast shadows. The [P3 lighting plan](https://github.com/emil28092005/Faset_Engine/blob/main/docs/superpowers/plans/2026-09-24-p3-lighting.md)
tracks cascades and the bounded local-shadow atlas. A scene with no Light component
keeps the legacy white sun so older projects retain their appearance. Adding any
Light component, even a disabled one, turns off that compatibility fallback. If
several directionals are enabled, Faset chooses the one with the smallest stable
entity ID and reports a diagnostic for the others.
| Field | Default | Meaning |
| --- | --- | --- |
| `kind` | `directional` | `directional`, `point`, or `spot` |
| `enabled` | `true` | A disabled light contributes no illumination |
| `color` | `[1, 1, 1, 1]` | RGB illumination color; alpha is part of the schema color value |
| `intensity` | `1` | Nonnegative brightness |
| `range` | `10` | Positive reach of point and spot lights |
| `inner_angle` | `0.35` | Full-strength spot cone half-angle, in radians |
| `outer_angle` | `0.7` | Outer spot cone half-angle, in radians; must be at least `inner_angle` |
| `casts_shadow` | `true` | Allow this light to use its shadow atlas |
| `shadow_priority` | `0` | Higher local-light selection and shadow priority; does not change brightness |
## Author a point light through MCP
The Inspector validates the spot angles together. Their allowed outer limit is
below π/2 radians. A point light does not depend on the entity's rotation.
After editing the light or Transform, save the scene as usual. [Editor
workspace](workspace.md) explains Inspector editing, Undo, and save conflicts.
Use `faset_schema` to inspect the current field IDs, then send a `faset_scene_edit`
batch with the document ID, current revision, and target entity ID. For example:
## Sun shadows and compatibility
With a 3D scene camera, a shadow-casting directional light uses four cascades
covering the camera near plane through at most **80 world units**, or the camera
far plane if it is closer. Faset blends samples near cascade splits and snaps
each shadow projection to texels to reduce shimmer during small camera moves.
Objects outside the camera view can still cast into a visible receiver: shadow
visibility uses each light's view and the source mesh's LOD 0, separately from
the main camera's Direct or GPU visibility result. A low-level renderer Snapshot
without an explicit camera frustum uses one compatibility sun view.
Only one enabled directional light is used. If there are several, Faset chooses
the one with the smallest stable entity ID and reports the ignored lights. A
scene with **no Light component** retains the older white sun. Adding any Light
component, including a disabled one, suppresses that compatibility sun. Thus a
local-only scene does not receive an unexpected directional light.
## Local shadow capacity and fallbacks
The renderer accepts at most **128** local lights per frame. It sorts candidates
by `shadow_priority` (highest first), then projected influence, then stable ID.
The `omitted_local_lights` counter reports lights beyond this limit; an omitted
light contributes no illumination. All authored light records are validated,
including candidates past the limit.
The separate local shadow atlas has **16 tiles**. A spot consumes one; a point
consumes six or none. Sun cascades and local shadows share a maximum of **4096
caster draws** per frame. A light whose shadow group does not fit the remaining
tiles or draw budget still illuminates, **without a shadow**. Disabling
`casts_shadow` also keeps illumination while skipping that light's shadow work.
The renderer reports requested faces, rendered faces, tiles, and drops by cause
in [Diagnostics](diagnostics.md) and the [Player profile](profiling.md).
Faset uses separate sampled D32 sun and local atlases, normally 2048×2048 pixels
each. If a device cannot use that size, the renderer tries 1024×1024; if a
sampled depth atlas cannot be created, the affected lights fall back to unshadowed
illumination and report unavailable shadow views. Scheduled atlas tiles are
cleared and redrawn each frame; there is no persistent shadow cache yet.
Sprite-only scenes, a missing sun, and a sun with `casts_shadow: false` skip sun
shadow raster work.
## Add a point light through MCP
MCP edits the **Editor document**, not entities in a running game. Use
`faset_schema` to inspect the current field IDs, then send a `faset_scene_edit`
batch with the document ID, its current revision, and a target entity ID:
```json
{
@@ -46,22 +91,24 @@ batch with the document ID, current revision, and target entity ID. For example:
"kind": "point",
"color": [1, 0.15, 0.1, 1],
"intensity": 8,
"range": 6
"range": 6,
"shadow_priority": 2
}
}]
}
```
Move the entity with its Transform component. `component.add` fills any omitted
light fields from the version-1 schema; use `component.set` for later edits. See
[MCP and command line](mcp.md) for revision and retry handling.
Move the entity with its Transform component. `component.add` fills omitted
fields from the schema; `component.set` changes an existing field. Save the
document with `faset_document_save`. [MCP and command line](mcp.md) covers
revisions, retries, and transactions.
## Supply lights directly from C++
When building a `faset::render::Snapshot` yourself, set
`authored_lights_present` to suppress the compatibility sun in a local-only scene.
Provide a stable ID for each light so future shadow scheduling remains independent
of submission order.
Code that constructs a renderer `faset::render::Snapshot` can supply lights
directly. Set `authored_lights_present` even when the only authored Light is
disabled, so the renderer does not synthesize the compatibility sun. Use stable,
unique IDs for deterministic capacity decisions:
```cpp
faset::render::Snapshot snapshot;
@@ -74,6 +121,7 @@ point.position = {-2, 1.5f, 0};
point.color = {1, 0.3f, 0.1f, 1};
point.intensity = 8;
point.range = 6;
point.shadow_priority = 2;
snapshot.local_lights.push_back(point);
faset::render::LocalLight spot;
@@ -88,5 +136,7 @@ spot.range = 9;
snapshot.local_lights.push_back(spot);
```
The renderer submits at most 128 local lights per frame in stable-ID order. Later
P3 work adds explicit overflow diagnostics and measured light-list optimization.
This is the **renderer Snapshot API**, not a gameplay `Update()` method. The
current gameplay scripting API does not expose live Light-component creation or
modification; author lights in the Inspector or through Editor MCP. See
[Gameplay scripting](../scripting/index.md) for the APIs available to game code.
+52 -6
View File
@@ -118,13 +118,59 @@ and reads back the full image, so `cpu_ms` is wall time including waits, not CPU
utilization. An open scene can run slower with HZB; visibility correctness and
full-frame speed are separate findings.
## Measure P3 lighting and shadows
A Player `--profile` sample includes `effective_lighting_path`, local lights
submitted/omitted, requested/effective sun cascades, requested/rasterized local
shadow faces, tile use, shadow drop reasons, caster draws, and explicit atlas
allocation bytes. `gpu_main_raster_ms`, `gpu_sun_shadow_ms`, and
`gpu_local_shadow_ms` are GPU timestamps or `null` when timestamps are
unavailable. A light can illuminate while its shadow faces are dropped. A
submitted-light count of zero is a different workload from 128 lights whose
shadows are disabled. See [Lighting](lighting.md) for the capacity policy and
[Diagnostics](diagnostics.md) for the Editor counters.
The fixed-scene benchmark compares 0, 4, 16, 32, 64, and 128 local lights under
Direct, GPU frustum, and GPU occlusion visibility, with shadows on and off. Its
wrapper runs three independent 1920×1080 repetitions per configuration, each
with ten warm-up and thirty recorded frames. First inspect the planned matrix:
```sh
python3 tools/benchmark_p3_lighting.py --list-runs
```
From the repository, after a Linux Release renderer build, run one shadow setting
into a new output directory. Supply the actual device driver identity:
```sh
python3 tools/benchmark_p3_lighting.py --sweep \
--executable build/linux-release/faset_p3_lighting_benchmark \
--output .cache/p3-lighting-off \
--shadows off --driver 'REPLACE_WITH_ACTUAL_DRIVER' --validation off
```
The wrapper writes one raw CSV per run, `merged.csv`, and `summary.json`. Keep
all three with the exact source revision and device. It checks that every run
used its requested visibility mode and submitted every requested light. GPU
timestamps for the main raster isolate fragment-heavy lighting better than
renderer wall time, which includes GPU waits and synchronous readback. Shadow
time is split into sun and local GPU durations. The Forward+ decision compares
the median of three run medians against the matching zero-light configuration;
the threshold is **1.0 ms extra main raster time or 15% of the zero-light GPU
frame** at 32, 64, or 128 lights on the Linux physical reference GPU. The
[P3 lighting validation record](https://github.com/emil28092005/Faset_Engine/blob/main/docs/validation/p3-lighting-2026-09-24/README.md)
states the measured decision and scope. A software Vulkan run checks
functionality, not physical GPU performance.
## Current performance scope
The accepted MVP path uses direct draws and CPU culling; P2 adds optional GPU
visibility for opaque static meshes, with prepared LODs supplied by the project.
Both paths currently use one graphics queue and synchronous full-image
capture/readback. Use measurements to find the next bottleneck before introducing
parallel jobs or expanding GPU-driven rendering. Neither an offscreen capture
benchmark nor a tiny demo is a promise of a production frame budget. Observed
measurements and follow-up targets belong in the implementation acceptance report
with their source revision and method.
P3 adds local lights and bounded sun/local shadow atlases. The benchmark's
`lighting_path` and a Player profile's `effective_lighting_path` identify the
algorithm actually used. Both paths currently use one graphics queue and
synchronous full-image capture/readback. Use measurements to find the next
bottleneck before introducing parallel jobs or expanding GPU-driven rendering.
Neither an offscreen capture benchmark nor a tiny demo is a promise of a
production frame budget. Observed measurements and follow-up targets belong in
the implementation acceptance report with their source revision and method.