Source code for simvx.graphics.frame_globals

"""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;" )