feat(render): upload original TEXM to Vulkan
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 2m5s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 07:29:31 +04:00
parent 283b48e8c5
commit 6e6496eebf
8 changed files with 529 additions and 21 deletions
+5 -4
View File
@@ -54,8 +54,9 @@ pub use self::instance::{
use self::instance::{cstring_vec, ensure_instance_extensions_available};
use self::resources::{
color_subresource_range, create_command_pool, create_frame_sync,
create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, destroy_allocated_buffer,
VulkanAllocatedBuffer, VulkanFrameSync,
create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, create_static_texture_image,
destroy_allocated_buffer, destroy_allocated_image, VulkanAllocatedBuffer, VulkanAllocatedImage,
VulkanFrameSync,
};
pub use self::runtime::{
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
@@ -64,8 +65,8 @@ pub use self::runtime::{
pub use self::smoke_types::{
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticVertex,
VulkanValidationReport,
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticTexture,
VulkanStaticVertex, VulkanValidationReport,
};
#[cfg(test)]
use self::surface::extension_name;
@@ -4,6 +4,7 @@ use ash::vk;
use super::{
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanStaticMesh,
VulkanStaticTexture,
};
pub(super) struct VulkanAllocatedBuffer {
@@ -11,6 +12,12 @@ pub(super) struct VulkanAllocatedBuffer {
pub(super) memory: vk::DeviceMemory,
}
pub(super) struct VulkanAllocatedImage {
pub(super) image: vk::Image,
pub(super) memory: vk::DeviceMemory,
pub(super) view: vk::ImageView,
}
pub(super) struct VulkanFrameSync {
pub(super) image_available: vk::Semaphore,
pub(super) render_finished: vk::Semaphore,
@@ -70,6 +77,272 @@ pub(super) fn create_static_mesh_index_buffer(
)
}
#[allow(clippy::too_many_lines)]
pub(super) fn create_static_texture_image(
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
command_pool: vk::CommandPool,
texture: &VulkanStaticTexture,
) -> Result<VulkanAllocatedImage, VulkanSmokeRendererError> {
let staging = create_host_visible_buffer(
instance,
device,
&texture.rgba8,
vk::BufferUsageFlags::TRANSFER_SRC,
"static texture staging buffer",
)?;
let image_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::R8G8B8A8_UNORM)
.extent(vk::Extent3D {
width: texture.width,
height: texture.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED);
// SAFETY: The create info only contains stack-owned values and a validated non-zero extent.
let image = unsafe { device.device().create_image(&image_info, None) }.map_err(|result| {
destroy_allocated_buffer(device, &staging);
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateImage(static texture)",
result,
}
})?;
// SAFETY: The image belongs to this device and is queried immediately after creation.
let requirements = unsafe { device.device().get_image_memory_requirements(image) };
let Some(memory_type_index) = find_memory_type(
instance,
device.physical_device(),
requirements.memory_type_bits,
vk::MemoryPropertyFlags::DEVICE_LOCAL,
) else {
// SAFETY: Both resources were created on this device and are being rolled back once.
unsafe { device.device().destroy_image(image, None) };
destroy_allocated_buffer(device, &staging);
return Err(VulkanSmokeRendererError::MissingMemoryType {
context: "static texture image",
});
};
let allocate_info = vk::MemoryAllocateInfo::default()
.allocation_size(requirements.size)
.memory_type_index(memory_type_index);
// SAFETY: The allocation matches the image memory requirements queried above.
let memory = {
// SAFETY: The allocation matches the image memory requirements queried above.
unsafe { device.device().allocate_memory(&allocate_info, None) }
}
.map_err(|result| {
// SAFETY: Both resources were created on this device and are being rolled back once.
unsafe { device.device().destroy_image(image, None) };
destroy_allocated_buffer(device, &staging);
VulkanSmokeRendererError::VulkanOperation {
context: "vkAllocateMemory(static texture)",
result,
}
})?;
// SAFETY: The allocation matches this image's queried requirements.
if let Err(result) = unsafe { device.device().bind_image_memory(image, memory, 0) } {
// SAFETY: Both resources were created on this device and are being rolled back once.
unsafe {
device.device().destroy_image(image, None);
device.device().free_memory(memory, None);
};
destroy_allocated_buffer(device, &staging);
return Err(VulkanSmokeRendererError::VulkanOperation {
context: "vkBindImageMemory(static texture)",
result,
});
}
if let Err(error) = upload_static_texture(
device,
command_pool,
staging.buffer,
image,
texture.width,
texture.height,
) {
// SAFETY: Both resources were created on this device and are being rolled back once.
unsafe {
device.device().destroy_image(image, None);
device.device().free_memory(memory, None);
};
destroy_allocated_buffer(device, &staging);
return Err(error);
}
destroy_allocated_buffer(device, &staging);
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R8G8B8A8_UNORM)
.subresource_range(color_subresource_range());
// SAFETY: The image is live, initialized and has the stated color subresource.
let view = {
// SAFETY: The image is live, initialized and has the stated color subresource.
unsafe { device.device().create_image_view(&view_info, None) }
}
.map_err(|result| {
// SAFETY: Both resources were created on this device and are being rolled back once.
unsafe {
device.device().destroy_image(image, None);
device.device().free_memory(memory, None);
};
VulkanSmokeRendererError::VulkanOperation {
context: "vkCreateImageView(static texture)",
result,
}
})?;
Ok(VulkanAllocatedImage {
image,
memory,
view,
})
}
#[allow(clippy::too_many_lines)]
fn upload_static_texture(
device: &VulkanLogicalDeviceProbe,
command_pool: vk::CommandPool,
staging_buffer: vk::Buffer,
image: vk::Image,
width: u32,
height: u32,
) -> Result<(), VulkanSmokeRendererError> {
let allocate_info = vk::CommandBufferAllocateInfo::default()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
// SAFETY: The command pool is live and owned by the current logical device.
let command_buffer = unsafe { device.device().allocate_command_buffers(&allocate_info) }
.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
context: "vkAllocateCommandBuffers(texture upload)",
result,
})?[0];
let begin =
vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
// SAFETY: The command buffer is freshly allocated from the current pool.
if let Err(result) = unsafe { device.device().begin_command_buffer(command_buffer, &begin) } {
// SAFETY: The command buffer belongs to this pool and is released on failure.
unsafe {
device
.device()
.free_command_buffers(command_pool, &[command_buffer]);
};
return Err(VulkanSmokeRendererError::VulkanOperation {
context: "vkBeginCommandBuffer(texture upload)",
result,
});
}
let range = color_subresource_range();
let to_transfer = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.image(image)
.subresource_range(range);
let region = vk::BufferImageCopy::default()
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.mip_level(0)
.base_array_layer(0)
.layer_count(1),
)
.image_extent(vk::Extent3D {
width,
height,
depth: 1,
});
let to_sampled = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ)
.image(image)
.subresource_range(range);
// SAFETY: The commands operate on live, exclusively owned resources and the stated color subresource.
unsafe {
device.device().cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
vk::DependencyFlags::empty(),
&[],
&[],
&[to_transfer],
);
device.device().cmd_copy_buffer_to_image(
command_buffer,
staging_buffer,
image,
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&[region],
);
device.device().cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::DependencyFlags::empty(),
&[],
&[],
&[to_sampled],
);
}
// SAFETY: Command recording is complete on the current command buffer.
if let Err(result) = unsafe { device.device().end_command_buffer(command_buffer) } {
// SAFETY: The command buffer belongs to this pool and is released on failure.
unsafe {
device
.device()
.free_command_buffers(command_pool, &[command_buffer]);
};
return Err(VulkanSmokeRendererError::VulkanOperation {
context: "vkEndCommandBuffer(texture upload)",
result,
});
}
let command_buffers = [command_buffer];
let submit = [vk::SubmitInfo::default().command_buffers(&command_buffers)];
// SAFETY: The graphics queue and submitted command buffer are live for the duration of the wait.
let submit_result = unsafe {
device
.device()
.queue_submit(device.graphics_queue(), &submit, vk::Fence::null())
};
let result = submit_result.and_then(|()| {
// SAFETY: The graphics queue remains live until the synchronous idle wait completes.
unsafe { device.device().queue_wait_idle(device.graphics_queue()) }
});
// SAFETY: Submission completed or failed synchronously and the command buffer is no longer needed.
unsafe {
device
.device()
.free_command_buffers(command_pool, &[command_buffer]);
};
result.map_err(|result| VulkanSmokeRendererError::VulkanOperation {
context: "vkQueueSubmit/WaitIdle(texture upload)",
result,
})
}
pub(super) fn destroy_allocated_image(
device: &VulkanLogicalDeviceProbe,
image: &VulkanAllocatedImage,
) {
// SAFETY: The image, view and allocation belong to this device and are destroyed once after idle.
unsafe {
device.device().destroy_image_view(image.view, None);
device.device().destroy_image(image.image, None);
device.device().free_memory(image.memory, None);
};
}
fn create_host_visible_buffer(
instance: &VulkanInstanceProbe,
device: &VulkanLogicalDeviceProbe,
@@ -4,15 +4,15 @@ use ash::vk;
use super::{
create_command_pool, create_frame_sync, create_static_mesh_index_buffer,
create_static_mesh_vertex_buffer, create_swapchain_resources, create_validation_messenger,
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
VulkanAllocatedBuffer, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
VulkanValidationReport,
create_static_mesh_vertex_buffer, create_static_texture_image, create_swapchain_resources,
create_validation_messenger, create_vulkan_instance_probe,
create_vulkan_logical_device_probe_for_request, create_vulkan_surface_probe,
create_vulkan_swapchain_probe_for_extent, destroy_allocated_buffer, destroy_allocated_image,
destroy_swapchain_resources, plan_vulkan_surface, VulkanAllocatedBuffer, VulkanInstanceConfig,
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, VulkanSmokeRendererReport,
VulkanSmokeShutdownReport, VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources,
VulkanValidationMessenger, VulkanValidationReport,
};
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
@@ -109,6 +109,11 @@ impl VulkanSmokeRenderer {
.mesh
.validate()
.map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?;
if let Some(texture) = &create_info.texture {
texture
.validate()
.map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?;
}
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
@@ -176,6 +181,21 @@ impl VulkanSmokeRenderer {
return Err(error);
}
};
let texture = match create_info.texture.as_ref() {
None => None,
Some(texture) => {
match create_static_texture_image(&instance, &device, command_pool, texture) {
Ok(image) => Some(image),
Err(error) => {
// SAFETY: These resources belong to this live device and are rolled back before it drops.
unsafe { device.device().destroy_command_pool(command_pool, None) };
destroy_allocated_buffer(&device, &index_buffer);
destroy_allocated_buffer(&device, &vertex_buffer);
return Err(error);
}
}
}
};
let mut renderer = Self {
instance: Some(instance),
validation,
@@ -186,6 +206,7 @@ impl VulkanSmokeRenderer {
swapchain_resources: None,
vertex_buffer: Some(vertex_buffer),
index_buffer: Some(index_buffer),
texture,
index_count: u32::try_from(create_info.mesh.indices.len()).unwrap_or(u32::MAX),
frame_sync: Vec::new(),
images_in_flight: Vec::new(),
@@ -640,6 +661,9 @@ impl VulkanSmokeRenderer {
fn destroy_device_owned_resources(&mut self) {
self.destroy_swapchain_resources();
if let Some(device) = self.device.as_ref() {
if let Some(texture) = self.texture.take() {
destroy_allocated_image(device, &texture);
}
if let Some(buffer) = self.index_buffer.take() {
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
unsafe {
@@ -4,9 +4,9 @@ use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use super::{
VulkanAllocatedBuffer, VulkanFrameSync, VulkanInstanceError, VulkanInstanceProbe,
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanSurfaceError, VulkanSurfaceProbe,
VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources,
VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanFrameSync, VulkanInstanceError,
VulkanInstanceProbe, VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanSurfaceError,
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources,
VulkanValidationMessenger,
};
use crate::shader_manifest::VulkanShaderManifestError;
@@ -29,6 +29,8 @@ pub struct VulkanSmokeRendererCreateInfo {
/// This initial bridge keeps positions in clip-space. MSH transforms,
/// materials, and textures are deliberately higher-level Stage 3 work.
pub mesh: VulkanStaticMesh,
/// Optional RGBA8 texture uploaded before the first live frame.
pub texture: Option<VulkanStaticTexture>,
/// Optional shared bootstrap progress tracker for failure evidence.
pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>,
}
@@ -51,6 +53,38 @@ pub struct VulkanStaticMesh {
pub indices: Vec<u16>,
}
/// Decoded RGBA8 image accepted by the initial Vulkan texture upload path.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanStaticTexture {
/// Image width in texels.
pub width: u32,
/// Image height in texels.
pub height: u32,
/// Row-major RGBA8 pixels.
pub rgba8: Vec<u8>,
}
impl VulkanStaticTexture {
pub(super) fn validate(&self) -> Result<(), &'static str> {
let pixels = usize::try_from(self.width)
.ok()
.and_then(|width| {
usize::try_from(self.height)
.ok()
.and_then(|height| width.checked_mul(height))
})
.and_then(|pixels| pixels.checked_mul(4));
match pixels {
None => Err("static texture dimensions overflow address space"),
Some(0) => Err("static texture has zero extent"),
Some(expected) if expected != self.rgba8.len() => {
Err("static texture rgba8 byte count does not match extent")
}
Some(_) => Ok(()),
}
}
}
impl VulkanStaticMesh {
/// Returns the compatibility triangle used by the native Stage 0 smoke app.
#[must_use]
@@ -293,6 +327,11 @@ pub enum VulkanSmokeRendererError {
/// Validation failure detail.
context: &'static str,
},
/// The submitted static texture cannot be represented by this path.
InvalidStaticTexture {
/// Validation failure detail.
context: &'static str,
},
/// Internal smoke renderer state was unexpectedly absent.
InvariantViolation {
/// Missing state context.
@@ -315,6 +354,9 @@ impl std::fmt::Display for VulkanSmokeRendererError {
write!(f, "{context}: no compatible Vulkan memory type")
}
Self::InvalidStaticMesh { context } => write!(f, "invalid static mesh: {context}"),
Self::InvalidStaticTexture { context } => {
write!(f, "invalid static texture: {context}")
}
Self::InvariantViolation { context } => {
write!(f, "renderer invariant violated: {context}")
}
@@ -335,6 +377,7 @@ pub struct VulkanSmokeRenderer {
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
pub(super) texture: Option<VulkanAllocatedImage>,
pub(super) index_count: u32,
pub(super) frame_sync: Vec<VulkanFrameSync>,
pub(super) images_in_flight: Vec<vk::Fence>,
+138 -2
View File
@@ -16,7 +16,7 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{
project_msh_to_static_mesh, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome,
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport,
VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanValidationReport,
VulkanSmokeShutdownReport, VulkanStaticMesh, VulkanStaticTexture, VulkanValidationReport,
};
use serde::Serialize;
use std::path::PathBuf;
@@ -56,6 +56,7 @@ fn main() {
fn run(args: &[String]) -> Result<String, String> {
let options = SmokeOptions::parse(args)?;
let mesh = options.load_mesh()?;
let texture = options.load_texture()?;
remove_stale_output(&options)?;
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
event_loop.set_control_flow(ControlFlow::Poll);
@@ -66,7 +67,7 @@ fn run(args: &[String]) -> Result<String, String> {
Arc::clone(&completed),
Arc::clone(&progress),
);
let mut app = SmokeApp::new(options, mesh, completed, progress);
let mut app = SmokeApp::new(options, mesh, texture, completed, progress);
if let Err(err) = event_loop.run_app(&mut app) {
app.error = Some(format!("winit event loop: {err}"));
}
@@ -112,6 +113,14 @@ struct SmokeOptions {
resize_frame: u32,
timeout_seconds: u64,
mesh_input: MeshInput,
texture_input: Option<ResourceInput>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ResourceInput {
root: PathBuf,
archive: String,
name: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -148,6 +157,7 @@ impl MeshInput {
}
impl SmokeOptions {
#[allow(clippy::too_many_lines)]
fn parse(args: &[String]) -> Result<Self, String> {
let mut out = None;
let mut frames = DEFAULT_TARGET_FRAMES;
@@ -156,6 +166,9 @@ impl SmokeOptions {
let mut model_root = None;
let mut model_archive = None;
let mut model_name = None;
let mut texture_root = None;
let mut texture_archive = None;
let mut texture_name = None;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
@@ -208,6 +221,27 @@ impl SmokeOptions {
.ok_or_else(|| "--model-name requires a value".to_string())?,
);
}
"--texture-root" => {
texture_root = Some(
iter.next()
.map(PathBuf::from)
.ok_or_else(|| "--texture-root requires a path".to_string())?,
);
}
"--texture-archive" => {
texture_archive = Some(
iter.next()
.cloned()
.ok_or_else(|| "--texture-archive requires a value".to_string())?,
);
}
"--texture-name" => {
texture_name = Some(
iter.next()
.cloned()
.ok_or_else(|| "--texture-name requires a value".to_string())?,
);
}
_ => return Err(format!("unknown native smoke option: {arg}")),
}
}
@@ -234,12 +268,27 @@ impl SmokeOptions {
);
}
};
let texture_input = match (texture_root, texture_archive, texture_name) {
(None, None, None) => None,
(Some(root), Some(archive), Some(name)) => Some(ResourceInput {
root,
archive,
name,
}),
_ => {
return Err(
"--texture-root, --texture-archive and --texture-name must be supplied together"
.to_string(),
);
}
};
Ok(Self {
out,
frames,
resize_frame,
timeout_seconds,
mesh_input,
texture_input,
})
}
@@ -256,11 +305,27 @@ impl SmokeOptions {
}
}
}
fn load_texture(&self) -> Result<Option<VulkanStaticTexture>, String> {
self.texture_input.as_ref().map_or(Ok(None), |input| {
let image = fparkan_inspection::load_texture_mip0_rgba8_from_root(
&input.root,
&input.archive,
&input.name,
)?;
Ok(Some(VulkanStaticTexture {
width: image.width,
height: image.height,
rgba8: image.rgba8,
}))
})
}
}
struct SmokeApp {
options: SmokeOptions,
mesh: VulkanStaticMesh,
texture: Option<VulkanStaticTexture>,
completed: Arc<AtomicBool>,
progress: Arc<SharedSmokeProgress>,
window_id: Option<WindowId>,
@@ -305,12 +370,14 @@ impl SmokeApp {
fn new(
options: SmokeOptions,
mesh: VulkanStaticMesh,
texture: Option<VulkanStaticTexture>,
completed: Arc<AtomicBool>,
progress: Arc<SharedSmokeProgress>,
) -> Self {
Self {
options,
mesh,
texture,
completed,
progress,
window_id: None,
@@ -407,6 +474,23 @@ impl SmokeApp {
mesh_name: self.options.mesh_input.name(),
mesh_vertex_count: self.mesh.vertices.len(),
mesh_index_count: self.mesh.indices.len(),
texture_source: self
.options
.texture_input
.as_ref()
.map_or("none", |_| "original-texm"),
texture_archive: self
.options
.texture_input
.as_ref()
.map_or("", |input| input.archive.as_str()),
texture_name: self
.options
.texture_input
.as_ref()
.map_or("", |input| input.name.as_str()),
texture_width: self.texture.as_ref().map_or(0, |texture| texture.width),
texture_height: self.texture.as_ref().map_or(0, |texture| texture.height),
shader_manifest_hash: renderer
.as_ref()
.map_or("", |snapshot| snapshot.report.shader_manifest_hash.as_str()),
@@ -611,6 +695,20 @@ fn render_timeout_failure_report(
mesh_name: options.mesh_input.name(),
mesh_vertex_count: 0,
mesh_index_count: 0,
texture_source: options
.texture_input
.as_ref()
.map_or("none", |_| "original-texm"),
texture_archive: options
.texture_input
.as_ref()
.map_or("", |input| input.archive.as_str()),
texture_name: options
.texture_input
.as_ref()
.map_or("", |input| input.name.as_str()),
texture_width: 0,
texture_height: 0,
shader_manifest_hash: "",
vulkan_loader_status: if bootstrap.loader_available {
"available"
@@ -706,6 +804,7 @@ impl ApplicationHandler for SmokeApp {
render_request: RenderRequest::conservative(),
enable_validation: true,
mesh: self.mesh.clone(),
texture: self.texture.clone(),
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
}) {
Ok(renderer) => renderer,
@@ -838,6 +937,11 @@ struct SmokeReport<'a> {
mesh_name: &'a str,
mesh_vertex_count: usize,
mesh_index_count: usize,
texture_source: &'a str,
texture_archive: &'a str,
texture_name: &'a str,
texture_width: u32,
texture_height: u32,
shader_manifest_hash: &'a str,
vulkan_loader_status: &'a str,
vulkan_instance_status: &'a str,
@@ -1080,6 +1184,7 @@ mod tests {
resize_frame: DEFAULT_RESIZE_FRAME,
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
mesh_input: MeshInput::SmokeTriangle,
texture_input: None,
})
);
}
@@ -1116,6 +1221,7 @@ mod tests {
resize_frame: DEFAULT_RESIZE_FRAME,
timeout_seconds: 45,
mesh_input: MeshInput::SmokeTriangle,
texture_input: None,
})
);
}
@@ -1175,6 +1281,28 @@ mod tests {
);
}
#[test]
fn parses_original_texm_input_as_one_atomic_request() {
let parsed = SmokeOptions::parse(&[
"--out".to_string(),
"target/report.json".to_string(),
"--texture-root".to_string(),
"C:/GOG".to_string(),
"--texture-archive".to_string(),
"Textures.lib".to_string(),
"--texture-name".to_string(),
"DEFAULT.0".to_string(),
]);
assert!(matches!(
parsed,
Ok(SmokeOptions {
texture_input: Some(ResourceInput { archive, name, .. }),
..
}) if archive == "Textures.lib" && name == "DEFAULT.0"
));
}
#[test]
fn rejects_deprecated_self_assertion_flags() {
for flag in [
@@ -1239,6 +1367,11 @@ mod tests {
mesh_name: "MTCHECK.MSH",
mesh_vertex_count: 128,
mesh_index_count: 252,
texture_source: "original-texm",
texture_archive: "Textures.lib",
texture_name: "DEFAULT.0",
texture_width: 16,
texture_height: 16,
shader_manifest_hash: "deadbeef",
vulkan_loader_status: "available",
vulkan_instance_status: "created",
@@ -1264,6 +1397,7 @@ mod tests {
assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\""));
assert!(json.contains("\"runner_architecture\": \"x86_64\""));
assert!(json.contains("\"mesh_source\": \"original-msh\""));
assert!(json.contains("\"texture_source\": \"original-texm\""));
}
#[test]
@@ -1286,8 +1420,10 @@ mod tests {
resize_frame: DEFAULT_RESIZE_FRAME,
timeout_seconds: 7,
mesh_input: MeshInput::SmokeTriangle,
texture_input: None,
},
mesh: VulkanStaticMesh::smoke_triangle(),
texture: None,
completed: Arc::new(AtomicBool::new(false)),
progress: Arc::new(SharedSmokeProgress::default()),
window_id: None,
+31
View File
@@ -331,6 +331,37 @@ pub fn inspect_texture_from_root(
})
}
/// Loads a decoded TEXM document through repository-backed lookup.
///
/// # Errors
///
/// Returns a string error when the resource cannot be resolved or parsed as a
/// valid TEXM payload.
pub fn load_texture_from_root(
root: &Path,
archive: &str,
resource: &str,
) -> Result<fparkan_texm::TexmDocument, String> {
let bytes = read_resource_bytes_diagnostic(root, archive, resource)
.map_err(|err| render_human(&err))?;
decode_texm(bytes).map_err(|err| err.to_string())
}
/// Loads and decodes TEXM mip 0 as RGBA8 through repository-backed lookup.
///
/// # Errors
///
/// Returns a string error when the resource cannot be resolved, decoded, or
/// converted to the shared RGBA8 upload representation.
pub fn load_texture_mip0_rgba8_from_root(
root: &Path,
archive: &str,
resource: &str,
) -> Result<fparkan_texm::RgbaImage, String> {
let document = load_texture_from_root(root, archive, resource)?;
fparkan_texm::decode_mip_rgba8(&document, 0).map_err(|err| err.to_string())
}
/// Inspects a terrain land file by path.
///
/// # Errors
+2 -2
View File
@@ -8,7 +8,7 @@
| Path | Native window / swapchain | Draws pixels | Uses original assets | Acceptance class | Что доказывает | Чего не доказывает |
| --- | --- | --- | --- | --- | --- | --- |
| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH only | `covered-gpu` for Stage 0 smoke and explicit MSH bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH vertex/index upload and indexed draw | Texture sampling, descriptors, terrain, gameplay rendering |
| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH plus TEXM upload | `covered-gpu` for Stage 0 smoke and explicit MSH/TEXM bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH indexed draw; TEXM RGBA8 staging upload and `SHADER_READ_ONLY_OPTIMAL` transition | Texture sampling, descriptors, terrain, gameplay rendering |
| `VulkanPlanningBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Deterministic command validation, canonical capture, frame submission planning | Любой live GPU draw, pixel parity, validation-clean asset frame |
| `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 |
@@ -29,7 +29,7 @@
## Current repository status
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static-MSH bridge: `fparkan-vulkan-smoke --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane и выполняет real indexed draw. Это не является texture/material/terrain renderer.
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge: `fparkan-vulkan-smoke --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_ROOT> --texture-archive Textures.lib --texture-name DEFAULT.0` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane, выполняет real indexed draw и загружает decoded TEXM mip-0 в device-local image. Image переходится в sampling layout, но ещё не связан descriptor set-ом или fragment shader; это не texture/material/terrain renderer.
- `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх
synthetic window descriptor и `VulkanPlanningBackend`.
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
+1 -1
View File
@@ -295,7 +295,7 @@ S2-PLANNING-RENDER-007 covered cargo test -p fparkan-render --offline capture_is
S2-PLANNING-RENDER-008 covered-planning cargo test -p fparkan-render --offline recording_backend_stores_captures
S2-PLANNING-RENDER-009 covered cargo xtask policy
S3-VK-MESH-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; original MSH reports 128 vertices and 252 indices
S3-VK-TEXM-UPLOAD-001 blocked awaits Stage 3 Vulkan texture upload path
S3-VK-TEXM-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-texm-upload.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH --texture-root <GOG_ROOT> --texture-archive Textures.lib --texture-name DEFAULT.0; original TEXM 16x16 decoded RGBA8, staged into device-local image and transitioned to SHADER_READ_ONLY_OPTIMAL
S3-VK-DESCRIPTOR-001 blocked awaits Stage 3 descriptor set contract implementation
S3-VK-PIPELINE-001 blocked awaits Stage 3 graphics pipeline key and cache implementation
S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root <GOG_ROOT> --model-archive system.rlb --model-name MTCHECK.MSH; live Win32 Vulkan indexed MSH draw/present
1 # Acceptance coverage manifest.
295 S2-PLANNING-RENDER-008
296 S2-PLANNING-RENDER-009
297 S3-VK-MESH-UPLOAD-001
298 S3-VK-TEXM-UPLOAD-001
299 S3-VK-DESCRIPTOR-001
300 S3-VK-PIPELINE-001
301 S3-VK-DRAW-MODEL-001