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/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 70cd3c78..552251d3 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -9,6 +9,9 @@ #pragma once #include #include "MG_State/GLState/TextureState/TextureEnum.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif namespace MobileGL { namespace MG_State::GLState { @@ -33,6 +36,9 @@ namespace MobileGL { struct CopyImageEndpoint { SharedPtr Texture; SharedPtr Renderbuffer; +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle; +#endif Bool IsRenderbuffer() const { return Renderbuffer != nullptr; } Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; } diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index f1629990..fcca12de 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -12297,8 +12297,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never // reaches here - but the assertion that says so is compiled out of a release build, and // SyncTextureObjectToBackend would register a null state object. +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + out.texture = TextureImpl::SyncTextureToBackendByHandle(endpoint.TextureHandle); + } else +#endif + { if (!endpoint.Texture) return false; out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture); + } if (!out.texture) return false; const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); @@ -12316,6 +12323,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* record = PipeTextureRecordForHandle(endpoint.TextureHandle); + return record ? static_cast(record->Desc.InternalFormat) + : TextureInternalFormat::Unknown; + } +#endif if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat(); return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; } 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/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 5d3b3640..39f972ac 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1806,8 +1806,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // the bytes were actually staged). // // What this arm deliberately does NOT have, and the loud answer each gets: - // * GL texture VIEWS (desc.ViewOf non-null): no view chain crosses the wire yet - - // declined, the texture stays unbacked; // * 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; @@ -2017,11 +2015,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { desc.Resource.Slot, desc.Resource.Gen); return false; } - if (!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) { - MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: GL texture views do not cross the wire yet; 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; @@ -2086,7 +2082,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) { imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; } - if (storageImageCapable && IsMutableStorageImageFormat(format) && + // 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; } @@ -2303,6 +2302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint16 level = 0; Uint32 baseArrayLayer = 0; IntVec3 texelSize = {0, 0, 0}; + VkOffset3D imageOffset{}; const void* source = nullptr; SizeT byteSize = 0; Vector owned; @@ -2353,13 +2353,75 @@ namespace MobileGL::MG_Backend::DirectVulkan { incomplete = true; continue; } - // Each VkBufferImageCopy offset must satisfy the format's texel-block alignment - // as well as Vulkan's four-byte rule. All supported blocks divide 16 bytes; - // AcquireUploadStagingSpace aligns the batch base to at least this boundary. - stagingSize = (stagingSize + 15u) & ~VkDeviceSize{15u}; - item.offset = stagingSize; - stagingSize += static_cast(item.byteSize); - items.push_back(Move(item)); + // 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()) { @@ -2413,7 +2475,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { copy.imageSubresource.mipLevel = item.level; copy.imageSubresource.baseArrayLayer = item.baseArrayLayer; copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1; - copy.imageOffset = {0, 0, 0}; + copy.imageOffset = item.imageOffset; copy.imageExtent = {static_cast(item.texelSize.x()), static_cast(item.texelSize.y()), depthSelectsArrayLayer ? 1u : depthOrLayers}; @@ -2459,6 +2521,54 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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"); @@ -2466,6 +2576,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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()) { @@ -2500,6 +2628,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { 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)) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index ac28bb1d..6b5e4543 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -551,9 +551,14 @@ public: // // 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. Textures whose GL object is a VIEW of another - // (desc.ViewOf) or whose storage is a buffer are declined here: no view chain crosses today. + // 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 @@ -562,6 +567,10 @@ public: // 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, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index dfde03b1..4bda914e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -9784,6 +9784,10 @@ void main() { srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.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; @@ -9937,6 +9941,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 @@ -9963,7 +9971,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; @@ -9982,6 +9994,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; @@ -10016,6 +10043,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 @@ -10144,6 +10177,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); } @@ -10255,6 +10291,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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index d6ff6460..e2adf6a3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -70,6 +70,7 @@ 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) { @@ -165,6 +166,8 @@ 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(surface.Res, image.level, image.layer, image.layers); if (image.isDefault) { if (color) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true); @@ -185,6 +188,7 @@ 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 : @@ -238,6 +242,7 @@ 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); @@ -270,6 +275,8 @@ vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,®ion, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); + if (!destination.isDefault && dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + m_textureManager->MarkWireTextureGpuWritten(dst.Res, destination.level, destination.layer, destination.layers); if (destination.isDefault) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired,true); }; if (mask & GL_COLOR_BUFFER_BIT) { @@ -290,6 +297,7 @@ "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"); @@ -319,9 +327,11 @@ } 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(state.VerbCopyTexDst,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); diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 3769e5f6..2cf4ba8f 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; @@ -2814,6 +2816,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. @@ -2839,7 +2844,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{}; @@ -2880,6 +2890,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_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 34ce71d0..06691198 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -846,6 +846,7 @@ namespace MobileGL::MG_Pipe { Bool VerbBlitNamedConsumed = false; // copy_framebuffer_to_texture's destination texture (MGPCopyFromFramebuffer::Dst). MGPipeHandle VerbCopyTexDst = kMGPipeNullHandle; + MGPipeHandle VerbStorageBlockProgram = kMGPipeNullHandle; // generate_mipmap's texture (MGPMipPlan::Res). MGPipeHandle VerbMipRes = kMGPipeNullHandle; Uint16 VerbMipBaseLevel = 0; @@ -872,6 +873,7 @@ namespace MobileGL::MG_Pipe { VerbBlitDrawFbo = kMGPipeNullHandle; VerbBlitNamedConsumed = false; VerbCopyTexDst = kMGPipeNullHandle; + VerbStorageBlockProgram = kMGPipeNullHandle; VerbMipRes = kMGPipeNullHandle; VerbMipBaseLevel = VerbMipLevelCount = 0; VerbIndirectBuffer = kMGPipeNullHandle; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index acdcac71..ae502ab3 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -855,9 +855,9 @@ namespace MobileGL::MG_Remote::Server { // retires them by making the backend take the handles that travel beside the names. MG_Backend::CopyImageEndpoint src{}; MG_Backend::CopyImageEndpoint dst{}; - src.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast(copy.SrcGlName)); - dst.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast(copy.DstGlName)); - if (!src.Exists() || !dst.Exists()) { + src.TextureHandle = copy.Src; + dst.TextureHandle = copy.Dst; + if (MG_Pipe::MGPipeHandleIsNull(copy.Src) || MG_Pipe::MGPipeHandleIsNull(copy.Dst)) { // The monolith's own answer to this, in its own words (DirectGLES.cpp:9067 // "source or destination image failed to sync; declining the copy"): the frontend // validator is what keeps it unreachable and what reports the INVALID_VALUE the @@ -921,6 +921,8 @@ namespace MobileGL::MG_Remote::Server { // Both backends resolve the PROGRAM through the barrier-pulled GetProgramObject(GlName) // / TryGetDirectVulkanProgram - `rsp` again, retired by P9. ShaderCso travels beside the // name for the phase that dispatches on it. + MG_Pipe::MGPipeApplier().ClearVerbHandles(); + MG_Pipe::MGPipeApplier().VerbStorageBlockProgram = binding.ShaderCso; table->GL.ShaderStorageBlockBinding(static_cast(binding.GlName), name, static_cast(binding.Binding)); ++m_storageBlockBindings;