Skip to content

Add first-person body compatibility and fix ray-visibility baseline defects - #42

Open
Overhatch wants to merge 13 commits into
ComfyFluffy:mainfrom
Overhatch:experiment/fix-first-person-baseline-defects
Open

Add first-person body compatibility and fix ray-visibility baseline defects#42
Overhatch wants to merge 13 commits into
ComfyFluffy:mainfrom
Overhatch:experiment/fix-first-person-baseline-defects

Conversation

@Overhatch

Copy link
Copy Markdown

Summary

  • Adds an opt-in FirstPerson Model compatibility bridge that submits a camera-safe first-person body alongside the world-space player stand-in.
  • Introduces explicit world, local-view, and reflection ray-visibility domains so the two representations do not contaminate each other's secondary rays.
  • Fixes first-person transmission continuity through water and glass, reflection self-visibility, overlapping dielectric exits, first-person shadow opacity, and ice shadow behaviour.

Safety and fallback behaviour

  • The feature is disabled by default.
  • Missing, unsafe, ambiguous, mismatched, or failing providers fall back to the baseline stand-in.
  • A two-slot geometry-table admission check prevents partial publication or out-of-bounds writes.

Verification

  • Game-verified locally on the first-person fix matrix; details available on request.
  • Unit coverage: visibility-domain algebra, reflection isolation, local-view admission and publication, packed 64-byte path records, exhaustive medium-stack semantics, and NaN-safe shadow-transmittance configuration.
  • git diff --check passes; branch is linear on main (330acd2).

Overhatch and others added 12 commits August 5, 2026 00:17
FirstPerson Model renders a complete first-person body state that vanilla's
pipeline never feeds into Caustica's ray tracer, so the player's own body
stayed in vanilla lighting while the world around it was path-traced. This
change brings that state into RT, in three parts.

First, a mod-agnostic extension point: the mainline gains a provider registry
and a per-frame camera-safety declaration, with no mod identity in the core.
The highest integer priority wins; a tie fails closed for the frame with one
WARN, and a false, missing, or throwing safety declaration is treated as
unsafe. The first-person instance's motion identity is -(entityId + 1),
disjoint from the vanilla body's positive id space, so history-map swaps need
no explicit reset. Off by default, the feature short-circuits before the
registry lookup: zero extraction, zero logging when disabled.

Second, the FirstPerson Model bridge. The mod already appends a complete
first-person state during vanilla's extraction pass, so the bridge picks that
state out and hands it to the extension point. No geometry is rebuilt; the
state's own x/y/z already carry the mod's computed offset, so the instance
anchor is correct automatically. The dependency is compileOnly; the mod is
neither bundled nor required at runtime.

Third, the camera entity now uses the first-person state as its single
instance with a fully visible mask when the provider delivers it, falling back
to the baseline mask and anchor otherwise. Dual-instance layouts blacked out
the torso: the offset layout let first-person surface rays hit the co-visible
vanilla body, and co-locating the instances wrapped the torso in the vanilla
head cube, measuring exactly (0,0,0) over 13%-33% of the frame. The
single-instance layout eliminates the black region.

Known issue: parts the provider hides do not participate in shadows, GI, or
reflections, so the player's own shadow has no head. This is the documented
cost of the single-instance layout, accepted in exchange for correct
first-person surface lighting.

Build / test: gradlew build, 52 testcases across 17 testsuites, all green.
Smoke-tested in a Fabric 26.2 instance with FirstPerson Model 2.7.2: the
first-person body participates in RT lighting with no black region.

Dependencies: build.gradle adds a Modrinth maven repository and a compileOnly
maven.modrinth:first-person-model dependency. CI's package job resolves it
from api.modrinth.com on the runner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Promote "local-view representation" and "world-space representation" into a
first-class renderer concept so both can coexist in one frame without
poisoning each other's secondary rays.

Adds a third ray inclusion mask (CULL_LOCAL_VIEW_SECONDARY) alongside the
existing secondary/primary masks, a payload bit (PAYLOAD_SURFACE_LOCAL_VIEW,
flags bit 8) that closest-hit sets from EntityGeom.reserved.x, and a single
derivation entry point secondaryMaskForSurface(). The domain is an OUTGOING
property of the surface a ray leaves: the indirect bounce loop latches it once
per hit and carries it to the next iteration from exactly one assignment site,
so a future continuation branch inherits the correct semantics instead of
silently keeping a stale domain.

