mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
* avoid undefined behavior from UNREACHABLE on reachable code paths
UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.
Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.
A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.
Where the converted site was a simple guard of the form
if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
Assertion(!x, ...);
if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* addendum
After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible. UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error". Also fix a few other sites and change a couple to Warnings.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
220 lines
7.7 KiB
C++
220 lines
7.7 KiB
C++
|
|
|
|
#include "UniformBufferManager.h"
|
|
#include "uniform_structs.h"
|
|
|
|
#include "tracing/tracing.h"
|
|
|
|
namespace {
|
|
|
|
size_t getElementSize(uniform_block_type type)
|
|
{
|
|
switch (type) {
|
|
case uniform_block_type::Lights:
|
|
return sizeof(graphics::deferred_light_data);
|
|
case uniform_block_type::ModelData:
|
|
return sizeof(graphics::model_uniform_data);
|
|
case uniform_block_type::ShadowMapData:
|
|
return sizeof(graphics::shadow_uniform_data);
|
|
case uniform_block_type::NanoVGData:
|
|
return sizeof(graphics::nanovg_draw_data);
|
|
case uniform_block_type::DecalInfo:
|
|
return sizeof(graphics::decal_info);
|
|
case uniform_block_type::Matrices:
|
|
return sizeof(graphics::matrix_uniforms);
|
|
case uniform_block_type::MovieData:
|
|
return sizeof(graphics::movie_uniforms);
|
|
case uniform_block_type::ShadowCascadeParams:
|
|
return 0;
|
|
case uniform_block_type::NUM_BLOCK_TYPES:
|
|
default:
|
|
UNREACHABLE("Invalid block type %d encountered!", static_cast<int>(type));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
size_t getHeaderSize(uniform_block_type type)
|
|
{
|
|
switch (type) {
|
|
case uniform_block_type::Lights:
|
|
return sizeof(graphics::deferred_global_data);
|
|
case uniform_block_type::DecalInfo:
|
|
return sizeof(graphics::decal_globals);
|
|
case uniform_block_type::ModelData:
|
|
case uniform_block_type::ShadowMapData:
|
|
case uniform_block_type::NanoVGData:
|
|
case uniform_block_type::Matrices:
|
|
case uniform_block_type::MovieData:
|
|
case uniform_block_type::GenericData:
|
|
case uniform_block_type::ShadowCascadeParams:
|
|
return 0;
|
|
case uniform_block_type::NUM_BLOCK_TYPES:
|
|
default:
|
|
UNREACHABLE("Invalid block type %d encountered!", static_cast<int>(type));
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace graphics {
|
|
namespace util {
|
|
|
|
UniformBufferManager::UniformBufferManager()
|
|
{
|
|
bool success = gr_get_property(gr_property::UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_offset_alignment);
|
|
Assertion(success, "Uniform buffer usage requires a backend which allows to query the offset alignment!");
|
|
|
|
_use_persistent_mapping = gr_is_capable(gr_capability::CAPABILITY_PERSISTENT_BUFFER_MAPPING);
|
|
|
|
_segment_fences.fill(nullptr);
|
|
changeSegmentSize(4096);
|
|
}
|
|
UniformBufferManager::~UniformBufferManager()
|
|
{
|
|
if (_active_uniform_buffer.isValid()) {
|
|
gr_delete_buffer(_active_uniform_buffer);
|
|
_active_uniform_buffer = gr_buffer_handle();
|
|
}
|
|
for (auto& fence : _segment_fences) {
|
|
if (fence != nullptr) {
|
|
gr_sync_delete(fence);
|
|
fence = nullptr;
|
|
}
|
|
}
|
|
|
|
for (auto& buffer : _retired_buffers) {
|
|
gr_sync_delete(std::get<0>(buffer));
|
|
gr_delete_buffer(std::get<1>(buffer));
|
|
// The shadow buffer pointer will be deleted automatically
|
|
}
|
|
_retired_buffers.clear();
|
|
}
|
|
void UniformBufferManager::onFrameEnd()
|
|
{
|
|
GR_DEBUG_SCOPE("Performing uniform frame end operations");
|
|
|
|
if (_segment_offset > _segment_size) {
|
|
// We needed more data than what is available in the segment
|
|
changeSegmentSize(_segment_offset);
|
|
} else {
|
|
// Set up the fence for the currently active segment
|
|
_segment_fences[_active_segment] = gr_sync_fence();
|
|
|
|
// Move the current segment to the next one
|
|
_active_segment = (_active_segment + 1) % NUM_SEGMENTS;
|
|
_segment_offset = 0;
|
|
|
|
// Now we need to wait until the segment is available again. In most cases this should succeed immediately.
|
|
if (_segment_fences[_active_segment] != nullptr) {
|
|
int i = 0;
|
|
while (i < 10 && !gr_sync_wait(_segment_fences[_active_segment], 500000000)) {
|
|
// This isn't good!
|
|
mprintf(("Missed uniform fence deadline!!\n"));
|
|
++i;
|
|
}
|
|
gr_sync_delete(_segment_fences[_active_segment]);
|
|
_segment_fences[_active_segment] = nullptr;
|
|
|
|
if (i == 10) {
|
|
// I don't know how to handle this properly but this probably means that something went wrong with the
|
|
// GPU
|
|
Error(LOCATION, "Failed to wait until uniform range is available! Get a coder.");
|
|
}
|
|
}
|
|
}
|
|
|
|
while (!_retired_buffers.empty()) {
|
|
if (gr_sync_wait(std::get<0>(_retired_buffers.front()), 0)) {
|
|
// Fence was signaled => buffer is not in use anymore
|
|
// Delete the sync object since we don't need it anymore
|
|
gr_sync_delete(std::get<0>(_retired_buffers.front()));
|
|
gr_delete_buffer(std::get<1>(_retired_buffers.front()));
|
|
|
|
// This will automatically clean up the shadow buffer pointer
|
|
_retired_buffers.erase(_retired_buffers.begin());
|
|
} else {
|
|
// The first fence element was not signaled yet so all the other fences also haven't been signaled yet
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
UniformBuffer UniformBufferManager::getUniformBuffer(uniform_block_type type, size_t num_elements,
|
|
size_t element_size_override)
|
|
{
|
|
if (element_size_override == 0) {
|
|
element_size_override = getElementSize(type);
|
|
}
|
|
auto size = UniformAligner::getBufferSize(num_elements, (size_t)_offset_alignment, element_size_override,
|
|
getHeaderSize(type));
|
|
|
|
auto end_offset = _segment_offset + size;
|
|
|
|
auto absolute_end = _segment_size * _active_segment + end_offset;
|
|
|
|
if (absolute_end >= _active_buffer_size) {
|
|
// This new element uses too much memory to fit into the active buffer so we need to allocate a new one right
|
|
// now. It may happen that we use more than the segment size but that is not an issue since the frame-end code
|
|
// will reallocate the buffer if that happens.
|
|
// We don't really know how much we are going to need here so 2 times the current amount seems like a good idea
|
|
changeSegmentSize(_segment_size * 2);
|
|
|
|
// Try the stuff above again.
|
|
return getUniformBuffer(type, num_elements, element_size_override);
|
|
}
|
|
|
|
auto data_offset = _segment_size * _active_segment + _segment_offset;
|
|
_segment_offset = end_offset;
|
|
|
|
// Even in the persistent mapping case we still use a temporary buffer since writing to GPU memory is not very fast
|
|
// when doing a lot of small writes (e.g. when building model uniform data). Instead we use a shadow buffer and
|
|
// do a single memcpy when we are done
|
|
return UniformBuffer(this, data_offset, _shadow_uniform_buffer.get() + data_offset, size, element_size_override,
|
|
getHeaderSize(type), static_cast<size_t>(_offset_alignment));
|
|
}
|
|
void UniformBufferManager::changeSegmentSize(size_t new_size)
|
|
{
|
|
if (_active_uniform_buffer.isValid()) {
|
|
// Retire the old buffer first
|
|
_retired_buffers.emplace_back(gr_sync_fence(), _active_uniform_buffer, std::move(_shadow_uniform_buffer));
|
|
}
|
|
|
|
// The current fences are meaningless now so we need to delete them
|
|
for (auto& fence : _segment_fences) {
|
|
if (fence != nullptr) {
|
|
gr_sync_delete(fence);
|
|
fence = nullptr;
|
|
}
|
|
}
|
|
|
|
_active_buffer_size = new_size * NUM_SEGMENTS;
|
|
_shadow_uniform_buffer.reset(new uint8_t[_active_buffer_size]);
|
|
_active_uniform_buffer = gr_create_buffer(
|
|
BufferType::Uniform, _use_persistent_mapping ? BufferUsageHint::PersistentMapping : BufferUsageHint::Dynamic);
|
|
|
|
gr_update_buffer_data(_active_uniform_buffer, _active_buffer_size, nullptr);
|
|
if (_use_persistent_mapping) {
|
|
_buffer_ptr = gr_map_buffer(_active_uniform_buffer);
|
|
}
|
|
|
|
_active_segment = 0;
|
|
_segment_size = new_size;
|
|
_segment_offset = 0;
|
|
}
|
|
void UniformBufferManager::submitData(void* buffer, size_t data_size, size_t offset)
|
|
{
|
|
if (_use_persistent_mapping) {
|
|
auto buffer_dest = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(_buffer_ptr) + offset);
|
|
memcpy(buffer_dest, buffer, data_size);
|
|
// The data is already in the buffer but we still need to flush the memory range
|
|
gr_flush_mapped_buffer(_active_uniform_buffer, offset, data_size);
|
|
} else {
|
|
gr_update_buffer_data_offset(_active_uniform_buffer, offset, data_size, buffer);
|
|
}
|
|
}
|
|
gr_buffer_handle UniformBufferManager::getActiveBufferHandle() { return _active_uniform_buffer; }
|
|
size_t UniformBufferManager::getBufferSize() { return _active_buffer_size; }
|
|
size_t UniformBufferManager::getCurrentlyUsedSize() { return _segment_offset; }
|
|
} // namespace util
|
|
} // namespace graphics
|