"""FrameGlobals: the one per-frame global uniform block (design D4).
A single fixed 256-byte std140 block, defined ONCE here and mirrored by GLSL
(desktop) and WGSL (web) declarations that a completeness test pins to this
layout. The block is append-only: v1 fills the first five ``vec4`` slots and
reserves the remaining 176 bytes (zeroed) so later work packages can add fields
without moving any existing offset.
Binding budget (design D14):
* Desktop: set 0, binding 13 (forward set), optional group0 binding 1 on the
ShaderMaterial ABI.
* Web: group0 binding 1 (landed by RM-A6).
v1 fields (each a std140 ``vec4``, 16-byte aligned, offset = index * 16):
0 time_info (time, delta, frame_index, reserved)
1 wind (dir.x, dir.y, strength, gustiness)
2 wetness (global_wetness, rain_intensity, ripple_strength, reserved)
3 render_info (internal_w, internal_h, 1/internal_w, 1/internal_h)
4 render_info2(render_scale, output_w, output_h, reserved)
5..15 reserved (176 bytes, zeroed)
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
__all__ = [
"FRAME_GLOBALS_SIZE",
"FRAME_GLOBALS_BINDING",
"FRAME_GLOBALS_FIELD_OFFSETS",
"FRAME_GLOBALS_GLSL",
"FRAME_GLOBALS_WGSL",
"FrameGlobalsEnv",
"glsl_block",
"pack",
"zeroed",
]
# Total std140 block size in bytes (fixed, append-only). 16 vec4 slots.
FRAME_GLOBALS_SIZE = 256
# Canonical desktop binding on the forward descriptor set (set 0).
FRAME_GLOBALS_BINDING = 13
# Byte offset of each named v1 field (the layout-mirror test pins the GLSL block
# and this table to the same offsets). Every field is a std140 vec4.
FRAME_GLOBALS_FIELD_OFFSETS: dict[str, int] = {
"time_info": 0,
"wind": 16,
"wetness": 32,
"render_info": 48,
"render_info2": 64,
}
# Number of trailing reserved vec4 slots (176 bytes). 5 live + 11 reserved = 16.
_RESERVED_VEC4 = (FRAME_GLOBALS_SIZE // 16) - len(FRAME_GLOBALS_FIELD_OFFSETS)
[docs]
@dataclass(slots=True)
class FrameGlobalsEnv:
"""WorldEnvironment-sourced inputs to the wind/wetness slots of FrameGlobals.
Held by the renderer and written each frame by ``EnvironmentSync`` from the
scene's ``WorldEnvironment`` (when present). Defaults are calm/dry so a scene
with no environment produces a feature-off, byte-identical frame.
"""
wind_direction: tuple[float, float] = (1.0, 0.0)
wind_strength: float = 0.0
wind_gustiness: float = 0.0
wetness: float = 0.0
rain_intensity: float = 0.0
ripple_strength: float = 0.0
[docs]
def pack(
*,
time: float,
delta: float,
frame_index: int,
wind_direction: tuple[float, float] = (1.0, 0.0),
wind_strength: float = 0.0,
wind_gustiness: float = 0.0,
wetness: float = 0.0,
rain_intensity: float = 0.0,
ripple_strength: float = 0.0,
internal_w: float = 0.0,
internal_h: float = 0.0,
render_scale: float = 1.0,
output_w: float = 0.0,
output_h: float = 0.0,
) -> np.ndarray:
"""Pack the v1 FrameGlobals block into a 256-byte ``uint8`` array (std140)."""
words = np.zeros(FRAME_GLOBALS_SIZE // 4, dtype=np.float32)
inv_w = 1.0 / internal_w if internal_w else 0.0
inv_h = 1.0 / internal_h if internal_h else 0.0
words[0:4] = (time, delta, float(frame_index), 0.0)
words[4:8] = (wind_direction[0], wind_direction[1], wind_strength, wind_gustiness)
words[8:12] = (wetness, rain_intensity, ripple_strength, 0.0)
words[12:16] = (internal_w, internal_h, inv_w, inv_h)
words[16:20] = (render_scale, output_w, output_h, 0.0)
return words.view(np.uint8)
[docs]
def zeroed() -> np.ndarray:
"""Return a 256-byte all-zero block for the never-unbound fallback write."""
return np.zeros(FRAME_GLOBALS_SIZE, dtype=np.uint8)
# --- Shader mirrors -------------------------------------------------------
#
# The block body is identical across every binding site; only the ``layout``
# qualifier (set/binding) differs, so callers render it with ``glsl_block``.
_GLSL_BODY = """uniform FrameGlobals {{
vec4 time_info; // x=time, y=delta, z=frame_index, w=reserved
vec4 wind; // x=dir.x, y=dir.y, z=strength, w=gustiness
vec4 wetness; // x=global_wetness, y=rain_intensity, z=ripple_strength, w=reserved
vec4 render_info; // x=internal_w, y=internal_h, z=1/internal_w, w=1/internal_h
vec4 render_info2; // x=render_scale, y=output_w, z=output_h, w=reserved
vec4 _fg_reserved[{reserved}];
}} {instance};"""
[docs]
def glsl_block(set_index: int, binding: int, instance: str = "fg") -> str:
"""Render the canonical FrameGlobals GLSL uniform block for a given binding."""
header = f"layout(set = {set_index}, binding = {binding}, std140) "
return header + _GLSL_BODY.format(reserved=_RESERVED_VEC4, instance=instance)
# Canonical desktop declaration (forward set 0, binding 13).
FRAME_GLOBALS_GLSL = glsl_block(0, FRAME_GLOBALS_BINDING)
# WGSL mirror (web group0 binding 1). Wired by RM-A6; defined here so the block
# has a single source of truth. std140 vec4 fields map 1:1 to WGSL ``vec4<f32>``.
FRAME_GLOBALS_WGSL = (
"struct FrameGlobals {\n"
" time_info : vec4<f32>, // x=time, y=delta, z=frame_index, w=reserved\n"
" wind : vec4<f32>, // x=dir.x, y=dir.y, z=strength, w=gustiness\n"
" wetness : vec4<f32>, // x=global_wetness, y=rain_intensity, z=ripple_strength, w=reserved\n"
" render_info : vec4<f32>, // x=internal_w, y=internal_h, z=1/internal_w, w=1/internal_h\n"
" render_info2 : vec4<f32>, // x=render_scale, y=output_w, z=output_h, w=reserved\n"
f" _fg_reserved : array<vec4<f32>, {_RESERVED_VEC4}>,\n"
"};\n"
"@group(0) @binding(1) var<uniform> fg : FrameGlobals;"
)