Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
779 lines
25 KiB
Rust
779 lines
25 KiB
Rust
//! Platform-independent parts of the Metal backend (`renderer/metal/api.zig`,
|
|
//! `metal/shaders.zig`, `metal/Texture.zig`, `Metal.zig`): raw enum values,
|
|
//! the Metal GPU data layouts, pixel format selection, vertex attribute
|
|
//! tables and the small pieces of math the backend does. Everything here is
|
|
//! plain Rust so it can be unit tested on any host; `crate::metal` converts
|
|
//! these values to `objc2-metal` types.
|
|
|
|
use bytemuck::{Pod, Zeroable};
|
|
use ghostty_config::types_b::AlphaBlending;
|
|
use ghostty_font::atlas::Format as AtlasFormat;
|
|
|
|
use crate::{
|
|
graphics_api::ImageTextureFormat,
|
|
math::Mat,
|
|
shader_types::{BgImage, CellText, Image as ImageVertex, Uniforms},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resource options
|
|
|
|
/// `MTLResourceOptions.CPUCacheMode`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
#[repr(u8)]
|
|
pub enum CpuCacheMode {
|
|
#[default]
|
|
Default = 0,
|
|
WriteCombined = 1,
|
|
}
|
|
|
|
/// `MTLResourceOptions.StorageMode`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum StorageMode {
|
|
Shared = 0,
|
|
Managed = 1,
|
|
Private = 2,
|
|
Memoryless = 3,
|
|
}
|
|
|
|
/// `MTLResourceOptions.HazardTrackingMode`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
#[repr(u8)]
|
|
pub enum HazardTrackingMode {
|
|
#[default]
|
|
Default = 0,
|
|
Untracked = 1,
|
|
Tracked = 2,
|
|
}
|
|
|
|
/// `MTLResourceOptions` (a `packed struct(c_ulong)` upstream: cache mode in
|
|
/// bits 0-3, storage mode in bits 4-7, hazard tracking in bits 8-9).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ResourceOptions {
|
|
pub cpu_cache_mode: CpuCacheMode,
|
|
pub storage_mode: StorageMode,
|
|
pub hazard_tracking_mode: HazardTrackingMode,
|
|
}
|
|
|
|
impl ResourceOptions {
|
|
/// "Indicate that the CPU writes to this resource but never reads it."
|
|
pub fn write_combined(storage_mode: StorageMode) -> ResourceOptions {
|
|
ResourceOptions {
|
|
cpu_cache_mode: CpuCacheMode::WriteCombined,
|
|
storage_mode,
|
|
hazard_tracking_mode: HazardTrackingMode::Default,
|
|
}
|
|
}
|
|
|
|
/// The raw `MTLResourceOptions` value.
|
|
pub fn bits(self) -> u64 {
|
|
u64::from(self.cpu_cache_mode as u8)
|
|
| u64::from(self.storage_mode as u8) << 4
|
|
| u64::from(self.hazard_tracking_mode as u8) << 8
|
|
}
|
|
}
|
|
|
|
/// `MTLTextureUsage` (a `packed struct(c_ulong)` upstream).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct TextureUsage {
|
|
pub shader_read: bool,
|
|
pub shader_write: bool,
|
|
pub render_target: bool,
|
|
pub pixel_format_view: bool,
|
|
pub shader_atomic: bool,
|
|
}
|
|
|
|
impl TextureUsage {
|
|
/// The raw `MTLTextureUsage` value.
|
|
pub fn bits(self) -> u64 {
|
|
u64::from(self.shader_read)
|
|
| u64::from(self.shader_write) << 1
|
|
| u64::from(self.render_target) << 2
|
|
// bit 3 is skipped by the enum
|
|
| u64::from(self.pixel_format_view) << 4
|
|
| u64::from(self.shader_atomic) << 5
|
|
}
|
|
|
|
/// Textures only read from a shader (atlases, images).
|
|
pub fn shader_read_only() -> TextureUsage {
|
|
TextureUsage {
|
|
shader_read: true,
|
|
..TextureUsage::default()
|
|
}
|
|
}
|
|
|
|
/// Custom shader textures: "require both the shader read (for when
|
|
/// multiple shaders are chained) and render target (for the final output)
|
|
/// usage. Disabling either of these will lead to metal validation errors
|
|
/// in Xcode."
|
|
pub fn custom_shader() -> TextureUsage {
|
|
TextureUsage {
|
|
shader_read: true,
|
|
render_target: true,
|
|
..TextureUsage::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The default storage mode to use for resources created with a device.
|
|
///
|
|
/// "This is based on whether the device is a discrete GPU or not, since
|
|
/// discrete GPUs do not have unified memory and therefore do not support the
|
|
/// 'shared' storage mode, instead we have to use the 'managed' mode." The
|
|
/// managed mode is not supported by iOS.
|
|
pub fn default_storage_mode(is_ios: bool, has_unified_memory: bool) -> StorageMode {
|
|
if is_ios || has_unified_memory {
|
|
StorageMode::Shared
|
|
} else {
|
|
StorageMode::Managed
|
|
}
|
|
}
|
|
|
|
/// The maximum 2D texture size by GPU family
|
|
/// (<https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf>).
|
|
pub fn max_texture_size(supports_apple10: bool, supports_apple3: bool) -> u32 {
|
|
if supports_apple10 {
|
|
32768
|
|
} else if supports_apple3 {
|
|
16384
|
|
} else {
|
|
8192
|
|
}
|
|
}
|
|
|
|
/// Clamp a layer's pixel size (`bounds * contentsScale`) to the maximum
|
|
/// texture size ("we can't create a screen buffer (texture) larger than
|
|
/// that"). Truncates like Zig's `@intFromFloat`.
|
|
pub fn surface_size(
|
|
bounds_width: f64,
|
|
bounds_height: f64,
|
|
scale: f64,
|
|
max_texture_size: u32,
|
|
) -> (u32, u32) {
|
|
let w = (bounds_width * scale) as u32;
|
|
let h = (bounds_height * scale) as u32;
|
|
(w.min(max_texture_size), h.min(max_texture_size))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pixel formats
|
|
|
|
/// The subset of `MTLPixelFormat` the renderer uses, with the raw values from
|
|
/// `api.zig`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u64)]
|
|
pub enum PixelFormat {
|
|
R8Unorm = 10,
|
|
R8UnormSrgb = 11,
|
|
Rgba8Unorm = 70,
|
|
Rgba8UnormSrgb = 71,
|
|
Bgra8Unorm = 80,
|
|
Bgra8UnormSrgb = 81,
|
|
}
|
|
|
|
impl PixelFormat {
|
|
/// Bytes per pixel (`Texture.bppOf`).
|
|
pub fn bytes_per_pixel(self) -> usize {
|
|
match self {
|
|
PixelFormat::R8Unorm | PixelFormat::R8UnormSrgb => 1,
|
|
PixelFormat::Rgba8Unorm
|
|
| PixelFormat::Rgba8UnormSrgb
|
|
| PixelFormat::Bgra8Unorm
|
|
| PixelFormat::Bgra8UnormSrgb => 4,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The pixel format of render targets, custom shader textures and pipeline
|
|
/// color attachments.
|
|
///
|
|
/// "Using an `*_srgb` pixel format makes Metal gamma encode the pixels
|
|
/// written to it *after* blending, which means we get linear alpha blending
|
|
/// rather than gamma-incorrect blending."
|
|
pub fn color_pixel_format(blending: AlphaBlending) -> PixelFormat {
|
|
if blending.is_linear() {
|
|
PixelFormat::Bgra8UnormSrgb
|
|
} else {
|
|
PixelFormat::Bgra8Unorm
|
|
}
|
|
}
|
|
|
|
/// The pixel format of an image texture (`ImageTextureFormat.toPixelFormat`).
|
|
pub fn image_pixel_format(format: ImageTextureFormat, srgb: bool) -> PixelFormat {
|
|
match (format, srgb) {
|
|
(ImageTextureFormat::Gray, true) => PixelFormat::R8UnormSrgb,
|
|
(ImageTextureFormat::Gray, false) => PixelFormat::R8Unorm,
|
|
(ImageTextureFormat::Rgba, true) => PixelFormat::Rgba8UnormSrgb,
|
|
(ImageTextureFormat::Rgba, false) => PixelFormat::Rgba8Unorm,
|
|
(ImageTextureFormat::Bgra, true) => PixelFormat::Bgra8UnormSrgb,
|
|
(ImageTextureFormat::Bgra, false) => PixelFormat::Bgra8Unorm,
|
|
}
|
|
}
|
|
|
|
/// The pixel format of a font atlas texture (`initAtlasTexture`). `None` for
|
|
/// formats Metal doesn't support (Zig panics).
|
|
pub fn atlas_pixel_format(format: AtlasFormat) -> Option<PixelFormat> {
|
|
match format {
|
|
AtlasFormat::Grayscale => Some(PixelFormat::R8Unorm),
|
|
AtlasFormat::Bgra => Some(PixelFormat::Bgra8UnormSrgb),
|
|
AtlasFormat::Bgr => None,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Vertex attributes
|
|
|
|
/// The subset of `MTLVertexFormat` used by our vertex structs (raw values from
|
|
/// `api.zig`).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u64)]
|
|
pub enum VertexFormat {
|
|
Uchar4 = 3,
|
|
Ushort2 = 13,
|
|
Short2 = 16,
|
|
Float = 28,
|
|
Float2 = 29,
|
|
Float4 = 31,
|
|
Uint2 = 37,
|
|
Uchar = 45,
|
|
}
|
|
|
|
/// One vertex attribute (what `Pipeline.autoAttribute` derives by reflection).
|
|
/// The attribute index is the position in the table; all attributes read from
|
|
/// buffer 0.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct VertexAttribute {
|
|
pub format: VertexFormat,
|
|
pub offset: usize,
|
|
}
|
|
|
|
/// A vertex struct usable with a Metal pipeline.
|
|
pub trait MetalVertex: Pod {
|
|
const ATTRIBUTES: &'static [VertexAttribute];
|
|
}
|
|
|
|
macro_rules! attr {
|
|
($ty:ty, $field:ident, $format:expr) => {
|
|
VertexAttribute {
|
|
format: $format,
|
|
offset: core::mem::offset_of!($ty, $field),
|
|
}
|
|
};
|
|
}
|
|
|
|
impl MetalVertex for CellText {
|
|
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
|
attr!(CellText, glyph_pos, VertexFormat::Uint2),
|
|
attr!(CellText, glyph_size, VertexFormat::Uint2),
|
|
attr!(CellText, bearings, VertexFormat::Short2),
|
|
attr!(CellText, grid_pos, VertexFormat::Ushort2),
|
|
attr!(CellText, color, VertexFormat::Uchar4),
|
|
// `atlas` is an enum(u8): its tag type.
|
|
attr!(CellText, atlas, VertexFormat::Uchar),
|
|
// `bools` is a packed struct(u8): its backing integer.
|
|
attr!(CellText, bools, VertexFormat::Uchar),
|
|
];
|
|
}
|
|
|
|
impl MetalVertex for ImageVertex {
|
|
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
|
attr!(ImageVertex, grid_pos, VertexFormat::Float2),
|
|
attr!(ImageVertex, cell_offset, VertexFormat::Float2),
|
|
attr!(ImageVertex, source_rect, VertexFormat::Float4),
|
|
attr!(ImageVertex, dest_size, VertexFormat::Float2),
|
|
];
|
|
}
|
|
|
|
impl MetalVertex for BgImage {
|
|
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
|
attr!(BgImage, opacity, VertexFormat::Float),
|
|
// `info` is a packed struct(u8): its backing integer.
|
|
attr!(BgImage, info, VertexFormat::Uchar),
|
|
];
|
|
}
|
|
|
|
/// `MTLVertexStepFunction`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u64)]
|
|
pub enum VertexStepFunction {
|
|
Constant = 0,
|
|
PerVertex = 1,
|
|
PerInstance = 2,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Blending
|
|
|
|
/// The color attachment blend state of a pipeline
|
|
/// ("We always use premultiplied alpha blending for now.").
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct BlendState {
|
|
pub rgb_operation: BlendOperation,
|
|
pub alpha_operation: BlendOperation,
|
|
pub source_rgb: BlendFactor,
|
|
pub source_alpha: BlendFactor,
|
|
pub destination_rgb: BlendFactor,
|
|
pub destination_alpha: BlendFactor,
|
|
}
|
|
|
|
/// `MTLBlendOperation` (subset).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u64)]
|
|
pub enum BlendOperation {
|
|
Add = 0,
|
|
}
|
|
|
|
/// `MTLBlendFactor` (subset).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u64)]
|
|
pub enum BlendFactor {
|
|
One = 1,
|
|
OneMinusSourceAlpha = 5,
|
|
}
|
|
|
|
/// The blend state used by every blending pipeline.
|
|
pub const PREMULTIPLIED_ALPHA: BlendState = BlendState {
|
|
rgb_operation: BlendOperation::Add,
|
|
alpha_operation: BlendOperation::Add,
|
|
source_rgb: BlendFactor::One,
|
|
source_alpha: BlendFactor::One,
|
|
destination_rgb: BlendFactor::OneMinusSourceAlpha,
|
|
destination_alpha: BlendFactor::OneMinusSourceAlpha,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Render pass buffer slots
|
|
|
|
/// The Metal buffer index for entry `i` of `Step.buffers`
|
|
/// (`RenderPass.step`): index 0 is the vertex buffer, and the rest start at
|
|
/// index 2 "so that we can use index 1 for the uniforms if present".
|
|
pub fn buffer_index(i: usize) -> usize {
|
|
if i == 0 { 0 } else { i + 1 }
|
|
}
|
|
|
|
/// The Metal buffer index of the uniforms.
|
|
pub const UNIFORMS_BUFFER_INDEX: usize = 1;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Uniforms
|
|
|
|
/// The uniforms that are passed to our Metal shaders (`metal/shaders.zig`
|
|
/// `Uniforms`, an `extern struct` with the alignments of the MSL struct).
|
|
///
|
|
/// This is the same layout as the OpenGL std140 struct except for `bools`,
|
|
/// which is four separate bytes instead of a bit mask, and the width of
|
|
/// `padding_extend`.
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
|
pub struct MetalUniforms {
|
|
pub projection_matrix: Mat,
|
|
pub screen_size: [f32; 2],
|
|
pub cell_size: [f32; 2],
|
|
pub grid_size: [u16; 2],
|
|
pub pad0: [u8; 12],
|
|
pub grid_padding: [f32; 4],
|
|
/// Bit mask: left, right, up, down (LSB first).
|
|
pub padding_extend: u8,
|
|
pub pad1: [u8; 3],
|
|
pub min_contrast: f32,
|
|
pub cursor_pos: [u16; 2],
|
|
pub cursor_color: [u8; 4],
|
|
pub bg_color: [u8; 4],
|
|
pub cursor_wide: u8,
|
|
pub use_display_p3: u8,
|
|
pub use_linear_blending: u8,
|
|
pub use_linear_correction: u8,
|
|
pub pad2: [u8; 8],
|
|
}
|
|
|
|
impl From<&Uniforms> for MetalUniforms {
|
|
fn from(u: &Uniforms) -> MetalUniforms {
|
|
use crate::shader_types::Bools;
|
|
MetalUniforms {
|
|
projection_matrix: u.projection_matrix,
|
|
screen_size: u.screen_size,
|
|
cell_size: u.cell_size,
|
|
grid_size: u.grid_size,
|
|
pad0: [0; 12],
|
|
grid_padding: u.grid_padding,
|
|
padding_extend: (u.padding_extend & 0xFF) as u8,
|
|
pad1: [0; 3],
|
|
min_contrast: u.min_contrast,
|
|
cursor_pos: u.cursor_pos,
|
|
cursor_color: u.cursor_color,
|
|
bg_color: u.bg_color,
|
|
cursor_wide: u8::from(u.bools & Bools::CURSOR_WIDE != 0),
|
|
use_display_p3: u8::from(u.bools & Bools::USE_DISPLAY_P3 != 0),
|
|
use_linear_blending: u8::from(u.bools & Bools::USE_LINEAR_BLENDING != 0),
|
|
use_linear_correction: u8::from(u.bools & Bools::USE_LINEAR_CORRECTION != 0),
|
|
pad2: [0; 8],
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Custom shaders
|
|
|
|
/// The entry point of the fragment function spirv-cross (and naga, see
|
|
/// `shadertoy::msl_from_compiled`) generates for custom shaders.
|
|
pub const POST_FRAGMENT_FN: &str = crate::shadertoy::MSL_ENTRY_POINT;
|
|
|
|
/// The vertex function post-processing pipelines share with the built-in
|
|
/// shaders.
|
|
pub const FULL_SCREEN_VERTEX_FN: &str = "full_screen_vertex";
|
|
|
|
/// The built-in pipelines (`pipeline_descs` in `metal/shaders.zig`).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct PipelineDesc {
|
|
pub name: &'static str,
|
|
pub vertex_fn: &'static str,
|
|
pub fragment_fn: &'static str,
|
|
pub step_fn: VertexStepFunction,
|
|
pub blending_enabled: bool,
|
|
/// Whether the pipeline has a vertex descriptor (vertex attributes).
|
|
pub has_vertex_attributes: bool,
|
|
}
|
|
|
|
pub const PIPELINE_DESCS: [PipelineDesc; 5] = [
|
|
PipelineDesc {
|
|
name: "bg_color",
|
|
vertex_fn: "full_screen_vertex",
|
|
fragment_fn: "bg_color_fragment",
|
|
step_fn: VertexStepFunction::PerVertex,
|
|
blending_enabled: false,
|
|
has_vertex_attributes: false,
|
|
},
|
|
PipelineDesc {
|
|
name: "cell_bg",
|
|
vertex_fn: "full_screen_vertex",
|
|
fragment_fn: "cell_bg_fragment",
|
|
step_fn: VertexStepFunction::PerVertex,
|
|
blending_enabled: true,
|
|
has_vertex_attributes: false,
|
|
},
|
|
PipelineDesc {
|
|
name: "cell_text",
|
|
vertex_fn: "cell_text_vertex",
|
|
fragment_fn: "cell_text_fragment",
|
|
step_fn: VertexStepFunction::PerInstance,
|
|
blending_enabled: true,
|
|
has_vertex_attributes: true,
|
|
},
|
|
PipelineDesc {
|
|
name: "image",
|
|
vertex_fn: "image_vertex",
|
|
fragment_fn: "image_fragment",
|
|
step_fn: VertexStepFunction::PerInstance,
|
|
blending_enabled: true,
|
|
has_vertex_attributes: true,
|
|
},
|
|
PipelineDesc {
|
|
name: "bg_image",
|
|
vertex_fn: "bg_image_vertex",
|
|
fragment_fn: "bg_image_fragment",
|
|
step_fn: VertexStepFunction::PerInstance,
|
|
blending_enabled: true,
|
|
has_vertex_attributes: true,
|
|
},
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use core::mem::{offset_of, size_of};
|
|
|
|
use super::*;
|
|
use crate::shader_types::{Bools, PaddingExtend};
|
|
|
|
#[test]
|
|
fn resource_options_bits() {
|
|
// MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeShared
|
|
assert_eq!(
|
|
ResourceOptions::write_combined(StorageMode::Shared).bits(),
|
|
1
|
|
);
|
|
// ... | MTLResourceStorageModeManaged (1 << 4)
|
|
assert_eq!(
|
|
ResourceOptions::write_combined(StorageMode::Managed).bits(),
|
|
0x11
|
|
);
|
|
let private = ResourceOptions {
|
|
cpu_cache_mode: CpuCacheMode::Default,
|
|
storage_mode: StorageMode::Private,
|
|
hazard_tracking_mode: HazardTrackingMode::Tracked,
|
|
};
|
|
assert_eq!(private.bits(), (2 << 4) | (2 << 8));
|
|
}
|
|
|
|
#[test]
|
|
fn texture_usage_bits() {
|
|
assert_eq!(TextureUsage::shader_read_only().bits(), 1);
|
|
assert_eq!(TextureUsage::custom_shader().bits(), 1 | 4);
|
|
let all = TextureUsage {
|
|
shader_read: true,
|
|
shader_write: true,
|
|
render_target: true,
|
|
pixel_format_view: true,
|
|
shader_atomic: true,
|
|
};
|
|
// The enum skips from 4 to 16.
|
|
assert_eq!(all.bits(), 1 | 2 | 4 | 16 | 32);
|
|
}
|
|
|
|
#[test]
|
|
fn storage_mode_selection() {
|
|
assert_eq!(default_storage_mode(false, true), StorageMode::Shared);
|
|
assert_eq!(default_storage_mode(false, false), StorageMode::Managed);
|
|
// Managed storage does not exist on iOS.
|
|
assert_eq!(default_storage_mode(true, false), StorageMode::Shared);
|
|
}
|
|
|
|
#[test]
|
|
fn max_texture_size_by_family() {
|
|
assert_eq!(max_texture_size(true, true), 32768);
|
|
assert_eq!(max_texture_size(false, true), 16384);
|
|
assert_eq!(max_texture_size(false, false), 8192);
|
|
}
|
|
|
|
#[test]
|
|
fn surface_size_clamps_and_truncates() {
|
|
assert_eq!(surface_size(800.0, 600.0, 2.0, 16384), (1600, 1200));
|
|
assert_eq!(surface_size(800.5, 600.5, 1.5, 16384), (1200, 900));
|
|
assert_eq!(surface_size(10000.0, 10.0, 2.0, 8192), (8192, 20));
|
|
assert_eq!(surface_size(0.0, 0.0, 2.0, 8192), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn pixel_format_selection() {
|
|
assert_eq!(
|
|
color_pixel_format(AlphaBlending::Native),
|
|
PixelFormat::Bgra8Unorm
|
|
);
|
|
assert_eq!(
|
|
color_pixel_format(AlphaBlending::Linear),
|
|
PixelFormat::Bgra8UnormSrgb
|
|
);
|
|
assert_eq!(
|
|
color_pixel_format(AlphaBlending::LinearCorrected),
|
|
PixelFormat::Bgra8UnormSrgb
|
|
);
|
|
assert_eq!(
|
|
image_pixel_format(ImageTextureFormat::Gray, true),
|
|
PixelFormat::R8UnormSrgb
|
|
);
|
|
assert_eq!(
|
|
image_pixel_format(ImageTextureFormat::Rgba, false),
|
|
PixelFormat::Rgba8Unorm
|
|
);
|
|
assert_eq!(
|
|
image_pixel_format(ImageTextureFormat::Bgra, true),
|
|
PixelFormat::Bgra8UnormSrgb
|
|
);
|
|
assert_eq!(
|
|
atlas_pixel_format(AtlasFormat::Grayscale),
|
|
Some(PixelFormat::R8Unorm)
|
|
);
|
|
assert_eq!(
|
|
atlas_pixel_format(AtlasFormat::Bgra),
|
|
Some(PixelFormat::Bgra8UnormSrgb)
|
|
);
|
|
assert_eq!(atlas_pixel_format(AtlasFormat::Bgr), None);
|
|
}
|
|
|
|
#[test]
|
|
fn pixel_format_raw_values_match_metal() {
|
|
// MTLPixelFormat values (api.zig).
|
|
assert_eq!(PixelFormat::R8Unorm as u64, 10);
|
|
assert_eq!(PixelFormat::Rgba8Unorm as u64, 70);
|
|
assert_eq!(PixelFormat::Bgra8Unorm as u64, 80);
|
|
assert_eq!(PixelFormat::Bgra8UnormSrgb as u64, 81);
|
|
assert_eq!(PixelFormat::Bgra8Unorm.bytes_per_pixel(), 4);
|
|
assert_eq!(PixelFormat::R8UnormSrgb.bytes_per_pixel(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn vertex_attribute_tables() {
|
|
let cell_text: Vec<_> = CellText::ATTRIBUTES
|
|
.iter()
|
|
.map(|a| (a.format, a.offset))
|
|
.collect();
|
|
assert_eq!(
|
|
cell_text,
|
|
[
|
|
(VertexFormat::Uint2, 0),
|
|
(VertexFormat::Uint2, 8),
|
|
(VertexFormat::Short2, 16),
|
|
(VertexFormat::Ushort2, 20),
|
|
(VertexFormat::Uchar4, 24),
|
|
(VertexFormat::Uchar, 28),
|
|
(VertexFormat::Uchar, 29),
|
|
]
|
|
);
|
|
let image: Vec<_> = ImageVertex::ATTRIBUTES
|
|
.iter()
|
|
.map(|a| (a.format, a.offset))
|
|
.collect();
|
|
assert_eq!(
|
|
image,
|
|
[
|
|
(VertexFormat::Float2, 0),
|
|
(VertexFormat::Float2, 8),
|
|
(VertexFormat::Float4, 16),
|
|
(VertexFormat::Float2, 32),
|
|
]
|
|
);
|
|
let bg: Vec<_> = BgImage::ATTRIBUTES
|
|
.iter()
|
|
.map(|a| (a.format, a.offset))
|
|
.collect();
|
|
assert_eq!(bg, [(VertexFormat::Float, 0), (VertexFormat::Uchar, 4)]);
|
|
|
|
// MTLVertexFormat raw values.
|
|
assert_eq!(VertexFormat::Uchar4 as u64, 3);
|
|
assert_eq!(VertexFormat::Ushort2 as u64, 13);
|
|
assert_eq!(VertexFormat::Short2 as u64, 16);
|
|
assert_eq!(VertexFormat::Float as u64, 28);
|
|
assert_eq!(VertexFormat::Float2 as u64, 29);
|
|
assert_eq!(VertexFormat::Float4 as u64, 31);
|
|
assert_eq!(VertexFormat::Uint2 as u64, 37);
|
|
assert_eq!(VertexFormat::Uchar as u64, 45);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_slots() {
|
|
// Vertex buffer at 0, uniforms at 1, the rest start at 2.
|
|
assert_eq!(buffer_index(0), 0);
|
|
assert_eq!(buffer_index(1), 2);
|
|
assert_eq!(buffer_index(2), 3);
|
|
assert_eq!(UNIFORMS_BUFFER_INDEX, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn blend_state_is_premultiplied() {
|
|
assert_eq!(PREMULTIPLIED_ALPHA.source_rgb as u64, 1);
|
|
assert_eq!(PREMULTIPLIED_ALPHA.destination_rgb as u64, 5);
|
|
assert_eq!(PREMULTIPLIED_ALPHA.rgb_operation as u64, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn metal_uniforms_layout_matches_msl() {
|
|
// The MSL `Uniforms` struct in shaders/shaders.metal.
|
|
assert_eq!(offset_of!(MetalUniforms, projection_matrix), 0);
|
|
assert_eq!(offset_of!(MetalUniforms, screen_size), 64);
|
|
assert_eq!(offset_of!(MetalUniforms, cell_size), 72);
|
|
assert_eq!(offset_of!(MetalUniforms, grid_size), 80);
|
|
assert_eq!(offset_of!(MetalUniforms, grid_padding), 96);
|
|
assert_eq!(offset_of!(MetalUniforms, padding_extend), 112);
|
|
assert_eq!(offset_of!(MetalUniforms, min_contrast), 116);
|
|
assert_eq!(offset_of!(MetalUniforms, cursor_pos), 120);
|
|
assert_eq!(offset_of!(MetalUniforms, cursor_color), 124);
|
|
assert_eq!(offset_of!(MetalUniforms, bg_color), 128);
|
|
assert_eq!(offset_of!(MetalUniforms, cursor_wide), 132);
|
|
assert_eq!(offset_of!(MetalUniforms, use_linear_correction), 135);
|
|
assert_eq!(size_of::<MetalUniforms>(), 144);
|
|
// Same size as the OpenGL std140 block.
|
|
assert_eq!(size_of::<MetalUniforms>(), size_of::<Uniforms>());
|
|
}
|
|
|
|
#[test]
|
|
fn metal_uniforms_from_common() {
|
|
let common = Uniforms {
|
|
screen_size: [800.0, 600.0],
|
|
padding_extend: PaddingExtend {
|
|
left: true,
|
|
right: false,
|
|
up: false,
|
|
down: true,
|
|
}
|
|
.to_bits(),
|
|
min_contrast: 3.0,
|
|
cursor_pos: [4, 5],
|
|
bg_color: [1, 2, 3, 4],
|
|
bools: Bools {
|
|
cursor_wide: true,
|
|
use_display_p3: false,
|
|
use_linear_blending: true,
|
|
use_linear_correction: false,
|
|
}
|
|
.to_bits(),
|
|
..Uniforms::default()
|
|
};
|
|
let m = MetalUniforms::from(&common);
|
|
assert_eq!(m.screen_size, [800.0, 600.0]);
|
|
assert_eq!(m.padding_extend, 0b1001);
|
|
assert!((m.min_contrast - 3.0).abs() < f32::EPSILON);
|
|
assert_eq!(m.cursor_pos, [4, 5]);
|
|
assert_eq!(m.bg_color, [1, 2, 3, 4]);
|
|
let raw: &[u8] = bytemuck::bytes_of(&m);
|
|
// cursor_wide, use_display_p3, use_linear_blending, use_linear_correction
|
|
assert_eq!(&raw[132..136], &[1, 0, 1, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn pipeline_descs_match_shader_functions() {
|
|
let names: Vec<_> = PIPELINE_DESCS.iter().map(|d| d.name).collect();
|
|
assert_eq!(
|
|
names,
|
|
["bg_color", "cell_bg", "cell_text", "image", "bg_image"]
|
|
);
|
|
// Every function exists in shaders.metal.
|
|
let src = include_str!("../shaders/shaders.metal");
|
|
for d in &PIPELINE_DESCS {
|
|
assert!(
|
|
src.contains(&format!("{}(", d.vertex_fn)),
|
|
"{}",
|
|
d.vertex_fn
|
|
);
|
|
assert!(
|
|
src.contains(&format!("{}(", d.fragment_fn)),
|
|
"{}",
|
|
d.fragment_fn
|
|
);
|
|
}
|
|
assert!(src.contains(FULL_SCREEN_VERTEX_FN));
|
|
// Instanced pipelines have vertex attributes.
|
|
for d in &PIPELINE_DESCS {
|
|
assert_eq!(
|
|
d.has_vertex_attributes,
|
|
d.step_fn == VertexStepFunction::PerInstance
|
|
);
|
|
}
|
|
assert!(!PIPELINE_DESCS[0].blending_enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn msl_uniforms_struct_matches_rust() {
|
|
// Guard the shader <-> Rust contract: the MSL struct has these fields
|
|
// in this order.
|
|
let src = include_str!("../shaders/shaders.metal");
|
|
let start = src.find("struct Uniforms {").unwrap();
|
|
let end = start + src[start..].find("};").unwrap();
|
|
let fields: Vec<&str> = src[start..end]
|
|
.lines()
|
|
.skip(1)
|
|
.map(|l| l.trim().trim_end_matches(';'))
|
|
.filter(|l| !l.is_empty())
|
|
.map(|l| l.rsplit(' ').next().unwrap())
|
|
.collect();
|
|
assert_eq!(
|
|
fields,
|
|
[
|
|
"projection_matrix",
|
|
"screen_size",
|
|
"cell_size",
|
|
"grid_size",
|
|
"grid_padding",
|
|
"padding_extend",
|
|
"min_contrast",
|
|
"cursor_pos",
|
|
"cursor_color",
|
|
"bg_color",
|
|
"cursor_wide",
|
|
"use_display_p3",
|
|
"use_linear_blending",
|
|
"use_linear_correction",
|
|
]
|
|
);
|
|
}
|
|
}
|