2026-06-23 22:05:16 +04:00
|
|
|
#![forbid(unsafe_code)]
|
2026-06-23 22:32:50 +04:00
|
|
|
#![cfg_attr(
|
|
|
|
|
test,
|
|
|
|
|
allow(
|
|
|
|
|
clippy::cast_possible_truncation,
|
|
|
|
|
clippy::cast_possible_wrap,
|
|
|
|
|
clippy::cast_precision_loss,
|
|
|
|
|
clippy::expect_used,
|
|
|
|
|
clippy::float_cmp,
|
|
|
|
|
clippy::identity_op,
|
|
|
|
|
clippy::too_many_lines,
|
|
|
|
|
clippy::uninlined_format_args,
|
|
|
|
|
clippy::map_unwrap_or,
|
|
|
|
|
clippy::needless_raw_string_hashes,
|
|
|
|
|
clippy::semicolon_if_nothing_returned,
|
|
|
|
|
clippy::type_complexity,
|
|
|
|
|
clippy::panic,
|
|
|
|
|
clippy::unwrap_used
|
|
|
|
|
)
|
|
|
|
|
)]
|
2026-06-23 22:05:16 +04:00
|
|
|
#![deny(unsafe_op_in_unsafe_fn)]
|
|
|
|
|
//! Vulkan adapter facade and migration-ready backend surface contract.
|
|
|
|
|
//!
|
|
|
|
|
//! This module intentionally keeps backend-agnostic command validation in the
|
|
|
|
|
//! shared render crate while exposing deterministic lifecycle telemetry used by
|
|
|
|
|
//! Stage 0 acceptance evidence.
|
|
|
|
|
//!
|
|
|
|
|
//! This crate is the declared low-level Vulkan boundary.
|
|
|
|
|
|
2026-06-23 22:32:50 +04:00
|
|
|
use fparkan_platform::RenderRequest;
|
2026-06-23 22:05:16 +04:00
|
|
|
use fparkan_render::{
|
|
|
|
|
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
|
|
|
|
};
|
|
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
|
|
/// Vulkan backend migration readiness.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum VulkanBackendState {
|
|
|
|
|
/// Adapter prepared and able to accept commands.
|
|
|
|
|
Ready,
|
|
|
|
|
/// Adapter is tracking a recoverable runtime surface/depth pipeline fault.
|
|
|
|
|
Degraded,
|
|
|
|
|
/// Adapter has encountered a non-recoverable error.
|
|
|
|
|
Error,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for VulkanBackendState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Degraded
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Diagnostics for Vulkan backend setup and frame progression.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
|
pub struct VulkanBackendReport {
|
|
|
|
|
/// Unix time at initialization.
|
|
|
|
|
pub initialized_at: u64,
|
|
|
|
|
/// Total frames executed.
|
|
|
|
|
pub frames_executed: u64,
|
|
|
|
|
/// Total command submissions.
|
|
|
|
|
pub submissions: u64,
|
|
|
|
|
/// Last command-capture byte size.
|
|
|
|
|
pub last_capture_size: usize,
|
|
|
|
|
/// Number of simulated present calls.
|
|
|
|
|
pub presents: u64,
|
|
|
|
|
/// Number of resize-driven surface plan refreshes.
|
|
|
|
|
pub resize_rebuilds: u64,
|
|
|
|
|
/// Last render request observed.
|
|
|
|
|
pub request: RenderRequest,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for VulkanBackendReport {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
initialized_at: SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.map_or(0, |duration| duration.as_secs()),
|
|
|
|
|
frames_executed: 0,
|
|
|
|
|
submissions: 0,
|
|
|
|
|
last_capture_size: 0,
|
|
|
|
|
presents: 0,
|
|
|
|
|
resize_rebuilds: 0,
|
|
|
|
|
request: RenderRequest::conservative(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Vulkan backend façade used by the game entrypoint.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct VulkanBackend {
|
|
|
|
|
state: VulkanBackendState,
|
|
|
|
|
report: VulkanBackendReport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for VulkanBackend {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl VulkanBackend {
|
|
|
|
|
/// Creates a new Vulkan-backed backend façade.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
state: VulkanBackendState::Ready,
|
|
|
|
|
report: VulkanBackendReport::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Replaces active surface/profile request.
|
|
|
|
|
pub fn set_render_request(&mut self, request: RenderRequest) {
|
|
|
|
|
self.report.request = request;
|
|
|
|
|
self.report.resize_rebuilds = self.report.resize_rebuilds.saturating_add(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns active render request policy.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn render_request(&self) -> RenderRequest {
|
|
|
|
|
self.report.request
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns adapter state.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn state(&self) -> VulkanBackendState {
|
|
|
|
|
self.state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns backend report.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn report(&self) -> &VulkanBackendReport {
|
|
|
|
|
&self.report
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn simulate_present(&mut self) {
|
|
|
|
|
self.report.presents = self.report.presents.saturating_add(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RenderBackend for VulkanBackend {
|
|
|
|
|
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
|
2026-06-23 22:32:50 +04:00
|
|
|
if !matches!(
|
|
|
|
|
self.state,
|
|
|
|
|
VulkanBackendState::Ready | VulkanBackendState::Degraded
|
|
|
|
|
) {
|
2026-06-23 22:05:16 +04:00
|
|
|
return Err(RenderError::InvalidRange);
|
|
|
|
|
}
|
|
|
|
|
let capture = canonical_capture(commands)?;
|
|
|
|
|
self.report.frames_executed = self.report.frames_executed.saturating_add(1);
|
|
|
|
|
self.report.submissions = self.report.submissions.saturating_add(1);
|
|
|
|
|
self.report.last_capture_size = capture.len();
|
|
|
|
|
self.simulate_present();
|
|
|
|
|
Ok(FrameOutput)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use fparkan_render::{
|
|
|
|
|
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn backend_tracks_render_request_and_presents() -> Result<(), RenderError> {
|
|
|
|
|
let mut backend = VulkanBackend::new();
|
|
|
|
|
let request = RenderRequest::conservative();
|
|
|
|
|
backend.set_render_request(request);
|
|
|
|
|
assert_eq!(backend.render_request(), request);
|
|
|
|
|
assert_eq!(backend.report().resize_rebuilds, 1);
|
|
|
|
|
|
|
|
|
|
let commands = fparkan_render::RenderCommandList {
|
|
|
|
|
commands: vec![
|
|
|
|
|
RenderCommand::BeginFrame,
|
|
|
|
|
RenderCommand::Draw(DrawCommand {
|
|
|
|
|
id: DrawId(11),
|
|
|
|
|
phase: RenderPhase::Opaque,
|
|
|
|
|
object_id: None,
|
|
|
|
|
mesh: GpuMeshId(1),
|
|
|
|
|
material: GpuMaterialId(2),
|
|
|
|
|
transform: [1.0; 16],
|
|
|
|
|
range: IndexRange { start: 0, count: 3 },
|
|
|
|
|
stable_order: 7,
|
|
|
|
|
}),
|
|
|
|
|
RenderCommand::EndFrame,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
backend.execute(&commands)?;
|
|
|
|
|
assert_eq!(backend.state(), VulkanBackendState::Ready);
|
|
|
|
|
assert_eq!(backend.report().frames_executed, 1);
|
|
|
|
|
assert_eq!(backend.report().submissions, 1);
|
|
|
|
|
assert_eq!(backend.report().presents, 1);
|
|
|
|
|
assert!(backend.report().last_capture_size > 0);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|