visibility(), shadeReservoir() and resolveTransmissionGuide() take the mask as
a leading parameter, making an omission a compile error rather than a silent
fallback. SpecSurface carries the interface's domain because the reflection
probe runs after the transmission chain has already overwritten the global
payload. PathSegment gains pathFlags bit 11 so Pass B resumes a split
dielectric continuation in the domain its interface belonged to; the packed
48-byte stride is unchanged.

No instance sets ENTITY_GEOM_LOCAL_VIEW yet, so this is behaviourally
equivalent to the baseline: the new bit is always zero, secondaryMaskForSurface
always returns CULL_SECONDARY, and the transmission guide mask reduces to
CULL_PRIMARY. The block_outline inline query is deliberately untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Give the geometry table a carrier for per-instance semantics and use it to tag
the first-person body as the camera entity's local-view representation.

EntityGeom.reserved's low word (offset +56) now holds instance flags;
writeTableEntry takes entityGeomFlags as a mandatory parameter rather than a
defaulted overload, so a new instance path cannot silently inherit zero. That
choice paid for itself immediately: the compiler caught two call sites the
manual sweep had missed. The high word stays zero.

The local-view instance mask changes from 0xFF to
MASK_PRIMARY | MASK_LOCAL_VIEW_SECONDARY (0x06), so it is visible to the camera
ray and to secondary rays leaving a local-view surface, but invisible to world
secondary rays. Block entities, rigid reuse, particles and ordinary entities all
pass zero flags and keep their existing masks.

The double-representation itself is not enabled yet: captureEntities still
returns early after publishing the first-person body, so the camera entity
continues to produce exactly one instance per frame. With the experiment toggle
off nothing writes ENTITY_GEOM_LOCAL_VIEW at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the early return after publishing the first-person body, so the camera
entity now falls through to the ordinary extraction path and produces both a
local-view representation and a world-space stand-in. Their instance masks are
disjoint on secondary rays (0x06 and 0x01), which is what lets the player's
shadow keep its head while the visible first-person body has no dark patch.

Emitting two instances from one loop iteration reopens a bounds problem the
single-instance path could not have. The geometry table is sized exactly
maxEntities() with no slack, writeTableEntry addresses it by the pre-increment
build.count, and the loop's full() guard is evaluated once before the iteration
starts. Entering the camera-entity iteration with one free slot would therefore
write one entry past the end of the buffer. A two-slot precheck placed BEFORE the
provider is queried closes it: a short budget degrades to the stand-in alone,
byte-identical to the fallback path, rather than publishing half a player or
writing out of bounds.

Counting is split per D10: logicalCount increments twice because there are two
physical table entries, while capturedThisFrame and entitiesCaptured increment
once because there is one logical entity. The two increments the early-return
branch used to perform are removed, since the ordinary path already does each.

Adds unit tests for the visibility algebra and the admission arithmetic. The
mask tests read the real constants reflectively rather than restating them, so
they fail if the wiring changes. The full P6/P9 matrices need Minecraft entities,
a provider registry and live Vulkan buffers, so the remainder stays a review
item rather than a test that asserts nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both degradation and success were silent: a player whose entity budget ran out
just saw their hands disappear, with nothing in the log or the frame stats to
say why, and no way to confirm from outside whether the two representations were
actually coexisting.

Adds localViewInstances and worldStandInInstances counters next to the existing
firstPersonInstances stat. worldStandInInstances is counted where the stand-in's
instance actually lands, alongside entitiesCaptured, so it stays zero on any path
that captures nothing for the camera entity rather than reporting an instance
that was never emitted.

The two-slot precheck failing now logs a warning, suppressed to once per session
in the same style as the provider circuit-breaker, since the condition recurs
every frame and would otherwise flood the log.

With the experiment toggle off, none of this runs: the eligibility test still
short-circuits on FIRST_PERSON_COMPAT_ENABLED, so no counter moves and no warning
is emitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RtTerrain.stream measures lightGrid.publishReady under the stage name
terrain.lightGridPublish, but the name was missing from RtFrameStats'
hardcoded stage whitelist. The config flag defaults to false, so the
mismatch was completely silent until frame stats were enabled, at which
point Profile.indexOf threw IllegalArgumentException on the first terrain
tick and crashed on world entry.

The historical CSV from 2026-08-03 had a terrain.lightGridPublishMs column,
so the entry was dropped at some point rather than never existing; it is
restored in its original column position, directly after terrain.publish.
A local-view surface's reflection-class continuations and specular probes now take a dedicated 0x08 domain that no player representation's instance mask contains, so a first-person reflection cannot contain the player itself. The bounce loop latches a base domain for its direct-light queries and selects each continuation's domain at a single point once the scatter lobe is known.

Path records grow from 48 to 64 bytes: a three-layer medium stack carrying 16-bit identities (air and water reserved, other dielectrics shared for now), a two-bit continuation domain, and a camera-transmission continuity bit wired constant-false until the publication signal exists. Water-ness derives from the identity instead of packed booleans, and the Java-side queue allocation follows the new stride.
Every dielectric hit now publishes a canonical 16-bit medium identity through the payload (water reserved, others materialId + 2, table size validated at load), and the depth-3 stack resolves each exit against it: a current-layer match keeps the normal Fresnel exit, a deeper match removes only that layer while the ray passes straight through, an unmatched exit is optically inert, and a push onto a full stack fails the continuation closed instead of corrupting a layer. Non-nested overlaps - enter ice, enter glass, exit ice, exit glass - now recover the true surrounding medium, which is what produced the false TIR fragments when a held dielectric clipped through a world one.

An exhaustive Java reference model mirrors the semantics over every enter/exit sequence up to length six and locks the invariants: bounded depth, current is the most recent unexited medium, nested sequences equal plain LIFO, and air stays a bottom sentinel.

The ice material feature bit and its payload transcription land here as inert carriers; nothing sets or consumes them yet.
The first-person world stand-in no longer blocks shadow rays outright. Its TLAS instance is forced non-opaque so the shadow any-hit runs, multiplies the ray's transmittance by a configurable neutral factor exactly once per ray (an unused shadow-payload lane is the marker), and lets traversal continue - so the hard black blob in front of the feet becomes a soft partial shadow while opaque geometry behind the body still occludes fully in any intersection order. The factor ships as a video-settings percent slider, default 0.35, clamped to [0,1], read per frame through a new WorldPush.shadowPolicy lane.

Ice-family terrain (classified by sprite name, never by optical parameters) replaces its colored Beer-Lambert shadow tint with a fixed neutral 0.15 per interface: one ice block attenuates to about 2 percent, so the separated second shadow and blue cast disappear while stacked ice keeps darkening. All other translucent materials keep their colored transmission byte for byte.
FrameEntities now carries whether the camera entity's local-view representation actually landed in the geometry table this frame, sourced from the exact write that counts localViewInstances - so the signal and the counter can never disagree. The verdict is a pure four-gate conjunction (compatibility toggle, two-slot budget admission, provider capture readiness, completed table write) locked by an exhaustive unit test; every degraded path leaves it false. RtComposite mirrors the fact into WorldPush.flags bit 2 in the same frame with no hysteresis. Nothing consumes the bit yet.
With the local view published, every camera-visible transmission continuation - the Pass A terminal and transmission split records, pure transmissions, and optically inert pass-throughs - carries a continuity bit and stays in the local-view domain even across world water and glass. The bounce loop updates continuity and domain together at its single assignment point: transmission lobes keep the chain, reflection-class and diffuse lobes end it and return to surface-derived domains. The visible player therefore resolves to one representation on both sides of the waterline instead of switching bodies wherever refraction crosses it.

The transmission guide follows the same rule: published means the whole chain keeps the camera plus local-view union mask instead of re-deriving per crossing, unpublished keeps the baseline. An ice interface now ends the guide chain with a coherent interface tuple - depth, normal and motion all from the ice face - instead of reporting the destination behind it.
Math.clamp passes NaN straight through because it is unordered, so a NaN system property or config value could reach the GPU - for the new shadow transmittance that would poison every shadow ray's transmittance. Clamped settings now fall back to their default on NaN while infinities keep clamping to the range ends, and the shadow-transmittance setting locks the whole algebra in a test. Found by the final cross-review.
The 16-bit payload field (flags bits 9..24, registry cap 65533) overflowed on
heavily modded instances: 112180 records > 65533 made RtMaterialRegistry.rebuild
fail closed and RT fall back to vanilla.

- PAYLOAD_MEDIUM_ID_MASK covers bits 9..28; PAYLOAD_SURFACE_ICE moves to bit 29
- PackedPathSegment layers widen to u20, parent1 split 12+8 across
  mediumIds01/mediumId2; the 64-byte record stride is unchanged
- MAX_MEDIUM_IDENTITY_RECORDS becomes 1048573 (2^20-3), keeping the derived
  identity at or below 0xFFFFE so the 0xFFFFF sentinel is never allocated
- Java test mirror updated in lock-step plus a new capacity boundary test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant