1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// src/metal/line.rs
//
// Per-frame encoder for the world-space line pass. Runs at the tail of
// the hdr_resolve decoration chain, after the main pass resolved colour into
// `hdr_targets.hdr_resolve` and depth into `hdr_targets.depth_resolve`, so the
// lines layer over the lit scene and SSR / TAA treat them like any other scene
// content.
//
// The ribbons arrive already expanded (`gfx::lines::build_vertices`):
// world-space quads whose width was sized off each corner's depth, so a line
// holds its pixel thickness at any distance. Like the decal pass this one
// attaches no depth buffer and instead samples the resolved depth, so an
// occluded line fades to `OCCLUDED_ALPHA` rather than being clipped by
// hardware.
#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBlendFactor, MTLCommandBuffer, MTLDevice as _, MTLLoadAction, MTLPixelFormat,
MTLPrimitiveType, MTLRenderCommandEncoder as _, MTLRenderPassDescriptor,
MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLResourceOptions, MTLStoreAction,
MTLVertexFormat, MTLVertexStepFunction,
};
use super::context::MtlContext;
use super::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use super::encode::RenderEncode;
use super::scoped_encoder::ScopedEncoder;
use crate::gfx::render_types::LineVertex;
// How much of a line still shows where scene geometry is in front of it. A
// faint trace keeps the axes readable inside a dense scene without letting
// them pretend to be unoccluded.
const OCCLUDED_ALPHA: f32 = 0.12;
// Vertex buffer index the ribbon geometry binds to, clear of buffer(0) (the
// view uniforms).
const VERTEX_BUFFER_INDEX: usize = 1;
// Line-pass state: the pipeline, built on the first frame that submits lines
// so a world that never draws any pays nothing, plus the build-failure latch
// that keeps a broken build from re-reporting every frame.
pub(crate) struct LineState {
pub pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub build_failed: bool,
}
impl MtlContext {
// Build the line pipeline if this frame has lines to draw and it is
// not built yet. A failed build latches, so the error is reported once and
// the pass stays skipped for the rest of the run.
pub(in crate::metal) fn ensure_line_pipeline(&mut self, has_lines: bool) {
if !has_lines || self.lines.pipeline.is_some() || self.lines.build_failed {
return;
}
match build_line_pipeline(&self.device, self.hot_reload.enabled) {
Ok(ps) => self.lines.pipeline = Some(ps),
Err(e) => {
self.lines.build_failed = true;
tracing::error!("line pipeline: {}", e);
}
}
}
// Encode the line pass: one unindexed triangle list covering every
// expanded ribbon, alpha-blended into `hdr_resolve`. `vp` is the same
// view-projection the main pass rasterised with (jittered under TAA), so a
// line sits on the pixel its geometry did. Returns the draw-call count.
// pub(in crate::metal) so the render-graph executor in metal/graph_exec.rs
// can dispatch this pass from a CompiledGraph.
pub(in crate::metal) fn encode_lines(
&self,
cmd_buf: &ProtocolObject<dyn MTLCommandBuffer>,
vp: [[f32; 4]; 4],
vertices: &[LineVertex],
) -> Result<u32, String> {
let pipeline = match &self.lines.pipeline {
Some(p) => p,
None => return Ok(0),
};
if vertices.is_empty() {
return Ok(0);
}
let view = concinnity_render::uniforms::LineView {
vp,
occluded_alpha: OCCLUDED_ALPHA,
_pad: [0.0; 3],
};
let bytes = std::mem::size_of_val(vertices);
// SAFETY: the pointer and length describe the live `vertices` allocation, and Metal copies
// those bytes into the new buffer before the call returns.
let vbuf = unsafe {
self.device
.newBufferWithBytes_length_options(
std::ptr::NonNull::from(vertices).cast(),
bytes,
MTLResourceOptions::StorageModeShared,
)
.ok_or("failed to create line vertex buffer")?
};
let pass_desc = MTLRenderPassDescriptor::new();
// SAFETY: plain descriptor property setters; the subscripted slots are ones this descriptor
// declares.
unsafe {
let ca = pass_desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
ca.setLoadAction(MTLLoadAction::Load);
ca.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&pass_desc, super::pass_timing::PassId::Lines);
}
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&pass_desc)
.ok_or("failed to get line render encoder")?,
"lines",
);
enc.set_pipeline(pipeline);
enc.set_vertex_value(&view, 0);
enc.set_fragment_value(&view, 0);
enc.set_vertex_buffer(&vbuf, 0, VERTEX_BUFFER_INDEX);
// Resolved scene depth at texture(0) for the manual depth test.
enc.set_fragment_texture(self.hdr_targets.depth_resolve.as_ref(), 0);
// SAFETY: the draw covers exactly the vertices uploaded into `vbuf`.
unsafe {
enc.drawPrimitives_vertexStart_vertexCount(
MTLPrimitiveType::Triangle,
0,
vertices.len(),
);
}
Ok(1)
}
}
// Build the line pipeline: world-space ribbon corners transformed by the
// camera VP and alpha-blended into the resolved HDR target. No depth
// attachment; the fragment shader tests the resolved depth itself so an
// occluded line can fade instead of vanishing.
fn build_line_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
// Each entry compiles to its own metallib, so the two stages come from
// separate libraries and pair by semantic.
let vert_fn =
super::slang_shaders::entry_function(device, &super::slang_shaders::LINE_VERT, hot_reload)?;
let frag_fn =
super::slang_shaders::entry_function(device, &super::slang_shaders::LINE_FRAG, hot_reload)?;
// Vertex layout: `LineVertex` (position, edge, colour) at 32 bytes,
// asserted by `line_vertex_layout_matches_shaders`.
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: VERTEX_BUFFER_INDEX,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float,
offset: 12,
buffer_index: VERTEX_BUFFER_INDEX,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float4,
offset: 16,
buffer_index: VERTEX_BUFFER_INDEX,
},
],
&[VertexLayout {
buffer_index: VERTEX_BUFFER_INDEX,
stride: std::mem::size_of::<LineVertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
// SAFETY: plain descriptor property setters; the subscripted slots are ones this descriptor
// declares.
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
}
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| format!("failed to create line pipeline state: {:?}", e))
}