feat(lib): port libghostty, Ghostty's embedded C API
Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every function of the unchanged include/ghostty.h - the ABI Ghostty's Swift macOS/iOS app links against. The embedded runtime implements the runtime traits over the host's C callbacks, so the real core drives it. - ghostty-core selects the renderer per target: Metal on Apple (surface NSView/UIView via RtSurface::platform_view), OpenGL elsewhere. - ghostty_config_get follows Zig's c_get rules via a CValue type (143 supported keys, checked against the Zig field types). - The inspector C API is complete; its Metal backend replaces ImGui's Metal renderer and has not run. Verification: generated C ABI probe (sizeof/alignof/offsetof of every struct/union, 755 enum values, every function linked, nm exports, headers byte-identical to upstream); a C host on Linux covering init, CLI actions, config, app, keys, focus and color scheme; an opt-in test-platform feature creating real surfaces on Linux with headless EGL and a pty (13 tests). Apple targets are clippy-clean but not run; the Swift app has not been built against it. Test Plan: - cargo test -p ghostty-lib (and --features test-platform) - cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env in docs/rewrite/notes/macos-M1.md)
This commit is contained in:
1 parent
f2252bb165
commit
75bf604375
47 files changed
+10296
-34
No files matched your search
@@ -0,0 +1,633 @@
|
||||
//! The Metal backend of the embedded inspector
|
||||
//! (`ghostty_inspector_metal_{init,render,shutdown}`).
|
||||
//!
|
||||
//! Upstream links Dear ImGui's `imgui_impl_metal.mm`; this is the same
|
||||
//! renderer written against `objc2-metal` and `dear-imgui-rs`'s managed
|
||||
//! texture protocol: the draw lists are uploaded to Metal buffers and drawn
|
||||
//! with the `imgui_impl_metal` shaders into the render pass the host
|
||||
//! provides.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{collections::HashMap, ffi::c_void, ptr::NonNull};
|
||||
|
||||
use ghostty_inspector::imgui::{
|
||||
BackendFlags,
|
||||
TextureFormat,
|
||||
TextureId,
|
||||
render::{
|
||||
DrawCmd,
|
||||
DrawIdx,
|
||||
DrawVert,
|
||||
PendingFrame,
|
||||
SnapshotTextureId,
|
||||
SynchronousRendererConsumer,
|
||||
TextureFeedback,
|
||||
TextureOp,
|
||||
TextureRequest,
|
||||
},
|
||||
};
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_metal::{
|
||||
MTLBlendFactor,
|
||||
MTLBlendOperation,
|
||||
MTLCommandBuffer,
|
||||
MTLCommandEncoder as _,
|
||||
MTLCompareFunction,
|
||||
MTLCullMode,
|
||||
MTLDepthStencilDescriptor,
|
||||
MTLDepthStencilState,
|
||||
MTLDevice,
|
||||
MTLIndexType,
|
||||
MTLLibrary,
|
||||
MTLOrigin,
|
||||
MTLPixelFormat,
|
||||
MTLPrimitiveType,
|
||||
MTLRegion,
|
||||
MTLRenderCommandEncoder,
|
||||
MTLRenderPassDescriptor,
|
||||
MTLRenderPipelineDescriptor,
|
||||
MTLRenderPipelineState,
|
||||
MTLResourceOptions,
|
||||
MTLScissorRect,
|
||||
MTLSize,
|
||||
MTLTexture,
|
||||
MTLTextureDescriptor,
|
||||
MTLVertexDescriptor,
|
||||
MTLVertexFormat,
|
||||
MTLVertexStepFunction,
|
||||
MTLViewport,
|
||||
};
|
||||
|
||||
use crate::inspector::Inspector;
|
||||
|
||||
/// The shaders of `imgui_impl_metal.mm`.
|
||||
const SHADERS: &str = r"
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 projectionMatrix;
|
||||
};
|
||||
|
||||
struct VertexIn {
|
||||
float2 position [[attribute(0)]];
|
||||
float2 texCoords [[attribute(1)]];
|
||||
uchar4 color [[attribute(2)]];
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 texCoords;
|
||||
float4 color;
|
||||
};
|
||||
|
||||
vertex VertexOut vertex_main(VertexIn in [[stage_in]],
|
||||
constant Uniforms &uniforms [[buffer(1)]]) {
|
||||
VertexOut out;
|
||||
out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);
|
||||
out.texCoords = in.texCoords;
|
||||
out.color = float4(in.color) / float4(255.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment half4 fragment_main(VertexOut in [[stage_in]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]]) {
|
||||
constexpr sampler linearSampler(coord::normalized,
|
||||
min_filter::linear,
|
||||
mag_filter::linear,
|
||||
mip_filter::linear);
|
||||
half4 texColor = texture.sample(linearSampler, in.texCoords);
|
||||
return half4(in.color) * texColor;
|
||||
}
|
||||
";
|
||||
|
||||
type Device = Retained<ProtocolObject<dyn MTLDevice>>;
|
||||
|
||||
/// A pipeline state for one render target configuration.
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
struct PipelineKey {
|
||||
color_format: usize,
|
||||
sample_count: usize,
|
||||
depth_format: usize,
|
||||
stencil_format: usize,
|
||||
}
|
||||
|
||||
/// The Metal draw backend of one inspector.
|
||||
pub struct MetalBackend {
|
||||
device: Device,
|
||||
library: Retained<ProtocolObject<dyn MTLLibrary>>,
|
||||
depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
|
||||
pipelines: HashMap<PipelineKey, Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
|
||||
consumer: SynchronousRendererConsumer,
|
||||
/// Managed textures by Dear ImGui's identity.
|
||||
textures: HashMap<SnapshotTextureId, (u64, Retained<ProtocolObject<dyn MTLTexture>>)>,
|
||||
next_texture_id: u64,
|
||||
}
|
||||
|
||||
impl MetalBackend {
|
||||
/// Takes ownership of one reference to `device` (Zig: `defer
|
||||
/// device.release()`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `device` must be a retained `MTLDevice`.
|
||||
pub unsafe fn init(
|
||||
device: *mut c_void,
|
||||
ctx: &mut ghostty_inspector::imgui::Context,
|
||||
) -> Result<MetalBackend, String> {
|
||||
// SAFETY: per the function contract we own one reference.
|
||||
let device: Device =
|
||||
unsafe { Retained::from_raw(device.cast::<ProtocolObject<dyn MTLDevice>>()) }
|
||||
.ok_or_else(|| "null Metal device".to_owned())?;
|
||||
|
||||
let library = device
|
||||
.newLibraryWithSource_options_error(&NSString::from_str(SHADERS), None)
|
||||
.map_err(|e| e.localizedDescription().to_string())?;
|
||||
|
||||
let depth_desc = MTLDepthStencilDescriptor::new();
|
||||
depth_desc.setDepthCompareFunction(MTLCompareFunction::Always);
|
||||
depth_desc.setDepthWriteEnabled(false);
|
||||
let depth_state = device
|
||||
.newDepthStencilStateWithDescriptor(&depth_desc)
|
||||
.ok_or_else(|| "failed to create the depth stencil state".to_owned())?;
|
||||
|
||||
// Publish the renderer capabilities and the managed texture
|
||||
// consumer.
|
||||
let consumer = ctx
|
||||
.create_synchronous_renderer_consumer()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let reset = ctx
|
||||
.prepare_renderer_texture_reset(&consumer)
|
||||
.map_err(|e| e.to_string())?;
|
||||
reset.commit();
|
||||
if let Err(err) = ctx.set_renderer_name(Some("ghostty_embedded_metal")) {
|
||||
log::warn!("unable to set the imgui renderer name err={err}");
|
||||
}
|
||||
let io = ctx.io_mut();
|
||||
io.set_backend_flags(
|
||||
io.backend_flags()
|
||||
| BackendFlags::RENDERER_HAS_VTX_OFFSET
|
||||
| BackendFlags::RENDERER_HAS_TEXTURES,
|
||||
);
|
||||
|
||||
Ok(MetalBackend {
|
||||
device,
|
||||
library,
|
||||
depth_state,
|
||||
pipelines: HashMap::new(),
|
||||
consumer,
|
||||
textures: HashMap::new(),
|
||||
next_texture_id: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Release the GPU objects.
|
||||
pub fn shutdown(self) {
|
||||
drop(self);
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
&mut self,
|
||||
key: PipelineKey,
|
||||
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
|
||||
if let Some(p) = self.pipelines.get(&key) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
|
||||
let desc = MTLRenderPipelineDescriptor::new();
|
||||
let vertex = self
|
||||
.library
|
||||
.newFunctionWithName(&NSString::from_str("vertex_main"))
|
||||
.ok_or("missing vertex_main")?;
|
||||
let fragment = self
|
||||
.library
|
||||
.newFunctionWithName(&NSString::from_str("fragment_main"))
|
||||
.ok_or("missing fragment_main")?;
|
||||
desc.setVertexFunction(Some(&vertex));
|
||||
desc.setFragmentFunction(Some(&fragment));
|
||||
|
||||
// ImDrawVert: float2 pos, float2 uv, uchar4 col.
|
||||
let vd = MTLVertexDescriptor::new();
|
||||
let stride = size_of::<DrawVert>();
|
||||
// SAFETY: attribute/layout indexes are valid; plain numbers.
|
||||
unsafe {
|
||||
let attrs = vd.attributes();
|
||||
let a0 = attrs.objectAtIndexedSubscript(0);
|
||||
a0.setFormat(MTLVertexFormat::Float2);
|
||||
a0.setOffset(std::mem::offset_of!(DrawVert, pos));
|
||||
a0.setBufferIndex(0);
|
||||
let a1 = attrs.objectAtIndexedSubscript(1);
|
||||
a1.setFormat(MTLVertexFormat::Float2);
|
||||
a1.setOffset(std::mem::offset_of!(DrawVert, uv));
|
||||
a1.setBufferIndex(0);
|
||||
let a2 = attrs.objectAtIndexedSubscript(2);
|
||||
a2.setFormat(MTLVertexFormat::UChar4);
|
||||
a2.setOffset(std::mem::offset_of!(DrawVert, col));
|
||||
a2.setBufferIndex(0);
|
||||
let layout = vd.layouts().objectAtIndexedSubscript(0);
|
||||
layout.setStepRate(1);
|
||||
layout.setStepFunction(MTLVertexStepFunction::PerVertex);
|
||||
layout.setStride(stride);
|
||||
}
|
||||
desc.setVertexDescriptor(Some(&vd));
|
||||
desc.setRasterSampleCount(key.sample_count);
|
||||
|
||||
// SAFETY: color attachment 0 exists.
|
||||
let color = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) };
|
||||
color.setPixelFormat(MTLPixelFormat(key.color_format));
|
||||
color.setBlendingEnabled(true);
|
||||
color.setRgbBlendOperation(MTLBlendOperation::Add);
|
||||
color.setAlphaBlendOperation(MTLBlendOperation::Add);
|
||||
color.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
|
||||
color.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
color.setSourceAlphaBlendFactor(MTLBlendFactor::One);
|
||||
color.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
|
||||
desc.setDepthAttachmentPixelFormat(MTLPixelFormat(key.depth_format));
|
||||
desc.setStencilAttachmentPixelFormat(MTLPixelFormat(key.stencil_format));
|
||||
|
||||
let state = self
|
||||
.device
|
||||
.newRenderPipelineStateWithDescriptor_error(&desc)
|
||||
.map_err(|e| e.localizedDescription().to_string())?;
|
||||
self.pipelines.insert(key, state.clone());
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Create (or recreate) the texture of `key` from a full upload.
|
||||
fn create_texture(
|
||||
&mut self,
|
||||
key: SnapshotTextureId,
|
||||
format: TextureFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
row_pitch: usize,
|
||||
pixels: &[u8],
|
||||
) -> Result<TextureId, String> {
|
||||
let rgba = to_rgba(format, width as usize, height as usize, row_pitch, pixels)?;
|
||||
|
||||
let desc = MTLTextureDescriptor::new();
|
||||
desc.setPixelFormat(MTLPixelFormat::RGBA8Unorm);
|
||||
// SAFETY: plain numbers validated by texture creation.
|
||||
unsafe {
|
||||
desc.setWidth(width as usize);
|
||||
desc.setHeight(height as usize);
|
||||
}
|
||||
let texture = self
|
||||
.device
|
||||
.newTextureWithDescriptor(&desc)
|
||||
.ok_or("failed to create the texture")?;
|
||||
replace_region(&texture, 0, 0, width as usize, height as usize, &rgba)?;
|
||||
|
||||
let id = if let Some((id, _)) = self.textures.get(&key) {
|
||||
*id
|
||||
} else {
|
||||
let id = self.next_texture_id;
|
||||
self.next_texture_id += 1;
|
||||
id
|
||||
};
|
||||
self.textures.insert(key, (id, texture));
|
||||
Ok(TextureId::new(id))
|
||||
}
|
||||
|
||||
fn process_request(&mut self, request: &TextureRequest) -> Result<TextureFeedback, String> {
|
||||
let key = request.texture();
|
||||
match request.operation() {
|
||||
TextureOp::Create {
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
row_pitch,
|
||||
pixels,
|
||||
} => {
|
||||
let id = self.create_texture(key, *format, *width, *height, *row_pitch, pixels)?;
|
||||
request.uploaded(id).map_err(|e| e.to_string())
|
||||
}
|
||||
TextureOp::Update {
|
||||
format,
|
||||
width: _,
|
||||
height: _,
|
||||
rects,
|
||||
} => {
|
||||
let Some((id, texture)) = self.textures.get(&key) else {
|
||||
// The texture was never created or was destroyed.
|
||||
return Ok(request.superseded());
|
||||
};
|
||||
for r in rects {
|
||||
let (w, h) = (usize::from(r.rect.w), usize::from(r.rect.h));
|
||||
let rgba = to_rgba(*format, w, h, r.row_pitch, &r.data)?;
|
||||
replace_region(
|
||||
texture,
|
||||
usize::from(r.rect.x),
|
||||
usize::from(r.rect.y),
|
||||
w,
|
||||
h,
|
||||
&rgba,
|
||||
)?;
|
||||
}
|
||||
request
|
||||
.uploaded(TextureId::new(*id))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
TextureOp::Destroy => {
|
||||
self.textures.remove(&key);
|
||||
request.destroyed().map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconcile the managed textures of the frame and draw it into
|
||||
/// `encoder` (`ImGui_ImplMetal_RenderDrawData`).
|
||||
fn render(
|
||||
&mut self,
|
||||
frame: PendingFrame<'_>,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
desc: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
let mut feedback = Vec::with_capacity(frame.texture_requests().len());
|
||||
for request in frame.texture_requests() {
|
||||
feedback.push(self.process_request(request)?);
|
||||
}
|
||||
let frame = frame
|
||||
.reconcile_texture_feedback(feedback)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let draw_data = frame.draw_data();
|
||||
|
||||
let [fb_w, fb_h] = {
|
||||
let size = draw_data.display_size();
|
||||
let scale = draw_data.framebuffer_scale();
|
||||
[size[0] * scale[0], size[1] * scale[1]]
|
||||
};
|
||||
// Avoid rendering when minimized.
|
||||
if fb_w <= 0.0 || fb_h <= 0.0 || draw_data.draw_lists_count() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The pipeline matches the render target the host gave us.
|
||||
// SAFETY: attachment 0 exists.
|
||||
let color_tex = unsafe { desc.colorAttachments().objectAtIndexedSubscript(0) }.texture();
|
||||
let depth_tex = desc.depthAttachment().texture();
|
||||
let stencil_tex = desc.stencilAttachment().texture();
|
||||
let key = PipelineKey {
|
||||
color_format: color_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::BGRA8Unorm.0, |t| t.pixelFormat().0),
|
||||
sample_count: color_tex.as_ref().map_or(1, |t| t.sampleCount()),
|
||||
depth_format: depth_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
|
||||
stencil_format: stencil_tex
|
||||
.as_ref()
|
||||
.map_or(MTLPixelFormat::Invalid.0, |t| t.pixelFormat().0),
|
||||
};
|
||||
let pipeline = self.pipeline(key)?;
|
||||
|
||||
let encoder = command_buffer
|
||||
.renderCommandEncoderWithDescriptor(desc)
|
||||
.ok_or("failed to create the render command encoder")?;
|
||||
|
||||
// Render state.
|
||||
encoder.setCullMode(MTLCullMode::None);
|
||||
encoder.setDepthStencilState(Some(&self.depth_state));
|
||||
encoder.setViewport(MTLViewport {
|
||||
originX: 0.0,
|
||||
originY: 0.0,
|
||||
width: f64::from(fb_w),
|
||||
height: f64::from(fb_h),
|
||||
znear: 0.0,
|
||||
zfar: 1.0,
|
||||
});
|
||||
encoder.setRenderPipelineState(&pipeline);
|
||||
|
||||
// The orthographic projection of the display.
|
||||
let [dx, dy] = draw_data.display_pos();
|
||||
let [dw, dh] = draw_data.display_size();
|
||||
let (l, r, t, b) = (dx, dx + dw, dy, dy + dh);
|
||||
#[rustfmt::skip]
|
||||
let projection: [[f32; 4]; 4] = [
|
||||
[2.0 / (r - l), 0.0, 0.0, 0.0],
|
||||
[0.0, 2.0 / (t - b), 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[(r + l) / (l - r), (t + b) / (b - t), 0.0, 1.0],
|
||||
];
|
||||
// SAFETY: the pointer/length describe `projection`, which outlives
|
||||
// the call (Metal copies the bytes).
|
||||
unsafe {
|
||||
encoder.setVertexBytes_length_atIndex(
|
||||
NonNull::from(&projection).cast::<c_void>(),
|
||||
size_of::<[[f32; 4]; 4]>(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
let clip_off = draw_data.display_pos();
|
||||
let clip_scale = draw_data.framebuffer_scale();
|
||||
for list in draw_data.draw_lists() {
|
||||
let vertices = list.vtx_buffer();
|
||||
let indices = list.idx_buffer();
|
||||
if vertices.is_empty() || indices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let vbuf =
|
||||
self.new_buffer(vertices.as_ptr().cast(), std::mem::size_of_val(vertices))?;
|
||||
let ibuf = self.new_buffer(indices.as_ptr().cast(), std::mem::size_of_val(indices))?;
|
||||
// SAFETY: slot 0 is the vertex stream.
|
||||
unsafe { encoder.setVertexBuffer_offset_atIndex(Some(&vbuf), 0, 0) };
|
||||
|
||||
for cmd in list.commands() {
|
||||
match cmd {
|
||||
DrawCmd::Elements { count, cmd_params } => {
|
||||
// Project the scissor rectangle into framebuffer
|
||||
// space and clamp it to the framebuffer.
|
||||
let cr = cmd_params.clip_rect;
|
||||
let min_x = ((cr[0] - clip_off[0]) * clip_scale[0]).max(0.0);
|
||||
let min_y = ((cr[1] - clip_off[1]) * clip_scale[1]).max(0.0);
|
||||
let max_x = ((cr[2] - clip_off[0]) * clip_scale[0]).min(fb_w);
|
||||
let max_y = ((cr[3] - clip_off[1]) * clip_scale[1]).min(fb_h);
|
||||
if max_x <= min_x || max_y <= min_y {
|
||||
continue;
|
||||
}
|
||||
encoder.setScissorRect(MTLScissorRect {
|
||||
x: min_x as usize,
|
||||
y: min_y as usize,
|
||||
width: (max_x - min_x) as usize,
|
||||
height: (max_y - min_y) as usize,
|
||||
});
|
||||
|
||||
// Bind the texture of the command.
|
||||
let id = cmd_params.texture_id.id();
|
||||
let Some((_, tex)) = self.textures.values().find(|(tid, _)| *tid == id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// SAFETY: slot 0 of the fragment stage.
|
||||
unsafe { encoder.setFragmentTexture_atIndex(Some(tex), 0) };
|
||||
|
||||
// The vertex offset applies through the buffer
|
||||
// offset (`RENDERER_HAS_VTX_OFFSET`).
|
||||
// SAFETY: the offset stays inside the buffer.
|
||||
unsafe {
|
||||
encoder.setVertexBufferOffset_atIndex(
|
||||
cmd_params.vtx_offset * size_of::<DrawVert>(),
|
||||
0,
|
||||
);
|
||||
encoder.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
|
||||
MTLPrimitiveType::Triangle,
|
||||
count,
|
||||
if size_of::<DrawIdx>() == 2 {
|
||||
MTLIndexType::UInt16
|
||||
} else {
|
||||
MTLIndexType::UInt32
|
||||
},
|
||||
&ibuf,
|
||||
cmd_params.idx_offset * size_of::<DrawIdx>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Nothing to reset: state is set per command.
|
||||
DrawCmd::ResetRenderState
|
||||
| DrawCmd::SetSamplerLinear
|
||||
| DrawCmd::SetSamplerNearest => {}
|
||||
DrawCmd::RawCallback(_) => {
|
||||
log::warn!("ignoring a raw imgui draw callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoder.endEncoding();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_buffer(
|
||||
&self,
|
||||
ptr: *const c_void,
|
||||
len: usize,
|
||||
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>, String> {
|
||||
let ptr = NonNull::new(ptr.cast_mut()).ok_or("null buffer")?;
|
||||
// SAFETY: `ptr` points to `len` readable bytes (a slice of the draw
|
||||
// list); Metal copies them.
|
||||
unsafe {
|
||||
self.device.newBufferWithBytes_length_options(
|
||||
ptr,
|
||||
len,
|
||||
MTLResourceOptions::StorageModeShared,
|
||||
)
|
||||
}
|
||||
.ok_or_else(|| "failed to create a buffer".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixels as tightly packed RGBA8 (alpha-only textures become white with
|
||||
/// that alpha).
|
||||
fn to_rgba(
|
||||
format: TextureFormat,
|
||||
width: usize,
|
||||
height: usize,
|
||||
row_pitch: usize,
|
||||
data: &[u8],
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let bpp = match format {
|
||||
TextureFormat::RGBA32 => 4,
|
||||
TextureFormat::Alpha8 => 1,
|
||||
};
|
||||
let pitch = if row_pitch == 0 {
|
||||
width * bpp
|
||||
} else {
|
||||
row_pitch
|
||||
};
|
||||
if height > 0 && data.len() < pitch * (height - 1) + width * bpp {
|
||||
return Err("texture upload is too short".to_owned());
|
||||
}
|
||||
let mut out = Vec::with_capacity(width * height * 4);
|
||||
for y in 0..height {
|
||||
let row = &data[y * pitch..y * pitch + width * bpp];
|
||||
match format {
|
||||
TextureFormat::RGBA32 => out.extend_from_slice(row),
|
||||
TextureFormat::Alpha8 => {
|
||||
for a in row {
|
||||
out.extend_from_slice(&[255, 255, 255, *a]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn replace_region(
|
||||
texture: &ProtocolObject<dyn MTLTexture>,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
rgba: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let ptr = NonNull::new(rgba.as_ptr().cast_mut().cast::<c_void>()).ok_or("empty texture")?;
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin { x, y, z: 0 },
|
||||
size: MTLSize {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
// SAFETY: `rgba` has `width * 4 * height` bytes (built by `to_rgba`).
|
||||
unsafe {
|
||||
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(region, 0, ptr, width * 4);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the inspector (Zig: `Inspector.renderMetal`). Takes ownership of
|
||||
/// one reference each to `command_buffer` and `descriptor`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Both must be retained Metal objects.
|
||||
pub unsafe fn render(
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: *mut c_void,
|
||||
descriptor: *mut c_void,
|
||||
) {
|
||||
// SAFETY: per the function contract we own one reference to each; they
|
||||
// are released when these `Retained`s drop.
|
||||
let command_buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>> =
|
||||
unsafe { Retained::from_raw(command_buffer.cast()) };
|
||||
// SAFETY: as above.
|
||||
let descriptor: Option<Retained<MTLRenderPassDescriptor>> =
|
||||
unsafe { Retained::from_raw(descriptor.cast()) };
|
||||
let (Some(command_buffer), Some(descriptor)) = (command_buffer, descriptor) else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = render_frames(inspector, &command_buffer, &descriptor) {
|
||||
log::error!("error rendering inspector err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frames(
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
descriptor: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
let mut backend = inspector
|
||||
.take_backend()
|
||||
.ok_or("the Metal backend isn't initialized")?;
|
||||
let result = draw_frame(&mut backend, inspector, command_buffer, descriptor);
|
||||
inspector.set_backend(backend);
|
||||
result
|
||||
}
|
||||
|
||||
fn draw_frame(
|
||||
backend: &mut MetalBackend,
|
||||
inspector: &mut Inspector,
|
||||
command_buffer: &ProtocolObject<dyn MTLCommandBuffer>,
|
||||
descriptor: &MTLRenderPassDescriptor,
|
||||
) -> Result<(), String> {
|
||||
// Setup our imgui frame. We need to render multiple frames to ensure
|
||||
// ImGui completes all its state processing. I don't know how to fix
|
||||
// this.
|
||||
inspector.build_frame();
|
||||
inspector.context().end_frame();
|
||||
|
||||
inspector.build_frame();
|
||||
let pending = inspector.context().render(&backend.consumer);
|
||||
backend.render(pending, command_buffer, descriptor)
|
||||
}
|
||||
Reference in new issue
Block a user