processors/processing_processor_render.cpp¶
Namespaces¶
| Name |
|---|
| sgns |
| sgns::sgprocessing Artifact and manifest binary serialization. |
Source code¶
#include "processors/processing_processor_render.hpp"
#include "processingbase/vulkan_init_guard.hpp"
#include "util/sha256.hpp"
#include "util/quantization.hpp"
#include <VkBootstrap.h>
#include <algorithm>
#include <cstring>
#include <mutex>
#include <ColorFormat.hpp>
#include <DepthFormat.hpp>
#include <Topology.hpp>
#include <CullMode.hpp>
#include <FrontFace.hpp>
#include <DepthTest.hpp>
#include <BlendFactor.hpp>
#include <VertexLayoutFormat.hpp>
namespace sgns::sgprocessing
{
bool RenderProcessor::IsAcceptable( VkPhysicalDeviceType type )
{
return type == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
|| type == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
}
namespace
{
const char *VkPhysicalDeviceTypeName( VkPhysicalDeviceType type )
{
switch ( type )
{
case VK_PHYSICAL_DEVICE_TYPE_OTHER: return "OTHER";
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "INTEGRATED_GPU";
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "DISCRETE_GPU";
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "VIRTUAL_GPU";
case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU";
default: return "UNKNOWN";
}
}
} // namespace
VkDeviceSize RenderProcessor::LargestDeviceLocalHeap( VkPhysicalDevice device )
{
VkPhysicalDeviceMemoryProperties memProps;
vkGetPhysicalDeviceMemoryProperties( device, &memProps );
VkDeviceSize largest = 0;
for ( uint32_t i = 0; i < memProps.memoryHeapCount; ++i )
{
if ( memProps.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT )
largest = (std::max)( largest, memProps.memoryHeaps[i].size );
}
return largest;
}
bool RenderProcessor::InitializeContext()
{
if ( m_contextInitialized )
return true;
std::lock_guard<std::mutex> lock( sgns::sgprocessing::VulkanInitMutex() );
if ( m_contextInitialized )
return true;
// On macOS MoltenVK is statically linked (libMoltenVK.a), so there is no
// libvulkan.dylib for vk-bootstrap's default dlopen path to find.
// Pass the statically-available vkGetInstanceProcAddr directly.
#if defined(__APPLE__)
vkb::InstanceBuilder instance_builder( vkGetInstanceProcAddr );
#else
vkb::InstanceBuilder instance_builder;
#endif
auto inst_ret = instance_builder
.set_app_name( "SGProcessingManager RenderProcessor" )
.set_app_version( 1, 0, 0 )
#ifdef ENABLE_VULKAN_VALIDATION
.request_validation_layers() // best-effort (D-20, D-21)
#else
.request_validation_layers( false )
#endif
.build();
if ( !inst_ret )
{
m_logger->error( "RenderProcessor: failed to create Vulkan instance: {}",
inst_ret.error().message() );
return false;
}
auto vkb_instance = inst_ret.value();
vkb::PhysicalDeviceSelector selector( vkb_instance );
// This is a headless/offscreen renderer -- no VkSurfaceKHR/swapchain ever exists
// (CTX-01/D-23). vk-bootstrap's PhysicalDeviceSelector defaults require_present to
// true, which rejects every device with vkb::PhysicalDeviceError::no_surface_provided
// when no surface was ever set. Disable that requirement explicitly.
selector.require_present( false );
auto devices_ret = selector.select_devices();
if ( !devices_ret )
{
m_logger->error( "RenderProcessor: failed to enumerate physical devices: {}",
devices_ret.error().message() );
vkb::destroy_instance( vkb_instance );
return false;
}
auto devices = devices_ret.value();
// Diagnostic (D-32 follow-up): log every enumerated device's name/type/vendor
// BEFORE the acceptability filter runs, so environments like WSL (whose Vulkan
// device reports an unexpected type) are debuggable from a plain run, not just
// via a debugger.
for ( const auto &d : devices )
{
m_logger->info( "RenderProcessor: enumerated device \"{}\" type={} vendorID=0x{:04x} "
"deviceID=0x{:04x} apiVersion={}.{}.{}",
d.properties.deviceName,
VkPhysicalDeviceTypeName( d.properties.deviceType ),
d.properties.vendorID,
d.properties.deviceID,
VK_API_VERSION_MAJOR( d.properties.apiVersion ),
VK_API_VERSION_MINOR( d.properties.apiVersion ),
VK_API_VERSION_PATCH( d.properties.apiVersion ) );
}
devices.erase(
std::remove_if( devices.begin(), devices.end(),
[]( const vkb::PhysicalDevice &d ) {
return !IsAcceptable( d.properties.deviceType );
} ),
devices.end() );
if ( devices.empty() )
{
m_logger->error( "RenderProcessor: no acceptable physical device found "
"(none with device type DISCRETE_GPU or INTEGRATED_GPU)" );
vkb::destroy_instance( vkb_instance );
return false;
}
std::sort( devices.begin(), devices.end(),
[]( const vkb::PhysicalDevice &a, const vkb::PhysicalDevice &b ) {
int rank_a = ( a.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ) ? 2 : 1;
int rank_b = ( b.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ) ? 2 : 1;
if ( rank_a != rank_b )
return rank_a > rank_b;
return LargestDeviceLocalHeap( a.physical_device )
> LargestDeviceLocalHeap( b.physical_device );
} );
vkb::DeviceBuilder device_builder( devices[0] );
auto dev_ret = device_builder.build();
if ( !dev_ret )
{
m_logger->error( "RenderProcessor: failed to create Vulkan device: {}",
dev_ret.error().message() );
vkb::destroy_instance( vkb_instance );
return false;
}
auto vkb_device = dev_ret.value();
auto queue_ret = vkb_device.get_queue( vkb::QueueType::graphics );
if ( !queue_ret )
{
m_logger->error( "RenderProcessor: failed to get graphics queue: {}",
queue_ret.error().message() );
vkb::destroy_device( vkb_device );
vkb::destroy_instance( vkb_instance );
return false;
}
// Reused (never re-queried) by RecordAndSubmit()'s VkCommandPool creation --
// the same graphics queue family InitializeContext() already selected m_queue
// from, not a fresh PhysicalDeviceSelector-style re-selection.
auto queue_family_ret = vkb_device.get_queue_index( vkb::QueueType::graphics );
if ( !queue_family_ret )
{
m_logger->error( "RenderProcessor: failed to get graphics queue family index: {}",
queue_family_ret.error().message() );
vkb::destroy_device( vkb_device );
vkb::destroy_instance( vkb_instance );
return false;
}
m_instance = vkb_instance.instance;
m_physicalDevice = vkb_device.physical_device;
m_device = vkb_device.device;
m_queue = queue_ret.value();
m_queueFamilyIndex = queue_family_ret.value();
m_contextInitialized = true;
return true;
}
ProcessingResult RenderProcessor::MakeError( sgns::sgprocessing::ProcessingErrorStage stage,
const std::string &message )
{
ProcessingResult result;
result.hash = std::vector<uint8_t>( 32, 0 );
ProcessingError error;
error.stage = stage;
error.message = message;
result.error = error;
return result;
}
namespace
{
bool ReadU32( const char *data, size_t size, size_t &offset, uint32_t &out )
{
if ( offset + sizeof( uint32_t ) > size )
{
return false;
}
std::memcpy( &out, data + offset, sizeof( uint32_t ) );
offset += sizeof( uint32_t );
return true;
}
bool ReadU8( const char *data, size_t size, size_t &offset, uint8_t &out )
{
if ( offset + sizeof( uint8_t ) > size )
{
return false;
}
out = static_cast<uint8_t>( data[offset] );
offset += sizeof( uint8_t );
return true;
}
bool ReadF32( const char *data, size_t size, size_t &offset, float &out )
{
if ( offset + sizeof( float ) > size )
{
return false;
}
std::memcpy( &out, data + offset, sizeof( float ) );
offset += sizeof( float );
return true;
}
bool ReadBytes( const char *data, size_t size, size_t &offset, size_t count, const char *&outPtr )
{
if ( offset + count > size )
{
return false;
}
outPtr = data + offset;
offset += count;
return true;
}
bool ReadString( const char *data, size_t size, size_t &offset, std::string &out )
{
uint32_t len = 0;
if ( !ReadU32( data, size, offset, len ) )
{
return false;
}
if ( len == 0 )
{
out.clear();
return true;
}
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, len, bytes ) )
{
return false;
}
out.assign( bytes, len );
return true;
}
}
bool RenderProcessor::ParseCompiledStages( const std::vector<char> &modelFile,
std::vector<ParsedStage> &outStages,
ProcessingResult &errorOut )
{
outStages.clear();
const char *data = modelFile.data();
const size_t size = modelFile.size();
size_t offset = 0;
uint32_t stageCount = 0;
if ( !ReadU32( data, size, offset, stageCount ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading stage_count" );
return false;
}
outStages.reserve( stageCount );
for ( uint32_t i = 0; i < stageCount; ++i )
{
ParsedStage stage;
uint32_t stageTag = 0;
if ( !ReadU32( data, size, offset, stageTag ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading stage_tag" );
return false;
}
stage.stage = static_cast<sgns::Stage>( stageTag );
uint32_t entryPointLen = 0;
if ( !ReadU32( data, size, offset, entryPointLen ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading entry_point_len" );
return false;
}
if ( entryPointLen > 0 )
{
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, entryPointLen, bytes ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading entry_point bytes" );
return false;
}
stage.entry_point.assign( bytes, entryPointLen );
}
uint32_t wordCount = 0;
if ( !ReadU32( data, size, offset, wordCount ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading word_count" );
return false;
}
if ( wordCount > 0 )
{
size_t byteCount = static_cast<size_t>( wordCount ) * sizeof( uint32_t );
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, byteCount, bytes ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ParseCompiledStages: truncated buffer reading spirv_words" );
return false;
}
stage.spirv.resize( wordCount );
std::memcpy( stage.spirv.data(), bytes, byteCount );
}
outStages.push_back( std::move( stage ) );
}
return true;
}
bool RenderProcessor::ParseRenderPassConfig(
const std::vector<char> &imageData,
sgns::RenderTarget &outTarget,
boost::optional<sgns::PipelineState> &outPipelineState,
std::vector<sgns::VertexLayoutEntry> &outVertexLayout,
boost::optional<std::map<std::string, sgns::RenderShaderUniform>> &outUniforms,
std::vector<uint8_t> &outVertexBytes,
bool &outHasIndex,
sgns::IndexType &outIndexType,
std::vector<uint8_t> &outIndexBytes,
uint32_t &outDataTransformCount,
bool &outHasTextureBuffer,
uint32_t &outTextureWidth,
uint32_t &outTextureHeight,
std::vector<uint8_t> &outTextureBytes,
ProcessingResult &errorOut )
// Function-try-block: several generated setters below (set_width/set_height/
// set_clear_depth/set_offset, etc.) enforce schema-level constraints and throw
// on violation. A malformed/truncated wire-format buffer must never crash the
// process -- convert any such exception into a structured RESOURCE_RESOLUTION
// error instead, per this task's "no crash/UB on out-of-bounds/malformed data"
// requirement.
try
{
outPipelineState = boost::none;
outVertexLayout.clear();
outUniforms = boost::none;
outVertexBytes.clear();
outHasIndex = false;
outIndexBytes.clear();
outDataTransformCount = 0;
outHasTextureBuffer = false;
outTextureWidth = 0;
outTextureHeight = 0;
outTextureBytes.clear();
const char *data = imageData.data();
const size_t size = imageData.size();
size_t offset = 0;
auto fail = [&]( const std::string &message ) -> bool
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION, message );
return false;
};
uint32_t width = 0, height = 0, colorFormatTag = 0, depthFormatTag = 0;
if ( !ReadU32( data, size, offset, width ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading width" );
}
if ( !ReadU32( data, size, offset, height ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading height" );
}
if ( !ReadU32( data, size, offset, colorFormatTag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading color_format_tag" );
}
if ( !ReadU32( data, size, offset, depthFormatTag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading depth_format_tag" );
}
outTarget.set_width( static_cast<int64_t>( width ) );
outTarget.set_height( static_cast<int64_t>( height ) );
outTarget.set_color_format( static_cast<sgns::ColorFormat>( colorFormatTag ) );
outTarget.set_depth_format( static_cast<sgns::DepthFormat>( depthFormatTag ) );
std::vector<double> clearColor( 4, 0.0 );
for ( size_t i = 0; i < 4; ++i )
{
float v = 0.0f;
if ( !ReadF32( data, size, offset, v ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading clear_color" );
}
clearColor[i] = static_cast<double>( v );
}
outTarget.set_clear_color( clearColor );
float clearDepth = 0.0f;
if ( !ReadF32( data, size, offset, clearDepth ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading clear_depth" );
}
outTarget.set_clear_depth( static_cast<double>( clearDepth ) );
uint8_t hasPipelineState = 0;
if ( !ReadU8( data, size, offset, hasPipelineState ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_pipeline_state" );
}
if ( hasPipelineState )
{
sgns::PipelineState ps;
uint8_t hasTopology = 0;
if ( !ReadU8( data, size, offset, hasTopology ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_topology" );
}
if ( hasTopology )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading topology_tag" );
}
ps.set_topology( static_cast<sgns::Topology>( tag ) );
}
uint8_t hasCullMode = 0;
if ( !ReadU8( data, size, offset, hasCullMode ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_cull_mode" );
}
if ( hasCullMode )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading cull_mode_tag" );
}
ps.set_cull_mode( static_cast<sgns::CullMode>( tag ) );
}
uint8_t hasFrontFace = 0;
if ( !ReadU8( data, size, offset, hasFrontFace ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_front_face" );
}
if ( hasFrontFace )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading front_face_tag" );
}
ps.set_front_face( static_cast<sgns::FrontFace>( tag ) );
}
uint8_t hasDepthTest = 0;
if ( !ReadU8( data, size, offset, hasDepthTest ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_depth_test" );
}
if ( hasDepthTest )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading depth_test_tag" );
}
ps.set_depth_test( static_cast<sgns::DepthTest>( tag ) );
}
uint8_t hasBlendEnable = 0;
if ( !ReadU8( data, size, offset, hasBlendEnable ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_blend_enable" );
}
if ( hasBlendEnable )
{
uint8_t blendEnableValue = 0;
if ( !ReadU8( data, size, offset, blendEnableValue ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading blend_enable_value" );
}
ps.set_blend_enable( blendEnableValue != 0 );
}
uint8_t hasBlendSrcFactor = 0;
if ( !ReadU8( data, size, offset, hasBlendSrcFactor ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_blend_src_factor" );
}
if ( hasBlendSrcFactor )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading blend_src_factor_tag" );
}
ps.set_blend_src_factor( static_cast<sgns::BlendFactor>( tag ) );
}
uint8_t hasBlendDstFactor = 0;
if ( !ReadU8( data, size, offset, hasBlendDstFactor ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_blend_dst_factor" );
}
if ( hasBlendDstFactor )
{
uint32_t tag = 0;
if ( !ReadU32( data, size, offset, tag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading blend_dst_factor_tag" );
}
ps.set_blend_dst_factor( static_cast<sgns::BlendFactor>( tag ) );
}
outPipelineState = ps;
}
uint32_t vertexLayoutCount = 0;
if ( !ReadU32( data, size, offset, vertexLayoutCount ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex_layout_count" );
}
outVertexLayout.reserve( vertexLayoutCount );
for ( uint32_t i = 0; i < vertexLayoutCount; ++i )
{
std::string name;
if ( !ReadString( data, size, offset, name ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex_layout name" );
}
uint32_t formatTag = 0;
if ( !ReadU32( data, size, offset, formatTag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex_layout format_tag" );
}
uint32_t entryOffset = 0;
if ( !ReadU32( data, size, offset, entryOffset ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex_layout offset" );
}
sgns::VertexLayoutEntry entry;
entry.set_name( name );
entry.set_format( static_cast<sgns::VertexLayoutFormat>( formatTag ) );
entry.set_offset( static_cast<int64_t>( entryOffset ) );
outVertexLayout.push_back( std::move( entry ) );
}
uint8_t hasUniforms = 0;
if ( !ReadU8( data, size, offset, hasUniforms ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_uniforms" );
}
if ( hasUniforms )
{
uint32_t uniformCount = 0;
if ( !ReadU32( data, size, offset, uniformCount ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform_count" );
}
std::map<std::string, sgns::RenderShaderUniform> uniformMap;
for ( uint32_t i = 0; i < uniformCount; ++i )
{
std::string name;
if ( !ReadString( data, size, offset, name ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform name" );
}
sgns::RenderShaderUniform uniform;
uint8_t hasSource = 0;
if ( !ReadU8( data, size, offset, hasSource ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform has_source" );
}
if ( hasSource )
{
std::string source;
if ( !ReadString( data, size, offset, source ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform source" );
}
uniform.set_source( source );
}
uint8_t hasType = 0;
if ( !ReadU8( data, size, offset, hasType ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform has_type" );
}
if ( hasType )
{
uint32_t typeTag = 0;
if ( !ReadU32( data, size, offset, typeTag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform type_tag" );
}
uniform.set_type( static_cast<sgns::DataType>( typeTag ) );
}
std::string valueJson;
if ( !ReadString( data, size, offset, valueJson ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading uniform value" );
}
if ( !valueJson.empty() )
{
try
{
uniform.set_value( nlohmann::json::parse( valueJson ) );
}
catch ( const std::exception &e )
{
return fail( std::string( "ParseRenderPassConfig: malformed uniform value JSON: " ) +
e.what() );
}
}
uniformMap[name] = std::move( uniform );
}
outUniforms = std::move( uniformMap );
}
uint32_t vertexLen = 0;
if ( !ReadU32( data, size, offset, vertexLen ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex_len" );
}
if ( vertexLen > 0 )
{
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, vertexLen, bytes ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading vertex bytes" );
}
outVertexBytes.assign( bytes, bytes + vertexLen );
}
uint8_t hasIndex = 0;
if ( !ReadU8( data, size, offset, hasIndex ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_index" );
}
if ( hasIndex )
{
uint32_t indexTypeTag = 0;
if ( !ReadU32( data, size, offset, indexTypeTag ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading index_type_tag" );
}
uint32_t indexLen = 0;
if ( !ReadU32( data, size, offset, indexLen ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading index_len" );
}
if ( indexLen > 0 )
{
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, indexLen, bytes ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading index bytes" );
}
outIndexBytes.assign( bytes, bytes + indexLen );
}
outHasIndex = true;
outIndexType = static_cast<sgns::IndexType>( indexTypeTag );
}
else
{
outHasIndex = false;
}
uint32_t dataTransformCount = 0;
if ( !ReadU32( data, size, offset, dataTransformCount ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading data_transform_count" );
}
outDataTransformCount = dataTransformCount;
uint8_t hasTextureBuffer = 0;
if ( !ReadU8( data, size, offset, hasTextureBuffer ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading has_texture_buffer" );
}
if ( hasTextureBuffer )
{
uint32_t textureWidth = 0;
if ( !ReadU32( data, size, offset, textureWidth ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading texture_width" );
}
uint32_t textureHeight = 0;
if ( !ReadU32( data, size, offset, textureHeight ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading texture_height" );
}
uint32_t textureLen = 0;
if ( !ReadU32( data, size, offset, textureLen ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading texture_len" );
}
if ( textureLen > 0 )
{
const char *bytes = nullptr;
if ( !ReadBytes( data, size, offset, textureLen, bytes ) )
{
return fail( "ParseRenderPassConfig: truncated buffer reading texture bytes" );
}
outTextureBytes.assign( bytes, bytes + textureLen );
}
outHasTextureBuffer = true;
outTextureWidth = textureWidth;
outTextureHeight = textureHeight;
}
return true;
}
catch ( const std::exception &e )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
std::string( "ParseRenderPassConfig: exception while parsing: " ) + e.what() );
return false;
}
namespace
{
void AppendPadded16( std::vector<uint8_t> &bytes, const uint8_t *data, size_t size )
{
bytes.insert( bytes.end(), data, data + size );
size_t remainder = bytes.size() % 16;
if ( remainder != 0 )
{
bytes.resize( bytes.size() + ( 16 - remainder ), 0 );
}
}
bool PackUniformValue( sgns::DataType dataType, const nlohmann::json &value, std::vector<uint8_t> &out )
{
auto appendFloat = [&out]( double v )
{
float f = static_cast<float>( v );
const uint8_t *bytes = reinterpret_cast<const uint8_t *>( &f );
out.insert( out.end(), bytes, bytes + sizeof( float ) );
};
// Reads up to `count` numeric components from a JSON array (missing/
// absent entries default to 0.0) -- never throws on a short/malformed
// array; a value that isn't an array at all yields an all-zero vector.
auto readVec = []( const nlohmann::json &v, size_t componentCount ) -> std::vector<double>
{
std::vector<double> result( componentCount, 0.0 );
if ( v.is_array() )
{
for ( size_t i = 0; i < componentCount && i < v.size(); ++i )
{
if ( v[i].is_number() )
{
result[i] = v[i].get<double>();
}
}
}
return result;
};
try
{
switch ( dataType )
{
case sgns::DataType::FLOAT:
{
appendFloat( value.is_number() ? value.get<double>() : 0.0 );
return true;
}
case sgns::DataType::INT:
{
int32_t i = value.is_number() ? static_cast<int32_t>( value.get<int64_t>() ) : 0;
const uint8_t *bytes = reinterpret_cast<const uint8_t *>( &i );
out.insert( out.end(), bytes, bytes + sizeof( int32_t ) );
return true;
}
case sgns::DataType::BOOL:
{
int32_t b = ( value.is_boolean() && value.get<bool>() ) ? 1 : 0;
const uint8_t *bytes = reinterpret_cast<const uint8_t *>( &b );
out.insert( out.end(), bytes, bytes + sizeof( int32_t ) );
return true;
}
case sgns::DataType::VEC2:
{
for ( double d : readVec( value, 2 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::VEC3:
{
for ( double d : readVec( value, 3 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::VEC4:
{
for ( double d : readVec( value, 4 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::MAT2:
{
for ( double d : readVec( value, 4 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::MAT3:
{
for ( double d : readVec( value, 9 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::MAT4:
{
for ( double d : readVec( value, 16 ) )
{
appendFloat( d );
}
return true;
}
case sgns::DataType::STRING:
case sgns::DataType::TENSOR:
case sgns::DataType::TEXTURE1_D:
case sgns::DataType::TEXTURE2_D:
case sgns::DataType::TEXTURE3_D:
case sgns::DataType::TEXTURE_CUBE:
case sgns::DataType::BUFFER:
default:
return false;
}
}
catch ( const std::exception & )
{
return false;
}
}
}
bool RenderProcessor::ResolveUniforms(
const boost::optional<std::map<std::string, sgns::RenderShaderUniform>> &uniforms,
const std::vector<sgns::Parameter> *parameters,
ResolvedUniforms &outResolved,
ProcessingResult &errorOut )
{
outResolved.packedBytes.clear();
outResolved.pushConstant = true;
if ( !uniforms )
{
return true;
}
// std::map's natural key-sorted iteration order -- deterministic,
// satisfies DETV-01, matches SerializeRenderPassConfig()'s own
// iteration order (03-02-SUMMARY.md).
for ( const auto &entry : uniforms.value() )
{
const std::string &name = entry.first;
const sgns::RenderShaderUniform &uniform = entry.second;
nlohmann::json resolvedValue;
if ( uniform.get_source() )
{
const std::string &source = uniform.get_source().value();
static const std::string kParameterPrefix = "parameter:";
if ( source.rfind( kParameterPrefix, 0 ) != 0 )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ResolveUniforms: unsupported uniform source prefix for '" + name + "'" );
return false;
}
std::string paramName = source.substr( kParameterPrefix.size() );
const sgns::Parameter *found = nullptr;
if ( parameters )
{
for ( const auto ¶m : *parameters )
{
if ( param.get_name() == paramName )
{
found = ¶m;
break;
}
}
}
if ( !found )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ResolveUniforms: unresolvable parameter '" + paramName +
"' for uniform '" + name + "'" );
return false;
}
resolvedValue = found->get_parameter_default();
}
else
{
resolvedValue = uniform.get_value();
}
if ( !uniform.get_type() )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ResolveUniforms: uniform '" + name + "' has no declared DataType" );
return false;
}
std::vector<uint8_t> packed;
if ( !PackUniformValue( uniform.get_type().value(), resolvedValue, packed ) )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"ResolveUniforms: unsupported DataType for uniform '" + name + "'" );
return false;
}
AppendPadded16( outResolved.packedBytes, packed.data(), packed.size() );
}
outResolved.pushConstant = ( outResolved.packedBytes.size() <= 128 );
return true;
}
bool RenderProcessor::CheckFormatSupport( VkFormat format,
VkFormatFeatureFlagBits requiredFeature,
ProcessingResult &errorOut )
{
VkFormatProperties props{};
vkGetPhysicalDeviceFormatProperties( m_physicalDevice, format, &props );
if ( !( props.optimalTilingFeatures & requiredFeature ) )
{
errorOut = MakeError( ProcessingErrorStage::FORMAT_UNSUPPORTED,
"CheckFormatSupport: VkFormat " + std::to_string( static_cast<int>( format ) ) +
" does not support required feature " +
std::to_string( static_cast<uint32_t>( requiredFeature ) ) +
" for optimal tiling" );
return false;
}
return true;
}
namespace
{
bool FindMemoryTypeIndex( const VkPhysicalDeviceMemoryProperties &memProps,
uint32_t typeBits,
VkMemoryPropertyFlags properties,
uint32_t &outIndex )
{
for ( uint32_t i = 0; i < memProps.memoryTypeCount; ++i )
{
if ( ( typeBits & ( 1u << i ) ) &&
( memProps.memoryTypes[i].propertyFlags & properties ) == properties )
{
outIndex = i;
return true;
}
}
return false;
}
}
bool RenderProcessor::CreateBufferDedicated( VkDeviceSize size,
VkBufferUsageFlags usage,
VkMemoryPropertyFlags properties,
VkBuffer &outBuffer,
VkDeviceMemory &outMemory,
ProcessingResult &errorOut )
{
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer buffer = VK_NULL_HANDLE;
VkResult result = vkCreateBuffer( m_device, &bufferInfo, nullptr, &buffer );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"vkCreateBuffer failed: VkResult=" + std::to_string( result ) );
return false;
}
VkMemoryRequirements memRequirements{};
vkGetBufferMemoryRequirements( m_device, buffer, &memRequirements );
VkPhysicalDeviceMemoryProperties memProps{};
vkGetPhysicalDeviceMemoryProperties( m_physicalDevice, &memProps );
uint32_t memTypeIndex = 0;
if ( !FindMemoryTypeIndex( memProps, memRequirements.memoryTypeBits, properties, memTypeIndex ) )
{
vkDestroyBuffer( m_device, buffer, nullptr );
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"CreateBufferDedicated: no suitable memory type found" );
return false;
}
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = memTypeIndex;
VkDeviceMemory memory = VK_NULL_HANDLE;
result = vkAllocateMemory( m_device, &allocInfo, nullptr, &memory );
if ( result != VK_SUCCESS )
{
vkDestroyBuffer( m_device, buffer, nullptr );
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"CreateBufferDedicated: dedicated memory allocation failed: VkResult=" +
std::to_string( result ) );
return false;
}
result = vkBindBufferMemory( m_device, buffer, memory, 0 );
if ( result != VK_SUCCESS )
{
vkFreeMemory( m_device, memory, nullptr );
vkDestroyBuffer( m_device, buffer, nullptr );
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"vkBindBufferMemory failed: VkResult=" + std::to_string( result ) );
return false;
}
outBuffer = buffer;
outMemory = memory;
VkDevice device = m_device;
PushTeardown( [device, buffer, memory]() {
vkDestroyBuffer( device, buffer, nullptr );
vkFreeMemory( device, memory, nullptr );
} );
return true;
}
bool RenderProcessor::CreateImageDedicated( const VkImageCreateInfo &imageInfo,
VkMemoryPropertyFlags properties,
VkImage &outImage,
VkDeviceMemory &outMemory,
ProcessingResult &errorOut )
{
VkImage image = VK_NULL_HANDLE;
VkResult result = vkCreateImage( m_device, &imageInfo, nullptr, &image );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"vkCreateImage failed: VkResult=" + std::to_string( result ) );
return false;
}
VkMemoryRequirements memRequirements{};
vkGetImageMemoryRequirements( m_device, image, &memRequirements );
VkPhysicalDeviceMemoryProperties memProps{};
vkGetPhysicalDeviceMemoryProperties( m_physicalDevice, &memProps );
uint32_t memTypeIndex = 0;
if ( !FindMemoryTypeIndex( memProps, memRequirements.memoryTypeBits, properties, memTypeIndex ) )
{
vkDestroyImage( m_device, image, nullptr );
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"CreateImageDedicated: no suitable memory type found" );
return false;
}
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = memTypeIndex;
VkDeviceMemory memory = VK_NULL_HANDLE;
result = vkAllocateMemory( m_device, &allocInfo, nullptr, &memory );
if ( result != VK_SUCCESS )
{
vkDestroyImage( m_device, image, nullptr );
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"CreateImageDedicated: dedicated memory allocation failed: VkResult=" +
std::to_string( result ) );
return false;
}
result = vkBindImageMemory( m_device, image, memory, 0 );
if ( result != VK_SUCCESS )
{
vkFreeMemory( m_device, memory, nullptr );
vkDestroyImage( m_device, image, nullptr );
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"vkBindImageMemory failed: VkResult=" + std::to_string( result ) );
return false;
}
outImage = image;
outMemory = memory;
VkDevice device = m_device;
PushTeardown( [device, image, memory]() {
vkDestroyImage( device, image, nullptr );
vkFreeMemory( device, memory, nullptr );
} );
return true;
}
bool RenderProcessor::UploadTexture( const std::vector<uint8_t> &textureBytes,
uint32_t width,
uint32_t height,
sgns::TextureFilter filter,
ProcessingResult &errorOut )
{
// (a) Fail closed if the device can't sample this format -- mirrors
// BuildRenderPass's existing format-check-before-create discipline.
if ( !CheckFormatSupport( VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT, errorOut ) )
{
return false;
}
// (b) T-17-09: byte-count-vs-dimensions validation BEFORE any GPU
// resource is created -- a mismatched declared-vs-actual byte count must
// never reach vkCmdCopyBufferToImage.
const size_t expectedBytes = static_cast<size_t>( width ) * static_cast<size_t>( height ) * 4;
if ( textureBytes.size() != expectedBytes )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"UploadTexture: textureBytes.size() (" + std::to_string( textureBytes.size() ) +
") does not match width*height*4 (" + std::to_string( expectedBytes ) + ")" );
return false;
}
// (c) Staging buffer -- HOST_VISIBLE|HOST_COHERENT direct write, identical
// style to UploadBuffers()'s existing vertex-buffer write.
if ( !CreateBufferDedicated( textureBytes.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
m_textureStagingBuffer, m_textureStagingMemory, errorOut ) )
{
return false;
}
{
void *mapped = nullptr;
VkResult result = vkMapMemory( m_device, m_textureStagingMemory, 0, textureBytes.size(), 0, &mapped );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"UploadTexture: vkMapMemory (staging) failed: VkResult=" +
std::to_string( result ) );
return false;
}
std::memcpy( mapped, textureBytes.data(), textureBytes.size() );
vkUnmapMemory( m_device, m_textureStagingMemory ); // HOST_COHERENT -- no flush needed (D-20)
}
// (d) Device-local sampled VkImage via the existing CreateImageDedicated()
// -- identical dedicated-allocation shape to the existing color/depth
// render-target images.
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.extent = { width, height, 1 };
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if ( !CreateImageDedicated( imageInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, m_textureImage, m_textureMemory,
errorOut ) )
{
return false;
}
// (e) VkImageView -- mirrors m_colorView's existing creation call shape.
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = m_textureImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VkResult result = vkCreateImageView( m_device, &viewInfo, nullptr, &m_textureView );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"UploadTexture: vkCreateImageView failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkImageView view = m_textureView;
PushTeardown( [device, view]() { vkDestroyImageView( device, view, nullptr ); } );
}
// (f) VkSampler -- RESEARCH.md Pattern 3 part 4 exactly. VK_FILTER_NEAREST
// is used by default (Pitfall 1's recommendation -- bit-reproducible texel
// lookup); anisotropy/mipmapping are deliberately minimal (T-17-11,
// accepted -- avoids an extra cross-vendor divergence axis).
VkFilter vkFilter = ( filter == sgns::TextureFilter::LINEAR ) ? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = vkFilter;
samplerInfo.minFilter = vkFilter;
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
result = vkCreateSampler( m_device, &samplerInfo, nullptr, &m_textureSampler );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"UploadTexture: vkCreateSampler failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkSampler sampler = m_textureSampler;
PushTeardown( [device, sampler]() { vkDestroySampler( device, sampler, nullptr ); } );
}
// (h) Write the descriptor set's binding=1 entry -- mirrors
// UploadBuffers()'s existing binding=0 uniform-buffer vkUpdateDescriptorSets
// call shape exactly. m_descriptorSet must already exist (BuildPipeline()'s
// hasTexture=true path, called before this method per StartProcessing()'s
// sequencing).
if ( m_descriptorSet != VK_NULL_HANDLE )
{
VkDescriptorImageInfo imageDescInfo{};
imageDescInfo.sampler = m_textureSampler;
imageDescInfo.imageView = m_textureView;
imageDescInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = m_descriptorSet;
write.dstBinding = 1;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imageDescInfo;
vkUpdateDescriptorSets( m_device, 1, &write, 0, nullptr );
}
// (i)
m_hasTexture = true;
m_textureWidth = width;
m_textureHeight = height;
return true;
}
VkFormat RenderProcessor::ToVkFormat( sgns::ColorFormat fmt )
{
switch ( fmt )
{
case sgns::ColorFormat::RGBA8:
return VK_FORMAT_R8G8B8A8_UNORM;
case sgns::ColorFormat::RGB8:
return VK_FORMAT_R8G8B8_UNORM;
}
return VK_FORMAT_R8G8B8A8_UNORM;
}
VkFormat RenderProcessor::ToVkFormat( sgns::DepthFormat fmt )
{
switch ( fmt )
{
case sgns::DepthFormat::D32_SFLOAT:
return VK_FORMAT_D32_SFLOAT;
case sgns::DepthFormat::D24_UNORM_S8_UINT:
return VK_FORMAT_D24_UNORM_S8_UINT;
}
return VK_FORMAT_D32_SFLOAT;
}
uint32_t RenderProcessor::ColorFormatByteSize( sgns::ColorFormat fmt )
{
switch ( fmt )
{
case sgns::ColorFormat::RGBA8:
return 4;
case sgns::ColorFormat::RGB8:
return 3;
}
return 4;
}
bool RenderProcessor::BuildRenderPass( const sgns::RenderTarget &target, ProcessingResult &errorOut )
{
if ( target.get_width() < 1 || target.get_width() > static_cast<int64_t>( kMaxRenderDimension ) ||
target.get_height() < 1 || target.get_height() > static_cast<int64_t>( kMaxRenderDimension ) )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"BuildRenderPass: render_target width/height out of bounds" );
return false;
}
VkFormat colorFormat = ToVkFormat( target.get_color_format() );
VkFormat depthFormat = ToVkFormat( target.get_depth_format() );
if ( !CheckFormatSupport( colorFormat, VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT, errorOut ) )
{
return false;
}
if ( !CheckFormatSupport( depthFormat, VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT, errorOut ) )
{
return false;
}
VkAttachmentDescription colorAttachment{};
colorAttachment.format = colorFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
VkAttachmentDescription depthAttachment{};
depthAttachment.format = depthFormat;
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentDescription attachments[2] = { colorAttachment, depthAttachment };
VkAttachmentReference colorRef{};
colorRef.attachment = 0;
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference depthRef{};
depthRef.attachment = 1;
depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 2;
renderPassInfo.pAttachments = attachments;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkResult result = vkCreateRenderPass( m_device, &renderPassInfo, nullptr, &renderPass );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::RENDER_PASS_CREATION,
"vkCreateRenderPass failed: VkResult=" + std::to_string( result ) );
return false;
}
m_renderPass = renderPass;
m_renderWidth = static_cast<uint32_t>( target.get_width() );
m_renderHeight = static_cast<uint32_t>( target.get_height() );
VkDevice device = m_device;
PushTeardown( [device, renderPass]() { vkDestroyRenderPass( device, renderPass, nullptr ); } );
return true;
}
bool RenderProcessor::BuildFramebuffer( const sgns::RenderTarget &target, ProcessingResult &errorOut )
{
uint32_t width = static_cast<uint32_t>( target.get_width() );
uint32_t height = static_cast<uint32_t>( target.get_height() );
VkFormat colorFormat = ToVkFormat( target.get_color_format() );
VkFormat depthFormat = ToVkFormat( target.get_depth_format() );
VkImageCreateInfo colorImageInfo{};
colorImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
colorImageInfo.imageType = VK_IMAGE_TYPE_2D;
colorImageInfo.format = colorFormat;
colorImageInfo.extent = { width, height, 1 };
colorImageInfo.mipLevels = 1;
colorImageInfo.arrayLayers = 1;
colorImageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
colorImageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
colorImageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
colorImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
colorImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if ( !CreateImageDedicated( colorImageInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, m_colorImage, m_colorMemory,
errorOut ) )
{
return false;
}
VkImageCreateInfo depthImageInfo{};
depthImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
depthImageInfo.imageType = VK_IMAGE_TYPE_2D;
depthImageInfo.format = depthFormat;
depthImageInfo.extent = { width, height, 1 };
depthImageInfo.mipLevels = 1;
depthImageInfo.arrayLayers = 1;
depthImageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
depthImageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
depthImageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
depthImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
depthImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if ( !CreateImageDedicated( depthImageInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, m_depthImage, m_depthMemory,
errorOut ) )
{
return false;
}
VkImageViewCreateInfo colorViewInfo{};
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorViewInfo.image = m_colorImage;
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorViewInfo.format = colorFormat;
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
colorViewInfo.subresourceRange.baseMipLevel = 0;
colorViewInfo.subresourceRange.levelCount = 1;
colorViewInfo.subresourceRange.baseArrayLayer = 0;
colorViewInfo.subresourceRange.layerCount = 1;
VkResult result = vkCreateImageView( m_device, &colorViewInfo, nullptr, &m_colorView );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"vkCreateImageView (color) failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkImageView view = m_colorView;
PushTeardown( [device, view]() { vkDestroyImageView( device, view, nullptr ); } );
}
// D24_UNORM_S8_UINT has a stencil component the schema never exposes/uses;
// the image view's aspectMask must still include it when present, per
// Vulkan's depth-stencil-attachment image-view rules.
VkImageAspectFlags depthAspect = VK_IMAGE_ASPECT_DEPTH_BIT;
if ( target.get_depth_format() == sgns::DepthFormat::D24_UNORM_S8_UINT )
{
depthAspect |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
VkImageViewCreateInfo depthViewInfo{};
depthViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
depthViewInfo.image = m_depthImage;
depthViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
depthViewInfo.format = depthFormat;
depthViewInfo.subresourceRange.aspectMask = depthAspect;
depthViewInfo.subresourceRange.baseMipLevel = 0;
depthViewInfo.subresourceRange.levelCount = 1;
depthViewInfo.subresourceRange.baseArrayLayer = 0;
depthViewInfo.subresourceRange.layerCount = 1;
result = vkCreateImageView( m_device, &depthViewInfo, nullptr, &m_depthView );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"vkCreateImageView (depth) failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkImageView view = m_depthView;
PushTeardown( [device, view]() { vkDestroyImageView( device, view, nullptr ); } );
}
VkImageView attachments[2] = { m_colorView, m_depthView };
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = m_renderPass;
framebufferInfo.attachmentCount = 2;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = width;
framebufferInfo.height = height;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
result = vkCreateFramebuffer( m_device, &framebufferInfo, nullptr, &framebuffer );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::IMAGE_ALLOCATION,
"vkCreateFramebuffer failed: VkResult=" + std::to_string( result ) );
return false;
}
m_framebuffer = framebuffer;
VkDevice device = m_device;
PushTeardown( [device, framebuffer]() { vkDestroyFramebuffer( device, framebuffer, nullptr ); } );
return true;
}
VkFormat RenderProcessor::ToVkFormat( sgns::VertexLayoutFormat fmt )
{
switch ( fmt )
{
case sgns::VertexLayoutFormat::FLOAT32:
return VK_FORMAT_R32_SFLOAT;
case sgns::VertexLayoutFormat::FLOAT16:
return VK_FORMAT_R16_SFLOAT;
case sgns::VertexLayoutFormat::INT32:
return VK_FORMAT_R32_SINT;
}
return VK_FORMAT_R32_SFLOAT;
}
VkPrimitiveTopology RenderProcessor::ToVkTopology( sgns::Topology t )
{
switch ( t )
{
case sgns::Topology::TRIANGLE_LIST:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
case sgns::Topology::LINE_LIST:
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
case sgns::Topology::POINT_LIST:
return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
}
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
}
VkCullModeFlags RenderProcessor::ToVkCullMode( sgns::CullMode c )
{
switch ( c )
{
case sgns::CullMode::NONE:
return VK_CULL_MODE_NONE;
case sgns::CullMode::FRONT:
return VK_CULL_MODE_FRONT_BIT;
case sgns::CullMode::BACK:
return VK_CULL_MODE_BACK_BIT;
}
return VK_CULL_MODE_BACK_BIT;
}
VkFrontFace RenderProcessor::ToVkFrontFace( sgns::FrontFace f )
{
switch ( f )
{
case sgns::FrontFace::CCW:
return VK_FRONT_FACE_COUNTER_CLOCKWISE;
case sgns::FrontFace::CW:
return VK_FRONT_FACE_CLOCKWISE;
}
return VK_FRONT_FACE_COUNTER_CLOCKWISE;
}
VkBool32 RenderProcessor::ToVkBool( sgns::DepthTest d )
{
return ( d == sgns::DepthTest::ENABLED ) ? VK_TRUE : VK_FALSE;
}
VkBlendFactor RenderProcessor::ToVkBlendFactor( sgns::BlendFactor f )
{
switch ( f )
{
case sgns::BlendFactor::ONE:
return VK_BLEND_FACTOR_ONE;
case sgns::BlendFactor::ZERO:
return VK_BLEND_FACTOR_ZERO;
case sgns::BlendFactor::SRC_ALPHA:
return VK_BLEND_FACTOR_SRC_ALPHA;
case sgns::BlendFactor::ONE_MINUS_SRC_ALPHA:
return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
}
return VK_BLEND_FACTOR_ONE;
}
uint32_t RenderProcessor::VertexFormatByteSize( sgns::VertexLayoutFormat f )
{
switch ( f )
{
case sgns::VertexLayoutFormat::FLOAT32:
return 4;
case sgns::VertexLayoutFormat::INT32:
return 4;
case sgns::VertexLayoutFormat::FLOAT16:
return 2;
}
return 4;
}
bool RenderProcessor::BuildPipeline( const std::vector<ParsedStage> &stages,
const std::vector<sgns::VertexLayoutEntry> &vertexLayout,
const boost::optional<sgns::PipelineState> &pipelineState,
const ResolvedUniforms &uniforms,
bool hasTexture,
ProcessingResult &errorOut )
{
std::vector<VkPipelineShaderStageCreateInfo> shaderStages;
shaderStages.reserve( stages.size() );
for ( const auto &s : stages )
{
VkShaderModuleCreateInfo moduleInfo{};
moduleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
moduleInfo.codeSize = s.spirv.size() * sizeof( uint32_t );
moduleInfo.pCode = s.spirv.data();
VkShaderModule module = VK_NULL_HANDLE;
VkResult result = vkCreateShaderModule( m_device, &moduleInfo, nullptr, &module );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::SHADER_MODULE_CREATION,
"vkCreateShaderModule failed: VkResult=" + std::to_string( result ) );
return false;
}
VkDevice device = m_device;
PushTeardown( [device, module]() { vkDestroyShaderModule( device, module, nullptr ); } );
VkPipelineShaderStageCreateInfo stageInfo{};
stageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stageInfo.stage = ( s.stage == sgns::Stage::VERTEX ) ? VK_SHADER_STAGE_VERTEX_BIT
: VK_SHADER_STAGE_FRAGMENT_BIT;
stageInfo.module = module;
stageInfo.pName = s.entry_point.c_str();
shaderStages.push_back( stageInfo );
}
uint32_t stride = 0;
for ( const auto &entry : vertexLayout )
{
stride += VertexFormatByteSize( entry.get_format() );
}
VkVertexInputBindingDescription bindingDesc{};
bindingDesc.binding = 0;
bindingDesc.stride = stride;
bindingDesc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::vector<VkVertexInputAttributeDescription> attributeDescs;
attributeDescs.reserve( vertexLayout.size() );
for ( size_t i = 0; i < vertexLayout.size(); ++i )
{
VkVertexInputAttributeDescription attr{};
attr.location = static_cast<uint32_t>( i );
attr.binding = 0;
attr.format = ToVkFormat( vertexLayout[i].get_format() );
attr.offset = static_cast<uint32_t>( vertexLayout[i].get_offset() );
attributeDescs.push_back( attr );
}
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = vertexLayout.empty() ? 0 : 1;
vertexInputInfo.pVertexBindingDescriptions = vertexLayout.empty() ? nullptr : &bindingDesc;
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>( attributeDescs.size() );
vertexInputInfo.pVertexAttributeDescriptions = attributeDescs.empty() ? nullptr : attributeDescs.data();
sgns::Topology topology = sgns::Topology::TRIANGLE_LIST;
sgns::CullMode cullMode = sgns::CullMode::BACK;
sgns::FrontFace frontFace = sgns::FrontFace::CCW;
sgns::DepthTest depthTest = sgns::DepthTest::ENABLED;
if ( pipelineState )
{
if ( pipelineState->get_topology() )
{
topology = pipelineState->get_topology().value();
}
if ( pipelineState->get_cull_mode() )
{
cullMode = pipelineState->get_cull_mode().value();
}
if ( pipelineState->get_front_face() )
{
frontFace = pipelineState->get_front_face().value();
}
if ( pipelineState->get_depth_test() )
{
depthTest = pipelineState->get_depth_test().value();
}
}
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = ToVkTopology( topology );
inputAssembly.primitiveRestartEnable = VK_FALSE;
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.cullMode = ToVkCullMode( cullMode );
rasterizer.frontFace = ToVkFrontFace( frontFace );
rasterizer.lineWidth = 1.0f;
VkPipelineDepthStencilStateCreateInfo depthStencil{};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = ToVkBool( depthTest );
depthStencil.depthWriteEnable = depthStencil.depthTestEnable; // [ASSUMED] tied to depthTestEnable -- no
// separate schema field exists (RESEARCH.md A1)
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; // fixed per D-14, never schema-configurable
VkPipelineMultisampleStateCreateInfo multisample{};
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; // ALWAYS -- DETV-02, never configurable
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
bool blendEnable = pipelineState && pipelineState->get_blend_enable().value_or( false );
colorBlendAttachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE;
if ( blendEnable )
{
colorBlendAttachment.srcColorBlendFactor =
ToVkBlendFactor( pipelineState->get_blend_src_factor().value_or( sgns::BlendFactor::SRC_ALPHA ) );
colorBlendAttachment.dstColorBlendFactor = ToVkBlendFactor(
pipelineState->get_blend_dst_factor().value_or( sgns::BlendFactor::ONE_MINUS_SRC_ALPHA ) );
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
}
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
// Fixed (never a runtime-settable pipeline attribute, per D-22) viewport/
// scissor sized to BuildRenderPass()'s already-validated render target
// dimensions.
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>( m_renderWidth );
viewport.height = static_cast<float>( m_renderHeight );
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
VkRect2D scissor{};
scissor.offset = { 0, 0 };
scissor.extent = { m_renderWidth, m_renderHeight };
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
// D-29/D-30: fixed 128-byte push-constant threshold, all-or-nothing.
bool usePushConstant = uniforms.pushConstant && !uniforms.packedBytes.empty();
// Uniforms alone route through a descriptor set (binding=0) only when
// NOT using push constants. A texture (Phase 17 Wave 3, D-05) is an
// independent signal that ALSO requires a descriptor set (binding=1) --
// hasTexture || hasUniformDescriptor, not either alone.
bool hasUniformDescriptor = !uniforms.pushConstant && !uniforms.packedBytes.empty();
bool useDescriptorSet = hasTexture || hasUniformDescriptor;
VkPushConstantRange pushConstantRange{};
if ( usePushConstant )
{
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = static_cast<uint32_t>( uniforms.packedBytes.size() );
}
VkResult result = VK_SUCCESS;
if ( useDescriptorSet )
{
std::vector<VkDescriptorSetLayoutBinding> bindings;
if ( hasUniformDescriptor )
{
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
binding.descriptorCount = 1;
binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.push_back( binding );
}
if ( hasTexture )
{
VkDescriptorSetLayoutBinding samplerBinding{};
samplerBinding.binding = 1;
samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerBinding.descriptorCount = 1;
samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.push_back( samplerBinding );
}
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>( bindings.size() );
layoutInfo.pBindings = bindings.data();
result = vkCreateDescriptorSetLayout( m_device, &layoutInfo, nullptr, &m_descriptorSetLayout );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"vkCreateDescriptorSetLayout failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkDescriptorSetLayout layout = m_descriptorSetLayout;
PushTeardown( [device, layout]() { vkDestroyDescriptorSetLayout( device, layout, nullptr ); } );
}
std::vector<VkDescriptorPoolSize> poolSizes;
if ( hasUniformDescriptor )
{
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = 1;
poolSizes.push_back( poolSize );
}
if ( hasTexture )
{
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSize.descriptorCount = 1;
poolSizes.push_back( poolSize );
}
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>( poolSizes.size() );
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = 1; // matches D-22's per-job-only lifetime
result = vkCreateDescriptorPool( m_device, &poolInfo, nullptr, &m_descriptorPool );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"vkCreateDescriptorPool failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkDescriptorPool pool = m_descriptorPool;
PushTeardown( [device, pool]() { vkDestroyDescriptorPool( device, pool, nullptr ); } );
}
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = m_descriptorPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &m_descriptorSetLayout;
result = vkAllocateDescriptorSets( m_device, &allocInfo, &m_descriptorSet );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"vkAllocateDescriptorSets failed: VkResult=" + std::to_string( result ) );
return false;
}
// m_descriptorSet is freed automatically when m_descriptorPool is
// destroyed -- no separate PushTeardown needed for the set itself.
}
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
if ( usePushConstant )
{
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
}
if ( useDescriptorSet )
{
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &m_descriptorSetLayout;
}
// If uniforms.packedBytes is empty (no uniforms declared at all), neither
// branch above ran -- pipelineLayoutInfo keeps zero push-constant ranges
// and zero descriptor sets, exactly as required.
result = vkCreatePipelineLayout( m_device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"vkCreatePipelineLayout failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkPipelineLayout layout = m_pipelineLayout;
PushTeardown( [device, layout]() { vkDestroyPipelineLayout( device, layout, nullptr ); } );
}
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = static_cast<uint32_t>( shaderStages.size() );
pipelineInfo.pStages = shaderStages.data();
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisample;
pipelineInfo.pDepthStencilState = &depthStencil;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.layout = m_pipelineLayout;
pipelineInfo.renderPass = m_renderPass;
pipelineInfo.subpass = 0;
result = vkCreateGraphicsPipelines( m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_pipeline );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::PIPELINE_CREATION,
"vkCreateGraphicsPipelines failed: VkResult=" + std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkPipeline pipeline = m_pipeline;
PushTeardown( [device, pipeline]() { vkDestroyPipeline( device, pipeline, nullptr ); } );
}
return true;
}
bool RenderProcessor::UploadBuffers( const std::vector<uint8_t> &vertexBytes,
bool hasIndex,
sgns::IndexType indexType,
const std::vector<uint8_t> &indexBytes,
uint32_t stride,
const ResolvedUniforms &uniforms,
ProcessingResult &errorOut )
{
// Validated BEFORE any buffer is created / any vkCmdBindVertexBuffers or
// vkCmdDrawIndexed is ever recorded -- closes T-03-03-02 (out-of-bounds GPU
// buffer read from a byte length that doesn't match the pipeline's implied
// stride/index count).
if ( stride == 0 || vertexBytes.size() % stride != 0 )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"UploadBuffers: vertex buffer byte length (" +
std::to_string( vertexBytes.size() ) +
") is not an exact multiple of the pipeline's computed stride (" +
std::to_string( stride ) + ")" );
return false;
}
m_vertexCount = static_cast<uint32_t>( vertexBytes.size() / stride );
m_hasIndexBuffer = hasIndex;
m_indexType = indexType;
m_indexCount = 0;
if ( hasIndex )
{
size_t indexElemSize = ( indexType == sgns::IndexType::UINT16 ) ? sizeof( uint16_t ) : sizeof( uint32_t );
if ( indexBytes.size() % indexElemSize != 0 )
{
errorOut = MakeError( ProcessingErrorStage::RESOURCE_RESOLUTION,
"UploadBuffers: index buffer byte length (" +
std::to_string( indexBytes.size() ) +
") is not an exact multiple of the index type's byte size (" +
std::to_string( indexElemSize ) + ")" );
return false;
}
m_indexCount = static_cast<uint32_t>( indexBytes.size() / indexElemSize );
}
// Vertex buffer -- HOST_VISIBLE|HOST_COHERENT direct write (D-20/D-21), no
// staging+device-local path.
if ( !CreateBufferDedicated( vertexBytes.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
m_vertexBuffer, m_vertexMemory, errorOut ) )
{
return false;
}
{
void *mapped = nullptr;
VkResult result = vkMapMemory( m_device, m_vertexMemory, 0, vertexBytes.size(), 0, &mapped );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"UploadBuffers: vkMapMemory (vertex) failed: VkResult=" +
std::to_string( result ) );
return false;
}
std::memcpy( mapped, vertexBytes.data(), vertexBytes.size() );
vkUnmapMemory( m_device, m_vertexMemory ); // HOST_COHERENT -- no flush needed (D-20)
}
if ( hasIndex )
{
if ( !CreateBufferDedicated( indexBytes.size(), VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
m_indexBuffer, m_indexMemory, errorOut ) )
{
return false;
}
void *mapped = nullptr;
VkResult result = vkMapMemory( m_device, m_indexMemory, 0, indexBytes.size(), 0, &mapped );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"UploadBuffers: vkMapMemory (index) failed: VkResult=" +
std::to_string( result ) );
return false;
}
std::memcpy( mapped, indexBytes.data(), indexBytes.size() );
vkUnmapMemory( m_device, m_indexMemory );
}
m_usePushConstant = uniforms.pushConstant && !uniforms.packedBytes.empty();
m_pushConstantBytes = m_usePushConstant ? uniforms.packedBytes : std::vector<uint8_t>();
// Descriptor-set path only -- the push-constant path needs no VkBuffer at
// all (bytes copied directly from m_pushConstantBytes at record time).
if ( !uniforms.packedBytes.empty() && !uniforms.pushConstant )
{
if ( !CreateBufferDedicated( uniforms.packedBytes.size(), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
m_uniformBuffer, m_uniformMemory, errorOut ) )
{
return false;
}
void *mapped = nullptr;
VkResult result = vkMapMemory( m_device, m_uniformMemory, 0, uniforms.packedBytes.size(), 0, &mapped );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::BUFFER_ALLOCATION,
"UploadBuffers: vkMapMemory (uniform) failed: VkResult=" +
std::to_string( result ) );
return false;
}
std::memcpy( mapped, uniforms.packedBytes.data(), uniforms.packedBytes.size() );
vkUnmapMemory( m_device, m_uniformMemory );
if ( m_descriptorSet != VK_NULL_HANDLE )
{
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = m_uniformBuffer;
bufferInfo.offset = 0;
bufferInfo.range = uniforms.packedBytes.size();
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = m_descriptorSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
write.pBufferInfo = &bufferInfo;
vkUpdateDescriptorSets( m_device, 1, &write, 0, nullptr );
}
}
return true;
}
bool RenderProcessor::RecordAndSubmit( const sgns::RenderTarget &target, ProcessingResult &errorOut )
{
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
poolInfo.queueFamilyIndex = m_queueFamilyIndex;
VkResult result = vkCreateCommandPool( m_device, &poolInfo, nullptr, &m_commandPool );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkCreateCommandPool failed: VkResult=" +
std::to_string( result ) );
return false;
}
{
VkDevice device = m_device;
VkCommandPool pool = m_commandPool;
// Pool destruction frees m_commandBuffer too -- no separate teardown entry.
PushTeardown( [device, pool]() { vkDestroyCommandPool( device, pool, nullptr ); } );
}
VkCommandBufferAllocateInfo cbAllocInfo{};
cbAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cbAllocInfo.commandPool = m_commandPool;
cbAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbAllocInfo.commandBufferCount = 1;
result = vkAllocateCommandBuffers( m_device, &cbAllocInfo, &m_commandBuffer );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkAllocateCommandBuffers failed: VkResult=" +
std::to_string( result ) );
return false;
}
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
result = vkBeginCommandBuffer( m_commandBuffer, &beginInfo );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkBeginCommandBuffer failed: VkResult=" +
std::to_string( result ) );
return false;
}
// Phase 17 Wave 3 (D-05): texture upload barrier/copy/barrier sequence,
// recorded into this SAME command buffer/submission before the render
// pass begins -- no second command buffer/vkQueueSubmit is introduced.
if ( m_hasTexture )
{
VkImageMemoryBarrier toTransferDst{};
toTransferDst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toTransferDst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.image = m_textureImage;
toTransferDst.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toTransferDst.subresourceRange.baseMipLevel = 0;
toTransferDst.subresourceRange.levelCount = 1;
toTransferDst.subresourceRange.baseArrayLayer = 0;
toTransferDst.subresourceRange.layerCount = 1;
toTransferDst.srcAccessMask = 0;
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier( m_commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &toTransferDst );
VkBufferImageCopy textureRegion{};
textureRegion.bufferOffset = 0;
textureRegion.bufferRowLength = 0;
textureRegion.bufferImageHeight = 0;
textureRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
textureRegion.imageSubresource.mipLevel = 0;
textureRegion.imageSubresource.baseArrayLayer = 0;
textureRegion.imageSubresource.layerCount = 1;
textureRegion.imageOffset = { 0, 0, 0 };
textureRegion.imageExtent = { m_textureWidth, m_textureHeight, 1 };
vkCmdCopyBufferToImage( m_commandBuffer, m_textureStagingBuffer, m_textureImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &textureRegion );
VkImageMemoryBarrier toShaderRead{};
toShaderRead.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
toShaderRead.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toShaderRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toShaderRead.image = m_textureImage;
toShaderRead.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
toShaderRead.subresourceRange.baseMipLevel = 0;
toShaderRead.subresourceRange.levelCount = 1;
toShaderRead.subresourceRange.baseArrayLayer = 0;
toShaderRead.subresourceRange.layerCount = 1;
toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier( m_commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead );
}
VkClearValue clearValues[2]{};
const auto &clearColor = target.get_clear_color();
for ( size_t i = 0; i < 4 && i < clearColor.size(); ++i )
{
clearValues[0].color.float32[i] = static_cast<float>( clearColor[i] );
}
clearValues[1].depthStencil.depth = static_cast<float>( target.get_clear_depth() );
clearValues[1].depthStencil.stencil = 0;
VkRenderPassBeginInfo rpBeginInfo{};
rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpBeginInfo.renderPass = m_renderPass;
rpBeginInfo.framebuffer = m_framebuffer;
rpBeginInfo.renderArea.offset = { 0, 0 };
rpBeginInfo.renderArea.extent = { m_renderWidth, m_renderHeight };
rpBeginInfo.clearValueCount = 2;
rpBeginInfo.pClearValues = clearValues;
vkCmdBeginRenderPass( m_commandBuffer, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE );
vkCmdBindPipeline( m_commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline );
VkDeviceSize vbOffset = 0;
vkCmdBindVertexBuffers( m_commandBuffer, 0, 1, &m_vertexBuffer, &vbOffset );
if ( m_hasIndexBuffer )
{
vkCmdBindIndexBuffer( m_commandBuffer, m_indexBuffer, 0,
( m_indexType == sgns::IndexType::UINT16 ) ? VK_INDEX_TYPE_UINT16
: VK_INDEX_TYPE_UINT32 );
}
if ( m_usePushConstant && !m_pushConstantBytes.empty() )
{
vkCmdPushConstants( m_commandBuffer, m_pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
static_cast<uint32_t>( m_pushConstantBytes.size() ), m_pushConstantBytes.data() );
}
else if ( m_descriptorSet != VK_NULL_HANDLE )
{
vkCmdBindDescriptorSets( m_commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1,
&m_descriptorSet, 0, nullptr );
}
if ( m_hasIndexBuffer )
{
vkCmdDrawIndexed( m_commandBuffer, m_indexCount, 1, 0, 0, 0 );
}
else
{
vkCmdDraw( m_commandBuffer, m_vertexCount, 1, 0, 0 );
}
vkCmdEndRenderPass( m_commandBuffer );
// Readback copy recorded INSIDE this same command buffer, immediately after
// vkCmdEndRenderPass and before vkEndCommandBuffer -- no second command
// buffer/submission (Pitfall 4). The render pass's color attachment
// finalLayout is already VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL (plan 03-04's
// BuildRenderPass), so no extra image-layout-transition barrier is needed
// here.
VkDeviceSize stagingSize = static_cast<VkDeviceSize>( target.get_width() ) *
static_cast<VkDeviceSize>( target.get_height() ) *
ColorFormatByteSize( target.get_color_format() );
if ( !CreateBufferDedicated( stagingSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
m_stagingBuffer, m_stagingMemory, errorOut ) )
{
return false;
}
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = { 0, 0, 0 };
region.imageExtent = { m_renderWidth, m_renderHeight, 1 };
vkCmdCopyImageToBuffer( m_commandBuffer, m_colorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_stagingBuffer,
1, ®ion );
result = vkEndCommandBuffer( m_commandBuffer );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkEndCommandBuffer failed: VkResult=" +
std::to_string( result ) );
return false;
}
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &m_commandBuffer;
result = vkQueueSubmit( m_queue, 1, &submitInfo, VK_NULL_HANDLE );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkQueueSubmit failed: VkResult=" + std::to_string( result ) );
return false;
}
// D-23: synchronous wait, RenderProcessor's own independent VkDevice -- this
// cannot stall a host application's separate VkDevice/queue.
result = vkDeviceWaitIdle( m_device );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::DRAW_SUBMISSION,
"RecordAndSubmit: vkDeviceWaitIdle failed: VkResult=" + std::to_string( result ) );
return false;
}
return true;
}
bool RenderProcessor::Readback( const sgns::RenderTarget &target, std::vector<uint8_t> &outBytes,
ProcessingResult &errorOut )
{
VkDeviceSize size = static_cast<VkDeviceSize>( target.get_width() ) *
static_cast<VkDeviceSize>( target.get_height() ) *
ColorFormatByteSize( target.get_color_format() );
void *mapped = nullptr;
VkResult result = vkMapMemory( m_device, m_stagingMemory, 0, size, 0, &mapped );
if ( result != VK_SUCCESS )
{
errorOut = MakeError( ProcessingErrorStage::READBACK,
"Readback: vkMapMemory failed: VkResult=" + std::to_string( result ) );
return false;
}
outBytes.resize( static_cast<size_t>( size ) );
std::memcpy( outBytes.data(), mapped, static_cast<size_t>( size ) );
vkUnmapMemory( m_device, m_stagingMemory ); // HOST_COHERENT -- no invalidate needed (D-20)
return true;
}
ProcessingResult RenderProcessor::StartProcessing(
std::vector<std::vector<uint8_t>> &chunkhashes,
const sgns::IoDeclaration &proc,
std::vector<char> &imageData,
std::vector<char> &modelFile,
const std::vector<sgns::Parameter> *parameters,
const ExecutionContext &execCtx )
{
(void)proc;
(void)chunkhashes;
// Extract pass_id for progress events
const std::string passId = proc.get_name();
if ( !InitializeContext() )
{
RunTeardown();
return MakeError( ProcessingErrorStage::CONTEXT_INIT_FAILED, "InitializeContext failed" );
}
ProcessingResult errorOut;
// (1) Invert plan 03-01's compiled-stage wire format.
std::vector<ParsedStage> stages;
if ( !ParseCompiledStages( modelFile, stages, errorOut ) )
{
RunTeardown();
return errorOut;
}
// COMPILE stage complete — fire progress and check cancel
if ( execCtx.progressCallback )
{
execCtx.progressCallback( ProgressEvent::ForRender( passId, RenderStage::COMPILE, 25.0f ) );
}
if ( execCtx.cancelToken.IsCancelled() )
{
RunTeardown();
return MakeError( ProcessingErrorStage::CANCELLED, "Render pass cancelled" );
}
// (2) ParseRenderPassConfig() is the ONLY source of RenderTarget/
// PipelineState/VertexLayoutEntry/uniforms/vertex-index bytes/
// dataTransformCount -- StartProcessing()'s own parameters never carry a
// Pass/RenderShaderConfig object.
sgns::RenderTarget renderTarget;
boost::optional<sgns::PipelineState> pipelineState;
std::vector<sgns::VertexLayoutEntry> vertexLayout;
boost::optional<std::map<std::string, sgns::RenderShaderUniform>> uniformsMap;
std::vector<uint8_t> vertexBytes;
bool hasIndex = false;
sgns::IndexType indexType = sgns::IndexType::UINT32;
std::vector<uint8_t> indexBytes;
uint32_t dataTransformCount = 0;
bool hasTextureBuffer = false;
uint32_t textureWidth = 0;
uint32_t textureHeight = 0;
std::vector<uint8_t> textureBytes;
if ( !ParseRenderPassConfig( imageData, renderTarget, pipelineState, vertexLayout, uniformsMap, vertexBytes,
hasIndex, indexType, indexBytes, dataTransformCount, hasTextureBuffer,
textureWidth, textureHeight, textureBytes, errorOut ) )
{
RunTeardown();
return errorOut;
}
// (3) Resolve literal/parameter:-sourced uniform values into packed bytes.
ResolvedUniforms resolvedUniforms;
if ( !ResolveUniforms( uniformsMap, parameters, resolvedUniforms, errorOut ) )
{
RunTeardown();
return errorOut;
}
const int maskBits = sgns::sgprocmanagerquant::ResolveByteQuantMode( parameters );
// (4)-(6): build the offscreen render pass/framebuffer/pipeline (plan 03-04).
if ( !BuildRenderPass( renderTarget, errorOut ) )
{
RunTeardown();
return errorOut;
}
if ( !BuildFramebuffer( renderTarget, errorOut ) )
{
RunTeardown();
return errorOut;
}
if ( !BuildPipeline( stages, vertexLayout, pipelineState, resolvedUniforms, hasTextureBuffer, errorOut ) )
{
RunTeardown();
return errorOut;
}
// BUILD_PIPELINE stage complete — fire progress and check cancel
if ( execCtx.progressCallback )
{
execCtx.progressCallback( ProgressEvent::ForRender( passId, RenderStage::BUILD_PIPELINE, 50.0f ) );
}
if ( execCtx.cancelToken.IsCancelled() )
{
RunTeardown();
return MakeError( ProcessingErrorStage::CANCELLED, "Render pass cancelled" );
}
// (7) Upload vertex/index/uniform buffers -- stride computed identically to
// BuildPipeline()'s own vertex-input stride (sum of VertexFormatByteSize()
// over vertexLayout), computed once and passed to both.
uint32_t stride = 0;
for ( const auto &entry : vertexLayout )
{
stride += VertexFormatByteSize( entry.get_format() );
}
if ( !UploadBuffers( vertexBytes, hasIndex, indexType, indexBytes, stride, resolvedUniforms, errorOut ) )
{
RunTeardown();
return errorOut;
}
// Phase 17 Wave 3 (D-05): upload the sampled texture, if the wire format
// declared one. The wire format (17-03) does not carry a per-texture
// filter mode across the SerializeRenderPassConfig/ParseRenderPassConfig
// boundary, so this always resolves to NEAREST -- Pitfall 1's recommended,
// bit-reproducible default for this phase's fixtures.
if ( hasTextureBuffer )
{
if ( !UploadTexture( textureBytes, textureWidth, textureHeight, sgns::TextureFilter::NEAREST, errorOut ) )
{
RunTeardown();
return errorOut;
}
}
// (8) RENDER-07: no data_transform executor exists anywhere in this codebase
if ( dataTransformCount > 0 )
{
RunTeardown();
return MakeError( ProcessingErrorStage::DATA_TRANSFORM_UNSUPPORTED,
"data_transform declared (" + std::to_string( dataTransformCount ) +
" entries) but no executor exists in this phase" );
}
// (9)-(10): record+submit the single command buffer (including the readback
// copy recorded inline) and map the staging buffer's bytes out.
if ( !RecordAndSubmit( renderTarget, errorOut ) )
{
RunTeardown();
return errorOut;
}
// DRAW stage complete — fire progress and check cancel
if ( execCtx.progressCallback )
{
execCtx.progressCallback( ProgressEvent::ForRender( passId, RenderStage::DRAW, 75.0f ) );
}
if ( execCtx.cancelToken.IsCancelled() )
{
RunTeardown();
return MakeError( ProcessingErrorStage::CANCELLED, "Render pass cancelled" );
}
std::vector<uint8_t> readbackBytes;
if ( !Readback( renderTarget, readbackBytes, errorOut ) )
{
RunTeardown();
return errorOut;
}
// READBACK stage complete — fire progress
if ( execCtx.progressCallback )
{
execCtx.progressCallback( ProgressEvent::ForRender( passId, RenderStage::READBACK, 100.0f ) );
}
// Output budget check (EXEC-03, D-03/D-08)
if ( execCtx.maxOutputArtifactBytes > 0 )
{
size_t outputSize = readbackBytes.size();
if ( outputSize > execCtx.maxOutputArtifactBytes )
{
RunTeardown();
return MakeError( ProcessingErrorStage::BUDGET_EXCEEDED,
"Output artifact size " + std::to_string( outputSize ) + " exceeds budget " + std::to_string( execCtx.maxOutputArtifactBytes ) );
}
}
// (11) Success: tear down every per-job Vulkan object (D-22/D-23) before
// populating the final ProcessingResult from the raw readback bytes.
RunTeardown();
// Phase 10 CAPT-02: quantize (no-op stub) then offer the pre-/post-quantize
// bytes to the opt-in capture callback before the single combined-hash call.
// readbackBytes is locally-owned (not foreign MNN tensor memory), so it is
// safe to mutate in place -- no copy-before-mutate constraint applies here.
std::vector<uint8_t> preQuantizeSnapshot;
if ( execCtx.rawOutputCapture )
{
preQuantizeSnapshot = readbackBytes;
}
sgns::sgprocmanagerquant::QuantizeByteBuffer( readbackBytes.data(), readbackBytes.size(), maskBits );
if ( execCtx.rawOutputCapture )
{
execCtx.rawOutputCapture( readbackBytes, preQuantizeSnapshot );
}
ProcessingResult result;
result.hash = sgns::sgprocmanagersha::sha256( readbackBytes.data(), readbackBytes.size() );
result.output_buffers =
std::make_shared<std::pair<std::vector<std::string>, std::vector<std::vector<char>>>>(
std::vector<std::string>{ std::string{} },
std::vector<std::vector<char>>{ std::vector<char>( readbackBytes.begin(), readbackBytes.end() ) } );
result.error = std::nullopt;
m_progress = 100.0f;
return result;
}
}
Updated on 2026-09-17 at 06:29:15 +0000