feat(game): bind preview diffuse materials
This commit is contained in:
@@ -32,7 +32,7 @@ Open source проект с реализацией компонентов игр
|
|||||||
## Текущий статус рендера
|
## Текущий статус рендера
|
||||||
|
|
||||||
- `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry.
|
- `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry.
|
||||||
- `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. Новый `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge без подтверждённого полного corpus run, текстур, transforms или pixel parity.
|
- `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge: для первого root он загружает первую MAT0 diffuse texture на каждый используемый MSH selector; полный corpus, material phases, transforms и pixel parity ещё не подтверждены.
|
||||||
- `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты.
|
- `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты.
|
||||||
- Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence).
|
- Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence).
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
#![allow(clippy::print_stderr, clippy::print_stdout)]
|
||||||
//! `FParkan` render-planning composition root.
|
//! `FParkan` render-planning composition root.
|
||||||
|
|
||||||
use fparkan_assets::PreparedVisual;
|
use fparkan_assets::{PreparedTextureUsage, PreparedVisual};
|
||||||
use fparkan_platform::WindowPort;
|
use fparkan_platform::WindowPort;
|
||||||
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
@@ -31,7 +31,8 @@ use fparkan_render::{
|
|||||||
};
|
};
|
||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMesh,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh,
|
||||||
|
VulkanStaticTexture,
|
||||||
};
|
};
|
||||||
use fparkan_runtime::{
|
use fparkan_runtime::{
|
||||||
create, frame, load_mission, load_mission_static_preview,
|
create, frame, load_mission, load_mission_static_preview,
|
||||||
@@ -81,12 +82,14 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
if args.backend == RenderBackendMode::StaticVulkan {
|
if args.backend == RenderBackendMode::StaticVulkan {
|
||||||
let mission_assets = loaded_mission_assets(&engine)
|
let mission_assets = loaded_mission_assets(&engine)
|
||||||
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
|
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
|
||||||
let model = mission_assets.models.first().ok_or_else(|| {
|
let (mesh, materials) = static_preview_mesh_and_materials(mission_assets)?;
|
||||||
"mission has no mesh-backed model for the static Vulkan bridge".to_string()
|
return run_static_vulkan_mode(
|
||||||
})?;
|
mesh,
|
||||||
let mesh = project_msh_to_static_mesh(&model.validated)
|
materials,
|
||||||
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
args.frames,
|
||||||
return run_static_vulkan_mode(mesh, args.frames, &args.mission, loaded.object_count);
|
&args.mission,
|
||||||
|
loaded.object_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut backend = VulkanPlanningBackend::new();
|
let mut backend = VulkanPlanningBackend::new();
|
||||||
@@ -174,6 +177,79 @@ fn load_requested_mission(
|
|||||||
.map_err(|err| err.to_string())
|
.map_err(|err| err.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Projects the explicitly bounded static-preview root into geometry and its
|
||||||
|
/// selector-keyed diffuse material descriptors.
|
||||||
|
///
|
||||||
|
/// This intentionally takes only the first MAT0 texture request for each MSH
|
||||||
|
/// batch selector. Later material phases, animation, lightmaps, transforms,
|
||||||
|
/// and gameplay visibility remain outside this preview bridge.
|
||||||
|
fn static_preview_mesh_and_materials(
|
||||||
|
assets: &MissionAssets,
|
||||||
|
) -> Result<(VulkanStaticMesh, Vec<VulkanStaticMaterial>), String> {
|
||||||
|
let model = assets.models.first().ok_or_else(|| {
|
||||||
|
"mission has no mesh-backed model for the static Vulkan bridge".to_string()
|
||||||
|
})?;
|
||||||
|
let mesh = project_msh_to_static_mesh(&model.validated)
|
||||||
|
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
||||||
|
let visual = assets
|
||||||
|
.visuals
|
||||||
|
.iter()
|
||||||
|
.find(|visual| visual.model_id == Some(model.id))
|
||||||
|
.ok_or_else(|| "static preview model has no prepared visual".to_string())?;
|
||||||
|
let mut selectors = mesh
|
||||||
|
.draw_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| range.material_index)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
selectors.sort_unstable();
|
||||||
|
selectors.dedup();
|
||||||
|
|
||||||
|
let materials = selectors
|
||||||
|
.into_iter()
|
||||||
|
.map(|material_index| {
|
||||||
|
let material_id = visual.material_ids.get(usize::from(material_index)).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"static preview MSH batch selector {material_index} has no prepared WEAR material"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let material = assets.material_by_id(*material_id).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"static preview prepared material {material_index} is unavailable"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let texture_name = material.texture_requests.first().ok_or_else(|| {
|
||||||
|
format!("static preview material {material_index} has no MAT0 diffuse texture")
|
||||||
|
})?;
|
||||||
|
let texture = assets
|
||||||
|
.textures
|
||||||
|
.iter()
|
||||||
|
.find(|texture| {
|
||||||
|
texture.usage == PreparedTextureUsage::Diffuse
|
||||||
|
&& texture.source.name == *texture_name
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"static preview diffuse texture {texture_name:?} for material {material_index} is unavailable"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let image = texture.decode_mip_rgba8(0).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"decode static preview diffuse texture {texture_name:?} for material {material_index}: {err}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(VulkanStaticMaterial {
|
||||||
|
material_index,
|
||||||
|
texture: VulkanStaticTexture {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
rgba8: image.rgba8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
Ok((mesh, materials))
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> {
|
fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
|
||||||
@@ -187,13 +263,14 @@ fn write_load_progress(path: &std::path::Path, phase: MissionLoadPhase) -> Resul
|
|||||||
|
|
||||||
fn run_static_vulkan_mode(
|
fn run_static_vulkan_mode(
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: &str,
|
mission: &str,
|
||||||
object_count: usize,
|
object_count: usize,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
||||||
event_loop.set_control_flow(ControlFlow::Poll);
|
event_loop.set_control_flow(ControlFlow::Poll);
|
||||||
let mut app = StaticVulkanApp::new(mesh, target_frames, mission, object_count);
|
let mut app = StaticVulkanApp::new(mesh, materials, target_frames, mission, object_count);
|
||||||
if let Err(err) = event_loop.run_app(&mut app) {
|
if let Err(err) = event_loop.run_app(&mut app) {
|
||||||
app.error = Some(format!("winit event loop: {err}"));
|
app.error = Some(format!("winit event loop: {err}"));
|
||||||
}
|
}
|
||||||
@@ -202,6 +279,7 @@ fn run_static_vulkan_mode(
|
|||||||
|
|
||||||
struct StaticVulkanApp {
|
struct StaticVulkanApp {
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: String,
|
mission: String,
|
||||||
object_count: usize,
|
object_count: usize,
|
||||||
@@ -214,9 +292,16 @@ struct StaticVulkanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StaticVulkanApp {
|
impl StaticVulkanApp {
|
||||||
fn new(mesh: VulkanStaticMesh, target_frames: u64, mission: &str, object_count: usize) -> Self {
|
fn new(
|
||||||
|
mesh: VulkanStaticMesh,
|
||||||
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
|
target_frames: u64,
|
||||||
|
mission: &str,
|
||||||
|
object_count: usize,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mesh,
|
mesh,
|
||||||
|
materials,
|
||||||
target_frames,
|
target_frames,
|
||||||
mission: mission.to_string(),
|
mission: mission.to_string(),
|
||||||
object_count,
|
object_count,
|
||||||
@@ -259,10 +344,11 @@ impl StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output = Some(format!(
|
self.output = Some(format!(
|
||||||
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
||||||
json_string(&self.mission),
|
json_string(&self.mission),
|
||||||
self.object_count,
|
self.object_count,
|
||||||
self.frames_presented,
|
self.frames_presented,
|
||||||
|
self.materials.len(),
|
||||||
report.renderer_report.swapchain_extent.0,
|
report.renderer_report.swapchain_extent.0,
|
||||||
report.renderer_report.swapchain_extent.1,
|
report.renderer_report.swapchain_extent.1,
|
||||||
report.renderer_report.swapchain_image_count,
|
report.renderer_report.swapchain_image_count,
|
||||||
@@ -320,7 +406,7 @@ impl ApplicationHandler for StaticVulkanApp {
|
|||||||
render_request: WinitWindow::default_render_request(),
|
render_request: WinitWindow::default_render_request(),
|
||||||
enable_validation: true,
|
enable_validation: true,
|
||||||
mesh: self.mesh.clone(),
|
mesh: self.mesh.clone(),
|
||||||
materials: Vec::new(),
|
materials: self.materials.clone(),
|
||||||
bootstrap_progress: None,
|
bootstrap_progress: None,
|
||||||
}) {
|
}) {
|
||||||
Ok(renderer) => renderer,
|
Ok(renderer) => renderer,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository};
|
|||||||
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
pub use fparkan_terrain::{TerrainError, TerrainWorld};
|
||||||
use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
|
use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
|
||||||
pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
|
pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
|
||||||
use fparkan_texm::{decode_texm, TexmDocument, TexmError};
|
use fparkan_texm::{decode_mip_rgba8, decode_texm, RgbaImage, TexmDocument, TexmError};
|
||||||
use std::collections::{hash_map::Entry, HashMap, HashSet};
|
use std::collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
@@ -307,6 +307,18 @@ pub struct PreparedTexture {
|
|||||||
pub usage: PreparedTextureUsage,
|
pub usage: PreparedTextureUsage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PreparedTexture {
|
||||||
|
/// Decodes one prepared texture mip into the renderer upload representation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the original TEXM decode error when the requested mip is invalid
|
||||||
|
/// or malformed.
|
||||||
|
pub fn decode_mip_rgba8(&self, level: u32) -> Result<RgbaImage, TexmError> {
|
||||||
|
decode_mip_rgba8(&self.texm, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PreparedVisual {
|
impl PreparedVisual {
|
||||||
/// Returns the primary material id, if any.
|
/// Returns the primary material id, if any.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
|
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
|
||||||
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
|
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
|
||||||
| `VulkanAssetRenderer` | Yes | Yes | Yes | `covered-gpu` | Static original asset rendering: MSH/Texm/WEAR/MAT0/terrain through Vulkan | Animation/FX parity unless explicitly wired |
|
| `VulkanAssetRenderer` | Yes | Yes | Yes | `covered-gpu` | Static original asset rendering: MSH/Texm/WEAR/MAT0/terrain through Vulkan | Animation/FX parity unless explicitly wired |
|
||||||
| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, static MSH draw | First MSH from first mission root | `covered-gpu` only for the narrow static-preview bridge | Opt-in mission-to-native-window bootstrap, static MSH projection and synchronized teardown telemetry | Full mission scene, texture/material binding, placed transforms/orientation, camera, gameplay, original-runtime parity |
|
| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, static MSH draw | First MSH from first mission root; first MAT0 diffuse TEXM per used selector | `covered-gpu` only for the narrow static-preview bridge | Opt-in mission-to-native-window bootstrap, static MSH projection, selector-keyed diffuse descriptor upload and synchronized teardown telemetry | Full mission scene, later MAT0 phases/animation, lightmaps, placed transforms/orientation, camera, gameplay, original-runtime parity |
|
||||||
| Future rendered `fparkan-game` mode | Yes | Yes | Yes | `covered-gpu` plus original-evidence IDs | Mission-driven render snapshot execution and pixel capture | Original-runtime parity for animation/FX/x87 without dedicated captures |
|
| Future rendered `fparkan-game` mode | Yes | Yes | Yes | `covered-gpu` plus original-evidence IDs | Mission-driven render snapshot execution and pixel capture | Original-runtime parity for animation/FX/x87 without dedicated captures |
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
@@ -35,14 +35,16 @@
|
|||||||
- `apps/fparkan-game` по умолчанию выдает `render-planning` JSON report поверх
|
- `apps/fparkan-game` по умолчанию выдает `render-planning` JSON report поверх
|
||||||
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
|
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
|
||||||
уже создаёт native `winit` window и передаёт первую подготовленную MSH модели миссии в
|
уже создаёт native `winit` window и передаёт первую подготовленную MSH модели миссии в
|
||||||
`VulkanSmokeRenderer` с пустым списком materials (явный white-fallback). Режим использует
|
`VulkanSmokeRenderer` вместе с первым diffuse TEXM из MAT0 для каждого реально используемого
|
||||||
|
`Batch20.material_index`. Режим использует
|
||||||
отдельный first-root preview loader: normal `load_mission` по-прежнему готовит все reachable
|
отдельный first-root preview loader: normal `load_mission` по-прежнему готовит все reachable
|
||||||
assets и весь graph, тогда как preview строит graph и готовит assets только для первого
|
assets и весь graph, тогда как preview строит graph и готовит assets только для первого
|
||||||
mission root. `--load-progress <file>` writes the last entered loader phase synchronously for
|
mission root. `--load-progress <file>` writes the last entered loader phase synchronously for
|
||||||
timeout diagnosis. Fresh GOG `MISSIONS/Autodemo.00/data.tma` run passed in 38.7 seconds with
|
timeout diagnosis. Fresh GOG `MISSIONS/Autodemo.00/data.tma` run passed in 38.7 seconds with
|
||||||
one presented frame, native 1280×720 swapchain (2 images), 7,372,800-byte readback hash
|
one presented frame, native 1280×720 swapchain (2 images), one original diffuse material
|
||||||
`10681850560830502773`, and validation warnings/errors `0/0`. This is `covered-gpu` evidence
|
descriptor, 7,372,800-byte readback hash `12332214764918703197`, and validation
|
||||||
for that narrow static-preview bridge only, not full-scene or original-renderer parity.
|
warnings/errors `0/0`. This is `covered-gpu` evidence for that narrow static-preview bridge
|
||||||
|
only, not full-scene or original-renderer parity.
|
||||||
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
|
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
|
||||||
asset viewer.
|
asset viewer.
|
||||||
- Следующий реальный milestone для rendered acceptance: `VulkanAssetRenderer`
|
- Следующий реальный milestone для rendered acceptance: `VulkanAssetRenderer`
|
||||||
|
|||||||
@@ -941,16 +941,18 @@ not the default planning path. It visits only the first mission root, selects
|
|||||||
its prepared MSH model, sends it through the existing static XZ clip-space
|
its prepared MSH model, sends it through the existing static XZ clip-space
|
||||||
projection and renders a requested number of frames through
|
projection and renders a requested number of frames through
|
||||||
`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports
|
`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports
|
||||||
swapchain/readback telemetry. It deliberately supplies no material textures,
|
swapchain/readback telemetry. For each used `Batch20.material_index`, it resolves
|
||||||
so the renderer's documented white fallback is used.
|
the positional prepared WEAR material and uploads mip 0 of that material's first
|
||||||
|
MAT0 diffuse texture request as a selector-keyed descriptor.
|
||||||
|
|
||||||
This is a narrow bootstrap from mission assets to a live Vulkan renderer. It
|
This is a narrow bootstrap from mission assets to a live Vulkan renderer. It
|
||||||
does not render every placed object, apply TMA transforms or orientation, bind
|
does not render every placed object, apply TMA transforms or orientation, select
|
||||||
WEAR/MAT0/TEXM material data, or establish a game camera. Fresh GOG
|
later MAT0 phases or animation, bind lightmaps, or establish a game camera. Fresh GOG
|
||||||
`MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one
|
`MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one
|
||||||
presented frame completed in 38.7 seconds with a native 1280×720 two-image
|
presented frame completed in 38.7 seconds with a native 1280×720 two-image
|
||||||
swapchain, 7,372,800-byte synchronized readback (FNV-1a
|
swapchain, one selector-keyed original diffuse descriptor, 7,372,800-byte
|
||||||
`10681850560830502773`) and validation warnings/errors `0/0`. This is not a
|
synchronized readback (FNV-1a `12332214764918703197`) and validation
|
||||||
|
warnings/errors `0/0`. This is not a
|
||||||
full-scene or original-renderer pixel-parity claim.
|
full-scene or original-renderer pixel-parity claim.
|
||||||
|
|
||||||
The preview now asks `load_mission_static_preview` for both graph and assets.
|
The preview now asks `load_mission_static_preview` for both graph and assets.
|
||||||
|
|||||||
Reference in New Issue
Block a user