rnd(M03): introduce renderer facade seam
Work-Package: M03-RND-FACADE-001 Agent: sindo-main-codex Tests: facade/focus/coordinate/manifest contracts, renderer dry-run, coordination and documentation gates Fidelity: focus targets, budgets, queues, caches and visible renderer behavior remain unchanged
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
@tool
|
||||
class_name WorldRenderFacade
|
||||
extends Node
|
||||
|
||||
## Stable main-thread boundary between runtime/tool callers and the monolithic
|
||||
## streaming renderer. The facade owns no queues, caches, nodes or RIDs.
|
||||
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
|
||||
## Internal renderer implementation resolved relative to this node.
|
||||
## The referenced node remains owned by its scene parent.
|
||||
@export var streaming_world_loader_path: NodePath = NodePath("..")
|
||||
|
||||
## Optional explicit Node3D source sampled in Godot world coordinates.
|
||||
## Runtime scenes use the player; capture and probe tools replace it with their camera.
|
||||
@export var streaming_focus_source_path: NodePath
|
||||
|
||||
var _streaming_world_loader: Node
|
||||
var _missing_loader_reported := false
|
||||
var _missing_focus_source_reported := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_resolve_streaming_world_loader()
|
||||
set_process(streaming_focus_source_path != NodePath())
|
||||
if streaming_focus_source_path != NodePath():
|
||||
# Child nodes become ready before their parent StreamingWorldLoader. Only
|
||||
# publish the initial focus here; the loader applies it after initialization.
|
||||
_capture_streaming_focus_from_source()
|
||||
|
||||
|
||||
func _process(_delta_seconds: float) -> void:
|
||||
_capture_streaming_focus_from_source()
|
||||
|
||||
|
||||
## Replaces the renderer focus. The immutable value remains caller-owned; the
|
||||
## internal streamer retains the reference for its next throttled refresh.
|
||||
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return
|
||||
loader.call("set_streaming_focus", streaming_focus)
|
||||
|
||||
|
||||
## Samples the configured source and requests target refresh from the internal
|
||||
## streamer. Returns false when either dependency or a valid focus is unavailable.
|
||||
func refresh_streaming_focus(force: bool = false) -> bool:
|
||||
_capture_streaming_focus_from_source()
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return false
|
||||
return bool(loader.call("refresh_streaming_focus", force))
|
||||
|
||||
|
||||
## Returns a detached read-only metrics snapshot for diagnostics and baselines.
|
||||
## Mutating the returned Dictionary cannot mutate renderer-owned state.
|
||||
func renderer_metrics_snapshot() -> Dictionary:
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return {}
|
||||
var metrics_variant = loader.call("render_baseline_snapshot")
|
||||
if not (metrics_variant is Dictionary):
|
||||
push_error("WorldRenderFacade received a non-Dictionary renderer metrics snapshot.")
|
||||
return {}
|
||||
return (metrics_variant as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _capture_streaming_focus_from_source() -> void:
|
||||
if streaming_focus_source_path == NodePath():
|
||||
return
|
||||
var focus_source := get_node_or_null(streaming_focus_source_path) as Node3D
|
||||
if focus_source == null:
|
||||
if not _missing_focus_source_reported:
|
||||
push_warning("WorldRenderFacade focus source is missing or is not Node3D: %s" % streaming_focus_source_path)
|
||||
_missing_focus_source_reported = true
|
||||
return
|
||||
|
||||
_missing_focus_source_reported = false
|
||||
var godot_position: Vector3 = focus_source.global_position
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||
godot_position.x,
|
||||
godot_position.y,
|
||||
godot_position.z
|
||||
)
|
||||
set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position))
|
||||
|
||||
|
||||
func _resolve_streaming_world_loader() -> Node:
|
||||
if is_instance_valid(_streaming_world_loader):
|
||||
return _streaming_world_loader
|
||||
var candidate := get_node_or_null(streaming_world_loader_path)
|
||||
if candidate == null:
|
||||
if not _missing_loader_reported:
|
||||
push_error("WorldRenderFacade cannot resolve StreamingWorldLoader at %s" % streaming_world_loader_path)
|
||||
_missing_loader_reported = true
|
||||
return null
|
||||
for required_method in [
|
||||
&"set_streaming_focus",
|
||||
&"refresh_streaming_focus",
|
||||
&"render_baseline_snapshot",
|
||||
]:
|
||||
if not candidate.has_method(required_method):
|
||||
if not _missing_loader_reported:
|
||||
push_error("WorldRenderFacade renderer implementation lacks %s" % required_method)
|
||||
_missing_loader_reported = true
|
||||
return null
|
||||
_streaming_world_loader = candidate
|
||||
_missing_loader_reported = false
|
||||
return _streaming_world_loader
|
||||
@@ -0,0 +1 @@
|
||||
uid://sxn3ai2ic18r
|
||||
Reference in New Issue
Block a user