Files
fs2open.github.com/code/graphics/vulkan/VulkanBuffer.h
the-eandClaude Opus 5 5e745f7535 Add RTAO and unify raytraced/shadow-mapped shadow softness on sun angular size
Raytraced shadow penumbras needed a hand-authored $SunAngularSize: in
stars.tbl, which no shipped content sets -- so in practice nothing ever
got a penumbra, and the shadow-mapped path's $Shadow Smoothness Factor:
had no relation to it at all. Derive a size from the sun bitmap instead
and drive both methods from the same number.

g3_render_rect_screen_aligned_2d() sizes the sun quad so its rad is the
tangent of the half-angle it subtends, making tan(angular radius) equal
to 0.05 * the mission's +Scale: * the emitting disc's fraction of the
bitmap. That fraction is the area of the pixels at or above 90% of the
brightest one, converted to an equivalent radius. Across 90 sun bitmaps
from retail, the MediaVPs, Blue Planet and BtA it lands at a median of
0.261 (retail's chunkier art at 0.487), and the threshold acts as a
constant factor rather than a per-bitmap judgement call -- Spearman rho
0.96 between a 90% and a 50% cutoff.

Taken literally that yields suns 3.9-8.5x Sol's apparent diameter once
+Scale: is folded in (median 1.55, up to 5.0 in retail's own missions),
so the measurement is scaled by 0.25 and clamped to 1.5 degrees, putting
typical content near Sol. Derivation is the default: $SunAngularSize: 0
asks for hard shadows, an explicit value still wins, and missions can
also set +AngularSize: on an individual sun to override both, with FRED
and QtFRED UI (validation and fallback included) to author it. Bitmaps
that can't be read -- and the deliberately blank sun bitmaps mods ship
to get a light source with no visible disc -- fall back to hard shadows
rather than to the widest possible penumbra.

That angular size now sizes the raytraced penumbra cone directly
(traceShadowRayCone() in shadows.sdr), with sample count following the
same Shadow_quality tier that picks the shadow map's resolution (1 ray
at Low, up to 16 at Ultra) rather than being a separate setting. The
shadow-mapped path's tabled smoothness values were tuned by eye against
Sol-sized retail/MediaVPs art, so shadow_smoothness_scale() in
shadows.cpp scales them by the active sun's size relative to Sol,
clamped to what the shadow map can represent (shadow_clamp_smoothness()).
The two methods still disagree about occluder distance -- the raytracer
uses the real one, the shadow map's tabled values stand in for a blocker
distance pinned to the cascade's own extent -- so RT_SHADOW_SUN_SIZE_CALIBRATION
in shadows.sdr is a constant factor that makes a given $SunAngularSize:
read as roughly the same softness either way; it's the number to revisit
if the shadow map ever grows a real blocker search.

Also adds RTAO (raytraced ambient occlusion) sharing the same TLAS as
the shadow rays, with lighting-profile-driven radius/strength, and lab
controls for RT shadow quality (Low/High), the local light cap, RT
shadow samples, RTAO, and an angular-size override for comparing both
shadow methods against the same sun. All are safe to change live: they
only gate which lights are picked as shadow casters or scale existing
uniforms while filling the per-frame light data.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:47 +01:00

352 lines
12 KiB
C++

#pragma once
#include "graphics/2d.h"
#include "VulkanConstants.h"
#include "VulkanMemory.h"
#include <array>
#include <vulkan/vulkan.hpp>
namespace graphics::vulkan {
/**
* @brief Per-frame bump allocator for streaming/dynamic buffers
*
* Two of these exist (one per frame-in-flight). At frame start the cursor
* resets to 0. Each streaming upload bumps the cursor forward.
* The buffer is persistently mapped for the lifetime of the allocator.
*/
struct FrameBumpAllocator {
vk::Buffer buffer;
VulkanAllocation allocation = {};
void* mappedPtr = nullptr;
size_t capacity = 0;
size_t cursor = 0;
};
/**
* @brief Internal representation of a Vulkan buffer
*
* Static buffers own their own VkBuffer. Streaming/Dynamic buffers are
* sub-allocated from a shared FrameBumpAllocator each frame.
*/
struct VulkanBufferObject {
BufferType type = BufferType::Vertex;
BufferUsageHint usage = BufferUsageHint::Static;
bool valid = false;
size_t dataSize = 0; // Usable data size. Static: total VkBuffer allocation. Streaming: current frame allocation.
// If true, the buffer is created with VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
// and VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR so it
// can be used as acceleration structure geometry input. Static buffers only.
bool rtCapable = false;
// Static buffer fields (unused for streaming)
vk::Buffer buffer = nullptr;
VulkanAllocation allocation = {};
// Incremented each time createOrResizeBuffer() actually creates a new
// VkBuffer (i.e. on growth past the current capacity), since that produces
// a new VK_KHR_buffer_device_address. Callers that cache a device address
// (e.g. a BLAS built against this buffer) must compare against this value
// to detect staleness after the heap resizes.
uint64_t generation = 0;
// Frame number of the most recent draw/bind that fetched this buffer via
// getVkBuffer() (static buffers only). Used to decide whether a full
// rewrite must orphan the VkBuffer instead of writing host-visible memory
// the GPU may still be reading from an in-flight frame. mutable: stamped
// from the const getVkBuffer() lookup path.
mutable uint64_t lastUsedFrameNumber = 0;
// Frame bump allocator sub-allocation (streaming/dynamic only)
vk::Buffer frameAllocBuffer; // VkBuffer at upload time (may be old allocator buffer after growth)
size_t frameAllocOffset = 0; // Byte offset within the frame allocator buffer
uint32_t frameAllocFrame = UINT32_MAX; // Frame index when last allocated
bool isStreaming() const {
return usage == BufferUsageHint::Streaming || usage == BufferUsageHint::Dynamic;
}
};
/**
* @brief Manages GPU buffer creation, updates, and destruction
*
* Streaming/Dynamic buffers are sub-allocated from a global per-frame bump
* allocator (two large VkBuffers, one per frame-in-flight). Static buffers
* keep their own VkBuffer. PersistentMapping buffers are handled separately.
*/
class VulkanBufferManager {
public:
VulkanBufferManager();
~VulkanBufferManager();
// Non-copyable
VulkanBufferManager(const VulkanBufferManager&) = delete;
VulkanBufferManager& operator=(const VulkanBufferManager&) = delete;
/**
* @brief Initialize the buffer manager
* @param device The Vulkan logical device
* @param memoryManager The memory manager for allocations
* @param graphicsQueueFamily Graphics queue family index
* @param minUboAlignment Minimum uniform buffer offset alignment from device limits
* @return true on success
*/
bool init(vk::Device device,
VulkanMemoryManager* memoryManager,
uint32_t graphicsQueueFamily,
uint32_t minUboAlignment);
/**
* @brief Shutdown and free all buffers
*/
void shutdown();
/**
* @brief Set the current frame and reset the bump allocator cursor
* Must be called at the start of each frame before any buffer updates
* @param frameIndex The current frame index (0 to MAX_FRAMES_IN_FLIGHT-1)
* @param frameNumber The monotonic total frame number (for in-use tracking
* of static buffers; see VulkanBufferObject::lastUsedFrameNumber)
*/
void setCurrentFrame(uint32_t frameIndex, uint64_t frameNumber);
/**
* @brief Get the current frame index
*/
uint32_t getCurrentFrame() const { return m_currentFrame; }
/**
* @brief Get the monotonic frame number set by setCurrentFrame (total frames
* rendered, never wraps back -- unlike getCurrentFrame()'s in-flight slot index)
*/
uint64_t getCurrentFrameNumber() const { return m_currentFrameNumber; }
/**
* @brief Get the Vulkan logical device
*/
vk::Device getDevice() const { return m_device; }
/**
* @brief Create a new buffer
* @param type The buffer type (Vertex, Index, Uniform)
* @param usage Usage hint for optimization
* @param rtCapable If true, adds the usage flags needed to use this buffer
* as acceleration structure geometry input (static buffers only)
* @return Handle to the created buffer, or invalid handle on failure
*/
gr_buffer_handle createBuffer(BufferType type, BufferUsageHint usage, bool rtCapable = false);
/**
* @brief Get the generation counter for a buffer's underlying VkBuffer
*
* Incremented each time the buffer is grown past its current capacity and
* a new VkBuffer is created (and thus a new device address). Used by
* callers that cache a buffer device address to detect staleness.
* @param handle The buffer to query
* @return The current generation, or 0 if the handle is invalid
*/
uint64_t getBufferGeneration(gr_buffer_handle handle) const;
/**
* @brief Delete a buffer
* @param handle The buffer to delete
*/
void deleteBuffer(gr_buffer_handle handle);
/**
* @brief Update buffer data (full replacement)
* @param handle The buffer to update
* @param size Size of data in bytes
* @param data Pointer to data
*/
void updateBufferData(gr_buffer_handle handle, size_t size, const void* data);
/**
* @brief Update buffer data at an offset
* @param handle The buffer to update
* @param offset Offset in bytes
* @param size Size of data in bytes
* @param data Pointer to data
*/
void updateBufferDataOffset(gr_buffer_handle handle, size_t offset, size_t size, const void* data);
/**
* @brief Map buffer for CPU access
* @param handle The buffer to map
* @return Pointer to mapped memory, or nullptr on failure
*/
void* mapBuffer(gr_buffer_handle handle);
/**
* @brief Flush a range of a mapped buffer
* @param handle The buffer to flush
* @param offset Offset in bytes
* @param size Size of range in bytes
*/
void flushMappedBuffer(gr_buffer_handle handle, size_t offset, size_t size);
/**
* @brief Bind uniform buffer to a binding slot
* @param blockType The uniform block type
* @param offset Offset within the buffer
* @param size Size of the bound range
* @param buffer The buffer to bind
*/
void bindUniformBuffer(uniform_block_type blockType, size_t offset, size_t size, gr_buffer_handle buffer) const;
/**
* @brief Get the Vulkan buffer handle for the current frame
* @param handle The buffer handle
* @return The VkBuffer, or VK_NULL_HANDLE if invalid
*/
vk::Buffer getVkBuffer(gr_buffer_handle handle) const;
/**
* @brief Get buffer size
* For streaming buffers, returns the current frame allocation size.
* For static buffers, returns the total buffer size.
* @param handle The buffer handle
* @return Size in bytes, or 0 if invalid
*/
size_t getBufferSize(gr_buffer_handle handle) const;
/**
* @brief Get the base offset for the current frame's allocation
* For streaming buffers, returns the bump allocator offset.
* For static buffers, returns 0.
* @param handle The buffer handle
* @return Byte offset for current frame's allocation
*/
size_t getFrameBaseOffset(gr_buffer_handle handle) const;
/**
* @brief Check if a handle is valid
*/
bool isValidHandle(gr_buffer_handle handle) const;
/**
* @brief Get statistics
*/
size_t getBufferCount() const { return m_activeBufferCount; }
size_t getTotalBufferMemory() const { return m_totalBufferMemory; }
/**
* @brief Get the constant white color buffer for fallback vertex colors
* This buffer contains vec4(1,1,1,1) for shaders expecting vertColor
*/
vk::Buffer getFallbackColorBuffer() const { return m_fallbackColorBuffer; }
/**
* @brief Get the constant zero texcoord buffer for fallback vertex texcoords
* This buffer contains vec4(0,0,0,0) for shaders expecting vertTexCoord
*/
vk::Buffer getFallbackTexCoordBuffer() const { return m_fallbackTexCoordBuffer; }
/**
* @brief Get the fallback uniform buffer for uninitialized descriptor bindings
* This buffer contains zeros and is used to pre-fill all UBO descriptor bindings
* to avoid undefined behavior from uninitialized descriptors after pool reset
*/
vk::Buffer getFallbackUniformBuffer() const { return m_fallbackUniformBuffer; }
/**
* @brief Get a ready-to-use DescriptorBufferInfo for the fallback uniform buffer
*/
vk::DescriptorBufferInfo getFallbackUniformBufferInfo() const {
return {m_fallbackUniformBuffer, 0, FALLBACK_UNIFORM_BUFFER_SIZE};
}
/**
* @brief Get the size of the fallback uniform buffer
*/
static size_t getFallbackUniformBufferSize() { return FALLBACK_UNIFORM_BUFFER_SIZE; }
private:
/**
* @brief Create a one-shot buffer (used in initialization only).
*/
bool createOneShotBuffer(vk::Flags<vk::BufferUsageFlagBits> usage, const void* data, size_t size, vk::Buffer& buf, VulkanAllocation& alloc) const;
/**
* @brief Convert BufferType (+ optional RT usage) to Vulkan usage flags
*/
static vk::BufferUsageFlags getVkUsageFlags(BufferType type, bool rtCapable) ;
/**
* @brief Convert BufferUsageHint to memory usage
*/
static MemoryUsage getMemoryUsage(BufferUsageHint hint) ;
/**
* @brief Create or resize a static buffer
* Streaming buffers must NOT call this — they use the frame bump allocator.
*/
bool createOrResizeBuffer(VulkanBufferObject& bufferObj, size_t size);
/**
* @brief Get buffer object from handle
*/
VulkanBufferObject* getBufferObject(gr_buffer_handle handle);
const VulkanBufferObject* getBufferObject(gr_buffer_handle handle) const;
// Frame bump allocator
static constexpr size_t FRAME_ALLOC_INITIAL_SIZE = 4 * 1024 * 1024;
bool createFrameAllocBuffer(FrameBumpAllocator& alloc, size_t size);
void initFrameAllocators();
void shutdownFrameAllocators();
size_t bumpAllocate(size_t size);
void growFrameAllocator();
std::array<FrameBumpAllocator, MAX_FRAMES_IN_FLIGHT> m_frameAllocs;
uint32_t m_uboAlignment = 256;
vk::Device m_device;
VulkanMemoryManager* m_memoryManager = nullptr;
uint32_t m_graphicsQueueFamily = 0;
uint32_t m_currentFrame = 0;
uint64_t m_currentFrameNumber = 0;
SCP_vector<VulkanBufferObject> m_buffers;
SCP_vector<int> m_freeIndices; // Recycled buffer indices
// Fallback color buffer containing white (1,1,1,1) for vertex data without colors
vk::Buffer m_fallbackColorBuffer;
VulkanAllocation m_fallbackColorAllocation;
// Fallback texcoord buffer containing (0,0,0,0) for vertex data without texcoords
vk::Buffer m_fallbackTexCoordBuffer;
VulkanAllocation m_fallbackTexCoordAllocation;
// Fallback uniform buffer (zeros) for uninitialized descriptor set UBO bindings
static constexpr size_t FALLBACK_UNIFORM_BUFFER_SIZE = 4096;
vk::Buffer m_fallbackUniformBuffer;
VulkanAllocation m_fallbackUniformAllocation;
size_t m_activeBufferCount = 0;
size_t m_totalBufferMemory = 0;
bool m_initialized = false;
};
// Global buffer manager instance (set during renderer init)
VulkanBufferManager* getBufferManager();
void setBufferManager(VulkanBufferManager* manager);
// ========== gr_screen function pointer implementations ==========
gr_buffer_handle vulkan_create_buffer(BufferType type, BufferUsageHint usage, bool rt_capable);
void vulkan_delete_buffer(gr_buffer_handle handle);
void vulkan_update_buffer_data(gr_buffer_handle handle, size_t size, const void* data);
void vulkan_update_buffer_data_offset(gr_buffer_handle handle, size_t offset, size_t size, const void* data);
void* vulkan_map_buffer(gr_buffer_handle handle);
void vulkan_flush_mapped_buffer(gr_buffer_handle handle, size_t offset, size_t size);
void vulkan_bind_uniform_buffer(uniform_block_type blockType, size_t offset, size_t size, gr_buffer_handle buffer);
} // namespace graphics::vulkan