refactor(vulkan-reporting): remove manual json assembly

This commit is contained in:
2026-06-25 05:23:41 +04:00
parent 5c4fbff2af
commit 0a78fc2460
3 changed files with 197 additions and 239 deletions
Generated
+2
View File
@@ -630,6 +630,8 @@ dependencies = [
"fparkan-binary",
"fparkan-platform",
"fparkan-render",
"serde",
"serde_json",
]
[[package]]
@@ -11,6 +11,8 @@ ash-window = "0.13"
fparkan-binary = { path = "../../crates/fparkan-binary" }
fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-render = { path = "../../crates/fparkan-render" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lints.rust]
unsafe_code = "allow"
+193 -239
View File
@@ -37,6 +37,7 @@ use fparkan_render::{
canonical_capture, validate_command_list, FrameOutput, RenderBackend, RenderCommand,
RenderCommandList, RenderError,
};
use serde::Serialize;
use std::collections::BTreeSet;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
@@ -461,7 +462,7 @@ const TRIANGLE_FRAGMENT_VALIDATE_COMMAND: &str =
"spirv-val --target-env vulkan1.0 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
/// Shader tool metadata pinned in the Stage 0 manifest.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct VulkanShaderToolManifest {
/// Tool executable name.
pub name: &'static str,
@@ -472,7 +473,8 @@ pub struct VulkanShaderToolManifest {
}
/// Vulkan shader stage.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VulkanShaderStage {
/// Vertex stage.
Vertex,
@@ -480,15 +482,6 @@ pub enum VulkanShaderStage {
Fragment,
}
impl VulkanShaderStage {
const fn as_str(self) -> &'static str {
match self {
Self::Vertex => "vertex",
Self::Fragment => "fragment",
}
}
}
/// Offline SPIR-V shader manifest entry.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanShaderModuleManifest {
@@ -534,7 +527,7 @@ pub struct VulkanShaderManifestReport {
}
/// Shader module validation report.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct VulkanShaderModuleReport {
/// Logical shader name.
pub name: &'static str,
@@ -3064,18 +3057,19 @@ fn live_surface_capabilities(
/// Renders a deterministic JSON Vulkan surface plan.
#[must_use]
pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&plan.schema.to_string());
out.push_str(",\"required_instance_extensions\":[");
for (index, extension) in plan.required_instance_extensions.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_json_string(&mut out, extension);
#[derive(Serialize)]
struct SurfacePlanJson<'a> {
schema: u32,
required_instance_extensions: &'a [String],
}
out.push_str("]}");
out
serialize_json_or_fallback(
&SurfacePlanJson {
schema: plan.schema,
required_instance_extensions: &plan.required_instance_extensions,
},
"{\"schema\":0,\"required_instance_extensions\":[]}",
)
}
fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
@@ -3243,26 +3237,23 @@ fn validation_layer_cstrings(
/// Renders a deterministic JSON Vulkan instance plan.
#[must_use]
pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&plan.schema.to_string());
out.push_str(",\"create_flags\":");
out.push_str(&plan.create_flags.to_string());
out.push_str(",\"validation_requested\":");
out.push_str(if plan.validation_requested {
"true"
} else {
"false"
});
out.push_str(",\"enabled_extensions\":[");
for (index, extension) in plan.enabled_extensions.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_json_string(&mut out, extension);
#[derive(Serialize)]
struct InstancePlanJson<'a> {
schema: u32,
create_flags: u32,
validation_requested: bool,
enabled_extensions: &'a [String],
}
out.push_str("]}");
out
serialize_json_or_fallback(
&InstancePlanJson {
schema: plan.schema,
create_flags: plan.create_flags,
validation_requested: plan.validation_requested,
enabled_extensions: &plan.enabled_extensions,
},
"{\"schema\":0,\"create_flags\":0,\"validation_requested\":false,\"enabled_extensions\":[]}",
)
}
fn cstring_vec(values: &[String]) -> Result<Vec<CString>, VulkanInstanceError> {
@@ -3348,19 +3339,21 @@ pub fn vulkan_entry_symbol_name() -> &'static CStr {
/// Renders a deterministic JSON Vulkan loader report.
#[must_use]
pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&report.schema.to_string());
out.push_str(",\"loader_available\":");
out.push_str(if report.loader_available {
"true"
} else {
"false"
});
out.push_str(",\"instance_api\":\"");
out.push_str(&format_api_version(report.instance_api_version));
out.push_str("\"}");
out
#[derive(Serialize)]
struct LoaderProbeReportJson {
schema: u32,
loader_available: bool,
instance_api: String,
}
serialize_json_or_fallback(
&LoaderProbeReportJson {
schema: report.schema,
loader_available: report.loader_available,
instance_api: format_api_version(report.instance_api_version),
},
"{\"schema\":0,\"loader_available\":false,\"instance_api\":\"0.0.0\"}",
)
}
/// Returns the built-in Stage 0 indexed-triangle shader manifest.
@@ -3445,16 +3438,23 @@ pub fn validate_shader_manifest(
}
fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
let mut normalized = String::new();
normalized.push_str("{\"stage\":\"");
normalized.push_str(module.stage.as_str());
normalized.push_str("\",\"entry_point\":");
push_json_string(&mut normalized, module.entry_point);
normalized.push_str(",\"descriptor_sets\":");
normalized.push_str(&module.descriptor_sets.to_string());
normalized.push_str(",\"push_constant_bytes\":");
normalized.push_str(&module.push_constant_bytes.to_string());
normalized.push('}');
#[derive(Serialize)]
struct ShaderInterfaceHashJson<'a> {
stage: VulkanShaderStage,
entry_point: &'a str,
descriptor_sets: u32,
push_constant_bytes: u32,
}
let normalized = serialize_json_or_fallback(
&ShaderInterfaceHashJson {
stage: module.stage,
entry_point: module.entry_point,
descriptor_sets: module.descriptor_sets,
push_constant_bytes: module.push_constant_bytes,
},
"{\"stage\":\"vertex\",\"entry_point\":\"main\",\"descriptor_sets\":0,\"push_constant_bytes\":0}",
);
sha256_hex(&sha256(normalized.as_bytes()))
}
@@ -3493,85 +3493,61 @@ fn spirv_words_to_bytes(words: &[u32]) -> Vec<u8> {
/// Renders a deterministic JSON shader manifest report.
#[must_use]
pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
let mut out = render_shader_manifest_without_hash_json(&report.modules);
out.push_str(",\"manifest_hash\":");
push_json_string(&mut out, &report.manifest_hash);
out.push('}');
out
#[derive(Serialize)]
struct ShaderManifestReportJson<'a> {
schema: u32,
target_env: &'a str,
compiler: &'a VulkanShaderToolManifest,
validator: &'a VulkanShaderToolManifest,
modules: &'a [VulkanShaderModuleReport],
manifest_hash: &'a str,
}
serialize_json_or_fallback(
&ShaderManifestReportJson {
schema: report.schema,
target_env: report.target_env,
compiler: &report.compiler,
validator: &report.validator,
modules: &report.modules,
manifest_hash: &report.manifest_hash,
},
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[],\"manifest_hash\":\"unknown\"}",
)
}
fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&SHADER_MANIFEST_SCHEMA.to_string());
out.push_str(",\"target_env\":");
push_json_string(&mut out, SHADER_TARGET_ENV);
out.push_str(",\"compiler\":");
out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
name: SHADER_COMPILER_NAME,
version: SHADER_COMPILER_VERSION,
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
}));
out.push_str(",\"validator\":");
out.push_str(&render_shader_tool_json(&VulkanShaderToolManifest {
name: SPIRV_VALIDATOR_NAME,
version: SPIRV_VALIDATOR_VERSION,
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
}));
out.push_str(",\"modules\":");
out.push_str(&render_shader_modules_json(modules));
out
}
fn render_shader_modules_json(modules: &[VulkanShaderModuleReport]) -> String {
let mut out = String::new();
out.push('[');
for (index, module) in modules.iter().enumerate() {
if index > 0 {
out.push(',');
}
out.push_str("{\"name\":");
push_json_string(&mut out, module.name);
out.push_str(",\"stage\":\"");
out.push_str(module.stage.as_str());
out.push_str("\",\"entry_point\":");
push_json_string(&mut out, module.entry_point);
out.push_str(",\"source_path\":");
push_json_string(&mut out, module.source_path);
out.push_str(",\"source_sha256\":");
push_json_string(&mut out, module.source_sha256);
out.push_str(",\"spirv_path\":");
push_json_string(&mut out, module.spirv_path);
out.push_str(",\"word_count\":");
out.push_str(&module.word_count.to_string());
out.push_str(",\"sha256\":");
push_json_string(&mut out, &module.sha256);
out.push_str(",\"descriptor_sets\":");
out.push_str(&module.descriptor_sets.to_string());
out.push_str(",\"push_constant_bytes\":");
out.push_str(&module.push_constant_bytes.to_string());
out.push_str(",\"compile_command\":");
push_json_string(&mut out, module.compile_command);
out.push_str(",\"validate_command\":");
push_json_string(&mut out, module.validate_command);
out.push_str(",\"interface_hash\":");
push_json_string(&mut out, &module.interface_hash);
out.push('}');
#[derive(Serialize)]
struct ShaderManifestWithoutHashJson<'a> {
schema: u32,
target_env: &'a str,
compiler: VulkanShaderToolManifest,
validator: VulkanShaderToolManifest,
modules: &'a [VulkanShaderModuleReport],
}
out.push(']');
out
}
fn render_shader_tool_json(tool: &VulkanShaderToolManifest) -> String {
let mut out = String::new();
out.push_str("{\"name\":");
push_json_string(&mut out, tool.name);
out.push_str(",\"version\":");
push_json_string(&mut out, tool.version);
out.push_str(",\"binary_sha256\":");
push_json_string(&mut out, tool.binary_sha256);
out.push('}');
out
let json = serialize_json_or_fallback(
&ShaderManifestWithoutHashJson {
schema: SHADER_MANIFEST_SCHEMA,
target_env: SHADER_TARGET_ENV,
compiler: VulkanShaderToolManifest {
name: SHADER_COMPILER_NAME,
version: SHADER_COMPILER_VERSION,
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
},
validator: VulkanShaderToolManifest {
name: SPIRV_VALIDATOR_NAME,
version: SPIRV_VALIDATOR_VERSION,
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
},
modules,
},
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[]}",
);
match json.strip_suffix('}') {
Some(stripped) => stripped.to_string(),
None => json,
}
}
/// Vulkan backend migration readiness.
@@ -3728,7 +3704,7 @@ pub struct VulkanSwapchainRecreationReport {
}
/// Deterministic frame submission plan for command buffers and sync objects.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct VulkanFrameSubmissionPlan {
/// Report schema version.
pub schema: u32,
@@ -4131,102 +4107,99 @@ fn compare_reports(
/// Renders a deterministic JSON capability report.
#[must_use]
pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&report.schema.to_string());
out.push_str(",\"vulkan_api\":\"");
out.push_str(&format_api_version(report.vulkan_api_version));
out.push_str("\",\"device_name\":");
push_json_string(&mut out, &report.device_name);
out.push_str(",\"score\":");
out.push_str(&report.score.to_string());
out.push_str(",\"graphics_queue_family\":");
out.push_str(&report.graphics_queue_family.to_string());
out.push_str(",\"present_queue_family\":");
out.push_str(&report.present_queue_family.to_string());
out.push_str(",\"portability_subset\":");
out.push_str(if report.portability_subset {
"true"
} else {
"false"
});
out.push_str(",\"enabled_extensions\":[");
for (index, extension) in report.enabled_extensions.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_json_string(&mut out, extension);
#[derive(Serialize)]
struct CapabilityReportJson<'a> {
schema: u32,
vulkan_api: String,
device_name: &'a str,
score: i32,
graphics_queue_family: u32,
present_queue_family: u32,
portability_subset: bool,
enabled_extensions: &'a [String],
}
out.push_str("]}");
out
serialize_json_or_fallback(
&CapabilityReportJson {
schema: report.schema,
vulkan_api: format_api_version(report.vulkan_api_version),
device_name: &report.device_name,
score: report.score,
graphics_queue_family: report.graphics_queue_family,
present_queue_family: report.present_queue_family,
portability_subset: report.portability_subset,
enabled_extensions: &report.enabled_extensions,
},
"{\"schema\":0,\"vulkan_api\":\"0.0.0\",\"device_name\":\"unknown\",\"score\":0,\"graphics_queue_family\":0,\"present_queue_family\":0,\"portability_subset\":false,\"enabled_extensions\":[]}",
)
}
/// Renders a deterministic JSON swapchain plan.
#[must_use]
pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&plan.schema.to_string());
out.push_str(",\"extent\":[");
out.push_str(&plan.extent.0.to_string());
out.push(',');
out.push_str(&plan.extent.1.to_string());
out.push_str("],\"format\":");
out.push_str(&plan.format.format.to_string());
out.push_str(",\"color_space\":");
out.push_str(&plan.format.color_space.to_string());
out.push_str(",\"present_mode\":");
out.push_str(&plan.present_mode.to_string());
out.push_str(",\"image_count\":");
out.push_str(&plan.image_count.to_string());
out.push('}');
out
#[derive(Serialize)]
struct SwapchainPlanJson {
schema: u32,
extent: [u32; 2],
format: i32,
color_space: i32,
present_mode: i32,
image_count: u32,
}
serialize_json_or_fallback(
&SwapchainPlanJson {
schema: plan.schema,
extent: [plan.extent.0, plan.extent.1],
format: plan.format.format,
color_space: plan.format.color_space,
present_mode: plan.present_mode,
image_count: plan.image_count,
},
"{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0}",
)
}
/// Renders a deterministic JSON swapchain recreation report.
#[must_use]
pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&report.schema.to_string());
out.push_str(",\"reason\":\"");
out.push_str(match report.reason {
VulkanSwapchainRecreationReason::Resize => "resize",
VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
});
out.push_str("\",\"previous_extent\":[");
out.push_str(&report.previous_extent.0.to_string());
out.push(',');
out.push_str(&report.previous_extent.1.to_string());
out.push_str("],\"next_extent\":[");
out.push_str(&report.next_extent.0.to_string());
out.push(',');
out.push_str(&report.next_extent.1.to_string());
out.push_str("]}");
out
#[derive(Serialize)]
struct SwapchainRecreationReportJson<'a> {
schema: u32,
reason: &'a str,
previous_extent: [u32; 2],
next_extent: [u32; 2],
}
serialize_json_or_fallback(
&SwapchainRecreationReportJson {
schema: report.schema,
reason: match report.reason {
VulkanSwapchainRecreationReason::Resize => "resize",
VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
},
previous_extent: [report.previous_extent.0, report.previous_extent.1],
next_extent: [report.next_extent.0, report.next_extent.1],
},
"{\"schema\":0,\"reason\":\"unknown\",\"previous_extent\":[0,0],\"next_extent\":[0,0]}",
)
}
/// Renders a deterministic JSON frame submission plan.
#[must_use]
pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
let mut out = String::new();
out.push_str("{\"schema\":");
out.push_str(&plan.schema.to_string());
out.push_str(",\"frames_in_flight\":");
out.push_str(&plan.frames_in_flight.to_string());
out.push_str(",\"command_buffers\":");
out.push_str(&plan.command_buffers.to_string());
out.push_str(",\"semaphores_per_frame\":");
out.push_str(&plan.semaphores_per_frame.to_string());
out.push_str(",\"fences_per_frame\":");
out.push_str(&plan.fences_per_frame.to_string());
out.push_str(",\"draw_count\":");
out.push_str(&plan.draw_count.to_string());
out.push_str(",\"indexed_vertex_count\":");
out.push_str(&plan.indexed_vertex_count.to_string());
out.push('}');
out
serialize_json_or_fallback(
plan,
"{\"schema\":0,\"frames_in_flight\":0,\"command_buffers\":0,\"semaphores_per_frame\":0,\"fences_per_frame\":0,\"draw_count\":0,\"indexed_vertex_count\":0}",
)
}
fn serialize_json_or_fallback<T: Serialize>(value: &T, fallback: &str) -> String {
match serde_json::to_string(value) {
Ok(json) => json,
Err(_) => fallback.to_string(),
}
}
fn format_api_version(version: u32) -> String {
@@ -4238,25 +4211,6 @@ fn format_api_version(version: u32) -> String {
)
}
fn push_json_string(out: &mut String, value: &str) {
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
/// Diagnostics for Vulkan planning backend setup and frame progression.
#[derive(Clone, Debug, PartialEq)]
pub struct VulkanPlanningBackendReport {