render: extract WMO runtime Mesh finalizer

This commit is contained in:
2026-07-18 14:33:48 +04:00
parent 49e70260ed
commit ae40e3f1d4
9 changed files with 659 additions and 72 deletions
+16
View File
@@ -1449,6 +1449,22 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- ADT parsing, quality tasks/results, tile state, cache format versions, material/ - ADT parsing, quality tasks/results, tile state, cache format versions, material/
Node/RID finalization, budgets and visible terrain behavior remain loader-owned. Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
## 2026-07-18 WMO Runtime Mesh Finalizer Extraction
- `WmoRuntimeMeshFinalizer` now owns cached WMO Mesh material refresh version
`10`, in-place ArrayMesh surface iteration and WMOBuilder material-definition
reconstruction.
- `StreamingWorldLoader` retains cached-scene traversal, lightweight build-job
traversal, Node/MultiMesh attachment, permits and lifetime, and delegates each
borrowed Mesh to the service.
- The historical metadata keys, compact texture0/texture1/texture2 ordering,
flags/shader/blend values, cached shader colors and exact Mesh identity are
unchanged. Null/unmarked surfaces and null builder results retain their prior
Material.
- Asset-free verification covers `27` identity/version/type/material/source
cases and 1,000 current-Mesh calls. This is orchestration extraction, not new
build-12340 material or visual parity evidence.
## Practical Rule For Future Work ## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it. If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
+1
View File
@@ -49,6 +49,7 @@
| WMO render Resource finalizer | Implemented extraction | [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) | | WMO render Resource finalizer | Implemented extraction | [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) |
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) | | WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
| WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) | | WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) |
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
+212
View File
@@ -0,0 +1,212 @@
# WMO Runtime Mesh Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-RUNTIME-MESH-FINALIZER-001` |
| Owners | Cached WMO runtime Mesh refresh version, surface iteration and material reconstruction |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-mesh-finalizer`, 2026-07-18 |
| Profiles/capabilities | Profile-independent cached WMO material refresh |
## Purpose
Finalize stale cached WMO runtime Mesh materials outside
`StreamingWorldLoader`: preserve exact Mesh identity, stamp the historical
refresh version and reconstruct eligible surface materials through WMOBuilder.
## Non-goals
- Traverse scene Nodes or own Mesh/MultiMesh attachment and lifetime.
- Select WMO cache paths, poll ResourceLoader or schedule render build jobs.
- Change WMOBuilder shaders, textures, blend rules or cache serialization.
- Share an abstraction with the behaviorally different M2 finalizer.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader traversal/build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer]
Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary]
Builder -->|rebuilt Material| Finalizer
Finalizer -->|same Mesh identity| Loader
```
The loader owns scene traversal and composition. The finalizer owns only the
in-place Resource operation and depends on an injected WMO material builder.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `finalize_mesh(mesh, extracted_directory)` | Command/query | Stamp and refresh a stale Mesh in place | Renderer main thread; stateless between calls | Null returns null; unsupported Mesh returns exact identity |
| `rebuild_cached_material(material, extracted_directory)` | Query/boundary | Reconstruct one metadata-bearing cached WMO Material | Renderer main thread; borrowed input | Null/unmarked/missing builder returns null |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `Mesh` and refresh metadata | Cached WMO scene/render group | Finalizer | Borrowed Resource | Call duration; identity retained |
| Input | Extracted content directory | Loader configuration | WMOBuilder | Borrowed String value | Call duration |
| Input | Texture paths, WMO flags/shader/blend and cached colors | Surface Material metadata/parameters | Finalizer | Borrowed values | One surface rebuild |
| Output | Compact texture-path array and material definition | Finalizer | WMOBuilder | Detached values | One builder call |
| Output | Rebuilt Material | WMOBuilder | ArrayMesh surface | Surface adopts exact Resource | Mesh lifetime |
| Output | Exact input Mesh | Finalizer | Loader traversal/build step | Caller retains ownership | Existing cache/scene lifetime |
## Data flow
```mermaid
flowchart TD
Mesh[Borrowed Mesh] --> Null{Null?}
Null -->|yes| ReturnNull[Return null]
Null -->|no| Current{Refresh version at least 10?}
Current -->|yes| ReturnSame[Return exact Mesh]
Current -->|no| Stamp[Stamp version 10]
Stamp --> Array{ArrayMesh?}
Array -->|no| ReturnSame
Array -->|yes| Surface[Iterate surfaces]
Surface --> Eligible{Material has texture0 metadata?}
Eligible -->|no| Keep[Keep exact Material]
Eligible -->|yes| Definition[Compact paths and copy metadata/colors]
Definition --> Builder[WMOBuilder build material]
Builder --> Result{Non-null result?}
Result -->|yes| Adopt[Replace surface Material]
Result -->|no| Keep
Keep --> Surface
Adopt --> Surface
Surface -->|done| ReturnSame
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Stale: missing/version below 10
[*] --> Current: version at least 10
Stale --> Current: stamp before optional surface refresh
Current --> Current: subsequent calls are identity-only
```
The version stamp is intentionally applied before type/material eligibility, as
in the extracted loader behavior. The finalizer retains no Mesh or Material.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as WmoRuntimeMeshFinalizer
participant Mesh as ArrayMesh
participant Builder as WMOBuilder
Loader->>Finalizer: finalize_mesh(mesh, extracted_directory)
Finalizer->>Mesh: read/stamp refresh metadata
loop each stale ArrayMesh surface
Finalizer->>Mesh: surface_get_material(index)
alt cached WMO metadata exists
Finalizer->>Builder: _build_material(definition, compact paths, directory)
Builder-->>Finalizer: rebuilt Material or null
Finalizer->>Mesh: surface_set_material when non-null
end
end
Finalizer-->>Loader: exact Mesh identity
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer]
Finalizer --> Engine[Mesh / ArrayMesh / Material]
Finalizer --> Builder[Injected WMO material builder]
Finalizer -. no dependency .-> Nodes[Node traversal/lifetime]
Finalizer -. no dependency .-> RL[ResourceLoader]
Finalizer -. no dependency .-> Queue[Build queue/scheduler]
```
## Ownership, threading and resources
- The renderer main thread serializes Mesh metadata and surface mutation.
- The caller owns Mesh identity, scene traversal, attachment and destruction.
- ArrayMesh surfaces adopt only non-null builder results; otherwise the exact
cached Material remains attached.
- The service retains no Resource, Node, RID, file, queue or per-path state.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null Mesh | Guard | Return null | Synthetic contract | Correct caller input |
| Current Mesh | Version metadata | Return exact identity, no builder call | Identity/version contract | Version bump makes it stale |
| Unsupported Mesh subtype | Type check after stamp | Return exact identity | PrimitiveMesh contract | No surface work required |
| Null/unmarked Material | Metadata guard | Preserve exact surface value | Surface contract | Rebuild cache with metadata |
| Missing builder/null result | Boundary guard/result | Preserve exact Material | Synthetic contract | Correct composition/retry after version bump |
| Shutdown | Loader lifecycle | No retained work to cancel | Shutdown suite | New loader composes a new service |
## Configuration and capabilities
Refresh version `10` and metadata key `wow_wmo_material_refresh_version` are the
existing runtime compatibility boundary. No project setting, profile, permit or
feature flag is introduced.
## Persistence, cache and migration
The module writes only the existing runtime Mesh metadata stamp and serializes
nothing. It does not change WMO scene/render cache formats or trigger rebakes.
A future material-rule change must deliberately bump the refresh version.
## Diagnostics and observability
The finalizer emits no logs and allocates no metrics. Existing `wmobuild` queue
metrics and loader diagnostics remain the operational correlation surface.
## Verification
- `verify_wmo_runtime_mesh_finalizer.gd`: null/current/non-ArrayMesh identity,
version stamp, null/unmarked surface retention, compact path indices,
flags/shader/blend/default and shader colors, exact builder adoption,
missing-builder behavior, source ownership and 1,000 current calls under one second.
- Adjacent WMO queue/cache/finalizer, shutdown, material and baseline regressions
protect orchestration and visible output.
- Fidelity evidence is exact behavior-preserving extraction; no private asset or
original-client visual-parity claim is added.
## Extension points
- Asset-backed WMO fixtures may compare reconstructed surface parameters without
changing the service contract.
- A deliberate material refresh change may bump the owned version with old/new
fixtures and paired visual evidence.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Runtime refresh admission | Implemented extraction | Identity/version/type contracts | Serialized cache fixture pending |
| Cached material definition reconstruction | Implemented extraction | Path/metadata/color contracts | Asset-backed visual comparison pending |
| Scene traversal/materialization | Loader-owned | Existing WMO regressions | Further safe extraction pending |
## Known gaps and risks
- Surface mutation and WMOBuilder material construction remain synchronous
main-thread work.
- No proprietary WMO corpus, long leak run, GPU timing, traversal p95/p99 or
paired build-12340 visual capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Refresh admission, surface iteration and material definition reconstruction |
| `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO shader/material construction semantics |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, Node traversal, build jobs and lifetime |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression |
## Related decisions and references
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md)
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+3
View File
@@ -622,6 +622,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending | | WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | | WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
| WMO scene Resource finalizer | Implemented extraction | Status/order/type/probe/lifetime/adoption/source/timing contract | Serialized stale/oversize and asset-backed evidence pending | | WMO scene Resource finalizer | Implemented extraction | Status/order/type/probe/lifetime/adoption/source/timing contract | Serialized stale/oversize and asset-backed evidence pending |
| WMO runtime Mesh finalizer | Implemented extraction | Identity/version/material-definition/source/timing contract | Asset-backed visual/leak/GPU/p95/p99 evidence pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity | | WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending | | ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity | | Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
@@ -698,6 +699,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes | | `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes |
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication | | `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication |
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication | | `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication |
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation | | `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | | `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
@@ -734,6 +736,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression | | `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression |
| `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression | | `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression |
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression | | `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,105 @@
class_name WmoRuntimeMeshFinalizer
extends RefCounted
## Refreshes cached WMO runtime Mesh materials through the configured builder.
## Scene traversal, placement and Node lifetime remain caller-owned.
const MATERIAL_REFRESH_VERSION := 10
const MATERIAL_REFRESH_VERSION_METADATA := "wow_wmo_material_refresh_version"
var _wmo_material_builder: Object
func _init(wmo_material_builder: Object) -> void:
_wmo_material_builder = wmo_material_builder
## Finalizes a stale Mesh in place and returns the exact input Resource identity.
## Null remains null; already-current meshes do not cross the builder boundary.
func finalize_mesh(mesh: Mesh, extracted_directory: String) -> Mesh:
if mesh == null:
return null
if int(mesh.get_meta(MATERIAL_REFRESH_VERSION_METADATA, 0)) >= MATERIAL_REFRESH_VERSION:
return mesh
mesh.set_meta(MATERIAL_REFRESH_VERSION_METADATA, MATERIAL_REFRESH_VERSION)
if not (mesh is ArrayMesh):
return mesh
var array_mesh := mesh as ArrayMesh
for surface_index in range(array_mesh.get_surface_count()):
var rebuilt_material := rebuild_cached_material(
array_mesh.surface_get_material(surface_index),
extracted_directory
)
if rebuilt_material != null:
array_mesh.surface_set_material(surface_index, rebuilt_material)
return mesh
## Rebuilds a material carrying the WMO cache metadata contract. Materials
## without texture0 metadata and failed builder results remain unchanged by the caller.
func rebuild_cached_material(
material: Material,
extracted_directory: String
) -> Material:
if (
material == null
or not material.has_meta("texture0_path")
or _wmo_material_builder == null
):
return null
var texture_paths := PackedStringArray()
var texture0_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture0_path", ""))
)
var texture1_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture1_path", ""))
)
var texture2_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture2_path", ""))
)
var diffuse_color := Color.WHITE
var emissive_color := Color.BLACK
var secondary_color := Color.WHITE
if material is ShaderMaterial:
var shader_material := material as ShaderMaterial
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
if diffuse_value is Color:
diffuse_color = diffuse_value
if emissive_value is Color:
emissive_color = emissive_value
if secondary_value is Color:
secondary_color = secondary_value
var material_definition := {
"texture0": texture0_index,
"texture1": texture1_index,
"texture2": texture2_index,
"flags": int(material.get_meta("wow_flags", 0)),
"shader": int(material.get_meta("wow_shader", 0)),
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
"diffuse_color": diffuse_color,
"emissive_color": emissive_color,
"color2": secondary_color,
}
return _wmo_material_builder.call(
"_build_material",
material_definition,
texture_paths,
extracted_directory
) as Material
func _append_texture_path(texture_paths: PackedStringArray, texture_path: String) -> int:
if texture_path.is_empty():
return -1
var texture_index := texture_paths.size()
texture_paths.append(texture_path)
return texture_index
@@ -0,0 +1 @@
uid://bsxqkjuk77cgf
+13 -71
View File
@@ -33,6 +33,9 @@ const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload( const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_finalizer.gd" "res://src/render/wmo/wmo_scene_resource_finalizer.gd"
) )
const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
)
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
@@ -123,7 +126,6 @@ const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/rend
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5 const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1 const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3 const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
const WMO_MATERIAL_REFRESH_VERSION := 10
const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0 const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
@@ -376,6 +378,9 @@ var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new
var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new( var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new(
WMO_BUILDER_SCRIPT WMO_BUILDER_SCRIPT
) )
var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new(
WMO_BUILDER_SCRIPT
)
var _wmo_missing_cache: Dictionary = {} var _wmo_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _world_wmo_root: Node3D var _world_wmo_root: Node3D
@@ -3840,7 +3845,7 @@ func _process_wmo_render_build_jobs() -> void:
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MESH: if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MESH:
var mesh := meshes[selected_index] as Mesh var mesh := meshes[selected_index] as Mesh
if mesh != null: if mesh != null:
_refresh_cached_wmo_mesh_materials(mesh) _wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir)
var mesh_instance := MeshInstance3D.new() var mesh_instance := MeshInstance3D.new()
mesh_instance.name = ( mesh_instance.name = (
mesh_names[selected_index] mesh_names[selected_index]
@@ -3871,7 +3876,7 @@ func _process_wmo_render_build_jobs() -> void:
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MULTIMESH: if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MULTIMESH:
var multimesh := multimeshes[selected_index] as MultiMesh var multimesh := multimeshes[selected_index] as MultiMesh
if multimesh != null: if multimesh != null:
_refresh_cached_wmo_mesh_materials(multimesh.mesh) _wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
var multimesh_instance := MultiMeshInstance3D.new() var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = ( multimesh_instance.name = (
multimesh_names[selected_index] multimesh_names[selected_index]
@@ -3950,81 +3955,18 @@ func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
func _refresh_cached_wmo_materials_recursive(node: Node) -> void: func _refresh_cached_wmo_materials_recursive(node: Node) -> void:
if node is MeshInstance3D: if node is MeshInstance3D:
_refresh_cached_wmo_mesh_materials((node as MeshInstance3D).mesh) _wmo_runtime_mesh_finalizer.finalize_mesh(
(node as MeshInstance3D).mesh,
extracted_dir
)
elif node is MultiMeshInstance3D: elif node is MultiMeshInstance3D:
var multimesh := (node as MultiMeshInstance3D).multimesh var multimesh := (node as MultiMeshInstance3D).multimesh
if multimesh != null: if multimesh != null:
_refresh_cached_wmo_mesh_materials(multimesh.mesh) _wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
for child in node.get_children(): for child in node.get_children():
_refresh_cached_wmo_materials_recursive(child) _refresh_cached_wmo_materials_recursive(child)
func _refresh_cached_wmo_mesh_materials(mesh: Mesh) -> void:
if mesh == null:
return
if int(mesh.get_meta("wow_wmo_material_refresh_version", 0)) >= WMO_MATERIAL_REFRESH_VERSION:
return
mesh.set_meta("wow_wmo_material_refresh_version", WMO_MATERIAL_REFRESH_VERSION)
if not (mesh is ArrayMesh):
return
var array_mesh := mesh as ArrayMesh
for surface_idx in array_mesh.get_surface_count():
var rebuilt := _rebuild_cached_wmo_material(array_mesh.surface_get_material(surface_idx))
if rebuilt != null:
array_mesh.surface_set_material(surface_idx, rebuilt)
func _rebuild_cached_wmo_material(material: Material) -> Material:
if material == null or not material.has_meta("texture0_path"):
return null
var tex0_path := String(material.get_meta("texture0_path", ""))
var tex1_path := String(material.get_meta("texture1_path", ""))
var tex2_path := String(material.get_meta("texture2_path", ""))
var textures := PackedStringArray()
var tex0_index := -1
var tex1_index := -1
var tex2_index := -1
if not tex0_path.is_empty():
tex0_index = textures.size()
textures.append(tex0_path)
if not tex1_path.is_empty():
tex1_index = textures.size()
textures.append(tex1_path)
if not tex2_path.is_empty():
tex2_index = textures.size()
textures.append(tex2_path)
var diffuse := Color.WHITE
var emissive := Color.BLACK
var secondary := Color.WHITE
if material is ShaderMaterial:
var shader_material := material as ShaderMaterial
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
if diffuse_value is Color:
diffuse = diffuse_value
if emissive_value is Color:
emissive = emissive_value
if secondary_value is Color:
secondary = secondary_value
var mat_def := {
"texture0": tex0_index,
"texture1": tex1_index,
"texture2": tex2_index,
"flags": int(material.get_meta("wow_flags", 0)),
"shader": int(material.get_meta("wow_shader", 0)),
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
"diffuse_color": diffuse,
"emissive_color": emissive,
"color2": secondary,
}
return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir)
func _cancel_wmo_build_job(tile_key: String) -> void: func _cancel_wmo_build_job(tile_key: String) -> void:
if _wmo_build_jobs.has(tile_key): if _wmo_build_jobs.has(tile_key):
_wmo_build_jobs.erase(tile_key) _wmo_build_jobs.erase(tile_key)
@@ -0,0 +1,292 @@
extends SceneTree
## Asset-free identity, refresh-version, material-definition, ownership and
## timing regression for cached WMO runtime Mesh finalization.
const FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
)
const FINALIZER_PATH := "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeWmoMaterialBuilder extends RefCounted:
var definitions: Array[Dictionary] = []
var texture_path_sets: Array[PackedStringArray] = []
var extracted_directories: Array[String] = []
var rebuilt_material: Material = StandardMaterial3D.new()
func _build_material(
material_definition: Dictionary,
texture_paths: PackedStringArray,
extracted_directory: String
) -> Material:
definitions.append(material_definition.duplicate(true))
texture_path_sets.append(texture_paths.duplicate())
extracted_directories.append(extracted_directory)
return rebuilt_material
func _initialize() -> void:
var failures: Array[String] = []
_verify_null_current_and_non_array_contract(failures)
_verify_unmarked_surface_contract(failures)
_verify_standard_material_definition(failures)
_verify_shader_material_colors(failures)
_verify_missing_builder_contract(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_RUNTIME_MESH_FINALIZER: %s" % failure)
quit(1)
return
print(
"WMO_RUNTIME_MESH_FINALIZER PASS cases=27 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_null_current_and_non_array_contract(failures: Array[String]) -> void:
var builder := FakeWmoMaterialBuilder.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
_expect_true(
finalizer.call("finalize_mesh", null, "res://data/extracted") == null,
"null Mesh remains null",
failures
)
var current_mesh := ArrayMesh.new()
current_mesh.set_meta("wow_wmo_material_refresh_version", 10)
_expect_true(
finalizer.call("finalize_mesh", current_mesh, "res://data/extracted") == current_mesh,
"current Mesh exact identity retained",
failures
)
_expect_equal(builder.definitions.size(), 0, "current Mesh skips builder", failures)
var primitive_mesh := BoxMesh.new()
_expect_true(
finalizer.call("finalize_mesh", primitive_mesh, "res://data/extracted") == primitive_mesh,
"non-ArrayMesh exact identity retained",
failures
)
_expect_equal(
int(primitive_mesh.get_meta("wow_wmo_material_refresh_version", 0)),
10,
"non-ArrayMesh stamped current",
failures
)
func _verify_unmarked_surface_contract(failures: Array[String]) -> void:
var builder := FakeWmoMaterialBuilder.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
var mesh := _mesh_with_surfaces(2)
var ordinary_material := StandardMaterial3D.new()
mesh.surface_set_material(1, ordinary_material)
var finalized_mesh: Mesh = finalizer.call(
"finalize_mesh",
mesh,
"res://data/extracted"
) as Mesh
_expect_true(finalized_mesh == mesh, "stale ArrayMesh exact identity retained", failures)
_expect_true(mesh.surface_get_material(0) == null, "null surface remains null", failures)
_expect_true(
mesh.surface_get_material(1) == ordinary_material,
"unmarked material exact identity retained",
failures
)
_expect_equal(builder.definitions.size(), 0, "unmarked surfaces skip builder", failures)
_expect_equal(
int(mesh.get_meta("wow_wmo_material_refresh_version", 0)),
10,
"stale ArrayMesh stamped current",
failures
)
func _verify_standard_material_definition(failures: Array[String]) -> void:
var builder := FakeWmoMaterialBuilder.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
var cached_material := StandardMaterial3D.new()
cached_material.set_meta("texture0_path", "World\\A.blp")
cached_material.set_meta("texture1_path", "")
cached_material.set_meta("texture2_path", "World\\C.blp")
cached_material.set_meta("wow_flags", 17)
cached_material.set_meta("wow_shader", 4)
cached_material.set_meta("wow_blend_mode", 6)
var mesh := _mesh_with_surfaces(1)
mesh.surface_set_material(0, cached_material)
finalizer.call("finalize_mesh", mesh, "res://fixture/extracted")
_expect_equal(builder.definitions.size(), 1, "eligible surface rebuilt once", failures)
_expect_true(
mesh.surface_get_material(0) == builder.rebuilt_material,
"exact builder Material adopted",
failures
)
_expect_true(
builder.texture_path_sets[0] == PackedStringArray(["World\\A.blp", "World\\C.blp"]),
"texture paths compacted in slot order",
failures
)
_expect_true(
builder.extracted_directories == ["res://fixture/extracted"],
"extracted directory forwarded exactly",
failures
)
var definition := builder.definitions[0]
_expect_equal(int(definition["texture0"]), 0, "texture0 compact index", failures)
_expect_equal(int(definition["texture1"]), -1, "empty texture1 index", failures)
_expect_equal(int(definition["texture2"]), 1, "texture2 compact index", failures)
_expect_equal(int(definition["flags"]), 17, "WMO flags forwarded", failures)
_expect_equal(int(definition["shader"]), 4, "WMO shader forwarded", failures)
_expect_equal(int(definition["blend_mode"]), 6, "WMO blend mode forwarded", failures)
_expect_true(definition["diffuse_color"] == Color.WHITE, "default diffuse color", failures)
_expect_true(definition["emissive_color"] == Color.BLACK, "default emissive color", failures)
_expect_true(definition["color2"] == Color.WHITE, "default secondary color", failures)
func _verify_shader_material_colors(failures: Array[String]) -> void:
var builder := FakeWmoMaterialBuilder.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
var shader := Shader.new()
shader.code = (
"shader_type spatial; uniform vec4 diffuse_color; "
+ "uniform vec4 emissive_color; uniform vec4 secondary_color;"
)
var cached_material := ShaderMaterial.new()
cached_material.shader = shader
cached_material.set_shader_parameter("diffuse_color", Color(0.1, 0.2, 0.3, 0.4))
cached_material.set_shader_parameter("emissive_color", Color(0.5, 0.6, 0.7, 0.8))
cached_material.set_shader_parameter("secondary_color", Color(0.9, 0.8, 0.7, 0.6))
cached_material.set_meta("texture0_path", "World\\Only.blp")
var mesh := _mesh_with_surfaces(1)
mesh.surface_set_material(0, cached_material)
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
var definition := builder.definitions[0]
_expect_true(
definition["diffuse_color"] == Color(0.1, 0.2, 0.3, 0.4),
"shader diffuse color forwarded",
failures
)
_expect_true(
definition["emissive_color"] == Color(0.5, 0.6, 0.7, 0.8),
"shader emissive color forwarded",
failures
)
_expect_true(
definition["color2"] == Color(0.9, 0.8, 0.7, 0.6),
"shader secondary color forwarded",
failures
)
func _verify_missing_builder_contract(failures: Array[String]) -> void:
var finalizer: RefCounted = FINALIZER_SCRIPT.new(null)
var cached_material := StandardMaterial3D.new()
cached_material.set_meta("texture0_path", "World\\A.blp")
var mesh := _mesh_with_surfaces(1)
mesh.surface_set_material(0, cached_material)
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
_expect_true(
mesh.surface_get_material(0) == cached_material,
"missing builder retains cached Material identity",
failures
)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var finalizer_source := _read_text(FINALIZER_PATH, failures)
_expect_true(
loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("),
"loader composes runtime Mesh finalizer",
failures
)
_expect_true(
loader_source.contains("_wmo_runtime_mesh_finalizer.finalize_mesh("),
"loader delegates runtime Mesh finalization",
failures
)
for released_token in [
"func _refresh_cached_wmo_mesh_materials(",
"func _rebuild_cached_wmo_material(",
"const WMO_MATERIAL_REFRESH_VERSION",
]:
_expect_false(
loader_source.contains(released_token),
"loader releases %s" % released_token,
failures
)
for owned_token in [
"const MATERIAL_REFRESH_VERSION := 10",
"wow_wmo_material_refresh_version",
"array_mesh.surface_get_material(surface_index)",
"_wmo_material_builder.call(",
]:
_expect_true(
finalizer_source.contains(owned_token),
"finalizer owns %s" % owned_token,
failures
)
_expect_true(
loader_source.contains("func _refresh_cached_wmo_materials_recursive(node: Node)"),
"loader retains scene traversal",
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var builder := FakeWmoMaterialBuilder.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
var mesh := ArrayMesh.new()
mesh.set_meta("wow_wmo_material_refresh_version", 10)
var started_microseconds := Time.get_ticks_usec()
for iteration in range(1000):
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
if elapsed_milliseconds >= 1000.0:
failures.append(
"1000 current Mesh finalizations took %.3fms (budget < 1000ms)"
% elapsed_milliseconds
)
return elapsed_milliseconds
func _mesh_with_surfaces(surface_count: int) -> ArrayMesh:
var mesh := ArrayMesh.new()
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
Vector3.ZERO,
Vector3.RIGHT,
Vector3.UP,
])
for surface_index in range(surface_count):
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot read %s" % path)
return ""
var source := file.get_as_text()
file.close()
return source
func _expect_true(value: bool, label: String, failures: Array[String]) -> void:
if not value:
failures.append(label)
func _expect_false(value: bool, label: String, failures: Array[String]) -> void:
if value:
failures.append(label)
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s: expected %d, got %d" % [label, expected, actual])
+16 -1
View File
@@ -44,6 +44,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`, `M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`,
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`, `M03-RND-WMO-SCENE-RESOURCE-CACHE-001`,
`M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001`, `M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001`,
`M03-RND-WMO-RUNTIME-MESH-FINALIZER-001`,
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`, `M03-RND-ADT-WATER-LOAD-PIPELINE-001`,
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`, `M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`, `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`,
@@ -154,6 +155,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
cache/finalizer/queue/planner/registry/resolver/shutdown, facade, cache/finalizer/queue/planner/registry/resolver/shutdown, facade,
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation
`48` and coordination. `48` and coordination.
WMO runtime Mesh finalizer passed `cases=27 iterations=1000` with null/current/
non-ArrayMesh exact identity, refresh-version stamping, null/unmarked surface
retention, compact texture indices, exact flags/shader/blend/default and
ShaderMaterial colors, builder-result adoption and source boundaries. All 63
autonomous headless verifiers passed; internal-access remained `30`,
documentation covered `49` module specifications, coordination retained `34`
historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans.
ADT water load pipeline state passed `cases=12 iterations=100 ADT water load pipeline state passed `cases=12 iterations=100
elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`), elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`),
WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent
@@ -423,6 +431,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
request admission remain loader-owned; terminal I/O, `PackedScene` probe, request admission remain loader-owned; terminal I/O, `PackedScene` probe,
WMOBuilder metadata validation and probe release belong to the scene finalizer. WMOBuilder metadata validation and probe release belong to the scene finalizer.
Rejected non-Node3D probes are now freed without changing fallback or output. Rejected non-Node3D probes are now freed without changing fallback or output.
Cached WMO runtime material refresh remains version `10`; exact Mesh identity,
stale stamping, surface order, compact texture path indices, cached metadata/
colors and WMOBuilder output adoption are unchanged. Those rules now belong to
the runtime Mesh finalizer while cached-scene/build-job Node traversal, permits,
attachment and lifetime remain loader-owned.
ADT water requests retain FIFO order and tile-key deduplication; tile release ADT water requests retain FIFO order and tile-key deduplication; tile release
removes pending requests without cancelling active work, task completion and removes pending requests without cancelling active work, task completion and
result publication order are unchanged, and the mutex remains limited to the result publication order are unchanged, and the mutex remains limited to the
@@ -503,6 +516,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
cached WMO PackedScene/missing/request state and loader file-size/admission/ cached WMO PackedScene/missing/request state and loader file-size/admission/
reset adapters, plus terminal PackedScene polling/probe/lifetime/publication reset adapters, plus terminal PackedScene polling/probe/lifetime/publication
finalizer, finalizer,
WMO runtime Mesh finalizer and four loader Mesh-delegation adapters,
ADT water pending/task/result pipeline state and loader enqueue/start/publish/ ADT water pending/task/result pipeline state and loader enqueue/start/publish/
drain/release/reset/shutdown/metrics adapters, drain/release/reset/shutdown/metrics adapters,
stateless ADT water scene finalizer and both loader build/attach adapters, stateless ADT water scene finalizer and both loader build/attach adapters,
@@ -563,7 +577,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
animation and static Mesh terminal ResourceLoader drains are extracted, while animation and static Mesh terminal ResourceLoader drains are extracted, while
action execution, material-prototype lookup and root cleanup remain loader-owned; action execution, material-prototype lookup and root cleanup remain loader-owned;
cached-scene WMO FileAccess/size admission, live fallback and group cached-scene WMO FileAccess/size admission, live fallback and group
materialization remain in the loader; both WMO terminal I/O paths are separated; materialization remain in the loader; both WMO terminal I/O paths and runtime
Mesh material refresh are separated;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
ADT water parsing and active worker interruption remain loader-owned; ADT water parsing and active worker interruption remain loader-owned;
materialization is separated but remains synchronous main-thread work behind materialization is separated but remains synchronous main-thread work behind