usesuper::{
adapter::{self, VERTEX_BUFFER_SLOT_START},
conv, TimestampQuerySupport,
}; usecrate::CommandEncoder as _; use alloc::{
borrow::{Cow, ToOwned as _},
sync::Arc,
vec::Vec,
}; use core::{ops::Range, ptr::NonNull, sync::atomic}; use smallvec::SmallVec;
// has to match `Temp::binding_sizes` const WORD_SIZE: usize = 4;
/// Helper for passing encoders to `update_bind_group_state`. /// /// Combines [`naga::ShaderStage`] and an encoder of the appropriate type for /// that stage. enum Encoder<'e> {
Vertex(&'e ProtocolObject<dyn MTLRenderCommandEncoder>),
Fragment(&'e ProtocolObject<dyn MTLRenderCommandEncoder>),
Task(&'e ProtocolObject<dyn MTLRenderCommandEncoder>),
Mesh(&'e ProtocolObject<dyn MTLRenderCommandEncoder>),
Compute(&'e ProtocolObject<dyn MTLComputeCommandEncoder>),
}
// Take care of pending timer queries. // If we can't use `sample_counters_in_buffer` we have to create a dummy blit encoder! // // There is a known bug in Metal where blit encoders won't write timestamps if they don't have a blit operation. // See https://github.com/gpuweb/gpuweb/issues/2046#issuecomment-1205793680 & https://source.chromium.org/chromium/chromium/src/+/006c4eb70c96229834bbaf271290f40418144cd3:third_party/dawn/src/dawn/native/metal/BackendMTL.mm;l=350 // // To make things worse: // * what counts as a blit operation is a bit unclear, experimenting seemed to indicate that resolve_counters doesn't count. // * in some cases (when?) using `set_start_of_encoder_sample_index` doesn't work, so we have to use `set_end_of_encoder_sample_index` instead // // All this means that pretty much the only *reliable* thing as of writing is to: // * create a dummy blit encoder using set_end_of_encoder_sample_index // * do a dummy write that is known to be not optimized out. // * close the encoder since we used set_end_of_encoder_sample_index and don't want to get any extra stuff in there. // * create another encoder for whatever we actually had in mind. let supports_sample_counters_in_buffer = self
.shared
.private_caps
.timestamp_query_support
.contains(TimestampQuerySupport::ON_BLIT_ENCODER);
if !self.state.pending_timer_queries.is_empty() && !supports_sample_counters_in_buffer {
autoreleasepool(|_| { let descriptor = MTLBlitPassDescriptor::new(); letmut last_query = None; for (i, (set, index)) inself.state.pending_timer_queries.drain(..).enumerate()
{ let sba_descriptor = unsafe {
descriptor
.sampleBufferAttachments()
.objectAtIndexedSubscript(i)
};
sba_descriptor
.setSampleBuffer(Some(set.counter_sample_buffer.as_ref().unwrap()));
// Here be dragons: // As mentioned above, for some reasons using the start of the encoder won't yield any results sometimes! unsafe {
sba_descriptor.setStartOfEncoderSampleIndex(MTLCounterDontSample)
}; unsafe { sba_descriptor.setEndOfEncoderSampleIndex(index as _) };
// As explained above, we need to do some write: // Conveniently, we have a buffer with every query set, that we can use for this for a dummy write, // since we know that it is going to be overwritten again on timer resolve and HAL doesn't define its state before that. let raw_range = NSRange {
location: last_query.as_ref().unwrap().1as usize
* crate::QUERY_SIZE as usize,
length: 1,
};
encoder.fillBuffer_range_value(
&last_query.as_ref().unwrap().0.raw_buffer,
raw_range, 255, // Don't write 0, so it's easier to identify if something went wrong.
);
#[allow(clippy::panicking_unwrap, reason = "false positive (fixed by 1.93.1)")] let encoder = self.state.blit.as_ref().unwrap();
// UNTESTED: // If the above described issue with empty blit encoder applies to `sample_counters_in_buffer` as well, we should use the same workaround instead! for (set, index) inself.state.pending_timer_queries.drain(..) {
debug_assert!(supports_sample_counters_in_buffer); unsafe {
encoder.sampleCountersInBuffer_atSampleIndex_withBarrier(
set.counter_sample_buffer.as_ref().unwrap(),
index as _, true,
)
};
}
} self.state.blit.as_ref().unwrap().clone()
}
// Extend with the sizes of the mapped vertex buffers, in the order // they were added to the map.
result_sizes.extend(stage_info.vertex_buffer_mappings.iter().map(|vbm| { self.vertex_buffer_size_map
.get(&vbm.id)
.map(|size| u32::try_from(size.get()).unwrap_or(u32::MAX))
.unwrap_or_default()
}));
if !result_sizes.is_empty() {
Some((slot as _, result_sizes))
} else {
None
}
}
}
implcrate::CommandEncoder forsuper::CommandEncoder { type A = super::Api;
unsafefn begin_encoding(&mutself, label: crate::Label) -> Result<(), crate::DeviceError> { let queue = &self.queue_shared.raw; let retain_references = self.shared.settings.retain_command_buffer_references;
// Guard against exhausting Metal's command buffer budget. Use the hard // limit (`MAX_COMMAND_BUFFERS`) so we fail before Metal can hang inside // `new_command_buffer`. let previous = self
.queue_shared
.command_buffer_created_not_submitted
.fetch_add(1, atomic::Ordering::AcqRel); if previous >= adapter::MAX_COMMAND_BUFFERS { let current = previous + 1;
log::warn!( "metal: refusing to create new command buffer; {current} outstanding command \
buffers exceeds the limit of {}. Treating this as device lost. \
Ensure command encoders are submitted or dropped rather than kept alive \
to avoid exhausting Metal's command buffer budget.",
adapter::MAX_COMMAND_BUFFERS
); return Err(crate::DeviceError::Lost);
}
let raw = autoreleasepool(move |_| { let cmd_buf_ref = if retain_references {
queue.commandBuffer()
} else {
queue.commandBufferWithUnretainedReferences()
}
.unwrap(); iflet Some(label) = label {
cmd_buf_ref.setLabel(Some(&NSString::from_str(label)));
}
cmd_buf_ref.to_owned()
});
self.raw_cmd_buf = Some(raw);
Ok(())
}
unsafefn discard_encoding(&mutself) { self.leave_blit(); self.leave_acceleration_structure_builder(); // when discarding, we don't have a guarantee that // everything is in a good state, so check carefully iflet Some(encoder) = self.state.render.take() {
encoder.endEncoding();
} iflet Some(encoder) = self.state.compute.take() {
encoder.endEncoding();
} let had_command_buffer = self.raw_cmd_buf.is_some(); // Clear the Option first so the underlying `metal::CommandBuffer` is // dropped before we update the counter. self.raw_cmd_buf = None; if had_command_buffer { self.queue_shared
.command_buffer_created_not_submitted
.fetch_sub(1, atomic::Ordering::AcqRel);
}
}
unsafefn end_encoding(&mutself) -> Result<super::CommandBuffer, crate::DeviceError> { // Handle pending timer query if any. if !self.state.pending_timer_queries.is_empty() { self.leave_blit(); self.enter_blit();
}
unsafefn begin_query(&mutself, set: &super::QuerySet, index: u32) { match set.ty {
wgt::QueryType::Occlusion => { self.state
.render
.as_ref()
.unwrap()
.setVisibilityResultMode_offset(
MTLVisibilityResultMode::Boolean,
index as usize * crate::QUERY_SIZE as usize,
);
}
_ => {}
}
} unsafefn end_query(&mutself, set: &super::QuerySet, index: u32) { match set.ty {
wgt::QueryType::Occlusion => { self.state
.render
.as_ref()
.unwrap()
.setVisibilityResultMode_offset(
MTLVisibilityResultMode::Disabled,
index as usize * crate::QUERY_SIZE as usize,
);
}
_ => {}
}
} unsafefn write_timestamp(&mutself, set: &super::QuerySet, index: u32) { let support = self.shared.private_caps.timestamp_query_support;
debug_assert!(
support.contains(TimestampQuerySupport::STAGE_BOUNDARIES), "Timestamp queries are not supported"
); let sample_buffer = set.counter_sample_buffer.as_ref().unwrap(); let with_barrier = true;
// Try to use an existing encoder for timestamp query if possible. // This works only if it's supported for the active encoder. iflet (true, Some(encoder)) = (
support.contains(TimestampQuerySupport::ON_BLIT_ENCODER), self.state.blit.as_ref(),
) { unsafe {
encoder.sampleCountersInBuffer_atSampleIndex_withBarrier(
sample_buffer,
index as _,
with_barrier,
)
};
} elseiflet (true, Some(encoder)) = (
support.contains(TimestampQuerySupport::ON_RENDER_ENCODER), self.state.render.as_ref(),
) { unsafe {
encoder.sampleCountersInBuffer_atSampleIndex_withBarrier(
sample_buffer,
index as _,
with_barrier,
)
};
} elseiflet (true, Some(encoder)) = (
support.contains(TimestampQuerySupport::ON_COMPUTE_ENCODER), self.state.compute.as_ref(),
) { unsafe {
encoder.sampleCountersInBuffer_atSampleIndex_withBarrier(
sample_buffer,
index as _,
with_barrier,
)
};
} else { // If we're here it means we either have no encoder open, or it's not supported to sample within them. // If this happens with render/compute open, this is an invalid usage!
debug_assert!(self.state.render.is_none() && self.state.compute.is_none());
// But otherwise it means we'll put defer this to the next created encoder. self.state.pending_timer_queries.push((set.clone(), index));
// Ensure we didn't already have a blit open. self.leave_blit();
};
}
unsafefn copy_query_results(
&mutself,
set: &super::QuerySet,
range: Range<u32>,
buffer: &super::Buffer,
offset: wgt::BufferAddress,
_: wgt::BufferSize, // Metal doesn't support queries that are bigger than a single element are not supported
) { let encoder = self.enter_blit(); match set.ty {
wgt::QueryType::Occlusion => { let size = (range.end - range.start) as u64 * crate::QUERY_SIZE; unsafe {
encoder.copyFromBuffer_sourceOffset_toBuffer_destinationOffset_size(
&set.raw_buffer,
range.start as usize * crate::QUERY_SIZE as usize,
&buffer.raw,
offset as usize,
size as usize,
)
};
}
wgt::QueryType::Timestamp => { unsafe {
encoder.resolveCounters_inRange_destinationBuffer_destinationOffset(
set.counter_sample_buffer.as_ref().unwrap(),
NSRange::new(range.start as usize, (range.end - range.start) as usize),
&buffer.raw,
offset as usize,
)
};
}
wgt::QueryType::PipelineStatistics(_) => todo!(),
}
}
unsafe {
sba_descriptor.setStartOfVertexSampleIndex(
timestamp_writes
.beginning_of_pass_write_index
.map_or(MTLCounterDontSample, |i| i as _),
)
}; unsafe {
sba_descriptor.setEndOfFragmentSampleIndex(
timestamp_writes
.end_of_pass_write_index
.map_or(MTLCounterDontSample, |i| i as _),
)
};
}
iflet Some(occlusion_query_set) = desc.occlusion_query_set {
descriptor.setVisibilityResultBuffer(Some(occlusion_query_set.raw_buffer.as_ref()))
} // This strangely isn't mentioned in https://developer.apple.com/documentation/metal/improving-rendering-performance-with-vertex-amplification. // The docs for [`renderTargetArrayLength`](https://developer.apple.com/documentation/metal/mtlrenderpassdescriptor/rendertargetarraylength) // also say "The number of active layers that all attachments must have for layered rendering," implying it is only for layered rendering. // However, when I don't set this, I get undefined behavior in nonzero layers, and all non-apple examples of vertex amplification set it. // So this is just one of those undocumented requirements. iflet Some(mv) = desc.multiview_mask {
descriptor.setRenderTargetArrayLength(32 - mv.leading_zeros() as usize);
} let raw = self.raw_cmd_buf.as_ref().unwrap(); let encoder = raw.renderCommandEncoderWithDescriptor(&descriptor).unwrap(); iflet Some(mv) = desc.multiview_mask { // Most likely the API just wasn't thought about enough. It's not like they ever allow you // to use enough views to overflow a 32-bit bitmask. let mv = mv.get(); let msb = 32 - mv.leading_zeros(); letmut maps: SmallVec<[MTLVertexAmplificationViewMapping; 32]> = SmallVec::new(); for i in0..msb { if (mv & (1 << i)) != 0 {
maps.push(MTLVertexAmplificationViewMapping {
renderTargetArrayIndexOffset: i,
viewportArrayIndexOffset: i,
});
}
} unsafe {
encoder.setVertexAmplificationCount_viewMappings(
mv.count_ones() as usize,
maps.as_ptr(),
)
};
} iflet Some(label) = desc.label {
encoder.setLabel(Some(&NSString::from_str(label)));
} self.state.render = Some(encoder);
});
autoreleasepool(|_| { // TimeStamp Queries and ComputePassDescriptor were both introduced in Metal 2.3 (macOS 11, iOS 14) // and we currently only need ComputePassDescriptor for timestamp queries let encoder = ifself.shared.private_caps.timestamp_query_support.is_empty() {
raw.computeCommandEncoder().unwrap()
} else { let descriptor = MTLComputePassDescriptor::new();
unsafefn set_acceleration_structure_dependencies(
command_buffers: &[&super::CommandBuffer],
dependencies: &[&super::AccelerationStructure],
) { let Some(first_command_buffer) = command_buffers.first() else { return;
}; let desc = MTLResidencySetDescriptor::new();
desc.setLabel(first_command_buffer.raw.label().as_deref()); let residency_set = first_command_buffer
.raw
.device()
.newResidencySetWithDescriptor_error(&desc)
.unwrap(); for command_buffer in command_buffers {
command_buffer.raw.useResidencySet(&residency_set);
} for dependency in dependencies {
residency_set.addAllocation(ProtocolObject::from_ref(&*dependency.raw));
}
residency_set.commit();
}
}
impl Drop forsuper::CommandEncoder { fn drop(&mutself) { // Metal raises an assert when a MTLCommandEncoder is deallocated without a call // to endEncoding. This isn't documented in the general case at // https://developer.apple.com/documentation/metal/mtlcommandencoder, but for the // more-specific MTLComputeCommandEncoder it is stated as a requirement at // https://developer.apple.com/documentation/metal/mtlcomputecommandencoder. It // appears to be a requirement for all MTLCommandEncoder objects. Failing to call // endEncoding causes a crash with the message 'Command encoder released without // endEncoding'. To prevent this, we explicitiy call discard_encoding, which // calls endEncoding on any still-held MTLCommandEncoders. unsafe { self.discard_encoding();
} self.counters.command_encoders.sub(1);
}
}
impl Drop forsuper::CommandBuffer { fn drop(&mutself) { // `command_buffer_created_not_submitted` is usually decremented when the command // buffer is submitted. But if we're dropping a command buffer that was never // submitted, we need to decrement the count here. let status = self.raw.status(); if status == MTLCommandBufferStatus::NotEnqueued
|| status == MTLCommandBufferStatus::Enqueued
{ let previous = self
.queue_shared
.command_buffer_created_not_submitted
.fetch_sub(1, atomic::Ordering::AcqRel);
debug_assert!(previous > 0);
}
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.