From ae40e3f1d4c7c92c478e791e4e79888333872baf Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 18 Jul 2026 14:33:48 +0400 Subject: [PATCH] render: extract WMO runtime Mesh finalizer --- RENDER.md | 16 + docs/modules/README.md | 1 + docs/modules/wmo-runtime-mesh-finalizer.md | 212 +++++++++++++ docs/modules/world-renderer.md | 3 + src/render/wmo/wmo_runtime_mesh_finalizer.gd | 105 +++++++ .../wmo/wmo_runtime_mesh_finalizer.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 84 +---- .../verify_wmo_runtime_mesh_finalizer.gd | 292 ++++++++++++++++++ targets/03-renderer-facade.md | 17 +- 9 files changed, 659 insertions(+), 72 deletions(-) create mode 100644 docs/modules/wmo-runtime-mesh-finalizer.md create mode 100644 src/render/wmo/wmo_runtime_mesh_finalizer.gd create mode 100644 src/render/wmo/wmo_runtime_mesh_finalizer.gd.uid create mode 100644 src/tools/verify_wmo_runtime_mesh_finalizer.gd diff --git a/RENDER.md b/RENDER.md index 20148d3..5d5b62a 100644 --- a/RENDER.md +++ b/RENDER.md @@ -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/ 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 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. diff --git a/docs/modules/README.md b/docs/modules/README.md index ceed940..9a2c584 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -49,6 +49,7 @@ | 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 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) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | diff --git a/docs/modules/wmo-runtime-mesh-finalizer.md b/docs/modules/wmo-runtime-mesh-finalizer.md new file mode 100644 index 0000000..667b389 --- /dev/null +++ b/docs/modules/wmo-runtime-mesh-finalizer.md @@ -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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index effb0d6..84cf232 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -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 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 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 | | 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 | @@ -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/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_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_policy.gd` | Immutable renderer radius/prefetch policy | | `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_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_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_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 | diff --git a/src/render/wmo/wmo_runtime_mesh_finalizer.gd b/src/render/wmo/wmo_runtime_mesh_finalizer.gd new file mode 100644 index 0000000..35a2380 --- /dev/null +++ b/src/render/wmo/wmo_runtime_mesh_finalizer.gd @@ -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 diff --git a/src/render/wmo/wmo_runtime_mesh_finalizer.gd.uid b/src/render/wmo/wmo_runtime_mesh_finalizer.gd.uid new file mode 100644 index 0000000..c26ed7a --- /dev/null +++ b/src/render/wmo/wmo_runtime_mesh_finalizer.gd.uid @@ -0,0 +1 @@ +uid://bsxqkjuk77cgf diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index f6568de..fe5ccc5 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -33,6 +33,9 @@ const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload( const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload( "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_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.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_SPLAT_TILE_FORMAT_VERSION := 1 const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3 -const WMO_MATERIAL_REFRESH_VERSION := 10 const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0 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( WMO_BUILDER_SCRIPT ) +var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new( + WMO_BUILDER_SCRIPT +) var _wmo_missing_cache: Dictionary = {} var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() 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: var mesh := meshes[selected_index] as Mesh if mesh != null: - _refresh_cached_wmo_mesh_materials(mesh) + _wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir) var mesh_instance := MeshInstance3D.new() mesh_instance.name = ( 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: var multimesh := multimeshes[selected_index] as MultiMesh 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() multimesh_instance.name = ( 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: 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: var multimesh := (node as MultiMeshInstance3D).multimesh 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(): _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: if _wmo_build_jobs.has(tile_key): _wmo_build_jobs.erase(tile_key) diff --git a/src/tools/verify_wmo_runtime_mesh_finalizer.gd b/src/tools/verify_wmo_runtime_mesh_finalizer.gd new file mode 100644 index 0000000..a5a2506 --- /dev/null +++ b/src/tools/verify_wmo_runtime_mesh_finalizer.gd @@ -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]) diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index 9dc3a00..e9bb59d 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -44,6 +44,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру `M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`, `M03-RND-WMO-SCENE-RESOURCE-CACHE-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-SCENE-FINALIZER-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, internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation `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 elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`), 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, WMOBuilder metadata validation and probe release belong to the scene finalizer. 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 removes pending requests without cancelling active work, task completion and 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/ reset adapters, plus terminal PackedScene polling/probe/lifetime/publication finalizer, + WMO runtime Mesh finalizer and four loader Mesh-delegation adapters, ADT water pending/task/result pipeline state and loader enqueue/start/publish/ drain/release/reset/shutdown/metrics 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 action execution, material-prototype lookup and root cleanup remain loader-owned; 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; ADT water parsing and active worker interruption remain loader-owned; materialization is separated but remains synchronous main-thread work behind