diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 001a86aa..61b81ca7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1391,7 +1391,7 @@ jobs: tail -5 "${RUNNER_TEMP}/dualblock-split.txt" echo "dual-block census run: rc=${rc}" - rows=$(grep -cvE '^\s*(#|$)' "${expected}") + rows=$(awk '!/^[[:space:]]*(#|$)/ { n++ } END { print n+0 }' "${expected}") if [ "${rc}" -eq 0 ] && [ "${rows}" -gt 0 ]; then echo "::error::the dual-block census lane went GREEN with ${rows} expected pair(s) outstanding - the knob stopped arming, or the fields retired and dualblock-expected-fatals.txt was not emptied in the same commit" exit 1 @@ -1407,42 +1407,17 @@ jobs: # ---- P5e (gl), CONTRACT-P5E §7: MAGMA'S OWN LANE, EXPECTED RED ----------------------- # - # The two DirectVulkan.Split.NamedBlit entries abort on GetFramebufferBindingSlot@Clear - # (VulkanRenderer.cpp:7648). That field retires in P5e ON ESPRYT and in P7 ON MAGMA, and - # FieldOwnership.def carries ONE retiring-phase string for both - so admitting the pair to - # keep them in the Espryt lane would forgive an Espryt Clear reading the frontend's binding - # slot again, which is exactly the regression fb's handle arm exists to prevent. Splitting - # the lane costs two entries; admitting the row would cost the gate. - # - # THE LABEL IS `integration-magma-split` AND THE SPELLING MATTERS: `ctest -L` is a REGEX, so - # a label named `integration-split-magma` is still matched by `-L integration-split` and the - # entries would never have left. Measured before the rename. - # - # IT IS ASSERTED RED, WITH ITS MARKER. "Magma is red" must not be allowed to decay into - # "Magma did not run": the step fails both when the run goes green (P7 landed early, or the - # entries stopped drawing) and when it goes red without the named abort in a private log. - - name: Integration-magma-split under strict - expected red until P7 (CONTRACT-P5E section 7) + # P5f fm: Magma's framebuffer, copy and mipmap verbs consume server records. + # The label remains separate so both backends' coverage stays explicit. + - name: Integration-magma-split under strict and dual-block state (P5f fm) working-directory: build-split env: MOBILEGL_ITEST_REQUIRE_GPU: "1" MOBILEGL_IPC_STRICT_ERRORS: "1" + MOBILEGL_IPC_ROLE_SPLIT_STATE: "1" MOBILEGL_TRANSPORT: "inproc" run: | - ulimit -c unlimited - rc=0 - ctest -L integration-magma-split --no-tests=error -j 2 > "${RUNNER_TEMP}/strict-magma.txt" 2>&1 || rc=$? - tail -5 "${RUNNER_TEMP}/strict-magma.txt" - if [ "${rc}" -eq 0 ]; then - echo "::error::integration-magma-split went GREEN under strict. Magma is in lockstep for the whole of P5e (ID-90) and GetFramebufferBindingSlot retires there in P7, so these entries are expected to abort. If P7 has landed, delete this step and fold the entries back into integration-split." - exit 1 - fi - marker='Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}' - if ! grep -rqF "${marker}" MobileGL/MG_IntegrationTest/p5b-DirectVulkan-*.log 2>/dev/null; then - echo "::error::integration-magma-split is red but no private log carries ${marker} - the lane failed for some OTHER reason, which is a defect rather than the expected-red control" - grep -rhoE '(Fatal|Admitted)\{[A-Za-z]*, "[^"]*"\}' MobileGL/MG_IntegrationTest/p5b-DirectVulkan-*.log 2>/dev/null | sort | uniq -c | sort -rn | head - exit 1 - fi - echo "integration-magma-split is red by design on ${marker}" + ctest -L integration-magma-split --no-tests=error -j 2 --output-on-failure # ---- P5e (ra2), ID-134 / ID-82: THE CLIENT-ARRAY LANE, EXPECTED RED UNDER RUN-AHEAD ---- # diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 03911879..41d3d554 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -20,6 +20,7 @@ #if MOBILEGL_BUILD_DISAGGREGATED #include #include +#include #endif #include #include @@ -712,6 +713,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + auto& state = MG_Pipe::MGPipeApplier(); + const auto handle = state.VerbStorageBlockProgram; + if (handle.Slot >= state.ShaderCsos.size() || !state.ShaderCsos[handle.Slot].Live || + state.ShaderCsos[handle.Slot].Gen != handle.Gen || !storageBlockName) { + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:storage-block-program-record\"}"); + std::abort(); + } + auto& record = state.ShaderCsos[handle.Slot]; + auto found = std::find_if(record.StorageOverrides.begin(), record.StorageOverrides.end(), + [&](const auto& entry) { return entry.Name == storageBlockName; }); + if (found == record.StorageOverrides.end()) + record.StorageOverrides.push_back({storageBlockName, static_cast(storageBlockBinding)}); + else found->Binding = static_cast(storageBlockBinding); + ++record.BindingsSerial; + return; + } +#endif auto* programObject = TryGetDirectVulkanProgram(program); if (!programObject || storageBlockName == nullptr) return; const Int maxBindings = diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h new file mode 100644 index 00000000..9b671cbd --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h @@ -0,0 +1,236 @@ +// MobileGL - server-owned program input for Magma (P5f fm). +#pragma once + +#include + +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#include "../DirectVulkanResourceState.h" +#include +#endif + +namespace MobileGL::MG_Backend::DirectVulkan { +#if MOBILEGL_BUILD_DISAGGREGATED + // A borrowed view, valid only while its frontend object or server record lives. + // It never constructs a ProgramObject on the server. Post-link mutable values + // come from the record tails, not from the archive's link-time defaults. + class MagmaProgramSource { + public: + using Program = MG_State::GLState::ProgramObject; + MagmaProgramSource(const Program& program) : m_frontend(&program) {} + MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record) + : m_handle(handle), m_record(&record) { + MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive"); + } + + Bool IsWire() const { return m_record != nullptr; } + MG_Pipe::MGPipeHandle Handle() const { return m_handle; } + const Program* Frontend() const { return m_frontend; } + Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); } + Uint64 GetLifetimeId() const { + return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId(); + } + Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); } + Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); } + Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); } + Bool GetSpirvValidationEnabled() const { + return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled(); + } + const Vector>& GetGeneratedSpirv() const { + return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv(); + } + Vector GetLinkedShaderStages() const { + if (!IsWire()) return m_frontend->GetLinkedShaderStages(); + Vector result; + for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast(stage)); + return result; + } + Bool HasLinkedShaderStage(ShaderStage stage) const { + if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage); + const auto& stages = m_record->Archive->LinkedStages; + return std::find(stages.begin(), stages.end(), static_cast(stage)) != stages.end(); + } + Bool GetBackendHashMemo(Uint flags, Uint64& hash) const { + // A view has no lifetime in which a memo can safely persist. The server + // factory's content cache still owns compiled programs; hash from current + // archive + binding tails rather than borrowing the client's mutable memo. + return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash); + } + void SetBackendHashMemo(Uint flags, Uint64 hash) const { + if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash); + } + Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); } + Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); } + Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); } + Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); } + + const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); } + Uint GetUBOSize() const { return IsWire() ? static_cast(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); } + Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); } + Int GetActiveUniformBlocksCount() const { + return IsWire() ? static_cast(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount(); + } + Uint GetUniformBlockIndex(const char* name) const { + if (!IsWire()) return m_frontend->GetUniformBlockIndex(name); + auto found = Link().uniformBlockIndexByName.find(name); + if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]"); + return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second; + } + Uint GetUniformBlockBinding(Uint index) const { + if (!IsWire()) return m_frontend->GetUniformBlockBinding(index); + return index < m_record->BlockBindings.size() ? static_cast(m_record->BlockBindings[index]) : 0; + } + const String& GetUniformBlockName(Uint index) const { + if (!IsWire()) return m_frontend->GetUniformBlockName(index); + const auto* block = Block(index); + static const String empty; + return block ? block->name : empty; + } + Uint GetUBOSizeAt(Uint index) const { + if (!IsWire()) return m_frontend->GetUBOSizeAt(index); + const auto* block = Block(index); + return block ? (static_cast(block->size) + 15u) & ~15u : 0; + } + Bool IsValidUniformLocation(Int location) const { + return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location); + } + Bool UniformLocationsAliasSameUniform(Int a, Int b) const { + if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b); + return IsValidUniformLocation(a) && IsValidUniformLocation(b) && + Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b]; + } + GLenum GetUniformType(Uint location) const { + if (!IsWire()) return m_frontend->GetUniformType(location); + return IsValidUniformLocation(static_cast(location)) + ? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0; + } + Int GetUniformSamplerOrImageUnitIndex(Uint location) const { + if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location); + const auto& units = m_record->SamplerUnits; + const auto found = std::lower_bound(units.begin(), units.end(), location, + [](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; }); + return found != units.end() && found->Location == location ? found->Unit : -1; + } + Int GetUniformLocation(const String& name) const { + if (!IsWire()) return m_frontend->GetUniformLocation(name); + const auto& locations = Link().uniformLocations; + auto found = locations.find(name); + if (found != locations.end()) return static_cast(found->second); + if (name.empty()) return -1; + found = locations.find(name + "[0]"); + if (found != locations.end()) return static_cast(found->second); + if (name.back() != ']' || name.size() < 4) return -1; + const SizeT bracket = name.rfind('['); + if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1; + Uint element = 0; + for (SizeT i = bracket + 1; i < name.size() - 1; ++i) { + if (name[i] < '0' || name[i] > '9') return -1; + element = element * 10 + static_cast(name[i] - '0'); + if (element > 0x0fffffffu) return -1; + } + found = locations.find(name.substr(0, bracket) + "[0]"); + if (found == locations.end()) found = locations.find(name.substr(0, bracket)); + if (found == locations.end()) return -1; + const Int base = static_cast(found->second); + if (!IsValidUniformLocation(base)) return -1; + const Int tIndex = Link().uniformIndexInTProgram[base]; + if (!Program::UniformAtIn(Link(), tIndex).type.isArray || + static_cast(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1; + const Int location = base + static_cast(element); + return UniformLocationsAliasSameUniform(base, location) ? location : -1; + } + + SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); } + const Vector& GetTransformFeedbackVaryings() const { + return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings(); + } + SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); } + Uint32 GetTransformFeedbackStride(Uint index) const { + if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index); + return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0; + } + GLenum GetTransformFeedbackBufferMode() const { + return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode(); + } + Uint GetShaderStorageBlockIndex(const String& name) const { + if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name); + EnsureStorageBlocks(); + String key = name; + auto find = [&] { + return std::find_if(m_storageBlocks.begin(), m_storageBlocks.end(), + [&](const StorageBlock& block) { return block.Name == key; }); + }; + auto found = find(); + if (found == m_storageBlocks.end() && !name.empty() && name.back() == ']') { + const auto bracket = name.rfind('['); + if (bracket != String::npos) { key = name.substr(0, bracket); found = find(); } + } + return found == m_storageBlocks.end() ? GL_INVALID_INDEX : static_cast(found - m_storageBlocks.begin()); + } + Uint GetShaderStorageBlockBinding(Uint index) const { + if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index); + EnsureStorageBlocks(); + if (index >= m_storageBlocks.size()) return 0; + const auto& block = m_storageBlocks[index]; + for (const auto& override : m_record->StorageOverrides) { + if (override.Name == block.Name) return override.Binding; + } + return block.Binding; + } + + private: + const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; } + const MG_State::GLState::BlockReflection* Block(Uint index) const { + if (index >= Link().glBlockIndexToTProgram.size()) return nullptr; + const Int tIndex = Link().glBlockIndexToTProgram[index]; + return tIndex >= 0 && static_cast(tIndex) < Link().blockReflection.size() + ? &Link().blockReflection[tIndex] : nullptr; + } + struct StorageBlock { String Name; Uint Binding; }; + void EnsureStorageBlocks() const { + if (m_storageBlocksReady) return; + m_storageBlocksReady = true; + // Match the legacy resource-query index order: stage order, then + // normalized descriptor name/binding, deduplicated across stages. + for (const auto& spirv : GetGeneratedSpirv()) { + if (spirv.empty()) continue; + SpvReflectShaderModule module{}; + if (spvReflectCreateShaderModule(spirv.size() * sizeof(Uint), spirv.data(), &module) != SPV_REFLECT_RESULT_SUCCESS) continue; + Uint32 count = 0; + if (spvReflectEnumerateDescriptorBindings(&module, &count, nullptr) == SPV_REFLECT_RESULT_SUCCESS) { + Vector bindings(count); + if (spvReflectEnumerateDescriptorBindings(&module, &count, bindings.data()) == SPV_REFLECT_RESULT_SUCCESS) { + auto name = [](const SpvReflectDescriptorBinding* binding) { + const char* raw = binding->name; + if (binding->type_description && binding->type_description->type_name) raw = binding->type_description->type_name; + String result = raw ? raw : ""; + const auto suffix = result.find("[0]"); + return suffix == String::npos ? result : result.substr(0, suffix); + }; + std::sort(bindings.begin(), bindings.end(), [&](const auto* a, const auto* b) { + const String an = a ? name(a) : String(), bn = b ? name(b) : String(); + return an != bn ? an < bn : (a && b && a->binding < b->binding); + }); + for (const auto* binding : bindings) { + if (!binding || binding->descriptor_type != SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER) continue; + const String key = name(binding); + if (key.empty() || std::any_of(m_storageBlocks.begin(), m_storageBlocks.end(), + [&](const StorageBlock& block) { return block.Name == key; })) continue; + m_storageBlocks.push_back({key, binding->binding}); + } + } + } + spvReflectDestroyShaderModule(&module); + } + } + const Program* m_frontend = nullptr; + MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle; + const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr; + mutable Bool m_storageBlocksReady = false; + mutable Vector m_storageBlocks; + }; +#else + using MagmaProgramSource = MG_State::GLState::ProgramObject; +#endif +} diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index a460d456..cde67a8f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1891,7 +1891,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool TransformSpirvForXfbCapture(const Vector& input, Vector& output, - const MG_State::GLState::ProgramObject& program) { + const MagmaProgramSource& program) { if (input.empty()) { output.clear(); return true; @@ -2466,7 +2466,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, + ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const { XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); // We expect shader stages in program object are sorted @@ -2832,7 +2832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // key and every element of an array of arrays seeds texture unit 0. Resolving those elements // would then paint silently-wrong pixels with no diagnostic at all - strictly worse than // declining. The decline goes away together with the seeding fix, not before it. - static Uint32 DescriptorCountForOpaqueUniformArray(const MG_State::GLState::ProgramObject& program, + static Uint32 DescriptorCountForOpaqueUniformArray(const MagmaProgramSource& program, const String& uniformName, Uint32 binding, Int baseLocation, Uint32 reflectedCount, Uint32 maxBindings, const char* kindLabel, Bool& outDeclined) { @@ -2868,7 +2868,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return count; } - void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program, + void ProgramFactory::ReflectLayout(const MagmaProgramSource& program, const Vector>& spirv, VkProgramObject& entry) const { // Initialize layout vectors entry.bindingKinds.assign(m_maxBindings, DescriptorBindingKind::None); @@ -3068,7 +3068,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.bindingKinds[binding] = descriptorKind; if (descriptorKind == DescriptorBindingKind::StorageBuffer) { - const GLuint blockIndex = GetShaderStorageBlockIndex(program, uniformName); + const GLuint blockIndex = +#if MOBILEGL_BUILD_DISAGGREGATED + program.GetShaderStorageBlockIndex(uniformName); +#else + GetShaderStorageBlockIndex(program, uniformName); +#endif if (blockIndex == GL_INVALID_INDEX) { MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u", uniformName.c_str(), binding); @@ -3426,7 +3431,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram( - const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) { + const MagmaProgramSource& program, CompileOptionFlags flags) { // Hashing the full SPIR-V of every stage is far too expensive to repeat per draw; // reuse the program's memoized hash while its backend state version is unchanged. // The memo keys on the flags word, which ComputeHash is no longer a pure function of: diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 9ac24faa..eb4d54ef 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -10,7 +10,7 @@ #include "../VkIncludes.h" #include "PipelineFactory.h" -#include "MG_State/GLState/ProgramState/ProgramObject.h" +#include "MagmaProgramSource.h" #include "MG_State/GLState/ProgramState/ShaderObject.h" #include "MG_State/GLState/TextureState/TextureEnum.h" @@ -471,9 +471,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { ~ProgramFactory(); ProgramFactory(const ProgramFactory&) = delete; - HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const; + HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const; const VkProgramObject& GetOrCreateProgram( - const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); + const MagmaProgramSource& program, CompileOptionFlags flags); // The default framebuffer's current image height, baked as a literal into every // FragCoordYFlip variant (there is no push-constant or specialization channel here, and @@ -600,7 +600,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ReflectFragmentOutputs(const Vector& stages, const Vector>& spirv, VkProgramObject& entry) const; - void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector>& spirv, + void ReflectLayout(const MagmaProgramSource& program, const Vector>& spirv, VkProgramObject& entry) const; // Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked // modules. Const and reflection-only: it decides nothing about the pipeline, it only diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 668c1264..a9014fa1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -29,6 +29,9 @@ #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/ShaderTranspiler/Types.h" #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif #include #include #include @@ -37,6 +40,162 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { +#if MOBILEGL_BUILD_DISAGGREGATED + [[noreturn]] static void WireDescriptorFatal(const char* detail) { + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); + std::abort(); + } + + [[noreturn]] static void WireBufferDescriptorDebt() { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} - Magma buffer descriptors remain P7"); + std::abort(); + } + + static VkImageViewType WireViewType(MG_Pipe::MGPipeResourceTarget target) { + using T = MG_Pipe::MGPipeResourceTarget; + switch (target) { + case T::Tex1D: return VK_IMAGE_VIEW_TYPE_1D; + case T::Tex1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case T::Tex3D: return VK_IMAGE_VIEW_TYPE_3D; + case T::TexCube: return VK_IMAGE_VIEW_TYPE_CUBE; + case T::TexCubeArray: return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + case T::Tex2DArray: case T::Tex2DMSArray: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + default: return VK_IMAGE_VIEW_TYPE_2D; + } + } + + static VkComponentSwizzle WireSwizzle(Uint8 value) { + switch (static_cast(value)) { + case TextureSwizzleParam::Red: return VK_COMPONENT_SWIZZLE_R; + case TextureSwizzleParam::Green: return VK_COMPONENT_SWIZZLE_G; + case TextureSwizzleParam::Blue: return VK_COMPONENT_SWIZZLE_B; + case TextureSwizzleParam::Alpha: return VK_COMPONENT_SWIZZLE_A; + case TextureSwizzleParam::Zero: return VK_COMPONENT_SWIZZLE_ZERO; + case TextureSwizzleParam::One: return VK_COMPONENT_SWIZZLE_ONE; + default: WireDescriptorFatal("sampler-swizzle"); + } + } + + Bool UniformManager::ResolveWireImageDescriptor( + VkCommandBuffer commandBuffer, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, + Bool storage, VkDescriptorImageInfo& out) const { + out = {}; + auto& state = MG_Pipe::MGPipeApplier(); + const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; + const Int location = baseLocation + static_cast(element); + if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) return false; + const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + if (unit < 0 || static_cast(unit) >= + (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; + const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; + // The old placeholder constructor enters the guarded client allocator. Keep that + // P7 boundary explicit instead of constructing a frontend texture on the server. + if (MG_Pipe::MGPipeHandleIsNull(handle)) WireDescriptorFatal("unbound-image-placeholder@P7"); + if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("image-record"); + const auto& record = state.TextureResources[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("image-record-generation"); + auto* resource = m_textureManager->SyncTextureResourceByHandle(handle); + if (!resource) WireDescriptorFatal("image-resource"); + m_textureManager->FlushPendingUploads(); + + Uint32 level = storage ? state.BoundShaderImages[unit].Level : record.Params.BaseLevel; + Uint32 layer = storage && !state.BoundShaderImages[unit].Layered ? state.BoundShaderImages[unit].Layer : 0; + const Uint32 localLevel = level; + const Uint32 localLayer = layer; + Uint32 layers = 0; + VkFormat aliasFormat = VK_FORMAT_UNDEFINED; + const auto root = m_textureManager->ResolveWireTextureStorage(handle, level, layer, &aliasFormat, &layers); + if (MG_Pipe::MGPipeHandleIsNull(root)) WireDescriptorFatal("image-view-window"); + const auto target = static_cast(record.Desc.Target); + VkImageViewType type = WireViewType(target); + if (storage && !state.BoundShaderImages[unit].Layered) { + layers = 1; + type = target == MG_Pipe::MGPipeResourceTarget::Tex1DArray ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + } + if (type == VK_IMAGE_VIEW_TYPE_3D) layers = 1; + Uint32 levels = 1; + if (!storage) { + if (record.Params.MaxLevel < localLevel || record.Desc.Levels <= localLevel) return false; + levels = std::min(record.Desc.Levels - localLevel, + record.Params.MaxLevel - localLevel + 1); + const auto viewHandle = state.BoundSamplerViews[unit].View; + if (!MG_Pipe::MGPipeHandleIsNull(viewHandle) && viewHandle.Slot < state.SamplerViewCsos.size()) { + const auto& view = state.SamplerViewCsos[viewHandle.Slot]; + if (!view.Live || view.Gen != viewHandle.Gen || view.View.NumLevels <= localLevel) + WireDescriptorFatal("sampler-view-record"); + levels = std::min(levels, view.View.NumLevels - localLevel); + } + levels = std::min(levels, resource->mipLevels - level); + } + VkFormat format = aliasFormat == VK_FORMAT_UNDEFINED ? resource->format : aliasFormat; + const auto domain = programObj.samplerNumericDomainByBinding[binding]; + if (storage) { + format = ResolveStorageImageViewFormat(programObj.storageImageFormatByBinding[binding], + state.BoundShaderImages[unit].InternalFormat, format, + programObj.storageImageUsesBindingFormatByBinding[binding]); + } else if (resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + format = VkTextureManager::ResolveSampledImageViewFormat(format, domain); + } + if (format == VK_FORMAT_UNDEFINED) return false; + VkImageAspectFlags aspect = resource->aspect; + if (!storage && (aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + aspect = record.Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil + ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + + // All wire image descriptors use GENERAL. A texture sampled and image-bound in + // the same shader then has one truthful layout, including views of the same owner. + if (!VkTextureManager::TransitionImageLayout(commandBuffer, resource->image, resource->layout, + VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource->aspect, 0, resource->mipLevels)) + WireDescriptorFatal("image-descriptor-transition"); + // A second descriptor/draw can keep GENERAL after a shader write. A layout + // equality fast return is not a memory dependency for that prior write. + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + 0, 1, &memory, 0, nullptr, 0, nullptr); + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = resource->image; + info.viewType = type; + info.format = format; + info.subresourceRange = {aspect, level, levels, layer, layers}; + if (!storage) { + const Bool alphaIsOne = ResolveTextureFormatInfo( + static_cast(record.Desc.InternalFormat)).expandRgbToRgba; + const auto swizzle = [&](Uint32 channel) { + const auto value = record.Params.Swizzle[channel]; + return alphaIsOne && static_cast(value) == TextureSwizzleParam::Alpha + ? VK_COMPONENT_SWIZZLE_ONE : WireSwizzle(value); + }; + info.components = {swizzle(0), swizzle(1), swizzle(2), swizzle(3)}; + } + VkImageView view = VK_NULL_HANDLE; + if (vkCreateImageView(m_device, &info, nullptr, &view) != VK_SUCCESS) return false; + m_frames[m_wireFrameIndex].wireImageViews.push_back(view); + out.imageView = view; + out.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + if (storage) { + if (state.BoundShaderImages[unit].Access != kMGPipeImageAccessReadOnly) + m_textureManager->MarkWireTextureGpuWritten(handle, localLevel, localLayer, layers); + } else { + auto samplerHandle = state.BoundSamplerStates[unit]; + if (MG_Pipe::MGPipeHandleIsNull(samplerHandle)) samplerHandle = record.Params.BuiltinSampler; + if (MG_Pipe::MGPipeHandleIsNull(samplerHandle) || samplerHandle.Slot >= state.SamplerCsos.size()) + WireDescriptorFatal("sampler-record"); + const auto& sampler = state.SamplerCsos[samplerHandle.Slot]; + if (!sampler.Live || sampler.Gen != samplerHandle.Gen) WireDescriptorFatal("sampler-record-generation"); + out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(sampler.Params, + static_cast(record.Desc.InternalFormat), + domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger, + levels); + if (!out.sampler) return false; + } + return true; + } +#endif namespace { constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u; // One id for every storage-image placeholder. They are never reachable through GL - no @@ -234,7 +393,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // per array element, so the element's location is the base plus its index - bounded by the // array's real extent so a descriptorCount that outran the reflection cannot walk onto the // next uniform. Element 0 is the ordinary non-array case and costs nothing extra. - static Int ResolveDescriptorElementLocation(const MG_State::GLState::ProgramObject& program, Int baseLocation, + static Int ResolveDescriptorElementLocation(const MagmaProgramSource& program, Int baseLocation, Uint32 element) { if (baseLocation < 0 || element == 0) { return baseLocation; @@ -252,7 +411,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { : 1u; } - static Int ResolveSamplerUnitIndex(const MG_State::GLState::ProgramObject& program, Int location, Uint32 binding) { + static Int ResolveSamplerUnitIndex(const MagmaProgramSource& program, Int location, Uint32 binding) { MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding); if (location < 0) { return 0; @@ -349,6 +508,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_unboundStorageImageTextures.clear(); for (auto& frame : m_frames) { if (m_device != VK_NULL_HANDLE) { +#if MOBILEGL_BUILD_DISAGGREGATED + for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr); + frame.wireImageViews.clear(); +#endif for (auto& view : frame.texelBufferViews) { if (view != VK_NULL_HANDLE) { vkDestroyBufferView(m_device, view, nullptr); @@ -390,6 +553,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { void UniformManager::BeginFrame(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_frames.size(), "UniformDescriptorBinder::BeginFrame invalid frame index"); auto& frame = m_frames[frameIndex]; +#if MOBILEGL_BUILD_DISAGGREGATED + m_wireFrameIndex = frameIndex; + for (const auto view : frame.wireImageViews) vkDestroyImageView(m_device, view, nullptr); + frame.wireImageViews.clear(); +#endif for (auto& view : frame.texelBufferViews) { if (view != VK_NULL_HANDLE) { vkDestroyBufferView(m_device, view, nullptr); @@ -467,11 +635,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo, Bool trustUnchangedHint) const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo); +#endif MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null"); MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null"); // The whole-descriptor memo below is keyed by binding alone, so it describes a binding @@ -753,7 +924,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::ProgramSamplesOnlySingleLevelTextures( - const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) { + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) return false; // Conservative optimization gate; no frontend probe. +#endif // A declined program never draws (see VkProgramObject::declinedDescriptors), and its // declined binding has no resolvable uniform location - so there is nothing to prove // about the textures it would have sampled. @@ -810,7 +984,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return sawSampler; } - Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveSamplerTexture(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, SharedPtr& outTexture) { outTexture.reset(); @@ -837,7 +1011,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw( - const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element) { MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null"); MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), @@ -864,10 +1038,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { return texture; } - Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { + const Int location = programObj.samplerUniformLocationByBinding[binding]; + const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + const auto& state = MG_Pipe::MGPipeApplier(); + if (unit < 0 || static_cast(unit) >= MG_Pipe::kMGPipeMaxTextureUnits) return false; + const auto handle = state.BoundSamplerViews[unit].Texture; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); + const auto& record = state.TextureResources[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation"); + if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt(); + } + outBufferView = AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, + programObj.samplerNumericDomainByBinding[binding], false); + return outBufferView != VK_NULL_HANDLE; + } +#endif outBufferView = VK_NULL_HANDLE; MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null"); MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range"); @@ -988,10 +1180,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { // binding at all for a uniform the shader still read, and lavapipe segfaulted inside pipeline // creation on the JIT worker thread. KHR-GL44.multi_bind.dispatch_bind_image_textures is the // case that carries it. - Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { + const Int location = programObj.samplerUniformLocationByBinding[binding]; + const Int unit = location < 0 ? 0 : program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + const auto& state = MG_Pipe::MGPipeApplier(); + if (unit < 0 || static_cast(unit) >= MG_Pipe::kMGPipeMaxImageUnits) return false; + const auto handle = state.BoundShaderImages[unit].Res; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("texel-buffer-record"); + const auto& record = state.TextureResources[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("texel-buffer-generation"); + if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.BufferForTexBuffer)) WireBufferDescriptorDebt(); + } + outBufferView = AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding], + programObj.samplerNumericDomainByBinding[binding], true); + return outBufferView != VK_NULL_HANDLE; + } +#endif outBufferView = VK_NULL_HANDLE; MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null"); @@ -1154,7 +1364,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveStorageBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const { @@ -1195,7 +1405,26 @@ namespace MobileGL::MG_Backend::DirectVulkan { const GLuint frontendBinding = isAtomicCounterBlock ? static_cast(atomicCounterBinding) - : GetShaderStorageBlockBinding(program, static_cast(blockIndex)) + element; + : +#if MOBILEGL_BUILD_DISAGGREGATED + (program.IsWire() ? program.GetShaderStorageBlockBinding(static_cast(blockIndex)) : + GetShaderStorageBlockBinding(*program.Frontend(), static_cast(blockIndex))) + element; +#else + GetShaderStorageBlockBinding(program, static_cast(blockIndex)) + element; +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { + const auto& state = MG_Pipe::MGPipeApplier(); + const Uint32 cls = isAtomicCounterBlock ? 2u : 1u; + if (frontendBinding < state.ShaderBufferCount[cls] && + !MG_Pipe::MGPipeHandleIsNull(state.BoundShaderBuffers[cls][frontendBinding].Res)) + WireBufferDescriptorDebt(); + const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); + if (!placeholder.IsValid()) return false; + outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; + return true; + } +#endif const Uint32 bindingPointCount = static_cast(MGB_CTX->GetBufferBindingPointCount(bufferTarget)); MOBILEGL_ASSERT(frontendBinding < bindingPointCount, @@ -1274,10 +1503,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo) const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo); +#endif outImageInfo = {}; MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null"); @@ -1657,7 +1889,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return texture; } - Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveSampledBinding(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, MG_State::GLState::ITextureObject*& outTexture, @@ -1726,10 +1958,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - Bool UniformManager::CollectSampledTextures(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::CollectSampledTextures(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures, Vector* outBindingRecords) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { outTextures.clear(); if (outBindingRecords) outBindingRecords->clear(); return true; } +#endif outTextures.clear(); if (outBindingRecords != nullptr) { outBindingRecords->clear(); @@ -1773,9 +2008,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - Bool UniformManager::SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::SampledBindingsUnchanged(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, const Vector& previousRecords) const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) return false; +#endif // A declined program takes the full path every time and is refused there. if (programObj.declinedDescriptors) { return false; @@ -1816,9 +2054,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::CollectStorageImageTextures( - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures) const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { outTextures.clear(); return true; } +#endif outTextures.clear(); MOBILEGL_ASSERT(MGB_CTX_LIVE, "CollectStorageImageTextures: GL context is null"); @@ -1898,9 +2139,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::CollectSamplerImageFeedback( - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outBindings) const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (program.IsWire()) { outBindings.clear(); return true; } +#endif outBindings.clear(); MOBILEGL_ASSERT(MGB_CTX_LIVE, "CollectSamplerImageFeedback: GL context is null"); @@ -1976,7 +2220,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveUniformBufferPayload(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const { const void* outData = nullptr; @@ -2002,6 +2246,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { return outData != nullptr && outSize > 0; } +#if MOBILEGL_BUILD_DISAGGREGATED + // Global uniforms above are record bytes; named UBOs retain P7's buffer boundary. + if (program.IsWire()) WireBufferDescriptorDebt(); +#endif + MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; @@ -2265,7 +2514,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { return VK_SUCCESS; } - Bool UniformManager::ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program, + Bool UniformManager::ResolveDynamicUboDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer, VkDeviceSize& outRange, @@ -2361,7 +2610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex, VkPipelineBindPoint bindPoint, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index d1b363ed..6ba7631d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -9,6 +9,7 @@ #pragma once #include "ProgramFactory.h" +#include "MagmaProgramSource.h" #include "VkBufferManager.h" #include "VkSamplerManager.h" #include "VkTextureManager.h" @@ -78,7 +79,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 textureLifetimeId = 0; Uint64 samplerLifetimeId = 0; }; - Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, + Bool CollectSampledTextures(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures, Vector* outBindingRecords = nullptr); @@ -87,14 +88,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a // redundant glBindSampler, which always bumps it) does not prove the sampled set // moved; this walk does, without rebuilding the set or falling off the fast path. - Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program, + Bool SampledBindingsUnchanged(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, const Vector& previousRecords) const; - Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, + Bool CollectStorageImageTextures(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures) const; Bool CollectSamplerImageFeedback( - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Vector& outBindings) const; static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, @@ -106,7 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // epochs AND per-resource layout values - so the per-binding cached // VkDescriptorImageInfo may be reused without re-running the resolve chain. Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, @@ -127,7 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads // only GL state, so a texture that ends up single-level for another reason (one uploaded // level under a wide level range) merely misses the rewrite. - static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program, + static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj); private: @@ -155,12 +156,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector descriptorPools; UnorderedMap descriptorSetCacheByLayout; Vector texelBufferViews; +#if MOBILEGL_BUILD_DISAGGREGATED + mutable Vector wireImageViews; +#endif Uint32 activeDescriptorPoolIndex = 0; Uint32 allocatedSetsThisFrame = 0; Uint32 peakAllocatedSetsThisFrame = 0; }; - static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program, + static Bool ResolveSamplerTexture(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, SharedPtr& outTexture); // Shared per-binding resolution for CollectSampledTextures and @@ -169,7 +173,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // exists), effective sampler = unit override else the texture's own sampler. // False = the binding is skipped (unbound with a non-2D fallback target). // `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor. - Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, + Bool ResolveSampledBinding(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, MG_State::GLState::ITextureObject*& outTexture, const MG_State::GLState::SamplerObject*& outSampler) const; @@ -177,7 +181,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // the bound texture stays alive through the draw via GL binding state, so callers that // only need the pointer skip the SharedPtr copy's atomic refcount churn. static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); // `numericDomain` is the sampler's class, and it matters only for the multisample arm - // see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it. @@ -224,13 +228,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof // obligations the caller carries). The cache is keyed by binding alone, so it is // used ONLY for single-descriptor bindings - see m_samplerResolveMemo. - Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, + Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo, Bool trustUnchangedHint = false) const; +#if MOBILEGL_BUILD_DISAGGREGATED + Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, + Uint32 element, Bool storage, VkDescriptorImageInfo& out) const; + Uint32 m_wireFrameIndex = 0; +#endif Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride, VkDescriptorImageInfo& outImageInfo) const; - Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView); // GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above, @@ -238,19 +248,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // made GPU-resident-writable because the shader may store to it. No `element` parameter: // an imageBuffer ARRAY is refused at program creation, so a binding is always one // descriptor (see the array gate in RemapDescriptorBindingsForVulkan). - Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView); // `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary // block. Each element resolves through its own GL storage block, and so its own GL // binding point, buffer and glBindBufferRange window. - Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program, + Bool ResolveStorageBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const; // `element` indexes an image ARRAY inside one binding; each element carries its own // independently assigned GL image unit. Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo) const; // Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident @@ -263,14 +273,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { const void* payload = nullptr; // fallback UploadTransient path VkDeviceSize payloadSize = 0; }; - Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, + Bool ResolveUniformBufferPayload(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const; // Shared resolution of one dynamic-UBO binding element into the // (buffer, range, dynamicOffset) triple the descriptor consumes: direct // bind, global-slice reuse, or transient upload. Used by the full walk // and by the dynamic-offset-only rebind (see FastRebindMemo). - Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program, + Bool ResolveDynamicUboDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer, VkDeviceSize& outRange, Uint32& outDynamicOffset); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index 0d93f094..07383710 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -15,7 +15,37 @@ namespace MobileGL::MG_Backend::DirectVulkan { namespace { +#if MOBILEGL_BUILD_DISAGGREGATED + // An accessor view over the wire's POD values, not a GL object with constructor, + // lifetime ID, allocator hooks or mutation callbacks. Both sources below use + // one sampler policy, including format-sensitive border-color conversion. + struct SamplerParametersSource { + const SamplerParameters& Params; + SamplerWrapMode GetWrapS() const { return Params.wrapS; } + SamplerWrapMode GetWrapT() const { return Params.wrapT; } + SamplerWrapMode GetWrapR() const { return Params.wrapR; } + SamplerFilterMode GetMinFilter() const { return Params.minFilter; } + SamplerFilterMode GetMagFilter() const { return Params.magFilter; } + SamplerMipmapMode GetMipmapMode() const { return Params.mipmapMode; } + Float GetMinLod() const { return Params.minLod; } + Float GetMaxLod() const { return Params.maxLod; } + Float GetLodBias() const { return Params.lodBias; } + Float GetMaxAnisotropy() const { return Params.maxAnisotropy; } + SamplerCompareMode GetCompareMode() const { return Params.compareMode; } + SamplerCompareFunc GetSamplerCompareFunc() const { return Params.compareFunc; } + const FloatVec4& GetBorderColor() const { return Params.borderColor; } + const IntVec4& GetBorderColorI() const { return Params.borderColorI; } + const UintVec4& GetBorderColorUI() const { return Params.borderColorUI; } + // Cache identity is entirely content-based; these fields are diagnostics. + Uint GetExternalIndex() const { return 0; } + Uint16 GetVersion() const { return 0; } + }; + + template + Bool UsesBorderColor(const SamplerSource& sampler) { +#else Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) { +#endif return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder || sampler.GetWrapT() == SamplerWrapMode::ClampToBorder || sampler.GetWrapR() == SamplerWrapMode::ClampToBorder; @@ -191,14 +221,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { return std::fabs(lhs - rhs) <= 1e-6f; } +#if MOBILEGL_BUILD_DISAGGREGATED + template + Float ResolveEffectiveMaxLod(const SamplerSource& sampler) { +#else Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) { +#endif if (sampler.GetMipmapMode() == SamplerMipmapMode::None) { return 0.0f; } return sampler.GetMaxLod(); } +#if MOBILEGL_BUILD_DISAGGREGATED + template + Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) { +#else Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) { +#endif return std::min(sampler.GetMinLod(), effectiveMaxLod); } @@ -209,7 +249,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // GL's non-mipmapped minification - large enough for lambda to stay positive, small enough // that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a // texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must. +#if MOBILEGL_BUILD_DISAGGREGATED + template + Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) { +#else Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) { +#endif const Float maxLod = ResolveEffectiveMaxLod(sampler); return singleLevelView ? std::min(maxLod, 0.25f) : maxLod; } @@ -230,7 +275,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + template + Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler, +#else Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, +#endif Bool forceNearestFiltering) const { if (!m_samplerAnisotropySupported) return 1.0f; if (forceNearestFiltering) return 1.0f; @@ -289,7 +339,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } +#if MOBILEGL_BUILD_DISAGGREGATED + template + Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler, +#else Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, +#endif Bool forceNearestFiltering, Bool singleLevelView, const ResolvedBorderColor& borderColor) const { MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); @@ -335,8 +390,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { return XXH64_digest(m_hashState); } +#if MOBILEGL_BUILD_DISAGGREGATED + template + VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler, + TextureInternalFormat format, +#else VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture, +#endif Bool forceNearestFiltering, Uint32 viewLevelCount) { // A view that exposes a single mip level has no second level to blend with, so GL's // *_MIPMAP_* minification filters degenerate to plain filtering on the base level - @@ -348,7 +409,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // the default-framebuffer blit shader had to work around with an explicit-LOD sample. const Bool singleLevelView = viewLevelCount == 1; // Resolved once and used for both the key and the create-info; see ResolvedBorderColor. +#if MOBILEGL_BUILD_DISAGGREGATED + const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format); +#else const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture); +#endif const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor); auto it = m_samplers.find(key); if (it != m_samplers.end()) { @@ -408,6 +473,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { return vkSampler; } +#if MOBILEGL_BUILD_DISAGGREGATED + VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, + const MG_State::GLState::ITextureObject& texture, + Bool forceNearestFiltering, Uint32 viewLevelCount) { + return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount); + } + + VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters, + TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) { + return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format, + forceNearestFiltering, viewLevelCount); + } +#endif + VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) { return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } @@ -463,8 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } +#if MOBILEGL_BUILD_DISAGGREGATED + template + VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor( + const SamplerSource& sampler, TextureInternalFormat format) const { +#else VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor( const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const { +#endif ResolvedBorderColor resolved{}; if (!UsesBorderColor(sampler)) { return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled @@ -472,7 +557,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Border colour is sampler state: a bound sampler object supplies its own, and a texture // with none reaches the very same value through the sampler object it owns. +#if !MOBILEGL_BUILD_DISAGGREGATED const auto format = texture.GetFormat(); +#endif const auto domain = ResolveBorderColorDomain(format); const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 && m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index 2c24aee0..199f326d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -46,6 +46,12 @@ public: const MG_State::GLState::ITextureObject& texture, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0); +#if MOBILEGL_BUILD_DISAGGREGATED + // The applier owns these values: effective bound/builtin sampler parameters and + // the sampled texture view's format/range. No frontend object is constructed. + VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters, + TextureInternalFormat format, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0); +#endif // Frame boundary hook: ages the sampler cache and destroys samplers not used // for many frames. The key hashes continuous float state (lodBias, LOD clamps, // anisotropy), so an app animating those would otherwise mint an unbounded @@ -86,19 +92,37 @@ private: Bool usesCustomBorderColor = false; }; +#if MOBILEGL_BUILD_DISAGGREGATED + template + VkSampler GetOrCreateSamplerImpl(const SamplerSource& sampler, TextureInternalFormat format, + Bool forceNearestFiltering, Uint32 viewLevelCount); + template + Uint64 BuildSamplerKey(const SamplerSource& sampler, Bool forceNearestFiltering, +#else Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering, +#endif Bool singleLevelView, const ResolvedBorderColor& borderColor) const; static VkFilter ToVkFilter(SamplerFilterMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkCompareOp ToVkCompareOp(SamplerCompareFunc func); +#if MOBILEGL_BUILD_DISAGGREGATED + template + ResolvedBorderColor ResolveBorderColor(const SamplerSource& sampler, TextureInternalFormat format) const; +#else ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const; +#endif // The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly // that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw. +#if MOBILEGL_BUILD_DISAGGREGATED + template + Float ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler, +#else Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, +#endif Bool forceNearestFiltering) const; VkDevice m_device = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 47413792..39f972ac 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -17,6 +17,12 @@ #include "MG_Util/Metrics/PipeStats.h" #include +#if MOBILEGL_BUILD_DISAGGREGATED +// P5f (fm): the handle-keyed texture arm's two sources - the applier's resource records +// (shape) and the server's staged-texture store (texels). +#include +#include +#endif #include #include #include @@ -281,7 +287,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandPool commandPool, VkQueue graphicsQueue, const VkTextureManager::TextureResource& oldResource, - VkTextureManager::TextureResource& newResource) { + VkTextureManager::TextureResource& newResource +#if MOBILEGL_BUILD_DISAGGREGATED + , Bool splitAspects = false +#endif + ) { MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: device is null"); MOBILEGL_ASSERT(commandPool != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: commandPool is null"); MOBILEGL_ASSERT(graphicsQueue != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: graphicsQueue is null"); @@ -339,6 +349,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { copy.extent.width = std::max(oldResource.extent.width >> level, 1u); copy.extent.height = std::max(oldResource.extent.height >> level, 1u); copy.extent.depth = std::max(oldResource.depth >> level, 1u); +#if MOBILEGL_BUILD_DISAGGREGATED + if (splitAspects) { + for (const VkImageAspectFlags aspect : {VK_IMAGE_ASPECT_COLOR_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, + VK_IMAGE_ASPECT_STENCIL_BIT}) { + if ((oldResource.aspect & aspect) == 0) continue; + copy.srcSubresource.aspectMask = aspect; + copy.dstSubresource.aspectMask = aspect; + copyRegions.push_back(copy); + } + } else +#endif copyRegions.push_back(copy); } @@ -661,6 +683,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { DestroyDeferredReleases(); ++m_resourceEraseEpoch; // every memoized resource pointer dies with the map m_textureResources.clear(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Destroy images while the device/allocator still exist, including after context death. + m_wireTextureResources.clear(); + m_wireRenderbufferResources.clear(); +#endif m_aliveObjects.clear(); m_storageImageTextures.clear(); @@ -1739,9 +1766,1021 @@ namespace MobileGL::MG_Backend::DirectVulkan { EraseTrackedTexture(identity); } prunedCount += orphanIdentities.size(); +#if MOBILEGL_BUILD_DISAGGREGATED + const auto pruneWire = [&](auto& resources, const auto& records) { + for (auto it = resources.begin(); it != resources.end();) { + const Uint32 slot = static_cast(it->first >> 32) & 0x7fffffffu; + const Uint32 generation = static_cast(it->first); + if (slot < records.size() && records[slot].Live && records[slot].Gen == generation) { + ++it; + continue; + } + DeferResourceRelease(Move(it->second)); + it = resources.erase(it); + ++m_resourceEraseEpoch; + ++prunedCount; + } + }; + const auto& applier = MG_Pipe::MGPipeApplier(); + pruneWire(m_wireTextureResources, applier.TextureResources); + pruneWire(m_wireRenderbufferResources, applier.RenderbufferResources); +#endif return prunedCount; } +#if MOBILEGL_BUILD_DISAGGREGATED + // --------------------------------------------------------------------------------- + // P5f (fm): THE HANDLE-KEYED TEXTURE ARM + // --------------------------------------------------------------------------------- + // + // SyncTexture reads the frontend ITextureObject: the target, the format, the level + // chain, the dirty scan and - through UploadDirtyMipLevels - the CLIENT's mip shadow + // texels. Under an active transport none of that memory is the server's to read + // (rule E), which is exactly what Magma's texture-legacy-arm scope has been waiving + // since P5c. This arm is the same sync re-sourced: the SHAPE comes from the applier's + // resource record (MGPipeApplier().TextureResources - the resource_create / + // resource_respecify descriptors), the TEXELS from the server's staged-texture store + // (P5c tx, adopted at apply time), and the WHICH-LEVELS from the record's + // pending-upload set, consumed entry by entry exactly where the upload lands + // (D-D5's rule: the set survives a bail, so a consumption is only ever written after + // the bytes were actually staged). + // + // What this arm deliberately does NOT have, and the loud answer each gets: + // * buffer textures (StorageKind == Buffer): the buffer twin is P7's - declined; + // * sampled/storage VIEW construction: clear/blit/readback/mipmap name images and + // subresources, never descriptors, so no VkImageView is built here at all; + // * the sampled-parameter sync (MGPTextureParams): nobody on those verbs samples. + // + // The map is keyed by StagedTextureStore::KeyForHandle - slot AND generation - so a + // recycled slot's new owner can never inherit its predecessor's image, and the sweep in + // PruneDeadTextures drops an entry whose record died. + + // TryResolveTextureShapeInfo's answers derived from the wire descriptor instead of the + // frontend object. The descriptor's Width/Height/Depth are the BASE level's GL extent and + // ArrayLayers the layer count the emitter already normalised (MGPipeTextureExtentOf: a 1D + // array's layers are already out of its height, a cube map reports 6). + static Bool ResolveWireTextureShapeInfo(const MG_Pipe::MGPResourceDesc& desc, TextureShapeInfo& outShape) { + using MG_Pipe::MGPipeResourceTarget; + outShape = {}; + switch (static_cast(desc.Target)) { + case MGPipeResourceTarget::Tex1D: + outShape.imageType = VK_IMAGE_TYPE_1D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_1D; + return desc.Width > 0; + case MGPipeResourceTarget::Tex2D: + case MGPipeResourceTarget::TexRect: + case MGPipeResourceTarget::Tex2DMS: + case MGPipeResourceTarget::Renderbuffer: + return desc.Width > 0 && desc.Height > 0; + case MGPipeResourceTarget::Tex3D: + outShape.imageType = VK_IMAGE_TYPE_3D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_3D; + outShape.depth = desc.Depth; + return desc.Width > 0 && desc.Height > 0 && desc.Depth > 0; + case MGPipeResourceTarget::Tex1DArray: + outShape.imageType = VK_IMAGE_TYPE_1D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.ArrayLayers > 0; + case MGPipeResourceTarget::Tex2DArray: + case MGPipeResourceTarget::Tex2DMSArray: + outShape.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.Height > 0 && desc.ArrayLayers > 0; + case MGPipeResourceTarget::TexCube: + outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + outShape.arrayLayers = 6; + return desc.Width > 0 && desc.Width == desc.Height; + case MGPipeResourceTarget::TexCubeArray: + // glTexStorage3D hands 6n through as the depth; the emitter normalised it into + // ArrayLayers. A non-whole cube count has no Vulkan shape - declined like every + // other unrepresentable target. + outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.Width == desc.Height && desc.ArrayLayers > 0 && + (desc.ArrayLayers % 6) == 0; + default: + // Buffer (a record routing fault on a texture path) and TexBuffer (P7's buffer + // twin) have no mipmapped image here. + return false; + } + } + + // WireTextureTargetOf: Espryt's BufferImpl::StagedTextureTargetForPipeTarget answered + // Magma-locally (the DirectGLES header is not this backend's to include). + static TextureTarget WireTextureTargetOfPipeTarget(Uint8 pipeTarget) { + using MG_Pipe::MGPipeResourceTarget; + switch (static_cast(pipeTarget)) { + case MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D; + case MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D; + case MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D; + case MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray; + case MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray; + case MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap; + case MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray; + case MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample; + case MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray; + case MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle; + case MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer; + default: return TextureTarget::Unknown; + } + } + + // The per-level byte transform UploadDirtyMipLevels runs between the shadow and the + // staging buffer, factored onto values so the wire arm can run it without the frontend + // object: RGB -> RGBA expansion, pure-depth word conversion (the image may be the + // D32_SFLOAT fallback for X8_D24), and the combined depth-stencil de-interleave. Returns + // false - the upload's decline - when the (format, layout) pair has no conversion here. + static Bool ConvertWireLevelBytes(const void* source, SizeT byteSize, const IntVec3& texelSize, + VkFormat imageFormat, VkImageAspectFlags aspect, + TextureInternalFormat internalFormat, + const TextureFormatInfo& formatInfo, Vector& outOwned, + const void*& outSource, SizeT& outByteSize) { + outSource = static_cast(source); + outByteSize = byteSize; + if (formatInfo.expandRgbToRgba) { + if (!ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo, outOwned)) { + return false; + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + return true; + } + const Bool isCombinedDepthStencil = + (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; + const SizeT texelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * + static_cast(std::max(texelSize.z(), 1)); + if (texelCount == 0) return false; + if (isCombinedDepthStencil) { + const Bool srcIsD24S8 = imageFormat == VK_FORMAT_D24_UNORM_S8_UINT; + const Bool srcIsD32FS8 = imageFormat == VK_FORMAT_D32_SFLOAT_S8_UINT; + if (!srcIsD24S8 && !srcIsD32FS8) return false; + const SizeT shadowTexelSize = byteSize / texelCount; + if (shadowTexelSize != 4 && shadowTexelSize != 8) return false; + outOwned.resize(texelCount * 4 + texelCount); + Uint8* depthPlane = outOwned.data(); + Uint8* stencilPlane = outOwned.data() + texelCount * 4; + const Uint8* shadow = static_cast(source); + for (SizeT t = 0; t < texelCount; ++t) { + if (shadowTexelSize == 8) { + // GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word with the + // stencil in its low 8 bits. + float depthValue; + Uint32 stencilWord; + std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue)); + std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord)); + if (srcIsD32FS8) { + std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue)); + } else { + const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f); + const Uint32 depthWord = static_cast(clamped * 16777215.0f + 0.5f); + std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord)); + } + stencilPlane[t] = static_cast(stencilWord & 0xFFu); + } else { + // GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8. + Uint32 packed; + std::memcpy(&packed, shadow + t * 4, sizeof(packed)); + if (srcIsD24S8) { + const Uint32 depthWord = packed >> 8; + std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord)); + } else { + const float depthValue = static_cast(packed >> 8) / 16777215.0f; + std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue)); + } + stencilPlane[t] = static_cast(packed & 0xFFu); + } + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + return true; + } + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) { + const Bool shadowIsFloat = internalFormat == TextureInternalFormat::DepthComponent32F; + const Bool dstIsFloat = imageFormat == VK_FORMAT_D32_SFLOAT; + const Bool dstIsD24Word = imageFormat == VK_FORMAT_X8_D24_UNORM_PACK32; + const SizeT shadowTexelSize = byteSize / texelCount; + const Bool needsConversion = + (dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat); + if (needsConversion) { + outOwned.resize(texelCount * 4); + const Uint8* shadow = static_cast(source); + for (SizeT t = 0; t < texelCount; ++t) { + Uint32 wide = 0; + if (shadowTexelSize == 2) { + Uint16 raw = 0; + std::memcpy(&raw, shadow + t * 2, sizeof(raw)); + wide = (static_cast(raw) << 16) | raw; + } else { + std::memcpy(&wide, shadow + t * 4, sizeof(wide)); + } + if (dstIsFloat) { + const float value = static_cast(static_cast(wide) / 4294967295.0); + std::memcpy(outOwned.data() + t * 4, &value, sizeof(value)); + } else { // X8_D24: depth in the low 24 bits of a 32-bit word + const Uint32 word = wide >> 8; + std::memcpy(outOwned.data() + t * 4, &word, sizeof(word)); + } + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + } + } + return true; + } + + // D-D5's consume, Magma-local: the level's entry leaves the applier's pending set ONLY + // here, where its bytes were actually staged. (Espryt's twin is Managers.cpp's + // ConsumePipeTextureUpload; the decode rule is the shared one.) + static void ConsumeWireTextureUpload(MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level) { + for (SizeT index = 0; index < record.PendingUploads.size(); ++index) { + const auto& pending = record.PendingUploads[index]; + if (MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget) != static_cast(uploadTarget) || + pending.Level != level) { + continue; + } + record.PendingUploads[index] = std::move(record.PendingUploads.back()); + record.PendingUploads.pop_back(); + return; + } + } + + Bool VkTextureManager::SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource) { + const MG_Pipe::MGPResourceDesc& desc = record.Desc; + if (desc.StorageKind != static_cast(TextureStorageType::Mipmap)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } + // ResolveWireTextureStorage has already followed ViewOf. Views share the storage + // owner's image and layout; allocating a second image would break aliasing. + if (!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) return false; + if (desc.Levels == 0) { + // A create before the first respecify: no storage, nothing to back. + return false; + } + TextureShapeInfo shapeInfo{}; + if (!ResolveWireTextureShapeInfo(desc, shapeInfo)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: no Vulkan shape for pipe target %u " + "(%ux%ux%u, %u layers); declined", + desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Target), desc.Width, + desc.Height, desc.Depth, static_cast(desc.ArrayLayers)); + return false; + } + const TextureInternalFormat internalFormat = static_cast(desc.InternalFormat); + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(internalFormat); + VkFormat format = formatInfo.format; + if (format == VK_FORMAT_UNDEFINED) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: no backing VkFormat for internal format 0x%x", + desc.Resource.Slot, desc.Resource.Gen, desc.InternalFormat); + return false; + } + // X8_D24 lacks optimal-tiling support on several drivers (lavapipe included); the same + // fallback SyncTextureResource applies, and the upload arm converts the shadow words. + if (format == VK_FORMAT_X8_D24_UNORM_PACK32) { + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + constexpr VkFormatFeatureFlags kDepthAttachmentAndSample = + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) { + format = VK_FORMAT_D32_SFLOAT; + } + } + const Bool isMultisample = + desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMS) || + desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMSArray) || + (desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Renderbuffer) && desc.Samples > 1); + VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT; + if (isMultisample) { + if (!TryResolveSampleCountFlagBits(static_cast(desc.Samples), resolvedSampleCount)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: unsupported sample count %u", + desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Samples)); + return false; + } + // glTexStorage*Multisample(samples = 1) is legal GL, but a one-sample image cannot + // back a sampler2DMS (VUID-RuntimeSpirv-samples-08726). + if (resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT) { + resolvedSampleCount = VK_SAMPLE_COUNT_2_BIT; + } + } + + const VkImageAspectFlags aspect = GetAspectMaskForFormat(format); + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + // The descriptor's sticky bind mask carries the image-unit hint, so the image is born + // with STORAGE usage and no re-mint ever pulls texels back (TextureEmit.h D-A3). + const Bool markedAsStorageImage = (desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0; + const Bool storageImageCapable = + !isMultisample && (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && + (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; + const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage; + VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags; + if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisample && + m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; + } + // A view can be created after its storage was first used. Give wire color storage + // its legal format-compatibility class from birth, so that view creation needs no + // frontend identity lookup and cannot silently use an immutable-format VkImage. + if (IsMutableStorageImageFormat(format) && + m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + // sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is off. + if (ResolveSrgbAttachmentWriteFormat(format, false) != format && + (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && + m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + + VkImageUsageFlags desiredUsage = + VK_IMAGE_USAGE_SAMPLED_BIT | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | + ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | + (((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) + ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT + : 0); + // Multisample attachments are also transfer sources for vkCmdResolveImage. + desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + // Round a multisample request up to a count the device supports for this format (GL + // only promises "at least"), the renderbuffer path's rule. + if (isMultisample && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) { + auto supportedIt = m_multisampleCountsByFormat.find(format); + if (supportedIt == m_multisampleCountsByFormat.end()) { + VkImageFormatProperties imageFormatProperties{}; + VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT; + if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType, + VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags, + &imageFormatProperties) == VK_SUCCESS) { + supported = imageFormatProperties.sampleCounts; + } + supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first; + } + const VkSampleCountFlags supported = supportedIt->second; + if ((supported & resolvedSampleCount) == 0) { + Uint32 rounded = 0; + for (Uint32 bit = static_cast(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; + bit <<= 1) { + if ((supported & bit) != 0) { + rounded = bit; + break; + } + } + if (rounded == 0) { + for (Uint32 bit = static_cast(resolvedSampleCount) >> 1; + bit > static_cast(VK_SAMPLE_COUNT_1_BIT); bit >>= 1) { + if ((supported & bit) != 0) { + rounded = bit; + break; + } + } + } + if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) { + MGLOG_W_ONCE("Magma wire texture: multisample format %d supports no count above one on this " + "device; backing it with a single sample", + static_cast(format)); + rounded = static_cast(VK_SAMPLE_COUNT_1_BIT); + } + if (rounded != 0) { + resolvedSampleCount = static_cast(rounded); + } + } + } + + // The ANGLE-shaped backing rule SyncTextureResource runs: a texture that has only ever + // defined level 0 gets a single-level backing; a second defined level recreates it ONCE + // with the full chain. The full-chain count bounds by the image's own extent (array + // layers are not a mip-able axis). + // The wire retains the GL base extent: a 1D-array's Height is its layer count. + const Uint32 imageHeight = shapeInfo.imageType == VK_IMAGE_TYPE_1D ? 1u : desc.Height; + const IntVec3 mipExtent{static_cast(desc.Width), static_cast(imageHeight), + static_cast(shapeInfo.depth)}; + const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent); + const Uint32 backingMipLevels = + isMultisample ? 1u + : (desc.Levels > 1 ? std::min(std::max(desc.Levels, fullMipLevels), fullMipLevels) + : 1u); + + const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && + resource.extent.width == desc.Width && resource.extent.height == imageHeight && + resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers && + resource.viewType == shapeInfo.viewType && + resource.sampleCount == resolvedSampleCount && + resource.imageCreateFlags == imageCreateFlags && + resource.usageFlags == desiredUsage && resource.mipLevels >= backingMipLevels; + if (compatible) { + resource.storageUsageResolved = markedAsStorageImage; + return true; + } + + // A named-level respecify changes ONLY that level; a BindMask update changes no + // storage at all. Preserve every compatible old level when growing/upgrading the + // allocation. A whole-store redefinition permits (but does not require) old texels. + const Bool preserve = resource.image != VK_NULL_HANDLE && resource.format == format && + resource.extent.width == desc.Width && resource.extent.height == imageHeight && + resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers && + resource.viewType == shapeInfo.viewType && + resource.sampleCount == VK_SAMPLE_COUNT_1_BIT && + resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && + resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; + TextureResource replacement; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.flags = imageCreateFlags; + imageInfo.imageType = shapeInfo.imageType; + imageInfo.extent.width = desc.Width; + imageInfo.extent.height = imageHeight; + imageInfo.extent.depth = shapeInfo.depth; + imageInfo.mipLevels = backingMipLevels; + imageInfo.arrayLayers = shapeInfo.arrayLayers; + imageInfo.format = format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = desiredUsage; + imageInfo.samples = resolvedSampleCount; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (isMultisample || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | + VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) { + VkImageFormatProperties imageFormatProperties{}; + VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags, + &imageFormatProperties); + if (imageFormatResult != VK_SUCCESS && !isMultisample && + (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { + // Losing reinterpreted views only degrades the formatless-image feature; + // failing creation would lose the texture entirely. + m_mutableFormatUnsupported.insert(format); + imageInfo.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + imageCreateFlags = imageInfo.flags; + imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, + imageInfo.flags, &imageFormatProperties); + } + if (imageFormatResult != VK_SUCCESS && !isMultisample && + (imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) { + m_2dArrayCompatibleUnsupported.insert(format); + imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; + imageCreateFlags = imageInfo.flags; + imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, + imageInfo.flags, &imageFormatProperties); + } + if (imageFormatResult != VK_SUCCESS || + (isMultisample && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) { + MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for texture {slot=%u, gen=%u}", + __func__, static_cast(imageInfo.flags), + static_cast(imageInfo.samples), desc.Resource.Slot, desc.Resource.Gen); + return false; + } + } + + VmaAllocationCreateInfo allocationInfo{}; + allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + // Soft failure, SyncTextureResource's rule: a driver can pass the pre-check yet refuse + // the creation; the texture stays unbacked and the caller declines. + const VkResult createImageResult = + vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &replacement.image, &replacement.allocation, nullptr); + if (createImageResult != VK_SUCCESS) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: vmaCreateImage failed (%d) extent=%ux%u depth=%u " + "layers=%u mips=%u samples=%d format=%d", + desc.Resource.Slot, desc.Resource.Gen, createImageResult, imageInfo.extent.width, + imageInfo.extent.height, imageInfo.extent.depth, imageInfo.arrayLayers, + imageInfo.mipLevels, static_cast(imageInfo.samples), + static_cast(imageInfo.format)); + return false; + } + ++m_textureImageEpoch; // a new attachment image invalidates cached render passes + + replacement.layout = VK_IMAGE_LAYOUT_UNDEFINED; + replacement.extent = {imageInfo.extent.width, imageInfo.extent.height}; + replacement.depth = imageInfo.extent.depth; + replacement.arrayLayers = imageInfo.arrayLayers; + replacement.mipLevels = backingMipLevels; + replacement.sampledBaseMipLevel = 0; + replacement.sampledLevelCount = backingMipLevels; + replacement.format = format; + replacement.aspect = aspect; + replacement.viewType = shapeInfo.viewType; + replacement.sampleCount = resolvedSampleCount; + replacement.imageCreateFlags = imageInfo.flags; + replacement.usageFlags = desiredUsage; + replacement.storageUsageResolved = markedAsStorageImage; + if (preserve) { + FlushPendingUploads(); + if (!PreserveTextureContentsOnRecreate(m_device, m_commandPool, m_graphicsQueue, + resource, replacement, true)) return false; + } + DeferResourceRelease(Move(resource)); + std::destroy_at(&resource); + std::construct_at(&resource, Move(replacement)); + return true; + } + + Bool VkTextureManager::UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, + MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource) { + if (record.PendingUploads.empty()) { + return true; + } + auto& store = MG_Remote::Server::ServerStagedTexture(); + // This arm is only ever entered under an active transport, where the store copies. + MOBILEGL_ASSERT(store.CopiesIntoServerStorage(), + "UploadPendingWireLevels under a non-copying staged store: the wire arm is transport-only"); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + const TextureInternalFormat internalFormat = static_cast(record.Desc.InternalFormat); + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(internalFormat); + const TextureTarget target = WireTextureTargetOfPipeTarget(record.Desc.Target); + + struct WireUploadItem { + Uint16 uploadTarget = 0; + Uint16 level = 0; + Uint32 baseArrayLayer = 0; + IntVec3 texelSize = {0, 0, 0}; + VkOffset3D imageOffset{}; + const void* source = nullptr; + SizeT byteSize = 0; + Vector owned; + VkDeviceSize offset = 0; + }; + Vector items; + VkDeviceSize stagingSize = 0; + Bool incomplete = false; + for (const auto& pending : record.PendingUploads) { + const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget); + const Uint16 level = pending.Level; + if (level >= resource.mipLevels) { + // A pending entry the current image cannot hold: the level was defined past the + // accepted descriptor's chain. Loud, and left in the set - the next respecify + // or generation grows into it or drops it. + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pending upload for level %u exceeds the " + "image's %u levels; left pending", + handle.Slot, handle.Gen, static_cast(level), resource.mipLevels); + incomplete = true; + continue; + } + const IntVec3 glExtent = store.LevelExtentOrUndefined(key, uploadTarget, level); + const SizeT byteSize = store.LevelByteSize(key, uploadTarget, level); + // The staged run IS the whole level shadow (StagedTextureStore.h's coverage rule): + // it exists because the applier accepted this entry, so a miss is the store's own + // Fatal{StageSnapshotTooNarrow}, not a silent zero. + const Uint8* bytes = store.RequireLevelBytes(key, uploadTarget, level, + "VkTextureManager::UploadPendingWireLevels"); + // Vulkan geometry, like the image this stages into: a 1D array's layers move out of + // the shadow's height into z. + const IntVec3 texelSize = ToVulkanLevelExtent(target, glExtent); + if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) { + incomplete = true; + continue; + } + WireUploadItem item{}; + item.uploadTarget = uploadTarget; + item.level = level; + item.baseArrayLayer = ResolveUploadArrayLayer(static_cast(uploadTarget)); + item.texelSize = texelSize; + if (!ConvertWireLevelBytes(bytes, byteSize, texelSize, resource.format, resource.aspect, + internalFormat, formatInfo, + item.owned, item.source, item.byteSize)) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: no shadow-to-image conversion for format %d; " + "level %u left pending", + handle.Slot, handle.Gen, static_cast(resource.format), + static_cast(level)); + incomplete = true; + continue; + } + // Adoption covers the complete CPU shadow, but only the emitted regions are + // authoritative after a GPU write. Uploading the whole run here overwrites + // untouched texels of a prior clear/blit/mipmap with stale client bytes. + const SizeT levelTexels = static_cast(texelSize.x()) * texelSize.y() * + std::max(texelSize.z(), 1); + const Bool combined = (resource.aspect & VK_IMAGE_ASPECT_DEPTH_BIT) && + (resource.aspect & VK_IMAGE_ASPECT_STENCIL_BIT); + if (vkuFormatIsCompressed(resource.format) || !levelTexels || + item.byteSize % levelTexels != 0 || item.byteSize / levelTexels == 0) { + incomplete = true; + continue; + } + const SizeT texelBytes = combined ? 4u : item.byteSize / levelTexels; + const Bool arrayLayers = resource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + const auto addRegion = [&](const MG_Pipe::MGPBox& glBox) { + IntVec3 offset{glBox.X, glBox.Y, glBox.Z}; + IntVec3 extent{static_cast(glBox.W), static_cast(glBox.H), + static_cast(glBox.D)}; + if (target == TextureTarget::Texture1DArray) { + offset = {glBox.X, 0, glBox.Y}; + extent = {static_cast(glBox.W), 1, static_cast(glBox.H)}; + } + for (Int axis = 0; axis < 3; ++axis) { + if (offset[axis] < 0 || extent[axis] <= 0 || + static_cast(offset[axis]) + static_cast(extent[axis]) > + static_cast(texelSize[axis])) { + incomplete = true; + return; + } + } + WireUploadItem region{}; + region.uploadTarget = uploadTarget; + region.level = level; + region.baseArrayLayer = item.baseArrayLayer + (arrayLayers ? offset.z() : 0); + region.texelSize = extent; + region.imageOffset = {offset.x(), offset.y(), arrayLayers ? 0 : offset.z()}; + const SizeT regionTexels = static_cast(extent.x()) * extent.y() * extent.z(); + region.owned.resize(regionTexels * (combined ? 5u : texelBytes)); + const auto* source = static_cast(item.source); + for (Int z = 0; z < extent.z(); ++z) { + for (Int y = 0; y < extent.y(); ++y) { + const SizeT sourceTexel = + (static_cast(offset.z() + z) * texelSize.y() + offset.y() + y) * + texelSize.x() + offset.x(); + const SizeT destinationTexel = + (static_cast(z) * extent.y() + y) * extent.x(); + std::memcpy(region.owned.data() + destinationTexel * texelBytes, + source + sourceTexel * texelBytes, extent.x() * texelBytes); + if (combined) { + std::memcpy(region.owned.data() + regionTexels * 4 + destinationTexel, + source + levelTexels * 4 + sourceTexel, extent.x()); + } + } + } + region.source = region.owned.data(); + region.byteSize = region.owned.size(); + stagingSize = (stagingSize + 15u) & ~VkDeviceSize{15u}; + region.offset = stagingSize; + stagingSize += static_cast(region.byteSize); + items.push_back(Move(region)); + }; + if (pending.Regions.empty()) { + addRegion(pending.UnionBox); + } else { + for (const auto& region : pending.Regions) + addRegion({region.X, region.Y, region.Z, region.W, region.H, region.D}); + } + } + if (incomplete) return false; + if (items.empty()) { + return true; + } + + // UploadDirtyMipLevels' batching rules, verbatim: flush an open batch that already + // writes this image and was touched by the open recording, and bound the staging bytes + // a batch pins. + if (m_uploadBatchOpen && WasTouchedThisRecording(resource) && + std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), resource.image) != + m_uploadBatchImages.end()) { + FlushPendingUploads(); + } + constexpr VkDeviceSize kMaxBatchStagingBytes = 64u * 1024u * 1024u; + if (m_uploadBatchOpen && m_uploadBatchStagingBytes + stagingSize > kMaxBatchStagingBytes) { + FlushPendingUploads(); + } + + VkCommandBuffer commandBuffer = EnsureUploadBatchOpen(); + VkBuffer stagingBuffer = VK_NULL_HANDLE; + VkDeviceSize stagingBase = 0; + Uint8* mapped = AcquireUploadStagingSpace(stagingSize, stagingBuffer, stagingBase); + for (const auto& item : items) { + std::memcpy(mapped + item.offset, item.source, item.byteSize); + } + + const VkImageAspectFlags aspectMask = resource.aspect; + VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags uploadSrcAccessMask = 0; + GetImageTransitionSourceState(resource.layout, uploadSrcStageMask, uploadSrcAccessMask); + Bool ok = TransitionImageLayout(commandBuffer, resource.image, resource.layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, uploadSrcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, uploadSrcAccessMask, + VK_ACCESS_TRANSFER_WRITE_BIT, aspectMask, 0, resource.mipLevels); + MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to TRANSFER_DST failed"); + + const Bool isCombinedDepthStencil = + (aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; + const Bool depthSelectsArrayLayer = resource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + for (const auto& item : items) { + const Uint32 depthOrLayers = + item.texelSize.z() > 0 ? static_cast(item.texelSize.z()) : 1u; + VkBufferImageCopy copy{}; + copy.bufferOffset = stagingBase + item.offset; + copy.bufferRowLength = 0; + copy.bufferImageHeight = 0; + copy.imageSubresource.aspectMask = aspectMask; + copy.imageSubresource.mipLevel = item.level; + copy.imageSubresource.baseArrayLayer = item.baseArrayLayer; + copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1; + copy.imageOffset = item.imageOffset; + copy.imageExtent = {static_cast(item.texelSize.x()), + static_cast(item.texelSize.y()), + depthSelectsArrayLayer ? 1u : depthOrLayers}; + if (isCombinedDepthStencil) { + // Per-aspect copies: VkBufferImageCopy's aspectMask must name exactly one + // aspect; the de-interleaved run is the depth plane followed by stencil bytes. + const SizeT texelCount = static_cast(item.texelSize.x()) * + static_cast(item.texelSize.y()) * + static_cast(std::max(item.texelSize.z(), 1)); + VkBufferImageCopy depthCopy = copy; + depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + VkBufferImageCopy stencilCopy = copy; + stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; + stencilCopy.bufferOffset = stagingBase + item.offset + static_cast(texelCount) * 4; + const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy}; + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies); + } else { + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } + } + + const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask); + VkImageLayout uploadLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + ok = TransitionImageLayout(commandBuffer, resource.image, uploadLayout, finalLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStages, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0, + resource.mipLevels); + MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to the sampled layout failed"); + resource.layout = finalLayout; + + if (std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), resource.image) == + m_uploadBatchImages.end()) { + m_uploadBatchImages.push_back(resource.image); + } + m_uploadBatchStagingBytes += stagingSize; + + // The bytes are staged; NOW the entries leave the pending set (D-D5). + for (const auto& item : items) { + ConsumeWireTextureUpload(record, item.uploadTarget, item.level); + } + return true; + } + + MG_Pipe::MGPipeHandle VkTextureManager::ResolveWireTextureStorage( + MG_Pipe::MGPipeHandle handle, Uint32& level, Uint32& layer, + VkFormat* viewFormat, Uint32* layerCount) { + const auto& state = MG_Pipe::MGPipeApplier(); + if (viewFormat) *viewFormat = VK_FORMAT_UNDEFINED; + Uint32 mappedLevel = level, mappedLayer = layer; + Uint32 remainingLayers = ~Uint32{0}; + VkFormat outerFormat = VK_FORMAT_UNDEFINED; + // Production views point directly to their root. The bound also makes a corrupt + // or future multi-hop record graph terminate without ever following client data. + for (SizeT hop = 0; hop <= state.TextureResources.size(); ++hop) { + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= state.TextureResources.size()) + return MG_Pipe::kMGPipeNullHandle; + const auto& record = state.TextureResources[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) return MG_Pipe::kMGPipeNullHandle; + if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) { + if (mappedLevel >= record.Desc.Levels) return MG_Pipe::kMGPipeNullHandle; + const Uint32 layers = record.Desc.Target == + static_cast(MG_Pipe::MGPipeResourceTarget::Tex3D) + ? std::max(record.Desc.Depth >> mappedLevel, 1u) + : std::max(record.Desc.ArrayLayers, 1u); + if (mappedLayer >= layers) return MG_Pipe::kMGPipeNullHandle; + level = mappedLevel; + layer = mappedLayer; + if (viewFormat) *viewFormat = outerFormat; + if (layerCount) *layerCount = std::min(remainingLayers, layers - mappedLayer); + return handle; + } + const auto viewHandle = record.ViewCso; + if (MG_Pipe::MGPipeHandleIsNull(viewHandle) || viewHandle.Slot >= state.SamplerViewCsos.size()) + return MG_Pipe::kMGPipeNullHandle; + const auto& viewRecord = state.SamplerViewCsos[viewHandle.Slot]; + const auto& view = viewRecord.View; + if (!viewRecord.Live || viewRecord.Gen != viewHandle.Gen || view.Texture != handle || + mappedLevel >= view.NumLevels || mappedLayer >= view.NumLayers) + return MG_Pipe::kMGPipeNullHandle; + if (outerFormat == VK_FORMAT_UNDEFINED) { + outerFormat = ResolveTextureFormatInfo(static_cast(view.InternalFormat)).format; + if (outerFormat == VK_FORMAT_UNDEFINED) return MG_Pipe::kMGPipeNullHandle; + } + remainingLayers = std::min(remainingLayers, static_cast(view.NumLayers) - mappedLayer); + mappedLevel += view.MinLevel; + mappedLayer += view.MinLayer; + handle = record.Desc.ViewOf; + } + return MG_Pipe::kMGPipeNullHandle; + } + + VkTextureManager::TextureResource* VkTextureManager::SyncTextureResourceByHandle( + MG_Pipe::MGPipeHandle handle, Bool renderbuffer) { + MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureResourceByHandle: m_device == VK_NULL_HANDLE"); + if (MG_Pipe::MGPipeHandleIsNull(handle)) { + return nullptr; + } + auto& applier = MG_Pipe::MGPipeApplier(); + if (!renderbuffer) { + Uint32 level = 0, layer = 0; + VkFormat viewFormat = VK_FORMAT_UNDEFINED; + const auto storage = ResolveWireTextureStorage(handle, level, layer, &viewFormat); + if (MG_Pipe::MGPipeHandleIsNull(storage)) return nullptr; + if (storage != handle) { + auto* resource = SyncTextureResourceByHandle(storage); + if (!resource) return nullptr; + if (viewFormat != resource->format && + ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0 || + vkuFormatCompatibilityClass(viewFormat) != vkuFormatCompatibilityClass(resource->format))) { + MGLOG_E_ONCE("Magma wire view: format %d cannot reinterpret storage format %d", + static_cast(viewFormat), static_cast(resource->format)); + return nullptr; + } + return resource; + } + } + auto& records = renderbuffer ? applier.RenderbufferResources : applier.TextureResources; + auto& resources = renderbuffer ? m_wireRenderbufferResources : m_wireTextureResources; + if (handle.Slot >= records.size()) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: no applier record at that slot; declined", + handle.Slot, handle.Gen); + return nullptr; + } + auto& record = records[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) { + // FramebufferRecordFor's rule: a dead or recycled slot is a loud refusal, never a + // quiet answer from whatever the slot holds today. + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: the applier's record is %s; declined", + handle.Slot, handle.Gen, + !record.Live ? "dead" : "of another generation"); + return nullptr; + } + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + auto [it, inserted] = resources.try_emplace(key); + (void)inserted; + TextureResource& resource = it->second; + if (resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial && + record.PendingUploads.empty()) { + return &resource; + } + if (!SyncWireTextureShape(record, resource)) { + return nullptr; + } + if (!renderbuffer && !UploadPendingWireLevels(handle, record, resource)) { + return nullptr; + } + resource.syncedWireSerial = record.Serial; + return &resource; + } + + void VkTextureManager::MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel, + Uint32 baseArrayLayer, Uint32 layerCount) { + Uint32 availableLayers = 0; + handle = ResolveWireTextureStorage(handle, mipLevel, baseArrayLayer, nullptr, &availableLayers); + layerCount = std::min(layerCount, availableLayers); + const auto& records = MG_Pipe::MGPipeApplier().TextureResources; + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= records.size()) return; + const auto& record = records[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) return; + using MG_Pipe::MGPipeResourceTarget; + TextureUploadTarget target; + switch (static_cast(record.Desc.Target)) { + case MGPipeResourceTarget::Tex1D: target = TextureUploadTarget::Texture1D; break; + case MGPipeResourceTarget::Tex1DArray: target = TextureUploadTarget::Texture1DArray; break; + case MGPipeResourceTarget::Tex3D: target = TextureUploadTarget::Texture3D; break; + case MGPipeResourceTarget::TexRect: target = TextureUploadTarget::TextureRectangle; break; + case MGPipeResourceTarget::Tex2DMS: target = TextureUploadTarget::Texture2DMultisample; break; + case MGPipeResourceTarget::Tex2DMSArray: target = TextureUploadTarget::Texture2DMultisampleArray; break; + case MGPipeResourceTarget::Tex2DArray: target = TextureUploadTarget::Texture2DArray; break; + case MGPipeResourceTarget::TexCubeArray: target = TextureUploadTarget::CubeMapArray; break; + case MGPipeResourceTarget::TexCube: { + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + for (Uint32 face = baseArrayLayer; face < 6 && face - baseArrayLayer < layerCount; ++face) + MG_Remote::Server::ServerStagedTexture().MarkLevelGpuDirty(key, + static_cast(TextureUploadTarget::CubeMapPositiveX) + face, + static_cast(mipLevel), true); + return; + } + default: target = TextureUploadTarget::Texture2D; break; + } + MG_Remote::Server::ServerStagedTexture().MarkLevelGpuDirty( + MG_Remote::Server::StagedTextureStore::KeyForHandle(handle), + static_cast(target), static_cast(mipLevel), true); + } + + Bool VkTextureManager::GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels, + VkCommandBuffer commandBuffer) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) { + return false; + } + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + auto it = m_wireTextureResources.find(key); + if (it == m_wireTextureResources.end() || it->second.image == VK_NULL_HANDLE) { + return false; + } + TextureResource& resource = it->second; + if (resource.mipLevels >= requiredMipLevels) { + return true; + } + if (resource.sampleCount != VK_SAMPLE_COUNT_1_BIT) { + return false; // a multisample image has no mip chain to grow + } + + // The caller (GenerateMipmap's record arm) flushed every pending submission, so the old + // image is GPU-idle; the carry-over copy is recorded straight into the frame's command + // buffer and the old image is parked on the deferred-release ring behind it. + // Keep the live allocation intact until allocation of its replacement succeeds. + TextureResource& old = resource; + TextureResource grown; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.flags = old.imageCreateFlags; + imageInfo.imageType = old.viewType == VK_IMAGE_VIEW_TYPE_1D || old.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY + ? VK_IMAGE_TYPE_1D + : (old.viewType == VK_IMAGE_VIEW_TYPE_3D ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D); + imageInfo.extent = {old.extent.width, old.extent.height, old.depth}; + imageInfo.mipLevels = requiredMipLevels; + imageInfo.arrayLayers = old.arrayLayers; + imageInfo.format = old.format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = old.usageFlags; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo allocationInfo{}; + allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const VkResult createImageResult = + vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &grown.image, &grown.allocation, nullptr); + if (createImageResult != VK_SUCCESS) { + MGLOG_E_ONCE("GrowWireTextureMipChain: vmaCreateImage failed (%d)", createImageResult); + return false; + } + ++m_textureImageEpoch; + + const VkImageAspectFlags aspect = old.aspect; + const VkImageLayout oldLayout = old.layout; + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcAccessMask = 0; + GetImageTransitionSourceState(oldLayout, srcStageMask, srcAccessMask); + Bool ok = TransitionImageLayout(commandBuffer, old.image, old.layout, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, + VK_ACCESS_TRANSFER_READ_BIT, aspect, 0, old.mipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: source transition failed"); + VkImageLayout grownLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ok = TransitionImageLayout(commandBuffer, grown.image, grownLayout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, aspect, 0, + requiredMipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: destination transition failed"); + + for (Uint32 level = 0; level < old.mipLevels; ++level) { + VkImageCopy copy{}; + copy.srcSubresource = {aspect, level, 0, old.arrayLayers}; + copy.dstSubresource = {aspect, level, 0, old.arrayLayers}; + copy.srcOffset = {0, 0, 0}; + copy.dstOffset = {0, 0, 0}; + copy.extent = {std::max(old.extent.width >> level, 1u), std::max(old.extent.height >> level, 1u), + std::max(old.depth >> level, 1u)}; + for (const VkImageAspectFlags copyAspect : {VK_IMAGE_ASPECT_COLOR_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, + VK_IMAGE_ASPECT_STENCIL_BIT}) { + if ((aspect & copyAspect) == 0) continue; + copy.srcSubresource.aspectMask = copyAspect; + copy.dstSubresource.aspectMask = copyAspect; + vkCmdCopyImage(commandBuffer, old.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, grown.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } + } + + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags dstAccessMask = 0; + GetImageTransitionDestinationState(oldLayout, dstStageMask, dstAccessMask); + ok = TransitionImageLayout(commandBuffer, grown.image, grownLayout, oldLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, + dstAccessMask, aspect, 0, requiredMipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: restore transition failed"); + + grown.layout = oldLayout; + grown.extent = old.extent; + grown.depth = old.depth; + grown.arrayLayers = old.arrayLayers; + grown.mipLevels = requiredMipLevels; + grown.sampledBaseMipLevel = old.sampledBaseMipLevel; + grown.sampledLevelCount = requiredMipLevels; + grown.format = old.format; + grown.aspect = old.aspect; + grown.viewType = old.viewType; + grown.sampleCount = old.sampleCount; + grown.imageCreateFlags = old.imageCreateFlags; + grown.usageFlags = old.usageFlags; + grown.storageUsageResolved = old.storageUsageResolved; + grown.syncedWireSerial = old.syncedWireSerial; + + DeferResourceRelease(Move(resource)); + std::destroy_at(&resource); + std::construct_at(&resource, Move(grown)); + return true; + } +#endif // MOBILEGL_BUILD_DISAGGREGATED + Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, TextureResource &outResource) { #if MOBILEGL_BUILD_DISAGGREGATED diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 117b8424..6b5e4543 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -17,9 +17,21 @@ #include #include +#include +#if MOBILEGL_BUILD_DISAGGREGATED +// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape +// MagmaPipeArms.h already gives this directory. +#include +#endif + namespace MobileGL::MG_State::GLState { class ITextureObject; } +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Pipe { +struct MGPipeResourceRecord; +} +#endif namespace MobileGL::MG_Backend::DirectVulkan { enum class SamplerNumericDomain : Uint8; @@ -333,6 +345,13 @@ public: // shape version and nothing else. Without this in the early-out key the image, its views // and therefore imageSize() all keep answering with the texture's PREVIOUS shape. Uint64 syncedShapeVersion = 0; + // The handle-keyed arm's (P5f fm) early-out key: the applier record's server-owned + // Serial at the last successful sync. 0 also means "this resource has only ever been + // synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves + // on every mutation, so the first handle sync of a fresh image always re-checks. +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 syncedWireSerial = 0; +#endif TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -365,6 +384,9 @@ public: std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedShapeVersion, that.syncedShapeVersion); +#if MOBILEGL_BUILD_DISAGGREGATED + std::swap(this->syncedWireSerial, that.syncedWireSerial); +#endif } void Reset() { @@ -429,6 +451,9 @@ public: syncedContentVersion = 0; syncedMipLevelCount = 0; syncedShapeVersion = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + syncedWireSerial = 0; +#endif } ~TextureResource() { @@ -514,6 +539,39 @@ public: TextureResource* SyncTextureAndGetDescriptor( MG_State::GLState::ITextureObject& texture); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport + // the apply thread may not name the client's ITextureObject (rule E), so the clear / blit / + // readback / mipmap verbs resolve their attachment textures from the wire handle the + // framebuffer or mip record carried: shape from the applier's resource record + // (MGPipeApplier().TextureResources), texels from the server's staged-texture store + // (P5c tx), pending-upload bookkeeping consumed from the same record. Returns null - never + // a frontend fallback - when the record or the shape does not resolve; the caller declines + // loudly, which is the honest answer a missing record deserves. + // + // The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle - + // a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen} + // can never inherit its predecessor's image. Texture views resolve the storage owner from + // Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm. + TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false); + // Map a texture/view-relative subresource to its live storage owner. layerCount, when + // supplied, receives the number of accessible layers starting at the input layer. + // viewFormat is UNDEFINED for an ordinary texture, or the outer view's format. + MG_Pipe::MGPipeHandle ResolveWireTextureStorage(MG_Pipe::MGPipeHandle handle, Uint32& level, + Uint32& layer, VkFormat* viewFormat = nullptr, Uint32* layerCount = nullptr); + // Grows the live image of a handle-keyed resource to `requiredMipLevels`, carrying the + // existing levels' content across with an in-command-buffer copy. The caller + // (GenerateMipmap's record arm) has flushed every pending submission first, so the old + // image is GPU-idle and the copy may be recorded into `commandBuffer` directly; the old + // image goes to the deferred-release ring. False when the chain cannot grow (the resource + // stays as it was). + Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels, + VkCommandBuffer commandBuffer); + // Records that GPU writes superseded the staged CPU snapshot of this level. Layer + // ranges select cube faces; array and 3D levels share one staged level shadow. + void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel, + Uint32 baseArrayLayer = 0, Uint32 layerCount = 1); +#endif VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, Uint32 baseArrayLayer, Uint32 layerCount, @@ -670,6 +728,21 @@ private: void EraseTrackedTexture(const TextureIdentity& identity); void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture); SizeT PruneDeadTextures(); +#if MOBILEGL_BUILD_DISAGGREGATED + // The two halves of SyncTextureResourceByHandle: shape from the resource record's + // descriptor (create / recreate / compatibility), then the pending-upload walk against the + // server's staged store. UploadPendingWireLevels mutates the applier's record (consumed + // entries are dropped), which is why the record is a non-const reference. + Bool SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource); + Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource); + // The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle. + // Node-based for the same reason m_renderbufferResources is: callers hold TextureResource* + // across map-touching calls. + std::unordered_map m_wireTextureResources; + // Handles are per-kind; a renderbuffer and texture may have identical slot/gen. + std::unordered_map m_wireRenderbufferResources; +#endif VkDevice m_device = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index b019c180..b32438f8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3154,9 +3154,15 @@ void main() { inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) { ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap; - const auto& currentDrawFBO = - MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); - if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) { +#if MOBILEGL_BUILD_DISAGGREGATED + const auto* wireFbo = MG_Config::Transport != MG_Config::TransportMode::Monolith ? MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr; + const Bool isDefault = wireFbo ? wireFbo->IsDefault : + MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject()->IsDefaultFramebuffer(); +#else + const auto& currentDrawFBO = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); + const Bool isDefault = currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer(); +#endif + if (isDefault) { flags |= ProgramFactory::CompileOptionBit::PositionYFlip; // gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow // (GetDefaultFramebufferRectMapping): flipped for identity/180, left alone under a @@ -3420,6 +3426,9 @@ void main() { if (m_device != VK_NULL_HANDLE) { VK_VERIFY(vkDeviceWaitIdle(m_device)); } +#if MOBILEGL_BUILD_DISAGGREGATED + DestroyWireDrawPass(); +#endif OnSubmitsCompletedUpTo(m_submitCounter); DestroySubmitFencePool(); @@ -5210,7 +5219,7 @@ void main() { // A program that runs a geometry shader AND captures transform feedback. Both halves are // link-time properties, so this is safe to fold into a pipeline keyed on the program hash. - static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) { + static Bool ProgramCapturesXfbFromGeometryStage(const MagmaProgramSource& program) { if (program.GetTransformFeedbackVaryingCount() == 0) return false; // Both halves are link-time properties, so both are asked of the LAST LINK. Reading the // live attach list would let a glAttachShader that has not been linked in yet - which GL @@ -5249,12 +5258,22 @@ void main() { VkPipeline VulkanRenderer::GetOrCreatePipeline( GLenum mode, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const MG_State::GLState::VertexArrayObject& vao, const RenderPassEntry& renderPassEntry, Bool primitiveRestartEnable) { +#if MOBILEGL_BUILD_DISAGGREGATED + return GetOrCreatePipelineWithInput(mode, program, programObj, transformFlags, + m_vertexInputStateFactory->GetOrCreateVertexInputState(vao), renderPassEntry, primitiveRestartEnable); + } + + VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, + const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, + Bool primitiveRestartEnable) { +#endif Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; if (programObj.stages.empty()) { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); @@ -5272,7 +5291,9 @@ void main() { // payload key on the resolved LAYOUT hash instead, so draws over identical // layouts share one pipeline. // The one-arg fetch rides the VAO's state-pointer memo (no hash, no map). +#if !MOBILEGL_BUILD_DISAGGREGATED auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); +#endif const Uint64 vertexLayoutHash = vis.layoutHash; const Uint64 renderPassHash = renderPassEntry.hash; // The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask @@ -5506,6 +5527,16 @@ void main() { // (stencil) test always passes and nothing is written - even when the bound // image is a packed depth-stencil texture attached through only one half. { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); + if (fbo && !fbo->IsDefault) { + if (fbo->Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) depthTestEnabled = false; + if (fbo->Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) stencilTestEnabled = false; + } + } else +#endif + { const auto& gatingFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) { @@ -5519,6 +5550,7 @@ void main() { } } } + } const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front); const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back); const VkPolygonMode requestedPolygonMode = @@ -5726,12 +5758,31 @@ void main() { MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments, "GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity", payload.colorAttachmentCount); +#if MOBILEGL_BUILD_DISAGGREGATED + const auto* wireFbo = MG_Config::Transport != MG_Config::TransportMode::Monolith ? + MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr; + const auto drawFboBinding = wireFbo ? SharedPtr{} : + MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); +#else const auto& drawFboBinding = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); +#endif MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); +#if MOBILEGL_BUILD_DISAGGREGATED + const Bool isDefaultDrawFbo = wireFbo ? wireFbo->IsDefault : drawFboBinding->IsDefaultFramebuffer(); + Array wireDrawBuffers{}; + if (wireFbo) for (SizeT i = 0; i < wireDrawBuffers.size(); ++i) + wireDrawBuffers[i] = wireFbo->DrawBuffers[i] < 0 ? FramebufferAttachmentType::None : + static_cast(static_cast(FramebufferAttachmentType::Color0) + wireFbo->DrawBuffers[i]); + const auto& drawBuffers = wireFbo ? wireDrawBuffers : drawFboBinding->GetDrawBuffers(); +#else const Bool isDefaultDrawFbo = drawFboBinding->IsDefaultFramebuffer(); const auto& drawBuffers = drawFboBinding->GetDrawBuffers(); +#endif auto resolveCompleteColorAttachmentTexture = [&](Uint32 drawBufferIndex) -> MG_State::GLState::ITextureObject* { +#if MOBILEGL_BUILD_DISAGGREGATED + if (wireFbo) return nullptr; +#endif if (isDefaultDrawFbo || drawBufferIndex >= drawBuffers.size()) { return nullptr; } @@ -5782,6 +5833,15 @@ void main() { attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } +#if MOBILEGL_BUILD_DISAGGREGATED + if (wireFbo && !isDefaultDrawFbo) { + const Int32 slot = wireFbo->DrawBuffers[i]; + if (slot < 0 || wireFbo->Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + attachmentColorWriteMask = 0; + effectiveBlendEnabled = false; + } + } else +#endif if (!isDefaultDrawFbo && i < drawBuffers.size()) { const auto drawBuffer = drawBuffers[i]; colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i); @@ -5877,6 +5937,12 @@ void main() { VkFormat colorAttachmentFormat = VK_FORMAT_UNDEFINED; Int textureExternalIndex = -1; +#if MOBILEGL_BUILD_DISAGGREGATED + if (wireFbo && !isDefaultDrawFbo) { + const Int32 slot = wireFbo->DrawBuffers[i]; + colorAttachmentFormat = ResolveWireImage(*wireFbo, wireFbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT).format; + } else +#endif if (isDefaultDrawFbo) { colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format; } else if (colorAttachmentRenderbuffer != nullptr) { @@ -6004,7 +6070,7 @@ void main() { Bool VulkanRenderer::PrepareStorageImageTextures( FrameContext::FrameData& frame, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { if (!programObj.hasStorageImages) { return true; @@ -6100,7 +6166,7 @@ void main() { } Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots( FrameContext::FrameData& frame, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, VkPipelineStageFlags consumerShaderStageMask) { auto& feedbackBindings = m_samplerImageFeedbackScratch; @@ -6801,6 +6867,10 @@ void main() { Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + return SetupWireDraw(frame, mode, aspects, drawParams); +#endif // Sync each sampled texture at most once across this whole draw: the layout // probe loop, the post-transition loop, and ResolveSamplerDescriptor would // otherwise each re-run the full SyncTexture path on the same textures. @@ -7417,6 +7487,12 @@ void main() { } void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ); + return; + } +#endif m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); // The DISPATCH accessor: with a pipeline bound this is its compute stage program @@ -7636,7 +7712,23 @@ void main() { return ScissoredClearPrep::Ready; } + #include "WireFramebuffer.inc" + #include "WireDraw.inc" + void VulkanRenderer::Clear(GLbitfield mask) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); + if (!fbo) MagmaWireFatal("clear-framebuffer-record"); + ClearAttachmentPayload payload{}; + payload.mask = mask; + payload.color = MGB_CTX->GetClearColor(); + payload.depth = MGB_CTX->GetClearDepth(); + payload.stencil = MGB_CTX->GetClearStencil(); + ClearWireFramebuffer(*fbo, payload); + return; + } +#endif m_clearManager->CollectGarbage(); if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { return; @@ -8013,6 +8105,14 @@ void main() { void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); + if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record"); + ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1); + return; + } +#endif auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); if (!fbo) { return; @@ -8971,55 +9071,42 @@ void main() { void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (G6, CONTRACT-P5C §3.3/§5.4): MAGMA'S NAMED ARM. A blit record whose - // ReadFbo/DrawFbo are non-null is a glBlitNamedFramebuffer: both framebuffers resolve - // from the record's handles, and the sink is told the pair was consumed - a backend - // that leaves the flag clear has no named arm and the verb declines there, loudly. - // Magma has no FBO twin registry (it reads the frontend object wherever it syncs), so - // the resolution here is frontend-keyed - the handle was minted over the frontend - // object's lifetime id, and the two probes below (the client allocator's slot entry - // and the frontend context's framebuffer pool) are named debt inside the scope, the - // same shape as Espryt's StateForHandle arm: P7 retires it by carrying the object - // identity in the record. P5e (id), ruling 12: the third of Magma's four debts, so the - // scope it rides in is MagmaP7AllocatorDebtScope. Magma stays lockstep for the whole of - // P5e (CONTRACT-P5E §6.1), so the record being applied here is always barriered and the - // exemption still holds. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - auto& applierState = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle readHandle = applierState.VerbBlitReadFbo; - const MG_Pipe::MGPipeHandle drawHandle = applierState.VerbBlitDrawFbo; - if (!MG_Pipe::MGPipeHandleIsNull(readHandle) || !MG_Pipe::MGPipeHandleIsNull(drawHandle)) { - const auto resolveEndpoint = [](MG_Pipe::MGPipeHandle handle) - -> SharedPtr { - const MG_Pipe::MagmaP7AllocatorDebtScope magmaP7AllocatorDebt; - if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) { - return MG_State::pGLContext ? MG_State::pGLContext->GetFramebufferObject(0) : nullptr; - } - if (!MG_Pipe::MGPipeSlots().IsLive(MG_Pipe::MGPipeKind::Framebuffer, handle)) { - return nullptr; - } - const Uint64 lifetimeId = - MG_Pipe::MGPipeSlots().LifetimeIdOfSlot(MG_Pipe::MGPipeKind::Framebuffer, handle.Slot); - if (lifetimeId == 0 || MG_State::pGLContext == nullptr) return nullptr; - return MG_State::pGLContext->FindFramebufferObjectByLifetimeId(lifetimeId); - }; - auto readFbo = resolveEndpoint(readHandle); - auto drawFbo = resolveEndpoint(drawHandle); - if (!readFbo || !drawFbo) { - // Leave the pair UNCONSUMED: the sink's decline is the loud answer a - // missing endpoint deserves, not a silent blit of whatever is bound. - MGLOG_E_ONCE("MGPipe: Magma's named blit could not resolve an endpoint (read {%u, %u}, draw " - "{%u, %u}) to a frontend framebuffer; the verb declines at the sink", - readHandle.Slot, readHandle.Gen, drawHandle.Slot, drawHandle.Gen); - return; + // The scissor test clips blit writes: intersect the destination rectangle with + // the scissor box and shrink the source proportionally. + if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + const IntVec4& scissor = MGB_CTX->GetScissorBox(); + const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool { + const Bool dstFlipped = d1 < d0; + GLint lo = dstFlipped ? d1 : d0; + GLint hi = dstFlipped ? d0 : d1; + const GLint newLo = std::max(lo, clipLo); + const GLint newHi = std::min(hi, clipHi); + if (newLo >= newHi) { + return false; } - applierState.VerbBlitNamedConsumed = true; - BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, - dstX1, dstY1, mask, filter); - return; + const double srcSpan = static_cast(s1 - s0); + const double dstSpan = static_cast(d1 - d0); + const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0; + const GLint origD0 = d0; + const GLint clippedD0 = dstFlipped ? newHi : newLo; + const GLint clippedD1 = dstFlipped ? newLo : newHi; + s0 = s0 + static_cast(std::lround((clippedD0 - origD0) * scale)); + s1 = s0 + static_cast(std::lround((clippedD1 - clippedD0) * scale)); + d0 = clippedD0; + d1 = clippedD1; + return true; + }; + if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) || + !clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) { + return; // fully scissored out } } + +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter); + return; + } #endif auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); @@ -9052,37 +9139,6 @@ void main() { return; } - // The scissor test clips blit writes: intersect the destination rectangle with - // the scissor box and shrink the source proportionally. - if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { - const IntVec4& scissor = MGB_CTX->GetScissorBox(); - const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool { - const Bool dstFlipped = d1 < d0; - GLint lo = dstFlipped ? d1 : d0; - GLint hi = dstFlipped ? d0 : d1; - const GLint newLo = std::max(lo, clipLo); - const GLint newHi = std::min(hi, clipHi); - if (newLo >= newHi) { - return false; - } - const double srcSpan = static_cast(s1 - s0); - const double dstSpan = static_cast(d1 - d0); - const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0; - const GLint origD0 = d0; - const GLint clippedD0 = dstFlipped ? newHi : newLo; - const GLint clippedD1 = dstFlipped ? newLo : newHi; - s0 = s0 + static_cast(std::lround((clippedD0 - origD0) * scale)); - s1 = s0 + static_cast(std::lround((clippedD1 - clippedD0) * scale)); - d0 = clippedD0; - d1 = clippedD1; - return true; - }; - if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) || - !clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) { - return; // fully scissored out - } - } - MOBILEGL_ASSERT(readFbo != nullptr, "VulkanRenderer::BlitFramebuffer: read framebuffer is null"); MOBILEGL_ASSERT(drawFbo != nullptr, "VulkanRenderer::BlitFramebuffer: draw framebuffer is null"); if (IsUnsupportedFramebufferForDirectVulkan(*readFbo) || @@ -9633,6 +9689,12 @@ void main() { void VulkanRenderer::CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + CopyWireFramebufferToTexture(target,level,xoffset,yoffset,x,y,width,height); + return; + } +#endif if (width <= 0 || height <= 0) { return; } @@ -9952,6 +10014,10 @@ void main() { const CopyImageEndpoint& dstEndpoint, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { +#if MOBILEGL_BUILD_DISAGGREGATED + const Bool wire = MG_Config::Transport != MG_Config::TransportMode::Monolith; + if (wire && HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("copy-image-flush"); +#endif MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(), "CopyImageSubData requires valid source and destination images."); // The frontend already declines a zero or negative extent, so anything else here is a @@ -9978,7 +10044,11 @@ void main() { srcEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*srcEndpoint.Texture) : nullptr; const auto* dstStorageTexture = dstEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*dstEndpoint.Texture) : nullptr; - if (srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) { + if ( +#if MOBILEGL_BUILD_DISAGGREGATED + !wire && +#endif + srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) { MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__, CopyImageEndpointName(srcEndpoint)); return; @@ -9997,6 +10067,21 @@ void main() { // SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the // render-pass manager, which is where a renderbuffer's VkImage lives. const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + auto* resource = m_textureManager->SyncTextureResourceByHandle(endpoint.TextureHandle); + if (!resource) MagmaWireFatal("copy-image-resource"); + out.image = resource->image; + out.trackedLayout = &resource->layout; + out.aspect = resource->aspect; + out.mipLevels = resource->mipLevels; + out.extent = resource->extent; + out.depth = resource->depth; + out.arrayLayers = resource->arrayLayers; + out.format = resource->format; + return out.image != VK_NULL_HANDLE; + } +#endif if (endpoint.IsRenderbuffer()) { auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer); if (resource == nullptr) return false; @@ -10031,6 +10116,12 @@ void main() { CopyImageVkImage dstImage{}; const Bool srcResolved = resolveImage(srcEndpoint, srcImage); const Bool dstResolved = resolveImage(dstEndpoint, dstImage); +#if MOBILEGL_BUILD_DISAGGREGATED + if (wire) { + if (srcImage.image == dstImage.image) MagmaWireFatal("copy-image-in-place@P7"); + m_textureManager->FlushPendingUploads(); + } +#endif // Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in // a release build, which is where both observed failures happened - a null resource // dereferenced right below (lavapipe) and a mip level the VkImage does not have handed @@ -10159,6 +10250,9 @@ void main() { } const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true; +#endif if (endpoint.IsRenderbuffer()) { return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer); } @@ -10270,6 +10364,10 @@ void main() { srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); +#if MOBILEGL_BUILD_DISAGGREGATED + if (wire) m_textureManager->MarkWireTextureGpuWritten(dstEndpoint.TextureHandle, + dstMipLevel, dstSlices.BaseArrayLayer(), dstSlices.slicesAreDepth ? 1u : copySliceCount); +#endif VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; @@ -10364,6 +10462,12 @@ void main() { void VulkanRenderer::ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + ReadWirePixels(x,y,width,height,format,type,pixels); + return; + } +#endif if (width <= 0 || height <= 0) { return; } @@ -11376,6 +11480,12 @@ void main() { } void VulkanRenderer::GenerateMipmap(GLenum target) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + GenerateWireMipmap(); + return; + } +#endif const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // Whatever is left here is a coverage gap in this backend, not a broken invariant, so it // declines (leaving the mip chain unwritten) rather than asserting the process down. What diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index de43cb2c..08a61c60 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -433,6 +433,44 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); +#if MOBILEGL_BUILD_DISAGGREGATED + struct WireImage { + VkImage image = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout* trackedLayout = nullptr; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + VkImageAspectFlags aspect = 0; + Uint32 level = 0, layer = 0, layers = 1, levels = 1; + Bool isDefault = false; + Bool is3D = false; + MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle; + }; + WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo, + const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect); + void TransitionWireImage(WireImage& image, VkImageLayout layout); + void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer = -1); + void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, + GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter); + void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, void* pixels); + void GenerateWireMipmap(); + Bool SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, + const DrawCmdParam& drawParams); + void DestroyWireDrawPass(); + void DispatchWireCompute(GLuint x, GLuint y, GLuint z); + UniquePtr m_wireDrawPass; + Vector m_wireDrawViews; + VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, + const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, + Bool primitiveRestartEnable); + + void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset, + GLint x, GLint y, GLsizei width, GLsizei height); +#endif void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); @@ -1508,7 +1546,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline GetOrCreatePipeline( GLenum mode, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const MG_State::GLState::VertexArrayObject& vao, @@ -1520,14 +1558,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // flush the pending recording (see the body), which retires the current command buffer. Bool PrepareStorageImageTextures( FrameContext::FrameData& frame, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj); // Vulkan forbids a sampled descriptor and writable storage descriptor from naming the // same image subresource in one shader operation. Snapshot only the sampler side; the // storage descriptor continues to name the application texture. Bool PrepareSamplerImageFeedbackSnapshots( FrameContext::FrameData& frame, - const MG_State::GLState::ProgramObject& program, + const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, VkPipelineStageFlags consumerShaderStageMask); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc new file mode 100644 index 00000000..dd612878 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -0,0 +1,179 @@ +// Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation. +#if MOBILEGL_BUILD_DISAGGREGATED + static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) { + auto& state = MG_Pipe::MGPipeApplier(); + const Bool composite = handle.Slot >= MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; + const auto& records = composite ? state.CompositeShaderCsos : state.ShaderCsos; + const SizeT index = composite ? handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase : handle.Slot; + if (MG_Pipe::MGPipeHandleIsNull(handle) || index >= records.size() || + !records[index].Live || records[index].Gen != handle.Gen || !records[index].Archive) + MagmaWireFatal("program-record"); + return MagmaProgramSource(handle, records[index]); + } + + [[noreturn]] static void WireBufferLegacyFatal() { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); + std::abort(); + } + + void VulkanRenderer::DestroyWireDrawPass() { + m_wireDrawPass.reset(); + for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr); + m_wireDrawViews.clear(); + } + + Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, + Flags aspects, const DrawCmdParam& drawParams) { + auto& state = MG_Pipe::MGPipeApplier(); + const auto program = WireProgramSource(state.BoundShaderCso); + if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return false; + if (aspects & DrawSetupAspect::IndexBuffer || aspects & DrawSetupAspect::IndirectDrawBuffer || + MGB_CTX->IsTransformFeedbackActive()) WireBufferLegacyFatal(); + const auto* fbo = state.DrawFramebuffer(); + if (!fbo) MagmaWireFatal("draw-framebuffer-record"); + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("draw-flush"); + DestroyWireDrawPass(); + + ProgramFactory::CompileOptionFlags flags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); + const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags); + if (programObject->readsBaseVertexBuiltin) { + flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; + programObject = &m_programFactory->GetOrCreateProgram(program, flags); + } + if (programObject->pointSizeCapabilityUnsupported) return false; + const auto& programObj = *programObject; + const auto vaoHandle = state.BoundVertexElements; + if (!MG_Pipe::MGPipeHandleIsNull(vaoHandle)) { + if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); + const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; + if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); + for (Uint32 i = 0; i < vao.AttributeCount; ++i) + if (vao.Attributes[i].Enabled && (programObj.activeVertexInputLocationMask & (1u << i))) + WireBufferLegacyFatal(); + } + + Vector attachments; + Vector views; + Vector colors; + VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkExtent2D extent{~0u, ~0u}; + Uint32 layers = ~0u; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + Uint64 passHash = 0; + const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 { + WireImage image = ResolveWireImage(*fbo, surface, aspect); + if (!image.image) return VK_ATTACHMENT_UNUSED; + m_textureManager->FlushPendingUploads(); + const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ? + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + TransitionWireImage(image, layout); + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image.image; + info.viewType = image.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + info.format = image.format; + info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers}; + if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + VkImageView view = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view)); + m_wireDrawViews.push_back(view); + views.push_back(view); + VkAttachmentDescription description{}; + description.format = image.format; + description.samples = image.samples; + description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + description.initialLayout = description.finalLayout = layout; + attachments.push_back(description); + extent.width = std::min(extent.width, image.extent.width); + extent.height = std::min(extent.height, image.extent.height); + layers = std::min(layers, image.layers); + samples = image.samples; + passHash = XXH64(&description, sizeof(description), passHash); + if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers); + if (image.isDefault) { + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); + else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true); + } + return static_cast(attachments.size() - 1); + }; + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const Int32 slot = fbo->DrawBuffers[i]; + const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); + colors.push_back({index, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}); + } + while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back(); + if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT); + else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); + if (attachments.empty()) return false; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = static_cast(colors.size()); + subpass.pColorAttachments = colors.data(); + subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = static_cast(attachments.size()); + passInfo.pAttachments = attachments.data(); + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + m_wireDrawPass = MakeUnique(); + auto& pass = *m_wireDrawPass; + VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass.renderPass)); + VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + fbInfo.renderPass = pass.renderPass; + fbInfo.attachmentCount = static_cast(views.size()); + fbInfo.pAttachments = views.data(); + fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers; + VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &pass.framebuffer)); + pass.attachmentCount = static_cast(attachments.size()); + pass.colorAttachmentCount = static_cast(colors.size()); + pass.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED; + pass.sampleCount = samples; + pass.extent = {static_cast(extent.width), static_cast(extent.height)}; + pass.layers = layers; + pass.hash = pass.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash); + VertexInputStateFactory::BackendVertexInputState input{}; + const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, false); + if (pipeline == VK_NULL_HANDLE) return false; + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, + m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) return false; + Uint32 binding = 0; + for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { + if (!(programObj.activeVertexInputLocationMask & (1u << location))) continue; + VkFormat format = VK_FORMAT_UNDEFINED; + const void* data = nullptr; + VkDeviceSize size = 0; + if (!TryGetCurrentVertexAttributeUploadPayload(MGB_CTX->GetCurrentVertexAttribute(location), + programObj.vertexInputTypes[location], format, data, size)) return false; + BufferSlice slice{}; + if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), + data, size, 16, slice)) return false; + vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset); + } + if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return false; + vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault, + ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); + (void)drawParams; + return true; + } + + void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) { + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("dispatch-flush"); + auto& frame = m_frameContext.GetCurrent(); + const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram); + if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return; + const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); + const auto pipeline = GetOrCreateComputePipeline(programObj); + if (!pipeline) return; + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, + m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) return; + vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + vkCmdDispatch(frame.commandBuffer, x, y, z); + } +#endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc new file mode 100644 index 00000000..71dad21b --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -0,0 +1,426 @@ +// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here +// belong to the applier; no frontend object, lifetime-id lookup or allocator is used. +#if MOBILEGL_BUILD_DISAGGREGATED + [[noreturn]] static void MagmaWireFatal(const char* detail) { + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); + std::abort(); + } + + VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage( + const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface, + VkImageAspectFlags aspect) { + WireImage out{}; + out.aspect = aspect; + if (fbo.IsDefault) { + out.isDefault = true; + out.extent = m_swapchainObject.GetExtent(); + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + out.image = m_swapchainObject.GetImage(m_imageIndexAcquired); + out.format = m_swapchainObject.GetSurfaceFormat().format; + out.layout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); + } else { + out.image = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired); + out.format = m_swapchainObject.GetDepthStencilFormat(); + out.layout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); + } + return out; + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return out; + const Bool renderbuffer = surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer; + Uint32 level = surface.Level, layer = surface.Layer, availableLayers = 1; + VkFormat viewFormat = VK_FORMAT_UNDEFINED; + out.storage = renderbuffer ? surface.Res : m_textureManager->ResolveWireTextureStorage( + surface.Res, level, layer, &viewFormat, &availableLayers); + if (MG_Pipe::MGPipeHandleIsNull(out.storage)) MagmaWireFatal("framebuffer-view-record"); + auto* resource = m_textureManager->SyncTextureResourceByHandle(out.storage, renderbuffer); + if (!resource) MagmaWireFatal("framebuffer-resource"); + out.image = resource->image; + out.format = viewFormat != VK_FORMAT_UNDEFINED ? viewFormat : resource->format; + out.extent = {std::max(resource->extent.width >> level, 1u), + std::max(resource->extent.height >> level, 1u)}; + out.layout = resource->layout; + out.trackedLayout = &resource->layout; + out.samples = resource->sampleCount; + out.level = level; + out.levels = resource->mipLevels; + out.layer = layer; + out.is3D = resource->depth > 1; + out.layers = surface.Layered ? (out.is3D ? std::max(resource->depth >> level, 1u) : availableLayers) : 1; + if (out.level >= resource->mipLevels || out.layer + out.layers > + (out.is3D ? std::max(resource->depth >> level, 1u) : resource->arrayLayers)) + MagmaWireFatal("framebuffer-subresource"); + return out; + } + + void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) { + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags access = 0; + GetImageTransitionSourceState(image.layout, stage, access); + // One tracked layout per image: move the complete mip chain, never just the surface. + VkImageAspectFlags barrierAspect = image.aspect; + if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) + barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout, + stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, barrierAspect, 0, image.levels)) + MagmaWireFatal("image-transition"); + if (image.trackedLayout) *image.trackedLayout = image.layout; + if (image.isDefault) { + if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT) + m_swapchainObject.SetImageLayout(m_imageIndexAcquired, image.layout); + else m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, image.layout); + } + } + + void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer) { + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return; + const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, + Uint32 colorIndex) { + if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return; + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + const auto mask = MGB_CTX->GetColorMaskIndexed(colorIndex); + if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return; + if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) { + MGLOG_W_ONCE("DirectVulkan: glClear with a partial color mask is not supported"); + return; + } + } + if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) { + if (!MGB_CTX->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT; + } + if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) { + const Uint32 mask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask & 255u; + if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT; + else if (mask != 255u) { + MGLOG_W_ONCE("DirectVulkan: glClear with a partial stencil write mask is not supported"); + aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT; + } + } + if (!aspect) return; + WireImage image = ResolveWireImage(fbo, surface, aspect); + if (!image.image) return; + m_textureManager->FlushPendingUploads(); + VkRect2D area{{0, 0}, image.extent}; + if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + const auto& box = MGB_CTX->GetScissorBox(); + const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0); + const Int32 x1 = std::min(box.x() + box.z(), static_cast(image.extent.width)); + const Int32 y1 = std::min(box.y() + box.w(), static_cast(image.extent.height)); + if (x0 >= x1 || y0 >= y1) return; + area = {{x0, y0}, {static_cast(x1-x0), static_cast(y1-y0)}}; + if (image.isDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1-x0, y1-y0, + image.extent, m_swapchainObject.GetPreTransform(), &area.offset, &area.extent)) + MagmaWireFatal("clear-surface-transform"); + } + const Bool color = aspect == VK_IMAGE_ASPECT_COLOR_BIT; + const auto layout = color ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + TransitionWireImage(image, layout); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image.image; + viewInfo.viewType = image.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = image.format; + viewInfo.subresourceRange = {color ? VK_IMAGE_ASPECT_COLOR_BIT : + (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT ? VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT : image.aspect), image.level, 1, image.layer, image.layers}; + VkImageView view = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view)); + VkAttachmentDescription attachment{}; + attachment.format = image.format; + attachment.samples = image.samples; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = layout; + attachment.finalLayout = layout; + VkAttachmentReference ref{0, layout}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = color ? 1 : 0; + subpass.pColorAttachments = color ? &ref : nullptr; + subpass.pDepthStencilAttachment = color ? nullptr : &ref; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass; + VkRenderPass pass = VK_NULL_HANDLE; + VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass)); + VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + fbInfo.renderPass = pass; fbInfo.attachmentCount = 1; fbInfo.pAttachments = &view; + fbInfo.width = image.extent.width; fbInfo.height = image.extent.height; fbInfo.layers = image.layers; + VkFramebuffer fb = VK_NULL_HANDLE; + VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &fb)); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; begin.framebuffer = fb; begin.renderArea = area; + auto clearPayload = payload; + if (color) PreCompensateSrgbClearColor(clearPayload, image.format); + VkClearAttachment clear{}; + clear.aspectMask = aspect; + clear.clearValue.color = MakeVkClearColorValue(clearPayload, false); + if (!color) clear.clearValue.depthStencil = {payload.depth, payload.stencil}; + VkClearRect rect{area, 0, image.layers}; + auto& frame = m_frameContext.GetCurrent(); + vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdClearAttachments(frame.commandBuffer, 1, &clear, 1, &rect); + vkCmdEndRenderPass(frame.commandBuffer); + // P5f changes ownership, not Magma's wait contract. Synchronous retirement keeps + // these temporary server resources bounded and makes their lifetime explicit. + if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("clear-submit"); + vkDestroyFramebuffer(m_device, fb, nullptr); + vkDestroyRenderPass(m_device, pass, nullptr); + vkDestroyImageView(m_device, view, nullptr); + if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers); + if (image.isDefault) { + if (color) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); + else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true); + } + }; + if (payload.mask & GL_COLOR_BUFFER_BIT) { + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + if (drawbuffer >= 0 && static_cast(drawbuffer) != i) continue; + const Int32 attachment = fbo.DrawBuffers[i]; + if (attachment >= 0 && attachment < static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) + clearSurface(fbo.Color[attachment], VK_IMAGE_ASPECT_COLOR_BIT, i); + } + } + if (payload.mask & GL_DEPTH_BUFFER_BIT) clearSurface(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + if (payload.mask & GL_STENCIL_BUFFER_BIT) clearSurface(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT, 0); + } + + void VulkanRenderer::ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, void* pixels) { + if (width <= 0 || height <= 0) return; + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + const auto* fbo = MG_Pipe::MGPipeApplier().ReadFramebuffer(); + if (!fbo) MagmaWireFatal("read-framebuffer-record"); + VkImageAspectFlags aspect = format == GL_DEPTH_COMPONENT ? VK_IMAGE_ASPECT_DEPTH_BIT : + format == GL_STENCIL_INDEX ? VK_IMAGE_ASPECT_STENCIL_BIT : + VK_IMAGE_ASPECT_COLOR_BIT; + if (format == GL_DEPTH_STENCIL) aspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + const auto& surface = (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) ? fbo->Depth : + aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? fbo->Stencil : fbo->ReadSurface; + WireImage source = ResolveWireImage(*fbo, surface, aspect); + if (!source.image) return; + m_textureManager->FlushPendingUploads(); + TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (aspect != VK_IMAGE_ASPECT_COLOR_BIT) { + ReadDepthStencilImageToClient(source.image,source.format, + source.trackedLayout ? source.trackedLayout : &source.layout,aspect, + source.level,source.layer,x,y,width,height,format,type,pixels,source.isDefault); + if (source.isDefault) m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired,source.layout); + return; + } + const SizeT texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : GetReadbackTexelSize(source.format); + if (!texelSize) MagmaWireFatal("read-format"); + const VkDeviceSize size = static_cast(width) * height * texelSize; + VkBufferObject buffer; + if (!buffer.Create({.allocator=m_allocator, .size=size, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage=VMA_MEMORY_USAGE_AUTO, .allocationFlags=VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) + MagmaWireFatal("read-buffer-allocation"); + VkOffset2D offset{x,y}; + VkExtent2D extent{static_cast(width), static_cast(height)}; + if (source.isDefault && !MapDefaultFramebufferReadbackRect(x,y,width,height,source.extent, + m_swapchainObject.GetPreTransform(),&offset,&extent)) MagmaWireFatal("read-surface-transform"); + VkBufferImageCopy region{}; + region.imageSubresource = {aspect,source.level,source.is3D ? 0u : source.layer,1}; + region.imageOffset = {offset.x,offset.y,source.is3D ? static_cast(source.layer) : 0}; + region.imageExtent = {extent.width,extent.height,1}; + auto& frame = m_frameContext.GetCurrent(); + vkCmdCopyImageToBuffer(frame.commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + buffer.GetHandle(),1,®ion); + if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("read-submit"); + const auto* bytes = static_cast(buffer.Map()); + if (!bytes || !buffer.Invalidate(size)) MagmaWireFatal("read-map"); + Vector remapped; + if (source.isDefault) { + remapped.resize(static_cast(size)); + if (!RemapDefaultFramebufferReadback(bytes,width,height,m_swapchainObject.GetPreTransform(), + texelSize,remapped.data())) MagmaWireFatal("read-remap"); + bytes = remapped.data(); + } + if (!PackReadbackToClientOrPbo(bytes,source.format,width,height,1,format,type,pixels,false,true)) + MagmaWireFatal("read-conversion"); + } + + void VulkanRenderer::BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, + GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) { + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + auto& state = MG_Pipe::MGPipeApplier(); + const Bool named = !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitReadFbo) || + !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitDrawFbo); + const auto* read = named ? state.FramebufferRecordFor(state.VerbBlitReadFbo) : state.ReadFramebuffer(); + const auto* draw = named ? state.FramebufferRecordFor(state.VerbBlitDrawFbo) : state.DrawFramebuffer(); + if (!read || !draw) MagmaWireFatal("blit-framebuffer-record"); + state.VerbBlitNamedConsumed = named; + const auto blit = [&](const MG_Pipe::MGPSurface& src, const MG_Pipe::MGPSurface& dst, + VkImageAspectFlags aspect) { + WireImage source = ResolveWireImage(*read,src,aspect); + WireImage destination = ResolveWireImage(*draw,dst,aspect); + if (!source.image || !destination.image) return; + m_textureManager->FlushPendingUploads(); + const Bool sameImage = source.image == destination.image; + const VkImageLayout srcLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkImageLayout dstLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + TransitionWireImage(source,srcLayout); + if (sameImage) destination.layout = source.layout; + else TransitionWireImage(destination,dstLayout); + VkImageBlit region{}; + region.srcSubresource = {aspect,source.level,source.layer,1}; + region.dstSubresource = {aspect,destination.level,destination.layer,1}; + region.srcOffsets[0] = {sx0,sy0,0}; region.srcOffsets[1] = {sx1,sy1,1}; + region.dstOffsets[0] = {dx0,dy0,0}; region.dstOffsets[1] = {dx1,dy1,1}; + // Headless and identity surfaces store GL's bottom row at Vulkan's last row. + if (source.isDefault) { + region.srcOffsets[0].y = static_cast(source.extent.height)-sy0; + region.srcOffsets[1].y = static_cast(source.extent.height)-sy1; + } + if (destination.isDefault) { + region.dstOffsets[0].y = static_cast(destination.extent.height)-dy0; + region.dstOffsets[1].y = static_cast(destination.extent.height)-dy1; + } + vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,srcLayout, + destination.image,dstLayout,1,®ion, + filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); + if (!destination.isDefault && dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, destination.layers); + if (destination.isDefault) { + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired,true); + else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired,true); + } + }; + if (mask & GL_COLOR_BUFFER_BIT) { + for (Uint32 i=0;iDrawBuffers[i]; + if (attachment>=0 && attachment(MG_Pipe::kMGPipeMaxColorAttachments)) + blit(read->ReadSurface,draw->Color[attachment],VK_IMAGE_ASPECT_COLOR_BIT); + } + } + if (mask & GL_DEPTH_BUFFER_BIT) blit(read->Depth,draw->Depth,VK_IMAGE_ASPECT_DEPTH_BIT); + if (mask & GL_STENCIL_BUFFER_BIT) blit(read->Stencil,draw->Stencil,VK_IMAGE_ASPECT_STENCIL_BIT); + } + void VulkanRenderer::CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset, + GLint x, GLint y, GLsizei width, GLsizei height) { + if (width <= 0 || height <= 0) return; + if (target != GL_TEXTURE_2D) { + RecordTextureCopyError(__func__,ErrorCode::InvalidOperation, + "CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations."); + return; + } + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + auto& state = MG_Pipe::MGPipeApplier(); + const auto* read = state.ReadFramebuffer(); + if (!read) MagmaWireFatal("copy-framebuffer-record"); + const auto* texture = m_textureManager->SyncTextureResourceByHandle(state.VerbCopyTexDst); + if (!texture) MagmaWireFatal("copy-destination-record"); + const VkImageAspectFlags aspect = texture->aspect; + MG_Pipe::MGPFramebufferState fake{}; + MG_Pipe::MGPSurface surface{}; + surface.Kind = MG_Pipe::kMGPipeSurfaceKindTexture; + surface.Res = state.VerbCopyTexDst; + surface.Level = static_cast(level); + WireImage source = ResolveWireImage(*read,aspect & VK_IMAGE_ASPECT_DEPTH_BIT ? read->Depth : + aspect & VK_IMAGE_ASPECT_STENCIL_BIT ? read->Stencil : read->ReadSurface,aspect); + WireImage destination = ResolveWireImage(fake,surface,aspect); + if (!source.image || !destination.image) return; + m_textureManager->FlushPendingUploads(); + TransitionWireImage(source,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + TransitionWireImage(destination,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + VkImageBlit region{}; + region.srcSubresource = {aspect,source.level,source.layer,1}; + region.dstSubresource = {aspect,destination.level,destination.layer,1}; + region.srcOffsets[0] = {x,y,0}; region.srcOffsets[1] = {x+width,y+height,1}; + region.dstOffsets[0] = {xoffset,yoffset,0}; region.dstOffsets[1] = {xoffset+width,yoffset+height,1}; + if (source.isDefault) { + region.srcOffsets[0].y = static_cast(source.extent.height)-y; + region.srcOffsets[1].y = static_cast(source.extent.height)-y-height; + } + vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,®ion,VK_FILTER_NEAREST); + m_textureManager->MarkWireTextureGpuWritten(destination.storage,destination.level,destination.layer); + } + + void VulkanRenderer::GenerateWireMipmap() { + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + auto& state = MG_Pipe::MGPipeApplier(); + const auto handle = state.VerbMipRes; + auto* resource = m_textureManager->SyncTextureResourceByHandle(handle); + if (!resource) MagmaWireFatal("mipmap-resource"); + m_textureManager->FlushPendingUploads(); + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("mipmap-flush"); + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + const Uint32 base = state.VerbMipBaseLevel; + const Uint32 count = state.VerbMipLevelCount; + if (count <= 1) return; + if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer)) + MagmaWireFatal("mipmap-growth"); + if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) + MagmaWireFatal("depth-stencil-mipmap@P7"); + VkFormatProperties props{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,resource->format,&props); + const auto required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; + if ((props.optimalTilingFeatures & required) != required) MagmaWireFatal("mipmap-native-format@P7"); + const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ? + VK_FILTER_LINEAR : VK_FILTER_NEAREST; + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags access = 0; + GetImageTransitionSourceState(resource->layout,stage,access); + if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access, + VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition"); + for (Uint32 level=base+1;levelimage,previous, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1); + VkImageBlit blit{}; + blit.srcSubresource = {resource->aspect,level-1,0,resource->arrayLayers}; + blit.dstSubresource = {resource->aspect,level,0,resource->arrayLayers}; + blit.srcOffsets[1] = {static_cast(std::max(resource->extent.width>>(level-1),1u)), + static_cast(std::max(resource->extent.height>>(level-1),1u)), + static_cast(std::max(resource->depth>>(level-1),1u))}; + blit.dstOffsets[1] = {static_cast(std::max(resource->extent.width>>level,1u)), + static_cast(std::max(resource->extent.height>>level,1u)), + static_cast(std::max(resource->depth>>level,1u))}; + vkCmdBlitImage(frame.commandBuffer,resource->image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + resource->image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,filter); + VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,level-1,1); + } + resource->layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + auto& record = state.TextureResources[handle.Slot]; + record.Desc.Levels = std::max(record.Desc.Levels,static_cast(base+count)); + resource->syncedWireSerial = ++record.Serial; + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + const auto target = static_cast(record.Desc.Target); + // The resource descriptor determines the upload-target set; no texture unit or + // client upload-target vector participates in defining the generated shadow. + for (Uint32 face=0;face<(target==MG_Pipe::MGPipeResourceTarget::TexCube ? 6u : 1u);++face) { + TextureUploadTarget upload = TextureUploadTarget::Texture2D; + switch (target) { + case MG_Pipe::MGPipeResourceTarget::Tex1D: upload=TextureUploadTarget::Texture1D; break; + case MG_Pipe::MGPipeResourceTarget::Tex3D: upload=TextureUploadTarget::Texture3D; break; + case MG_Pipe::MGPipeResourceTarget::Tex1DArray: upload=TextureUploadTarget::Texture1DArray; break; + case MG_Pipe::MGPipeResourceTarget::Tex2DArray: upload=TextureUploadTarget::Texture2DArray; break; + case MG_Pipe::MGPipeResourceTarget::TexCubeArray: upload=TextureUploadTarget::CubeMapArray; break; + case MG_Pipe::MGPipeResourceTarget::TexCube: + upload=static_cast(static_cast(TextureUploadTarget::CubeMapPositiveX)+face); break; + default: break; + } + for (Uint32 level=base+1;level(std::max(resource->extent.width>>level,1u)), + static_cast(std::max(resource->extent.height>>level,1u)), + static_cast(std::max(resource->depth>>level,1u))}; + store.NoteLevelDefined(key,static_cast(upload),static_cast(level),extent); + store.MarkLevelGpuDirty(key,static_cast(upload),static_cast(level),true); + } + } + } +#endif diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 6d947032..e19b7fea 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -113,7 +113,8 @@ namespace MobileGL::MG_Backend { switch (type) { case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e; case BackendType::DirectVulkan: - return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a & ~MG_Pipe::kMGPipeSubsystemResources; + return (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings) & + ~MG_Pipe::kMGPipeSubsystemResources; default: return 0; } } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 9acd5a4e..b46c8074 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -6077,6 +6077,10 @@ namespace MobileGL::MG_Impl::GLImpl { viewObject->SetInternalFormat(viewInternalFormat); viewObject->SetSamples(storageOwner->GetSamples()); viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations()); +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject); +#endif } void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) { diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index e9f7931a..07f1cea7 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -1392,8 +1392,10 @@ namespace MobileGL::MG_Pipe { // family here would be a NEW rule, and a client that withheld more than the server // refuses leaves the server's handle arm live with no records to read. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + // P5f fm: texture/framebuffer records have their own server consumers; + // Magma consumes them without claiming the still-unmigrated buffer family. return MG_Remote::Client::CapsMirrorInstance().ServerConsumes( - kMGPipeSubsystemResources); + subsystem & kMGPipeP4aFamilySubsystems); } #endif return MGPipeGetResourceOps() != nullptr; @@ -2832,6 +2834,9 @@ namespace MobileGL::MG_Pipe { Bool ApplierDerives = false; Bool ContextValuesWireLive = false; Bool P4aConsumer = false; +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 CapsGeneration = 0; +#endif // One bit per FIELD - not per verb class. The class mask is applied at the walk // exactly as it always was, so "does this verb read this field" stays the walk's // business and this stays a statement about EMISSION alone. @@ -2857,7 +2862,12 @@ namespace MobileGL::MG_Pipe { if (g_fillPlan.Valid && g_fillPlan.PushMask == pushMask && g_fillPlan.ApplierDerives == applierDerives && g_fillPlan.ContextValuesWireLive == contextValuesWireLive && - g_fillPlan.P4aConsumer == p4aConsumer) { + g_fillPlan.P4aConsumer == p4aConsumer +#if MOBILEGL_BUILD_DISAGGREGATED + && g_fillPlan.CapsGeneration == (MG_Config::Transport != MG_Config::TransportMode::Monolith ? + MG_Remote::Client::CapsMirrorInstance().Generation() : 0) +#endif + ) { return g_fillPlan.Supplied; } MGPipeFieldMask built{}; @@ -2898,6 +2908,10 @@ namespace MobileGL::MG_Pipe { g_fillPlan.ApplierDerives = applierDerives; g_fillPlan.ContextValuesWireLive = contextValuesWireLive; g_fillPlan.P4aConsumer = p4aConsumer; +#if MOBILEGL_BUILD_DISAGGREGATED + g_fillPlan.CapsGeneration = MG_Config::Transport != MG_Config::TransportMode::Monolith ? + MG_Remote::Client::CapsMirrorInstance().Generation() : 0; +#endif g_fillPlan.Supplied = built; return built; } diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 1f2a80d4..44c603f3 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2378,6 +2378,21 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5b-f1-${f1Slot}.log" ) endforeach() + # P5f fm: the framebuffer/copy/mipmap verbs now consume server records on Magma too. + mgl_itest_join_environment(MGL_ITEST_MAGMA_WIRE_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_TRANSPORT=inproc" + "MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) + foreach(fmSlot PartialTextureUploadPreservesGpuClear TextureViewClearTargetsItsRootMipAndLayer ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap GenerateMipmapPackedFloat GenerateMipmapDepth) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.Split.Fm." + TEST_FILTER "F1WireScenario.${fmSlot}Pixels" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-magma-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_MAGMA_WIRE_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm-${fmSlot}.log") + endforeach() endif() # P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, split-only source for the diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index acc02e95..8a8dce51 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -259,6 +259,84 @@ TEST_F(F1WireScenario, GenerateMipmapDepthPixels) { EXPECT_NEAR(pixel, 0.375f, 0.00001f); } +TEST_F(F1WireScenario, PartialTextureUploadPreservesGpuClearPixels) { + if (!Ready()) return; + Attach(GL_RGBA8); + // The client shadow is red. The GPU then changes every texel to green, so + // applying a later one-pixel upload as a whole shadow loses observable data. + std::array red{}; + for (size_t i = 0; i < red.size(); i += 4) { + red[i] = 255; + red[i + 3] = 255; + } + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, red.data()); + glClearColor(0, 1, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + const std::array blue{0, 0, 255, 255}; + glTexSubImage2D(GL_TEXTURE_2D, 0, 1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data()); + + std::array pixels{}; + glReadPixels(0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + for (int y = 0; y < 8; ++y) { + for (int x = 0; x < 8; ++x) { + const std::array expected = x == 1 && y == 2 + ? blue : std::array{0, 255, 0, 255}; + const size_t at = static_cast(y * 8 + x) * 4; + const std::array actual{pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]}; + EXPECT_EQ(actual, expected) << "GPU clear survived outside upload at " << x << ", " << y; + } + } +} + +TEST_F(F1WireScenario, TextureViewClearTargetsItsRootMipAndLayer) { + if (!Ready()) return; + GLuint root = 0, view = 0; + glGenTextures(1, &root); + glBindTexture(GL_TEXTURE_2D_ARRAY, root); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 2, GL_RGBA8, 8, 8, 2); + std::array red{}; + for (size_t i = 0; i < red.size(); i += 4) { + red[i] = 255; + red[i + 3] = 255; + } + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 2, + GL_RGBA, GL_UNSIGNED_BYTE, red.data()); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 4, 4, 2, + GL_RGBA, GL_UNSIGNED_BYTE, red.data()); + std::array blue{}; + for (size_t i = 0; i < blue.size(); i += 4) { + blue[i + 2] = 255; + blue[i + 3] = 255; + } + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 1, 0, 0, 0, 4, 4, 1, + GL_RGBA, GL_UNSIGNED_BYTE, blue.data()); + glGenTextures(1, &view); + glTextureView(view, GL_TEXTURE_2D, root, GL_RGBA8, 1, 1, 1, 1); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "view construction"; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + glClearColor(0, 1, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + + std::array pixel{}; + glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_EQ(pixel, (std::array{0, 255, 0, 255})) << "view reads its cleared window"; + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 1, 1); + glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_EQ(pixel, (std::array{0, 255, 0, 255})) << "view clear reaches root mip 1, layer 1"; + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 1, 0); + glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_EQ(pixel, (std::array{0, 0, 255, 255})) << "the neighboring root layer is preserved"; + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 0, 1); + glReadPixels(1, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_EQ(pixel, (std::array{255, 0, 0, 255})) << "the neighboring root mip is preserved"; + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0); + glDeleteTextures(1, &view); + glDeleteTextures(1, &root); +} + TEST_F(F1WireScenario, NamedBlitPreservesBindingsAndRestoresNextVerbsPixels) { if (!Ready()) return; Attach(GL_RGBA8); diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 5190a0b0..6074bd61 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -44,6 +44,8 @@ // server-role-only fixture: there the direct call is the only reset that exists). #include #include +#include +#include #endif #include @@ -1235,6 +1237,13 @@ namespace MobileGL::MG_Pipe { // site: ApplyUnitWindow validates and writes in one step, so the question has to be // asked in front of it. Bool NoP4aConsumer() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* session = MG_Remote::Server::ServerSession::Active(); + if (session && session->CallMaskIsSet() && MG_Remote::MGCapsServerConsumes( + session->CallMask(), kMGPipeSubsystemTextureResources)) return false; + } +#endif if (g_resourceOps != nullptr) return false; ++g_applier.RefusedNoConsumer; return true; diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 8e5ce01b..419a0e5d 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -851,6 +851,8 @@ namespace MobileGL::MG_Pipe { UnorderedMap StreamOutputSpans; // generate_mipmap's texture (MGPMipPlan::Res). MGPipeHandle VerbMipRes = kMGPipeNullHandle; + Uint16 VerbMipBaseLevel = 0; + Uint16 VerbMipLevelCount = 0; // The current indirect draw's command buffer and (for the *Count forms) parameter // buffer (MGPDrawIndirect::Buffer / ParameterBuffer). MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle; @@ -875,6 +877,7 @@ namespace MobileGL::MG_Pipe { VerbCopyTexDst = kMGPipeNullHandle; VerbStorageBlockProgram = kMGPipeNullHandle; VerbMipRes = kMGPipeNullHandle; + VerbMipBaseLevel = VerbMipLevelCount = 0; VerbIndirectBuffer = kMGPipeNullHandle; VerbIndirectParameterBuffer = kMGPipeNullHandle; VerbDispatchIndirectBuffer = kMGPipeNullHandle; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 157c9a19..426e6c0b 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -1047,6 +1047,8 @@ namespace MobileGL::MG_Remote::Server { auto& applierState = MG_Pipe::MGPipeApplier(); applierState.ClearVerbHandles(); applierState.VerbMipRes = plan.Res; + applierState.VerbMipBaseLevel = plan.BaseLevel; + applierState.VerbMipLevelCount = plan.LevelCount; #endif table->GL.GenerateMipmap(plan.Target); return true; diff --git a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp index c84f6f6d..229e58a3 100644 --- a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp +++ b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp @@ -44,6 +44,9 @@ #include #include #endif +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif using namespace MobileGL; @@ -258,4 +261,99 @@ namespace { "kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10"); } #endif // MOBILEGL_PIPE_PUSH + + TEST(MagmaProgramSourceTest, ServerBindingTailsReplaceLinkTimeDefaults) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto archive = MakeShared(); + archive->Link.glBlockIndexToTProgram = {0}; + archive->Link.blockReflection.resize(1); + archive->Link.blockReflection[0].name = "Block"; + archive->Link.blockReflection[0].size = 12; + archive->Link.uniformBlockBinding = {9}; + archive->Link.uniformSamplerOrImageUnitIndex = {12, 13}; + archive->Spirv.globalUboScratch = {99, 98}; + MG_Pipe::MGPipeShaderCsoRecord record{}; + record.Archive = archive; + record.BlockBindings = {3}; + record.SamplerUnits = {{0, 4}}; + record.GlobalConstants = {5, 6}; + record.GlobalConstantsVersion = 11; + const MagmaProgramSource source({7, 2}, record); + EXPECT_TRUE(source.IsWire()); + EXPECT_EQ(source.Frontend(), nullptr); + EXPECT_EQ(source.GetUniformBlockBinding(0), 3u); + EXPECT_EQ(source.GetUniformBlockName(0), "Block"); + EXPECT_EQ(source.GetUBOSizeAt(0), 16u); + EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(0), 4); + EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(1), -1) + << "an absent tail entry must not resurrect the archive's stale unit"; + ASSERT_EQ(source.GetUBOSize(), 2u); + EXPECT_EQ(static_cast(source.GetUBOData())[0], 5u); + EXPECT_EQ(source.GetUBOContentVersion(), 11u); + record.BlockBindings[0] = 6; + record.SamplerUnits[0].Unit = 8; + record.GlobalConstants[0] = 42; + record.GlobalConstantsVersion = 12; + EXPECT_EQ(source.GetUniformBlockBinding(0), 6u); + EXPECT_EQ(source.GetUniformSamplerOrImageUnitIndex(0), 8); + EXPECT_EQ(static_cast(source.GetUBOData())[0], 42u); + EXPECT_EQ(source.GetUBOContentVersion(), 12u); +#else + GTEST_SKIP() << "server program sources require the disaggregated build"; +#endif + } + + TEST(MagmaProgramSourceTest, ArrayUniformNamesResolveWithinTheArchivedUniform) { +#if MOBILEGL_BUILD_DISAGGREGATED + auto archive = MakeShared(); + auto& link = archive->Link; + link.maxUniformLocation = 2; + link.uniformLocations["arr[0]"] = 0; + link.uniformIndexInTProgram = {0, 0, 0}; + link.tProgramUniformIndexToGl = {0}; + link.uniformReflection.resize(1); + link.uniformReflection[0].type.isArray = true; + link.uniformReflection[0].arraySize = 3; + link.uniformReflection[0].glDefineType = GL_SAMPLER_2D; + MG_Pipe::MGPipeShaderCsoRecord record{}; + record.Archive = archive; + const MagmaProgramSource source({8, 1}, record); + EXPECT_EQ(source.GetUniformLocation("arr"), 0); + EXPECT_EQ(source.GetUniformLocation("arr[2]"), 2); + EXPECT_EQ(source.GetUniformLocation("arr[3]"), -1); + EXPECT_EQ(source.GetUniformLocation("arr[-1]"), -1); + EXPECT_EQ(source.GetUniformLocation("arr[999999999999999]"), -1); + EXPECT_EQ(source.GetUniformLocation("unknown"), -1); + EXPECT_TRUE(source.UniformLocationsAliasSameUniform(0, 2)); + EXPECT_FALSE(source.UniformLocationsAliasSameUniform(0, 3)); + EXPECT_EQ(source.GetUniformType(2), GL_SAMPLER_2D); +#else + GTEST_SKIP() << "server program sources require the disaggregated build"; +#endif + } + + TEST(MagmaProgramSourceTest, ReusedHandlesAndRecordVersionsHaveDistinctCacheIdentity) { +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeShaderCsoRecord record{}; + record.Archive = MakeShared(); + record.Serial = 17; + record.BindingsSerial = 21; + record.GlobalConstantsVersion = 25; + const MagmaProgramSource first({9, 2}, record); + const MagmaProgramSource recycled({9, 3}, record); + EXPECT_NE(first.GetLifetimeId(), recycled.GetLifetimeId()); + EXPECT_EQ(first.Handle(), (MG_Pipe::MGPipeHandle{9, 2})); + const auto identity = first.GetLifetimeId(); + record.Serial = 18; + record.BindingsSerial = 22; + record.GlobalConstantsVersion = 26; + EXPECT_EQ(first.GetLifetimeId(), identity) << "content changes do not mint object identities"; + EXPECT_EQ(first.GetBackendStateVersion(), 18u); + EXPECT_EQ(first.GetBlockBindingVersion(), 22u); + EXPECT_EQ(first.GetImageUnitVersion(), 22u); + EXPECT_EQ(first.GetUBOContentVersion(), 26u); +#else + GTEST_SKIP() << "server program sources require the disaggregated build"; +#endif + } } // namespace