docs(workspace): standardize function doc sections and enforce # Errors
This commit is contained in:
parent
c6d8d1bd70
commit
092d546c52
|
|
@ -124,6 +124,12 @@ The workspace opts into strict linting: Clippy's `pedantic` group plus restricti
|
||||||
- **Voice:** Use the passive voice or neutral descriptive language. Instead of "We initialize the buffer," use "The buffer is initialized." Instead of "Your vertex shader needs this," use "The vertex shader requires this."
|
- **Voice:** Use the passive voice or neutral descriptive language. Instead of "We initialize the buffer," use "The buffer is initialized." Instead of "Your vertex shader needs this," use "The vertex shader requires this."
|
||||||
- **Focus:** Describe the code's behavior, the system's state, or technical invariants.
|
- **Focus:** Describe the code's behavior, the system's state, or technical invariants.
|
||||||
- **Struct Documentation:** Every field in a public or internal struct must have a doc comment (`///`) explaining its purpose and any invariants.
|
- **Struct Documentation:** Every field in a public or internal struct must have a doc comment (`///`) explaining its purpose and any invariants.
|
||||||
|
- **Function documentation sections:** Function doc comments follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/documentation.html) standard sections, in this fixed order after the summary and any extended description: `# Errors`, then `# Panics`, then `# Safety`. The sections apply to **all** functions, public and private (clippy only enforces the public ones; the same standard is expected on private helpers by hand).
|
||||||
|
- **`# Errors`** is mandatory on every function returning `Result`, and states the conditions under which each error variant is returned. `fn main` is exempt.
|
||||||
|
- **`# Panics`** is mandatory on any function that can panic (an `expect`/`unwrap`/`panic!`/`assert!`/indexing/arithmetic that can trip), and states the condition that triggers the panic.
|
||||||
|
- **`# Safety`** is mandatory on every `unsafe fn`, and states the invariants the caller must uphold.
|
||||||
|
- Test functions (`#[test]`, and helpers inside `#[cfg(test)]`) are exempt from all three; they are not part of the documented surface.
|
||||||
|
- Enforcement: `missing_errors_doc`, `missing_panics_doc`, and `missing_safety_doc` are warnings in the workspace lint set, so a missing section on a public item fails CI.
|
||||||
- **Stability:** Treat the documentation as a technical specification for the engine.
|
- **Stability:** Treat the documentation as a technical specification for the engine.
|
||||||
- **Line breaks:** Do not insert line returns inside a comment unless necessary. A comment that fits on a single line stays on a single line; do not pre-wrap at ~80 chars for aesthetics. Only break across lines when the comment is genuinely long (multi-sentence prose, enumerated invariants) or when a hard break carries meaning (separating an intro line from a bullet list, for instance).
|
- **Line breaks:** Do not insert line returns inside a comment unless necessary. A comment that fits on a single line stays on a single line; do not pre-wrap at ~80 chars for aesthetics. Only break across lines when the comment is genuinely long (multi-sentence prose, enumerated invariants) or when a hard break carries meaning (separating an intro line from a bullet list, for instance).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,4 +42,3 @@ unimplemented = "warn"
|
||||||
# Pedantic exceptions (too noisy)
|
# Pedantic exceptions (too noisy)
|
||||||
module_name_repetitions = "allow"
|
module_name_repetitions = "allow"
|
||||||
must_use_candidate = "allow"
|
must_use_candidate = "allow"
|
||||||
missing_errors_doc = "allow"
|
|
||||||
|
|
@ -33,6 +33,10 @@ fn write_varint(value: u64, buf: &mut Vec<u8>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads an unsigned LEB128 varint from the front of `bytes`, returning the decoded value and the number of bytes consumed.
|
/// Reads an unsigned LEB128 varint from the front of `bytes`, returning the decoded value and the number of bytes consumed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::MalformedVarint`] if the encoding exceeds the ten bytes a `u64` may occupy, or [`NetError::UnexpectedEof`] if the buffer ends while the continuation bit is still set.
|
||||||
fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> {
|
fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> {
|
||||||
let mut value: u64 = 0;
|
let mut value: u64 = 0;
|
||||||
let mut shift: u32 = 0;
|
let mut shift: u32 = 0;
|
||||||
|
|
@ -52,6 +56,10 @@ fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes `msg` as a single length-prefixed `postcard` frame into a freshly allocated buffer.
|
/// Encodes `msg` as a single length-prefixed `postcard` frame into a freshly allocated buffer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::Postcard`] if `msg` fails to serialize.
|
||||||
fn encode_frame<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>, NetError> {
|
fn encode_frame<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>, NetError> {
|
||||||
let payload = postcard::to_stdvec(msg)?;
|
let payload = postcard::to_stdvec(msg)?;
|
||||||
let mut frame = Vec::new();
|
let mut frame = Vec::new();
|
||||||
|
|
@ -61,6 +69,10 @@ fn encode_frame<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>, NetError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes one length-prefixed `postcard` frame to a quinn send stream.
|
/// Writes one length-prefixed `postcard` frame to a quinn send stream.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::Postcard`] if `msg` fails to serialize, or [`NetError::Write`] if the send stream rejects the bytes.
|
||||||
pub async fn write_frame<T: serde::Serialize>(
|
pub async fn write_frame<T: serde::Serialize>(
|
||||||
stream: &mut quinn::SendStream,
|
stream: &mut quinn::SendStream,
|
||||||
msg: &T,
|
msg: &T,
|
||||||
|
|
@ -71,6 +83,10 @@ pub async fn write_frame<T: serde::Serialize>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads one length-prefixed `postcard` frame from a quinn recv stream and decodes it.
|
/// Reads one length-prefixed `postcard` frame from a quinn recv stream and decodes it.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::MalformedVarint`] if the length prefix is overlong, [`NetError::FrameTooLarge`] if the declared length exceeds `max_len`, [`NetError::Read`] if the stream ends before the frame is complete, or [`NetError::Postcard`] if the payload fails to deserialize.
|
||||||
pub async fn read_frame<T: serde::de::DeserializeOwned>(
|
pub async fn read_frame<T: serde::de::DeserializeOwned>(
|
||||||
stream: &mut quinn::RecvStream,
|
stream: &mut quinn::RecvStream,
|
||||||
max_len: usize,
|
max_len: usize,
|
||||||
|
|
@ -98,6 +114,10 @@ pub async fn read_frame<T: serde::de::DeserializeOwned>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a declared frame length against `max_len`.
|
/// Validates a declared frame length against `max_len`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::FrameTooLarge`] if `len` exceeds `max_len`.
|
||||||
fn check_frame_len(len: u64, max_len: usize) -> Result<usize, NetError> {
|
fn check_frame_len(len: u64, max_len: usize) -> Result<usize, NetError> {
|
||||||
if len > max_len as u64 {
|
if len > max_len as u64 {
|
||||||
return Err(NetError::FrameTooLarge { len, max: max_len });
|
return Err(NetError::FrameTooLarge { len, max: max_len });
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ fn ensure_crypto_provider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a QUIC server endpoint bound to `bind`, using a freshly generated self-signed certificate and the `synvael` ALPN.
|
/// Builds a QUIC server endpoint bound to `bind`, using a freshly generated self-signed certificate and the `synvael` ALPN.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::Rcgen`] if certificate generation fails, [`NetError::Rustls`] if the TLS configuration cannot be built, [`NetError::NoInitialCipherSuite`] if the configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the UDP socket cannot be bound.
|
||||||
pub fn server_endpoint(bind: SocketAddr) -> Result<Endpoint, NetError> {
|
pub fn server_endpoint(bind: SocketAddr) -> Result<Endpoint, NetError> {
|
||||||
ensure_crypto_provider();
|
ensure_crypto_provider();
|
||||||
|
|
||||||
|
|
@ -51,6 +55,10 @@ pub fn server_endpoint(bind: SocketAddr) -> Result<Endpoint, NetError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a QUIC client endpoint bound to an ephemeral local address, configured with the `synvael` ALPN and a permissive certificate verifier.
|
/// Builds a QUIC client endpoint bound to an ephemeral local address, configured with the `synvael` ALPN and a permissive certificate verifier.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NetError::NoInitialCipherSuite`] if the TLS configuration lacks a TLS 1.3 cipher suite, or [`NetError::Io`] if the local UDP socket cannot be bound.
|
||||||
pub fn client_endpoint() -> Result<Endpoint, NetError> {
|
pub fn client_endpoint() -> Result<Endpoint, NetError> {
|
||||||
ensure_crypto_provider();
|
ensure_crypto_provider();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ use crate::error::RendererError;
|
||||||
use ash::{Device, Instance, khr, vk};
|
use ash::{Device, Instance, khr, vk};
|
||||||
|
|
||||||
/// Picks a physical device (GPU) that supports the required features and extensions.
|
/// Picks a physical device (GPU) that supports the required features and extensions.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if physical devices cannot be enumerated, or [`RendererError::NoSuitableGpu`] if none meets the requirements.
|
||||||
pub fn pick_physical_device(
|
pub fn pick_physical_device(
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
surface_loader: &khr::surface::Instance,
|
surface_loader: &khr::surface::Instance,
|
||||||
|
|
@ -27,6 +31,10 @@ pub fn pick_physical_device(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a logical device and retrieves the graphics queue.
|
/// Creates a logical device and retrieves the graphics queue.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the device cannot be created.
|
||||||
pub fn create_logical_device(
|
pub fn create_logical_device(
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
physical_device: vk::PhysicalDevice,
|
physical_device: vk::PhysicalDevice,
|
||||||
|
|
@ -58,6 +66,10 @@ pub fn create_logical_device(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds a queue family that supports both graphics commands and presentation.
|
/// Finds a queue family that supports both graphics commands and presentation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if surface-support queries fail, or [`RendererError::NoSuitableGpu`] if no family supports both graphics and presentation.
|
||||||
pub fn find_graphics_queue_family(
|
pub fn find_graphics_queue_family(
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
physical_device: vk::PhysicalDevice,
|
physical_device: vk::PhysicalDevice,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ use std::ffi::{CStr, c_char};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
/// Creates a Vulkan instance and optionally a debug messenger.
|
/// Creates a Vulkan instance and optionally a debug messenger.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if instance creation fails, or if the debug messenger cannot be created in debug builds.
|
||||||
pub fn create_instance(
|
pub fn create_instance(
|
||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
required_extensions: &[*const c_char],
|
required_extensions: &[*const c_char],
|
||||||
|
|
@ -62,6 +66,10 @@ pub fn create_instance(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The callback function invoked by Vulkan's validation layers.
|
/// The callback function invoked by Vulkan's validation layers.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Invoked by the Vulkan loader, which must pass a valid `p_callback_data` pointer whose `p_message` is either null or a valid NUL-terminated C string. Not to be called directly.
|
||||||
unsafe extern "system" fn vulkan_debug_callback(
|
unsafe extern "system" fn vulkan_debug_callback(
|
||||||
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
||||||
_message_type: vk::DebugUtilsMessageTypeFlagsEXT,
|
_message_type: vk::DebugUtilsMessageTypeFlagsEXT,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ impl Renderer {
|
||||||
/// This function loads the Vulkan library, creates an instance, selects a GPU,
|
/// This function loads the Vulkan library, creates an instance, selects a GPU,
|
||||||
/// and initializes a logical device with a graphics queue.
|
/// and initializes a logical device with a graphics queue.
|
||||||
///
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError`] if any initialization step fails: loading Vulkan, creating the instance, surface, device, swapchain, pipeline, allocator, or initial geometry.
|
||||||
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `MAX_FRAMES_IN_FLIGHT` or vertex data sizes exceed `u32`/`u64` limits.
|
/// Panics if `MAX_FRAMES_IN_FLIGHT` or vertex data sizes exceed `u32`/`u64` limits.
|
||||||
|
|
@ -166,6 +170,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a GPU memory allocator.
|
/// Creates a GPU memory allocator.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::AllocationError`] if the allocator cannot be initialized.
|
||||||
fn create_allocator(
|
fn create_allocator(
|
||||||
instance: &ash::Instance,
|
instance: &ash::Instance,
|
||||||
device: &ash::Device,
|
device: &ash::Device,
|
||||||
|
|
@ -186,6 +194,10 @@ fn create_allocator(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the 3D geometry buffers (vertex and index) for a cube.
|
/// Creates the 3D geometry buffers (vertex and index) for a cube.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if a buffer cannot be created.
|
||||||
fn create_geometry(
|
fn create_geometry(
|
||||||
device: &ash::Device,
|
device: &ash::Device,
|
||||||
allocator: &mut Allocator,
|
allocator: &mut Allocator,
|
||||||
|
|
@ -260,6 +272,10 @@ fn create_geometry(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the depth buffer resources (image, memory, and view).
|
/// Creates the depth buffer resources (image, memory, and view).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the depth image or its view cannot be created.
|
||||||
fn create_depth_resources(
|
fn create_depth_resources(
|
||||||
device: &ash::Device,
|
device: &ash::Device,
|
||||||
allocator: &mut Allocator,
|
allocator: &mut Allocator,
|
||||||
|
|
@ -321,6 +337,10 @@ fn create_depth_resources(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to create and populate a GPU buffer.
|
/// Helper function to create and populate a GPU buffer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the buffer cannot be created or bound.
|
||||||
fn create_gpu_buffer(
|
fn create_gpu_buffer(
|
||||||
device: &ash::Device,
|
device: &ash::Device,
|
||||||
allocator: &mut Allocator,
|
allocator: &mut Allocator,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ use std::io::Cursor;
|
||||||
/// Helper to load SPIR-V bytes and create a Vulkan Shader Module.
|
/// Helper to load SPIR-V bytes and create a Vulkan Shader Module.
|
||||||
///
|
///
|
||||||
/// Vulkan expects shader code to be 32-bit aligned; `ash::util::read_spv` is used to correctly interpret the raw bytes as a slice of `u32`.
|
/// Vulkan expects shader code to be 32-bit aligned; `ash::util::read_spv` is used to correctly interpret the raw bytes as a slice of `u32`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::IoError`] if `bytes` is not valid, 32-bit-aligned SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device.
|
||||||
pub fn create_shader_module(
|
pub fn create_shader_module(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
|
@ -27,6 +31,10 @@ pub fn create_shader_module(
|
||||||
/// Defines the 'interface' of the pipeline (what data we can pass to the shaders).
|
/// Defines the 'interface' of the pipeline (what data we can pass to the shaders).
|
||||||
///
|
///
|
||||||
/// This layout defines any push constants or descriptor sets (textures/UBOs) accessed by the shaders during execution.
|
/// This layout defines any push constants or descriptor sets (textures/UBOs) accessed by the shaders during execution.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the device fails to create the pipeline layout.
|
||||||
pub fn create_pipeline_layout(device: &Device) -> Result<vk::PipelineLayout, RendererError> {
|
pub fn create_pipeline_layout(device: &Device) -> Result<vk::PipelineLayout, RendererError> {
|
||||||
// A single push constant range is defined for the MVP matrix, allowing it to be updated for every draw call with high efficiency.
|
// A single push constant range is defined for the MVP matrix, allowing it to be updated for every draw call with high efficiency.
|
||||||
#[expect(
|
#[expect(
|
||||||
|
|
@ -50,6 +58,10 @@ pub fn create_pipeline_layout(device: &Device) -> Result<vk::PipelineLayout, Ren
|
||||||
/// Creates a Graphics Pipeline for voxel rendering using Vulkan 1.3 Dynamic Rendering.
|
/// Creates a Graphics Pipeline for voxel rendering using Vulkan 1.3 Dynamic Rendering.
|
||||||
///
|
///
|
||||||
/// The pipeline encapsulates the entire state of the GPU for a specific draw operation, including shader stages, vertex input layout, rasterization settings, and blending.
|
/// The pipeline encapsulates the entire state of the GPU for a specific draw operation, including shader stages, vertex input layout, rasterization settings, and blending.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::InvalidString`] if the shader entry-point name cannot be built, [`RendererError::IoError`] if an embedded shader is not valid SPIR-V, or [`RendererError::VulkanError`] if shader-module or pipeline creation fails on the device.
|
||||||
pub fn create_graphics_pipeline(
|
pub fn create_graphics_pipeline(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
layout: vk::PipelineLayout,
|
layout: vk::PipelineLayout,
|
||||||
|
|
@ -150,6 +162,10 @@ pub fn create_graphics_pipeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the vertex and fragment shader modules from embedded bytes.
|
/// Loads the vertex and fragment shader modules from embedded bytes.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::IoError`] if an embedded shader is not valid SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device.
|
||||||
fn load_shader_modules(
|
fn load_shader_modules(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
) -> Result<(vk::ShaderModule, vk::ShaderModule), RendererError> {
|
) -> Result<(vk::ShaderModule, vk::ShaderModule), RendererError> {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ pub struct Renderer {
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// Renders a single frame.
|
/// Renders a single frame.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::SyncPrimitivesMissing`] if the synchronization primitives have been torn down, or [`RendererError::VulkanError`] if any device operation (fence wait, image acquire, command recording, submit, or present) fails.
|
||||||
pub fn draw_frame(&mut self, camera_view: glam::Mat4) -> Result<(), RendererError> {
|
pub fn draw_frame(&mut self, camera_view: glam::Mat4) -> Result<(), RendererError> {
|
||||||
let sync = self
|
let sync = self
|
||||||
.sync
|
.sync
|
||||||
|
|
@ -157,6 +161,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records the drawing commands into the given command buffer.
|
/// Records the drawing commands into the given command buffer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if beginning or ending command-buffer recording fails.
|
||||||
fn record_commands(
|
fn record_commands(
|
||||||
&self,
|
&self,
|
||||||
cmd: vk::CommandBuffer,
|
cmd: vk::CommandBuffer,
|
||||||
|
|
@ -322,6 +330,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transitions the swapchain image back to the presentation layout.
|
/// Transitions the swapchain image back to the presentation layout.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the pipeline barrier command cannot be recorded.
|
||||||
fn transition_to_present_layout(
|
fn transition_to_present_layout(
|
||||||
&self,
|
&self,
|
||||||
cmd: vk::CommandBuffer,
|
cmd: vk::CommandBuffer,
|
||||||
|
|
@ -359,7 +371,8 @@ impl Renderer {
|
||||||
/// Replaces the currently rendering mesh with a new set of vertices and indices.
|
/// Replaces the currently rendering mesh with a new set of vertices and indices.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a `RendererError` if new Vulkan buffers cannot be allocated or created.
|
///
|
||||||
|
/// Returns [`RendererError::AllocationError`] if GPU memory cannot be allocated, or [`RendererError::VulkanError`] if the vertex or index buffers cannot be created.
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::cast_possible_truncation,
|
clippy::cast_possible_truncation,
|
||||||
reason = "a chunk mesh's index count never approaches u32::MAX"
|
reason = "a chunk mesh's index count never approaches u32::MAX"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ use ash::{Entry, Instance, khr, vk};
|
||||||
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
|
||||||
/// Creates a Vulkan surface for the given window.
|
/// Creates a Vulkan surface for the given window.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the platform surface cannot be created for the given display and window handles.
|
||||||
pub fn create_surface(
|
pub fn create_surface(
|
||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ use crate::error::RendererError;
|
||||||
use ash::{Device, Instance, khr, vk};
|
use ash::{Device, Instance, khr, vk};
|
||||||
|
|
||||||
/// Creates a swapchain and retrieves its images.
|
/// Creates a swapchain and retrieves its images.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if a surface query fails or the swapchain and its images cannot be created.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the driver reports zero surface formats, which the Vulkan specification forbids for a supported surface.
|
||||||
pub fn create_swapchain(
|
pub fn create_swapchain(
|
||||||
instance: &Instance,
|
instance: &Instance,
|
||||||
physical_device: vk::PhysicalDevice,
|
physical_device: vk::PhysicalDevice,
|
||||||
|
|
@ -87,6 +95,10 @@ pub fn create_swapchain(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates image views for the swapchain images.
|
/// Creates image views for the swapchain images.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the device fails to create an image view.
|
||||||
pub fn create_image_views(
|
pub fn create_image_views(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
images: &[vk::Image],
|
images: &[vk::Image],
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ pub struct SyncPrimitives {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates all synchronization primitives for the given number of frames and images.
|
/// Creates all synchronization primitives for the given number of frames and images.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RendererError::VulkanError`] if the device fails to create a semaphore or fence.
|
||||||
pub fn create_sync_primitives(
|
pub fn create_sync_primitives(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
max_frames_in_flight: usize,
|
max_frames_in_flight: usize,
|
||||||
|
|
@ -43,6 +47,10 @@ pub fn create_sync_primitives(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Destroys all synchronization primitives.
|
/// Destroys all synchronization primitives.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The caller must ensure every primitive in `sync` was created from `device`, is no longer in use by any in-flight GPU work, and is not destroyed again.
|
||||||
pub unsafe fn destroy_sync_primitives(device: &Device, sync: SyncPrimitives) {
|
pub unsafe fn destroy_sync_primitives(device: &Device, sync: SyncPrimitives) {
|
||||||
unsafe {
|
unsafe {
|
||||||
for semaphore in sync.image_available {
|
for semaphore in sync.image_available {
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ fn actor_loop(region_dir: &Path, request_rx: &Receiver<SaveRequest>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes every dirty region to disk, returning the first error while still attempting the rest.
|
/// Flushes every dirty region to disk, returning the first error while still attempting the rest.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the first [`SaveError`] produced by [`RegionFile::save`]; remaining dirty regions are still flushed.
|
||||||
fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), SaveError> {
|
fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), SaveError> {
|
||||||
let mut result = Ok(());
|
let mut result = Ok(());
|
||||||
for region in regions.values_mut() {
|
for region in regions.values_mut() {
|
||||||
|
|
@ -127,6 +131,10 @@ fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), Save
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the region file covering `pos`, opening and caching it on first access.
|
/// Returns the region file covering `pos`, opening and caching it on first access.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a [`SaveError`] from [`RegionFile::open`] if the region file exists but cannot be read or decoded.
|
||||||
fn region_mut<'a>(
|
fn region_mut<'a>(
|
||||||
regions: &'a mut HashMap<(i32, i32), RegionFile>,
|
regions: &'a mut HashMap<(i32, i32), RegionFile>,
|
||||||
region_dir: &Path,
|
region_dir: &Path,
|
||||||
|
|
@ -143,6 +151,10 @@ fn region_mut<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the stored chunk at `pos`, opening and caching its region file on first access.
|
/// Reads the stored chunk at `pos`, opening and caching its region file on first access.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a [`SaveError`] if the region file cannot be opened or the stored record cannot be decoded.
|
||||||
fn read_chunk(
|
fn read_chunk(
|
||||||
regions: &mut HashMap<(i32, i32), RegionFile>,
|
regions: &mut HashMap<(i32, i32), RegionFile>,
|
||||||
region_dir: &Path,
|
region_dir: &Path,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ pub struct RegionFile {
|
||||||
|
|
||||||
impl RegionFile {
|
impl RegionFile {
|
||||||
/// Opens the region file at `path`, or yields an empty region if the file does not yet exist.
|
/// Opens the region file at `path`, or yields an empty region if the file does not yet exist.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Io`] if the file cannot be read, a decoding error from [`RegionIndex::decode`] if the index is malformed, or [`SaveError::PayloadTooLarge`] / [`SaveError::Truncated`] if a header entry's span falls outside the file.
|
||||||
pub fn open(path: PathBuf) -> Result<Self, SaveError> {
|
pub fn open(path: PathBuf) -> Result<Self, SaveError> {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
|
|
@ -107,6 +111,10 @@ impl RegionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes and returns the chunk at `pos`, or `None` if the region holds no record for it.
|
/// Decodes and returns the chunk at `pos`, or `None` if the region holds no record for it.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a decoding error from [`record::decode`] if the stored record is malformed.
|
||||||
pub fn read_chunk(&self, pos: ChunkPos) -> Result<Option<ChunkData>, SaveError> {
|
pub fn read_chunk(&self, pos: ChunkPos) -> Result<Option<ChunkData>, SaveError> {
|
||||||
match self.records.get(&pos) {
|
match self.records.get(&pos) {
|
||||||
Some(bytes) => {
|
Some(bytes) => {
|
||||||
|
|
@ -118,6 +126,10 @@ impl RegionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes `data` into a `SYNC` record stamped with `last_modified` and stores it under `pos`.
|
/// Encodes `data` into a `SYNC` record stamped with `last_modified` and stores it under `pos`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an encoding error from [`record::encode`] if serialization fails, or [`SaveError::PayloadTooLarge`] if the encoded record exceeds `u32::MAX` bytes.
|
||||||
pub fn write_chunk(
|
pub fn write_chunk(
|
||||||
&mut self,
|
&mut self,
|
||||||
pos: ChunkPos,
|
pos: ChunkPos,
|
||||||
|
|
@ -150,6 +162,10 @@ impl RegionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flushes the region to disk with a crash-safe whole-file atomic rewrite, clearing the dirty flag.
|
/// Flushes the region to disk with a crash-safe whole-file atomic rewrite, clearing the dirty flag.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::PayloadTooLarge`] if a record's length exceeds `u32::MAX`, or [`SaveError::Io`] if the atomic write to disk fails.
|
||||||
pub fn save(&mut self) -> Result<(), SaveError> {
|
pub fn save(&mut self) -> Result<(), SaveError> {
|
||||||
let image = self.serialize()?;
|
let image = self.serialize()?;
|
||||||
atomic_write(&self.path, &image)?;
|
atomic_write(&self.path, &image)?;
|
||||||
|
|
@ -158,6 +174,10 @@ impl RegionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the complete on-disk file image: the encoded index followed by every record.
|
/// Builds the complete on-disk file image: the encoded index followed by every record.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::PayloadTooLarge`] if the index or any record length exceeds `u32::MAX` bytes.
|
||||||
// * NOTE: this is a whole-file rewrite. The right way to do it for large saves is to append changed records into free space and rewriting only the header table, so save cost scales with chunks modified rather than total file size. The free list and absolute offsets already on disk support that switch without a format change.
|
// * NOTE: this is a whole-file rewrite. The right way to do it for large saves is to append changed records into free space and rewriting only the header table, so save cost scales with chunks modified rather than total file size. The free list and absolute offsets already on disk support that switch without a format change.
|
||||||
// TODO: incremental save.
|
// TODO: incremental save.
|
||||||
fn serialize(&mut self) -> Result<Vec<u8>, SaveError> {
|
fn serialize(&mut self) -> Result<Vec<u8>, SaveError> {
|
||||||
|
|
@ -188,6 +208,10 @@ impl RegionFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes `bytes` to `path` via the POSIX atomic-write pattern: `.tmp` + fsync + rename.
|
/// Writes `bytes` to `path` via the POSIX atomic-write pattern: `.tmp` + fsync + rename.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Io`] if the parent directory cannot be created, or if writing, syncing, or renaming the temporary file fails.
|
||||||
fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), SaveError> {
|
fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), SaveError> {
|
||||||
// The region directory is created on demand so the first write to a fresh world succeeds.
|
// The region directory is created on demand so the first write to a fresh world succeeds.
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,10 @@ fn load_chunk(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a read request to the save actor and blocks for its reply, mapping a departed actor to an absent record so generation can still proceed.
|
/// Sends a read request to the save actor and blocks for its reply, mapping a departed actor to an absent record so generation can still proceed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the [`SaveError`] reported by the save actor if reading the stored chunk fails. A departed actor yields `Ok(None)` rather than an error.
|
||||||
fn request_saved_chunk(
|
fn request_saved_chunk(
|
||||||
save_tx: &Sender<SaveRequest>,
|
save_tx: &Sender<SaveRequest>,
|
||||||
pos: ChunkPos,
|
pos: ChunkPos,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ impl<'a> Reader<'a> {
|
||||||
Self { bytes, offset: 0 }
|
Self { bytes, offset: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the next `n` bytes and advances the cursor, or [`SaveError::Truncated`] if fewer remain.
|
/// Returns the next `n` bytes and advances the cursor.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Truncated`] if fewer than `n` bytes remain, or if the offset addition overflows.
|
||||||
pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], SaveError> {
|
pub(crate) fn take(&mut self, n: usize) -> Result<&'a [u8], SaveError> {
|
||||||
let end = self.offset.checked_add(n).ok_or(SaveError::Truncated {
|
let end = self.offset.checked_add(n).ok_or(SaveError::Truncated {
|
||||||
offset: self.offset,
|
offset: self.offset,
|
||||||
|
|
@ -37,6 +41,10 @@ impl<'a> Reader<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the next `N` bytes as a fixed-size array and advances the cursor.
|
/// Returns the next `N` bytes as a fixed-size array and advances the cursor.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Truncated`] if fewer than `N` bytes remain.
|
||||||
pub(crate) fn take_array<const N: usize>(&mut self) -> Result<[u8; N], SaveError> {
|
pub(crate) fn take_array<const N: usize>(&mut self) -> Result<[u8; N], SaveError> {
|
||||||
let mut array = [0u8; N];
|
let mut array = [0u8; N];
|
||||||
array.copy_from_slice(self.take(N)?);
|
array.copy_from_slice(self.take(N)?);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ pub struct RecordMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes `data` into a `SYNC` record, stamping it with `last_modified` (unix-ms).
|
/// Encodes `data` into a `SYNC` record, stamping it with `last_modified` (unix-ms).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Postcard`] if serialization fails, [`SaveError::Io`] if zstd compression fails, or [`SaveError::PayloadTooLarge`] if either the uncompressed or compressed length exceeds `u32::MAX`.
|
||||||
pub fn encode(data: &ChunkData, last_modified: u64) -> Result<Vec<u8>, SaveError> {
|
pub fn encode(data: &ChunkData, last_modified: u64) -> Result<Vec<u8>, SaveError> {
|
||||||
let uncompressed = postcard::to_stdvec(data)?;
|
let uncompressed = postcard::to_stdvec(data)?;
|
||||||
let compressed = zstd::encode_all(uncompressed.as_slice(), ZSTD_LEVEL)?;
|
let compressed = zstd::encode_all(uncompressed.as_slice(), ZSTD_LEVEL)?;
|
||||||
|
|
@ -61,6 +65,10 @@ pub fn encode(data: &ChunkData, last_modified: u64) -> Result<Vec<u8>, SaveError
|
||||||
///
|
///
|
||||||
/// `bytes` is untrusted on-disk input, so every field is bounds-checked and the decompressed
|
/// `bytes` is untrusted on-disk input, so every field is bounds-checked and the decompressed
|
||||||
/// payload length is validated against the header before deserialization is attempted.
|
/// payload length is validated against the header before deserialization is attempted.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Truncated`] if the buffer ends mid-field, [`SaveError::BadMagic`] if the leading tag is not `SYNC`, [`SaveError::Io`] if zstd decompression fails, [`SaveError::LengthMismatch`] if the decompressed length disagrees with the header, or [`SaveError::Postcard`] if the payload fails to deserialize.
|
||||||
pub fn decode(bytes: &[u8]) -> Result<(RecordMeta, ChunkData), SaveError> {
|
pub fn decode(bytes: &[u8]) -> Result<(RecordMeta, ChunkData), SaveError> {
|
||||||
let mut reader = Reader::new(bytes);
|
let mut reader = Reader::new(bytes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,10 @@ impl RegionIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializes the index to its on-disk framing bytes.
|
/// Serializes the index to its on-disk framing bytes.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::PayloadTooLarge`] if the header, free-list, or stamp table holds more than `u32::MAX` entries.
|
||||||
pub fn encode(&self) -> Result<Vec<u8>, SaveError> {
|
pub fn encode(&self) -> Result<Vec<u8>, SaveError> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
out.extend_from_slice(&MAGIC);
|
out.extend_from_slice(&MAGIC);
|
||||||
|
|
@ -171,6 +175,10 @@ impl RegionIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a region index from its framing bytes, ignoring any chunk records that follow it.
|
/// Parses a region index from its framing bytes, ignoring any chunk records that follow it.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Truncated`] if the buffer ends mid-field, [`SaveError::BadMagic`] if the leading tag is not the region magic, or [`SaveError::UnsupportedVersion`] if the format version is not recognised.
|
||||||
pub fn decode(bytes: &[u8]) -> Result<Self, SaveError> {
|
pub fn decode(bytes: &[u8]) -> Result<Self, SaveError> {
|
||||||
let mut reader = Reader::new(bytes);
|
let mut reader = Reader::new(bytes);
|
||||||
|
|
||||||
|
|
@ -232,6 +240,10 @@ impl RegionIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a chunk position as three little-endian `i32`s.
|
/// Reads a chunk position as three little-endian `i32`s.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::Truncated`] if fewer than twelve bytes remain.
|
||||||
fn read_pos(reader: &mut Reader) -> Result<ChunkPos, SaveError> {
|
fn read_pos(reader: &mut Reader) -> Result<ChunkPos, SaveError> {
|
||||||
let x = i32::from_le_bytes(reader.take_array()?);
|
let x = i32::from_le_bytes(reader.take_array()?);
|
||||||
let y = i32::from_le_bytes(reader.take_array()?);
|
let y = i32::from_le_bytes(reader.take_array()?);
|
||||||
|
|
@ -240,6 +252,10 @@ fn read_pos(reader: &mut Reader) -> Result<ChunkPos, SaveError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Narrows a table length to the `u32` the framing uses, failing loudly rather than truncating.
|
/// Narrows a table length to the `u32` the framing uses, failing loudly rather than truncating.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`SaveError::PayloadTooLarge`] if `len` exceeds `u32::MAX`.
|
||||||
fn len_u32(len: usize) -> Result<u32, SaveError> {
|
fn len_u32(len: usize) -> Result<u32, SaveError> {
|
||||||
u32::try_from(len).map_err(|_| SaveError::PayloadTooLarge { len })
|
u32::try_from(len).map_err(|_| SaveError::PayloadTooLarge { len })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue