diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b776965a..2b56206c 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -11625,51 +11625,49 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - // P5c (hd), P5e (tx2): the verification half, with the RECORD passed in instead of re-read - // from the verb stash - the caller has it and the two must not disagree about which texture - // is being verified. - static void EnsureGenerateMipmapStorageDescribed(const MG_Pipe::MGPipeResourceRecord& record) { + // P8-B1: THE LEVELS A generate_mipmap RECORD GENERATES, end-exclusive. GL generates levels + // BASE_LEVEL+1 .. q, q = min(p, MAX_LEVEL) with p the last level the base image's own chain + // reaches, and an immutable texture's storage bounds the chain too (GL 4.6 core 8.14.4). The + // client already computed that window (ComputeMipmapGenerationRange) and defined its levels + // before emitting the verb, so the record's plan is the window and the descriptor is what + // holds it: Base = VerbMipBaseLevel, End = min(VerbMipLevelCount, Desc.Levels, MaxLevel + 1, + // Base + the base extent's chain) - the same clip Magma's GenerateWireMipmap makes. + // + // What stood here instead demanded the full chain from LEVEL 0 and died with + // Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the descriptor held less: a + // MAX_LEVEL below the chain, or glTexStorage2D with fewer levels, both legal GL. + struct RecordMipWindow { + Uint32 Base = 0; + Uint32 End = 0; // end-exclusive; End <= Base + 1 generates nothing + }; + + static RecordMipWindow GenerateMipmapWindowByRecord(const MG_Pipe::MGPipeResourceRecord& record) { + const auto& applier = MG_Pipe::MGPipeApplier(); const auto& desc = record.Desc; - if (desc.Width == 0 || desc.Levels == 0) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); - } - Uint maxDimension = desc.Width; - const auto target = static_cast(desc.Target); - if (target != MG_Pipe::MGPipeResourceTarget::Tex1D && - target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) { - maxDimension = std::max(maxDimension, desc.Height); - } - if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) { - maxDimension = std::max(maxDimension, desc.Depth); - } - Uint requiredLevels = 1; - while (maxDimension > 1) { - maxDimension /= 2; - ++requiredLevels; - } - if (desc.Levels < requiredLevels) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); + RecordMipWindow window{applier.VerbMipBaseLevel, applier.VerbMipBaseLevel}; + if (desc.Width == 0 || desc.Levels == 0 || window.Base >= desc.Levels) return window; + Uint32 end = std::min(applier.VerbMipLevelCount, desc.Levels); + if (record.ParamsSerial) end = std::min(end, static_cast(record.Params.MaxLevel) + 1u); + const IntVec3 baseExtent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, window.Base); + const Int shrinking = MG_Remote::Server::StagedTextureShrinkingAxisCount(desc.Target); + Int largest = 1; + for (Int axis = 0; axis < shrinking && axis < 3; ++axis) largest = std::max(largest, baseExtent[axis]); + Uint32 chain = 1; + while (largest > 1) { + largest /= 2; + ++chain; } + window.End = std::max(window.Base, std::min(end, window.Base + chain)); + return window; } #endif static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr& texture) { -#if MOBILEGL_BUILD_DISAGGREGATED - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - // The frontend has already defined the generated chain before emitting this verb. - // Its resource_respecify carries that shape, so the server only verifies the - // descriptor; it must not allocate or dirty the client's level shadows again. - // P5c (hd): identity is the handle the generate_mipmap record carried - // (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2). - const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes; - const auto* record = PipeTextureRecordForHandle(handle); - if (record == nullptr) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); - } - EnsureGenerateMipmapStorageDescribed(*record); - return false; // No server-side shadow allocation was necessary. - } -#endif + // P8-B1: the transport arm that stood here (a descriptor check under the same + // `generate-mipmap-storage` name) was unreachable - GenerateMipmap returns through + // GenerateMipmapByRecord before this call on every split arm - and the record arm's + // window above replaces the check it made. auto* mipmapTexture = dynamic_cast(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage."); Bool allocatedStorage = false; @@ -12403,11 +12401,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as // barriered), and the handle is now simply passed in - the extent derivation, the per-level // blits and the format question are §1's descriptor reads exactly as P5c wrote them. + // + // P8-B1: both chains run over the record's WINDOW (GenerateMipmapWindowByRecord), not from + // level 1 to the end of the descriptor: a BASE_LEVEL above 0 is the source and a level past + // MAX_LEVEL is left as it is, which is what the monolith twins below still get wrong. static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture) { + const SharedPtr& backendTexture, + const RecordMipWindow& window) { const auto& desc = record.Desc; const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = 1; level < desc.Levels; ++level) { + for (Uint32 level = window.Base + 1; level < window.End; ++level) { const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level - 1); const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, @@ -12420,12 +12423,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, - const SharedPtr& backendTexture) { + const SharedPtr& backendTexture, + const RecordMipWindow& window) { const auto& desc = record.Desc; const GLenum filter = IsIntegerColorFormat(static_cast(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR; const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = 1; level < desc.Levels; ++level) { + for (Uint32 level = window.Base + 1; level < window.End; ++level) { const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level - 1); const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, @@ -12879,65 +12883,238 @@ namespace MobileGL::MG_Backend::DirectGLES { } #if MOBILEGL_BUILD_DISAGGREGATED + // P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL. + // + // ES generates a chain only for a colour-renderable, filterable format, and whether the + // driver's storage for these two is one depends on the driver: Espryt stores them natively + // where the driver renders the three-channel form and widens them to RGBA16F / RGBA32F where + // it does not (TextureImpl::BackendTextureFormatAddsAlpha), and a widened RGBA32F still needs + // OES_texture_float_linear to be filterable. So GenerateMipmapByRecord asks the driver first + // and comes here only when it refuses - or when MGITEST_ESPRYT_FORCE_CPU_MIPMAP says to, which + // is the only way a host whose driver never refuses reaches this arm. + // + // THE SOURCE IS THE SERVER'S LEVEL, NEVER THE CLIENT'S. The base level is read back from the + // driver (ReadTextureLevelTight), because a render target's texels exist only there - the + // monolith arm (GenerateThreeChannelFloatMipmapOnCpu) filters the frontend's level shadow + // instead and reads a stale base after a draw. Only when the driver cannot read the level back + // (storage it cannot attach) does the staged store answer, and a store that does not cover the + // level declines by name rather than inventing texels. + static Bool ForcedServerCpuMipmap() { + // The R-16 control. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs. + static const Bool s_forced = std::getenv("MGITEST_ESPRYT_FORCE_CPU_MIPMAP") != nullptr; + return s_forced; + } + + // The base level as tight RGB floats: the driver's texels first, the staged store second. + static Bool ReadThreeChannelFloatLevelOnServer(MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + TextureTarget textureTarget, TextureUploadTarget face, Uint32 level, + const IntVec3& extent, Vector& rgb) { + const auto format = static_cast(record.Desc.InternalFormat); + const SizeT texels = static_cast(extent.x()) * static_cast(extent.y()); + Vector bytes; + if (ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format, + static_cast(level), extent, backendTexture->RequiresImageBindableStorage(), + GL_RGB, GL_FLOAT, bytes) && + bytes.size() == texels * 3 * sizeof(Float)) { + rgb.resize(texels * 3); + Memcpy(rgb.data(), bytes.data(), bytes.size()); + return true; + } + // A view's staged bytes live under its storage owner's key, in the owner's levels; a view + // whose level the driver cannot read has no answer here. + if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) return false; + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(mipRes); + const auto faceCode = static_cast(face); + const auto levelCode = static_cast(level); + if (!store.CopiesIntoServerStorage() || !store.IsCovered(key, faceCode, levelCode)) return false; + const Bool half = format == TextureInternalFormat::RGB16F; + const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float); + if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) return false; + const Uint8* staged = store.RequireLevelBytes(key, faceCode, levelCode, "generate-mipmap-server-cpu"); + rgb.resize(texels * 3); + for (SizeT i = 0; i < texels * 3; ++i) { + if (half) { + Uint16 bits = 0; + Memcpy(&bits, staged + i * componentBytes, sizeof(bits)); + rgb[i] = MG_Util::DecodeHalfBitsToFloat(bits); + } else { + Memcpy(&rgb[i], staged + i * componentBytes, sizeof(Float)); + } + } + MGLOG_W_ONCE("MGPipe: generate-mipmap-server-cpu: texture {%u, %u} level %u could not be read back from " + "the driver; filtering the staged store's copy of it instead", + mipRes.Slot, mipRes.Gen, level); + return true; + } + + static void GenerateThreeChannelFloatMipmapOnServer(MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture, + const char* why) { + const auto& desc = record.Desc; + const auto format = static_cast(desc.InternalFormat); + const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target); + const RecordMipWindow window = GenerateMipmapWindowByRecord(record); + if (window.End <= window.Base + 1) return; + Vector faces; + if (textureTarget == TextureTarget::Texture2D) { + faces.push_back(TextureUploadTarget::Texture2D); + } else if (textureTarget == TextureTarget::TextureCubeMap) { + for (Int face = 0; face < 6; ++face) + faces.push_back(static_cast( + static_cast(TextureUploadTarget::CubeMapPositiveX) + face)); + } + // The upload spelling the driver's storage takes: GL_FLOAT is legal for all four float + // internal formats (ES 3.2 table 8.2), and the channel count follows the storage. + GLenum esInternal = GL_NONE, esFormat = GL_NONE, esType = GL_NONE; + TextureImpl::GenerateTextureFormatInfo(format, &esInternal, &esFormat, &esType, textureTarget); + const Bool fourChannel = esInternal == GL_RGBA16F || esInternal == GL_RGBA32F; + if (faces.empty() || (!fourChannel && esInternal != GL_RGB16F && esInternal != GL_RGB32F)) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: target %s stored as %s has " + "no server CPU arm (%s); levels %u..%u are left as they are", + mipRes.Slot, mipRes.Gen, MG_Util::ConvertTextureTargetToString(textureTarget).c_str(), + MG_Util::ConvertGLEnumToString(esInternal).c_str(), why, window.Base + 1, window.End - 1); + return; + } + MGLOG_I_ONCE("MGPipe: generate-mipmap-server-cpu: %s levels filtered on the server's CPU from its own " + "level (%s)", MG_Util::ConvertTextureInternalFormatToString(format).c_str(), why); + + const GLuint textureId = backendTexture->GetBackendTextureId(); + const GLenum bindTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget); + for (const TextureUploadTarget face : faces) { + IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, window.Base); + Vector level; + if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base, + extent, level)) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u is " + "neither readable from the driver nor covered by the staged store; levels %u..%u are " + "left as they are", + mipRes.Slot, mipRes.Gen, window.Base, window.Base + 1, window.End - 1); + return; + } + for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) { + const IntVec3 dstExtent = + MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, dst); + // The monolith's box filter: the 2x2 source footprint, clamped where a dimension + // is already 1 (GL 4.6 core 8.14.4 leaves the filter to the implementation). + Vector next(static_cast(dstExtent.x()) * static_cast(dstExtent.y()) * 3); + const auto at = [&](Int x, Int y, Int c) { + return level[(static_cast(y) * static_cast(extent.x()) + static_cast(x)) * 3 + + static_cast(c)]; + }; + for (Int y = 0; y < dstExtent.y(); ++y) { + for (Int x = 0; x < dstExtent.x(); ++x) { + const Int x0 = std::min(x * 2, extent.x() - 1), x1 = std::min(x * 2 + 1, extent.x() - 1); + const Int y0 = std::min(y * 2, extent.y() - 1), y1 = std::min(y * 2 + 1, extent.y() - 1); + for (Int c = 0; c < 3; ++c) + next[(static_cast(y) * static_cast(dstExtent.x()) + static_cast(x)) * + 3 + static_cast(c)] = + 0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c)); + } + } + Vector upload; + if (fourChannel) { + const SizeT count = next.size() / 3; + upload.resize(count * 4); + for (SizeT i = 0; i < count; ++i) { + upload[i * 4] = next[i * 3]; + upload[i * 4 + 1] = next[i * 3 + 1]; + upload[i * 4 + 2] = next[i * 3 + 2]; + upload[i * 4 + 3] = 1.0f; + } + } + // Tight float rows are a multiple of four bytes, which the resting UNPACK state + // (4/0/0/0, and the unpack-PBO binding at 0) parses as they are - CopyR32FTexture2D's + // discipline, including the re-bind that keeps the binding cache truthful. + ClearGLErrors(); + BufferImpl::BindPixelUnpackBufferId(0); + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + g_GLESFuncs.glBindTexture(bindTarget, textureId); + g_GLESFuncs.glTexSubImage2D(TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(face), + static_cast(dst), 0, 0, static_cast(dstExtent.x()), + static_cast(dstExtent.y()), fourChannel ? GL_RGBA : GL_RGB, + GL_FLOAT, fourChannel ? upload.data() : next.data()); + const GLenum err = g_GLESFuncs.glGetError(); + auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(textureTarget)]; + g_GLESFuncs.glBindTexture(bindTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); + if (err != GL_NO_ERROR) { + MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu: uploading level %u of texture {%u, %u} " + "failed: %s", dst, mipRes.Slot, mipRes.Gen, + MG_Util::ConvertGLEnumToString(err).c_str()); + return; + } + level = Move(next); + extent = dstExtent; + } + } + } + // P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from // VerbMipRes. Same four arms as the frontend body, with every shape question answered from // the descriptor. // - // RULING 14, AND WHY THE ROW IS NOT FLIPPED HERE. The unit read this arm retires is what the - // wait existed for, but ONE frontend contact is left on the path and it is not tx2's to - // retire: the RGB16F/RGB32F CPU filter reaches into the client's level shadows - // (GenerateThreeChannelFloatMipmapOnCpu, kimi audit row 61) and P8/P9 own the pull. So the - // brief's escape applies - the arm is not complete, the row stays kWaitApplied and the flip - // is listed as trailing - and the record stays barriered, which is what keeps the CPU arm's - // refusal a refusal rather than a wrong picture. + // RULING 14's TRAILING FLIP. tx2 kept this verb's PipeCalls.def row at kWaitApplied because + // one frontend contact was left on the path: the RGB16F/RGB32F CPU filter reached into the + // client's level shadows. P8-B2 retired it (GenerateThreeChannelFloatMipmapOnServer reads the + // server's own level), so no arm here reads client memory any more; the WaitClass flip itself + // is a protocol change and is left to the integrator. static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes, const MG_Pipe::MGPipeResourceRecord& record, const SharedPtr& backendTexture) { const auto format = static_cast(record.Desc.InternalFormat); const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target); + const Bool threeChannelFloat = + format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F; - if (format == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(format) || - format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) { - // The storage verification EnsureGenerateMipmapStorageAllocated's disaggregated arm - // already makes, with the record passed in rather than re-read from VerbMipRes: the - // frontend defined the generated chain before emitting this verb, so the server only - // checks that the descriptor it was given can carry it. - EnsureGenerateMipmapStorageDescribed(record); - } - if (format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) { - // The CPU filter reads and writes the CLIENT's level shadows. It already aborts - // three frames deep at the MipmapStorage guard under a transport; naming it here - // says which emulation, which is what the guard cannot. - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-filter"); - } // A view's layer window is the one bound the native arm cannot express, so it is asked // for before the format arms - those two are 2D-only emulations of the same generation // and a view reaches them too. if (GenerateMipmapThroughViewWindowByRecord(mipRes, record, format)) return; if (IsDepthOnlyFormat(format)) { - GenerateDepthTexture2DMipmapByRecord(record, backendTexture); + GenerateDepthTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); return; } if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) { - GenerateColorTexture2DMipmapByRecord(record, backendTexture); + GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record)); + return; + } + if (threeChannelFloat && ForcedServerCpuMipmap()) { + GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, + "forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP"); return; } const GLenum backendTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); - // TempTextureUnit, not the active unit: the scratch bind is the backend's own and - // BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit - // was never anything but the frontend's way of naming the texture, and the record names - // it now. - backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit); - // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the - // source texture is detached from every framebuffer that attaches it - by handle, over - // the applier's own framebuffer records. - ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes); - ScopedCompleteFramebufferBinding completeFramebuffer; - ClearGLErrors(); - g_GLESFuncs.glGenerateMipmap(backendTarget); - RecordGLError("glGenerateMipmap", backendTarget, format); + GLenum refused = GL_NO_ERROR; + { + // TempTextureUnit, not the active unit: the scratch bind is the backend's own and + // BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit + // was never anything but the frontend's way of naming the texture, and the record + // names it now. + backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit); + // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the + // source texture is detached from every framebuffer that attaches it - by handle, + // over the applier's own framebuffer records. + ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes); + ScopedCompleteFramebufferBinding completeFramebuffer; + ClearGLErrors(); + g_GLESFuncs.glGenerateMipmap(backendTarget); + if (!threeChannelFloat) { + RecordGLError("glGenerateMipmap", backendTarget, format); + return; + } + // The driver's refusal of a three-channel float chain is not the application's + // error: the CPU arm below answers it. + refused = g_GLESFuncs.glGetError(); + } + if (refused == GL_NO_ERROR) return; + GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, "the driver refused glGenerateMipmap"); } #endif @@ -12965,7 +13142,8 @@ namespace MobileGL::MG_Backend::DirectGLES { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes; // Every arm GenerateMipmapByRecord takes writes the levels above the base on the - // driver only (its CPU filter is unmigrated and aborts), so the note is unconditional. + // driver only (P8-B2's CPU arm uploads its levels there too), so the note is + // unconditional. TextureImpl::NoteDriverSideTextureWriteByHandle(mipRes); auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes); const auto* record = PipeTextureRecordForHandle(mipRes); @@ -14988,10 +15166,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // Session::Fail entirely, which is the funnel the census gate watches. // // THE FUNNEL KEEPS ITS TEETH FOR THE OTHER SITES: MGPipeUnmigratedEmulation still - // aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback and - // generate-mipmap-cpu-filter, which really do reach into a client address space. Only - // this call site is retired here, and only because its own premise did not hold - // (texture-remint-pull went the same way in P9 W2, for the same reason). + // names generate-mipmap-storage, generate-mipmap-cpu-fallback and + // copy-image-shadow-mirror, on monolith arms only. Only this call site is retired here, + // and only because its own premise did not hold (texture-remint-pull went the same way + // in P9 W2, and P8-B the two split generate-mipmap names, for the same reason). if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { if (shadow == nullptr) { MGLOG_E_ONCE("GetTexImage: no level shadow on this side for target=0x%x level=%d, and under an " diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 48c3874f..f8851619 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -111,6 +111,9 @@ add_executable(MobileGLIntegrationTest Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageFormatQualifierScenario.cpp Scenarios/TextureRemintPullScenario.cpp + # P8-B: glGenerateMipmap's window and RGB16F / RGB32F on Espryt's server. Ordinary GL, so the + # monolith arms of both backends run it too. + Scenarios/GenerateMipmapServerScenario.cpp Scenarios/NonCoreImageFormatScenario.cpp Scenarios/ImageSizeAfterRespecScenario.cpp Scenarios/EmptyTextureLevelScenario.cpp @@ -3360,6 +3363,34 @@ if (MOBILEGL_BUILD_DISAGGREGATED) TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_STRICT_ENVIRONMENT}" ) + + # P8-B: glGenerateMipmap ON ESPRYT'S SERVER (notes/p8/B.md). The window a record names (B1) and + # RGB16F / RGB32F (B2) on the three Espryt arms; the Magma control is the gated + # `DirectVulkan..Mip.` block beside `Remint.`, and both monoliths come from the ambient + # discovery. The FORCED-CPU entries run the three-channel cases with + # MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1, which the SERVER reads: inproc and spawn only, because the + # tcp arm's server is the one shared TcpServer.Start fixture and the knob there would take the + # native arm off every tcp entry. Their own label keeps the spawn-parity sets knob-free, as the + # `RemintFallback.` entries do; integration-gpu is what runs them in the gate. + mgl_itest_register_split_arms("GenerateMipmapServerScenario.*" "") + foreach(mglItestArm IN ITEMS Split Spawn) + string(TOUPPER "${mglItestArm}" mglItestArmUpper) + string(TOLOWER "${mglItestArm}" mglItestArmLower) + foreach(cpuMipCase Rgb16fUploadedBaseGeneratesItsChain + Rgb16fRenderTargetBaseGeneratesFromTheGpuWrittenLevel + Rgb32fUploadedBaseGeneratesItsChain + Rgb32fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.${mglItestArm}.CpuMip." + TEST_FILTER "GenerateMipmapServerScenario.${cpuMipCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\\;integration-cpu-mip-${mglItestArmLower}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_${mglItestArmUpper}_ENVIRONMENT}\;MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p8b-cpu-mip-${mglItestArmLower}-${cpuMipCase}.log") + endforeach() + endforeach() + # The configure_file that used to sit here MOVED TO THE END OF THIS FILE (P6 `t6`). # # It substitutes @MGL_SPLIT_ARM_TEST_LISTS@, which the registration macro appends to - so @@ -4065,6 +4096,35 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() endif() +# P8-B: THE MAGMA CONTROL for GenerateMipmapServerScenario, all three arms, GATED, in the band +# flavour. Magma generates every case on the GPU (GenerateWireMipmap), so these entries say what +# the Espryt arms above must match. One entry per case for a private log each. +if (MOBILEGL_BUILD_DISAGGREGATED) + foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS) + mgl_itest_magma_arm_shape(${mglItestArm}) + mgl_itest_join_environment(mglItestMagmaMipEnvironment + ${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + foreach(mipCase PackedFloatMutableMaxLevelBelowTheChain + PackedFloatStorageWithFewerLevelsThanTheChain + PackedFloatMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow + DepthMutableMaxLevelBelowTheChain + DepthStorageWithFewerLevelsThanTheChain + DepthMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow + Rgb16fUploadedBaseGeneratesItsChain + Rgb16fRenderTargetBaseGeneratesFromTheGpuWrittenLevel + Rgb32fUploadedBaseGeneratesItsChain + Rgb32fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Mip." + TEST_FILTER "GenerateMipmapServerScenario.${mipCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaMipEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p8b-mip${mglItestArmLogTag}-${mipCase}.log") + endforeach() + endforeach() +endif() + # P11 M: DEV'S TWO GPU-CONTENT SCENARIOS ON MAGMA, all three arms, GATED, in the band flavour - # ImageBindableRemintScenario (Magma is its control backend: a storage upgrade copies image to # image) and PartialUploadOverGpuContentScenario (8966d5c2's RGB->RGBA region cut and b1d60523's diff --git a/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp new file mode 100644 index 00000000..dadb194b --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp @@ -0,0 +1,460 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - glGenerateMipmap ON THE FORMATS ESPRYT DOES NOT HAND STRAIGHT TO THE DRIVER +// (P8-B, docs/Disaggregated/notes/p8/B.md). +// +// Espryt generates R11F_G11F_B10F and depth chains with per-level blits, and RGB16F / RGB32F +// chains outside glGenerateMipmap because ES refuses a three-channel float format there. Until +// P8-B the split arms of both were named refusals: +// B1 Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the texture's descriptor +// held fewer levels than the full chain from level 0 - a MAX_LEVEL below the chain, or a +// glTexStorage2D with fewer levels - which is legal GL (the generated window is +// BASE_LEVEL+1 .. min(p, MAX_LEVEL), and immutable storage bounds it too); +// B2 Fatal{UnmigratedEmulation, "generate-mipmap-cpu-filter"} on every RGB16F / RGB32F +// generation, because the monolith filters the frontend's level shadows on the CPU. +// +// EVERY CASE CHECKS THE GL ANSWER BY SAMPLING. The base level is split into two halves (bottom +// rows A, top rows B) so a generation that loses the row order, or reads another level, cannot +// average to the right colour. Each generated level is read back through texelFetch into an RGBA8 +// target, and the levels outside the window are checked unchanged (or still undefined). +// +// ARMS. Both backends, monolith and Split / Spawn / Tcp; Magma is the control. The forced-CPU arm +// (MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1, read by the SERVER) runs the three-channel cases through +// Espryt's server-side CPU filter, which a host driver never needs: llvmpipe generates both +// formats natively. Under the knob the server log must name that arm, or the entry proved nothing. +// +// MONOLITH DEFECTS, recorded for dev (ID-P8-3) and skipped by name on the monolith arm they live +// on unless MGITEST_RUN_MONOLITH_MIP_DEFECTS=1 is set (which is how each was shown red): +// M1 Espryt: the RGB16F / RGB32F CPU filter reads the frontend's level shadow, which a GPU +// write to level 0 leaves stale (DirectGLES.cpp GenerateThreeChannelFloatMipmapOnCpu); +// M2 Espryt: the R11F / depth blit chains start at level 1 and run to the end of the chain, +// ignoring BASE_LEVEL and MAX_LEVEL, and the storage grow before them defines levels past +// MAX_LEVEL (DirectGLES.cpp GenerateColorTexture2DMipmap / GenerateDepthTexture2DMipmap / +// EnsureGenerateMipmapStorageAllocated); +// M3 Magma: a colour format without BLIT_SRC|BLIT_DST (R11F_G11F_B10F on lavapipe) is skipped +// with a warning and its levels stay undefined texels - the monolith has no shader arm, +// which the wire arm has had since P7 (VulkanRenderer.cpp GenerateMipmap). + +#include +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/PipeStatsWindow.h" +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr int kEdge = 16; + constexpr const char* kForceCpuMipmap = "MGITEST_ESPRYT_FORCE_CPU_MIPMAP"; + constexpr const char* kCpuMipmapMarker = "generate-mipmap-server-cpu"; + constexpr const char* kRunMonolithDefects = "MGITEST_RUN_MONOLITH_MIP_DEFECTS"; + + constexpr const char* kVertexSource = R"(#version 430 core +void main() { + vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); +} +)"; + constexpr const char* kSampleSource = R"(#version 430 core +uniform sampler2D u_texture; +uniform int u_level; +out vec4 o_color; +void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy), u_level); } +)"; + constexpr const char* kPaintSource = R"(#version 430 core +uniform vec4 u_bottom; +uniform vec4 u_top; +uniform float u_split; +out vec4 o_color; +void main() { o_color = gl_FragCoord.y < u_split ? u_bottom : u_top; } +)"; + + using Rgb = std::array; + // Exact in R11F_G11F_B10F, RGB16F and RGB32F alike, and apart in every channel. + constexpr Rgb kBottom = {0.25f, 0.5f, 0.75f}; + constexpr Rgb kTop = {0.875f, 0.375f, 0.125f}; + constexpr Rgb kSentinel = {1.0f, 0.0f, 1.0f}; + constexpr Rgb kLevelZero = {1.0f, 0.0f, 0.0f}; + constexpr Rgb kLevelTwo = {0.0f, 0.0f, 1.0f}; + constexpr Rgb kLevelThree = {1.0f, 1.0f, 0.0f}; + constexpr float kDepthBottom = 0.25f; + constexpr float kDepthTop = 0.75f; + constexpr float kDepthLevelZero = 0.125f; + constexpr float kDepthLevelTwo = 0.5f; + constexpr float kDepthLevelThree = 1.0f; + + struct Format { + GLenum internalFormat; + bool depth; + const char* name; + }; + constexpr Format kPackedFloat = {GL_R11F_G11F_B10F, false, "R11F_G11F_B10F"}; + constexpr Format kDepth32F = {GL_DEPTH_COMPONENT32F, true, "DEPTH_COMPONENT32F"}; + constexpr Format kRgb16F = {GL_RGB16F, false, "RGB16F"}; + constexpr Format kRgb32F = {GL_RGB32F, false, "RGB32F"}; + + int ToByte(float value) { return static_cast(std::lround(value * 255.0f)); } + + class GenerateMipmapServerScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + m_wire = SplitRuntimeSkipReason().empty(); + m_espryt = Gl().BackendName() == "DirectGLES"; + m_forcedCpu = std::getenv(kForceCpuMipmap) != nullptr; + std::string error; + m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error); + ASSERT_NE(m_sampleProgram, 0u) << error; + m_paintProgram = CompileProgram(kVertexSource, kPaintSource, &error); + ASSERT_NE(m_paintProgram, 0u) << error; + m_target = MakeColorFbo(kEdge, kEdge); + ASSERT_NE(m_target.fbo, 0u) << "could not create the sampling target"; + glGenVertexArrays(1, &m_vao); + glGenFramebuffers(1, &m_fbo); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + ASSERT_EQ(FirstGLError(), 0u); + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram); + if (m_paintProgram != 0) glDeleteProgram(m_paintProgram); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo); + if (m_texture != 0) glDeleteTextures(1, &m_texture); + BindDefaultFramebuffer(); + if (m_target.fbo != 0) DestroyColorFbo(m_target); + glViewport(0, 0, Gl().Width(), Gl().Height()); + FirstGLError(); + } + + // Whether this case meets a recorded monolith defect on this arm (see the header). The + // knob runs it anyway, which is how each defect is shown red and how dev's fix is checked. + bool MonolithDefect(bool onEspryt, bool onMagma) const { + if (m_wire || std::getenv(kRunMonolithDefects) != nullptr) return false; + return m_espryt ? onEspryt : onMagma; + } + static constexpr const char* kM1 = + "monolith defect M1 (recorded for dev, ID-P8-3): Espryt's CPU filter reads the frontend's " + "level shadow, which the draw into level 0 left stale"; + static constexpr const char* kM2 = + "monolith defect M2 (recorded for dev, ID-P8-3): Espryt's blit chain ignores BASE_LEVEL and " + "MAX_LEVEL and its storage grow defines levels past MAX_LEVEL"; + static constexpr const char* kM3 = + "monolith defect M3 (recorded for dev, ID-P8-3): Magma skips a format without native blit " + "and leaves the generated levels empty"; + + GLuint NewTexture() { + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + return m_texture; + } + + // A level `edge` x `edge` whose rows below the middle are `bottom` and above it `top`. + static std::vector Halves(const Format& format, int edge, const Rgb& bottom, const Rgb& top, + float depthBottom, float depthTop) { + const int channels = format.depth ? 1 : 3; + std::vector texels(static_cast(edge * edge * channels)); + for (int y = 0; y < edge; ++y) { + for (int x = 0; x < edge; ++x) { + float* texel = texels.data() + static_cast((y * edge + x) * channels); + const bool low = y < edge / 2; + if (format.depth) { + texel[0] = low ? depthBottom : depthTop; + } else { + for (int c = 0; c < 3; ++c) texel[c] = low ? bottom[c] : top[c]; + } + } + } + return texels; + } + + static std::vector Flat(const Format& format, int edge, const Rgb& colour, float depth) { + return Halves(format, edge, colour, colour, depth, depth); + } + + static void DefineLevel(const Format& format, int level, int edge, const std::vector& texels) { + glTexImage2D(GL_TEXTURE_2D, level, static_cast(format.internalFormat), edge, edge, 0, + format.depth ? GL_DEPTH_COMPONENT : GL_RGB, GL_FLOAT, texels.data()); + } + + static void UploadLevel(const Format& format, int level, int edge, const std::vector& texels) { + glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, edge, edge, format.depth ? GL_DEPTH_COMPONENT : GL_RGB, + GL_FLOAT, texels.data()); + } + + static int LevelWidth(int level) { + GLint width = 0; + glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width); + return width; + } + + // texelFetch's level is relative to BASE_LEVEL and bounded by MAX_LEVEL, so the window is + // reopened over every defined level before any of them is read. + Image SampleLevel(int level, int maxLevel) { + const int edge = std::max(kEdge >> level, 1); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glUseProgram(m_sampleProgram); + glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0); + glUniform1i(glGetUniformLocation(m_sampleProgram, "u_level"), level); + BindFbo(m_target); + glViewport(0, 0, edge, edge); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + return ReadPixelsRect(0, 0, edge, edge); + } + + // Every row of `image` against the half it belongs to. A depth level is read in its red + // channel only, and only in its bottom and top rows: the rows next to the boundary are + // where a shader-based depth mip is entitled to a half-texel offset. + static void ExpectHalves(const Image& image, const Format& format, const Rgb& bottom, const Rgb& top, + float depthBottom, float depthTop, const std::string& what) { + const int height = image.Height(); + for (int y = 0; y < height; ++y) { + if (format.depth && y != 0 && y != height - 1) continue; + const bool low = y < height / 2; + for (int x = 0; x < image.Width(); ++x) { + const Rgba8 got = image.At(x, y); + if (format.depth) { + const int want = ToByte(low ? depthBottom : depthTop); + EXPECT_NEAR(got.r, want, 2) << what << " depth at (" << x << "," << y << ") got " << got; + if (std::abs(got.r - want) > 2) return; + continue; + } + const Rgb& colour = low ? bottom : top; + const int want[3] = {ToByte(colour[0]), ToByte(colour[1]), ToByte(colour[2])}; + const int have[3] = {got.r, got.g, got.b}; + for (int c = 0; c < 3; ++c) { + EXPECT_NEAR(have[c], want[c], 2) << what << " channel " << c << " at (" << x << "," + << y << ") got " << got; + if (std::abs(have[c] - want[c]) > 2) return; + } + } + } + } + + static void ExpectFlat(const Image& image, const Format& format, const Rgb& colour, float depth, + const std::string& what) { + ExpectHalves(image, format, colour, colour, depth, depth, what); + } + + void Generate() { + m_mark = PipeStatsWindow::MarkLaneLog(); + glBindTexture(GL_TEXTURE_2D, m_texture); + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_EQ(FirstGLError(), 0u) << "glGenerateMipmap"; + } + + // Under the knob the SERVER must say it filtered on the CPU; without that line the entry + // ran the native arm and its green says nothing about the arm it is named for. + void ExpectCpuArmNamed() { + if (!m_forcedCpu || !m_wire || !m_espryt) return; + const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark); + EXPECT_NE(log.find(kCpuMipmapMarker), std::string::npos) + << kForceCpuMipmap << " is set but the server log after glGenerateMipmap carries no '" + << kCpuMipmapMarker << "' line: the knob did not reach the server, or the three-channel " + << "generation never took the server's CPU arm"; + } + + // ---- B1: the generated window, on the two formats Espryt blits level by level ---------- + + // Levels 0..2 defined, MAX_LEVEL = 2: the descriptor holds three of the chain's five levels. + void MutableMaxLevelTwo(const Format& format) { + if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3); + NewTexture(); + DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop)); + DefineLevel(format, 1, kEdge / 2, Flat(format, kEdge / 2, kSentinel, kDepthLevelZero)); + DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kSentinel, kDepthLevelZero)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2); + ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..2"; + Generate(); + EXPECT_EQ(LevelWidth(3), 0) << format.name << ": level 3 is past MAX_LEVEL and must stay undefined"; + for (int level = 1; level <= 2; ++level) + ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop, + std::string(format.name) + " MAX_LEVEL=2 level " + std::to_string(level)); + ExpectHalves(SampleLevel(0, 2), format, kBottom, kTop, kDepthBottom, kDepthTop, + std::string(format.name) + " MAX_LEVEL=2 level 0 (the base, unchanged)"); + EXPECT_EQ(FirstGLError(), 0u); + } + + // glTexStorage2D with three of the chain's five levels. + void PartialStorage(const Format& format) { + if (MonolithDefect(false, !format.depth)) GTEST_SKIP() << kM3; + NewTexture(); + glTexStorage2D(GL_TEXTURE_2D, 3, format.internalFormat, kEdge, kEdge); + UploadLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop)); + ASSERT_EQ(FirstGLError(), 0u) << "partial storage"; + Generate(); + for (int level = 1; level <= 2; ++level) + ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop, + std::string(format.name) + " three-level storage level " + std::to_string(level)); + EXPECT_EQ(FirstGLError(), 0u); + } + + // BASE_LEVEL = 1, MAX_LEVEL = 2 over four defined levels: only level 2 is generated, from + // level 1; levels 0 and 3 keep what they held and level 4 is never defined. + void MutableBaseLevelOne(const Format& format) { + if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3); + NewTexture(); + DefineLevel(format, 0, kEdge, Flat(format, kEdge, kLevelZero, kDepthLevelZero)); + DefineLevel(format, 1, kEdge / 2, + Halves(format, kEdge / 2, kBottom, kTop, kDepthBottom, kDepthTop)); + DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kLevelTwo, kDepthLevelTwo)); + DefineLevel(format, 3, kEdge / 8, Flat(format, kEdge / 8, kLevelThree, kDepthLevelThree)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2); + ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..3"; + Generate(); + EXPECT_EQ(LevelWidth(4), 0) << format.name << ": level 4 is past MAX_LEVEL and must stay undefined"; + const std::string name(format.name); + ExpectFlat(SampleLevel(0, 3), format, kLevelZero, kDepthLevelZero, name + " level 0 (below BASE_LEVEL)"); + ExpectHalves(SampleLevel(1, 3), format, kBottom, kTop, kDepthBottom, kDepthTop, name + " level 1 (base)"); + ExpectHalves(SampleLevel(2, 3), format, kBottom, kTop, kDepthBottom, kDepthTop, + name + " level 2 (generated from level 1)"); + ExpectFlat(SampleLevel(3, 3), format, kLevelThree, kDepthLevelThree, + name + " level 3 (past MAX_LEVEL)"); + EXPECT_EQ(FirstGLError(), 0u); + } + + // ---- B2: RGB16F / RGB32F ---------------------------------------------------------------- + + void UploadedBase(const Format& format) { + NewTexture(); + DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, 0, 0)); + ASSERT_EQ(FirstGLError(), 0u) << "define level 0"; + Generate(); + const int last = 4; + for (int level = 1; level < last; ++level) + ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0, + std::string(format.name) + " uploaded base, level " + std::to_string(level)); + EXPECT_EQ(LevelWidth(last), 1) << format.name << ": the generated chain ends at 1x1"; + ExpectCpuArmNamed(); + EXPECT_EQ(FirstGLError(), 0u); + } + + // Complementary's colortex2 shape: the base is a render target the GPU wrote after its one + // upload, so the chain must come from the GPU's level 0 and never from the uploaded texels. + void RenderTargetBase(const Format& format) { + if (MonolithDefect(true, false)) GTEST_SKIP() << kM1; + NewTexture(); + DefineLevel(format, 0, kEdge, Flat(format, kEdge, kSentinel, 0)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0); + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + FirstGLError(); + GTEST_SKIP() << format.name << " is not colour-renderable on " << Gl().BackendName() + << " (status 0x" << std::hex << status << ")"; + } + glViewport(0, 0, kEdge, kEdge); + glUseProgram(m_paintProgram); + glUniform4f(glGetUniformLocation(m_paintProgram, "u_bottom"), kBottom[0], kBottom[1], kBottom[2], 1); + glUniform4f(glGetUniformLocation(m_paintProgram, "u_top"), kTop[0], kTop[1], kTop[2], 1); + glUniform1f(glGetUniformLocation(m_paintProgram, "u_split"), kEdge / 2.0f); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + ASSERT_EQ(FirstGLError(), 0u) << "draw into level 0"; + Generate(); + const int last = 4; + for (int level = 0; level < last; ++level) + ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0, + std::string(format.name) + " render-target base, level " + std::to_string(level)); + ExpectCpuArmNamed(); + EXPECT_EQ(FirstGLError(), 0u); + } + + bool m_wire = false; + bool m_espryt = false; + bool m_forcedCpu = false; + GLuint m_sampleProgram = 0; + GLuint m_paintProgram = 0; + GLuint m_vao = 0; + GLuint m_fbo = 0; + GLuint m_texture = 0; + ColorFbo m_target{}; + PipeStatsWindow::LogMark m_mark{}; + }; + + } // namespace + + // B1. + TEST_F(GenerateMipmapServerScenario, PackedFloatMutableMaxLevelBelowTheChain) { + if (!Ready()) return; + MutableMaxLevelTwo(kPackedFloat); + } + TEST_F(GenerateMipmapServerScenario, PackedFloatStorageWithFewerLevelsThanTheChain) { + if (!Ready()) return; + PartialStorage(kPackedFloat); + } + TEST_F(GenerateMipmapServerScenario, PackedFloatMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) { + if (!Ready()) return; + MutableBaseLevelOne(kPackedFloat); + } + TEST_F(GenerateMipmapServerScenario, DepthMutableMaxLevelBelowTheChain) { + if (!Ready()) return; + MutableMaxLevelTwo(kDepth32F); + } + TEST_F(GenerateMipmapServerScenario, DepthStorageWithFewerLevelsThanTheChain) { + if (!Ready()) return; + PartialStorage(kDepth32F); + } + TEST_F(GenerateMipmapServerScenario, DepthMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) { + if (!Ready()) return; + MutableBaseLevelOne(kDepth32F); + } + + // B2. + TEST_F(GenerateMipmapServerScenario, Rgb16fUploadedBaseGeneratesItsChain) { + if (!Ready()) return; + UploadedBase(kRgb16F); + } + TEST_F(GenerateMipmapServerScenario, Rgb16fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) { + if (!Ready()) return; + RenderTargetBase(kRgb16F); + } + TEST_F(GenerateMipmapServerScenario, Rgb32fUploadedBaseGeneratesItsChain) { + if (!Ready()) return; + UploadedBase(kRgb32F); + } + TEST_F(GenerateMipmapServerScenario, Rgb32fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) { + if (!Ready()) return; + RenderTargetBase(kRgb32F); + } + +} // namespace MGITest diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 6c9a7f13..bd63dc3e 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -1435,9 +1435,17 @@ TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) { // the re-mint is the server's own (RequireImageBindableStorageByHandle reads the GPU level and // replays the staged store), the frontend arm that carried the marker is monolith-only, and no // pull protocol or ResourceSubDataComplete terminator exists (notes/p9/W2-REMINT.md). + // + // AND THE LIST HAD DRIFTED BY ONE UNTIL P8-B. The tree carried a sixth name this list never + // had: "generate-mipmap-cpu-filter", the split arm's RGB16F/RGB32F refusal in + // GenerateMipmapByRecord. P8-B2 retired it (the server filters its own level, + // GenerateThreeChannelFloatMipmapOnServer), and P8-B1 retired the split arm's two + // "generate-mipmap-storage" sites (the record's plan is the window). What is left is these + // four names, one call site each, and every one of them is on a MONOLITH arm - the split + // arms no longer reach an unmigrated emulation (notes/p8/B.md). const char* const kNames[] = { "copy-image-shadow-mirror", // the glCopyImageSubData CPU-shadow mirror - "generate-mipmap-storage", // EnsureGenerateMipmapStorageAllocated + "generate-mipmap-storage", // EnsureGenerateMipmapStorageAllocated (monolith grow) "generate-mipmap-cpu-fallback", // GenerateThreeChannelFloatMipmapOnCpu "get-tex-image-shadow", // GetTexImageViaShadowConversion }; diff --git a/docs/Disaggregated/notes/p8/B.md b/docs/Disaggregated/notes/p8/B.md new file mode 100644 index 00000000..cb7334bb --- /dev/null +++ b/docs/Disaggregated/notes/p8/B.md @@ -0,0 +1,97 @@ +# P8-B:Espryt `glGenerateMipmap` 在 split 下(B1 生成窗口、B2 RGB16F / RGB32F) + +> 2026-09-29,分支 `p8/b`(基线 `8d3e2317`),代码提交 `8dac1fd8`。计划:[`PLAN-P8.md`](PLAN-P8.md) 包 B。 + +## 1. 结论 + +| 名 | 基线(split 臂) | 本包之后 | 位置 | +|---|---|---|---| +| `generate-mipmap-storage`(split 检查) | 描述符层数少于「从 level 0 起的整链」即 `Fatal{UnmigratedEmulation}`:MAX_LEVEL 偏小、`glTexStorage2D` 层数不足(合法 GL) | 删;生成窗口 = 记录的计划 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:11644` | +| `generate-mipmap-cpu-filter` | RGB16F / RGB32F 生成一律 Fatal | 删;先原生 `glGenerateMipmap`,驱动拒绝或旋钮强制时 server 从自己的层做 CPU 滤波 | `DirectGLES.cpp:12953`、`:13066` | + +- split 臂的 `UnmigratedEmulation` 可达名清零;树里剩 4 名(`copy-image-shadow-mirror`、`generate-mipmap-storage` 的 monolith 生长、`generate-mipmap-cpu-fallback`、`get-tex-image-shadow`),都只在 monolith 臂上。 +- `PipeCatalogueTest.cpp:1446` 名单保持 4 名,注释补记漂移(它从未列过 `cpu-filter`)与本包的两处退役。 +- 主机(llvmpipe)上 RGB16F(原生存储)与 RGB32F(加宽为 RGBA32F)都由原生 `glGenerateMipmap` 完成;CPU 臂只在旋钮下走到(日志核实:非强制条目 0 行 `generate-mipmap-server-cpu`)。 + +## 2. 设计 + +| 项 | 做法 | 位置 | +|---|---|---| +| B1 窗口 | `Base = VerbMipBaseLevel`;`End = min(VerbMipLevelCount, Desc.Levels, MaxLevel+1, Base + 基准层的整链)`,与 Magma 的裁剪同式(`WireFramebuffer.inc:1356`);client 已按同一窗口定义层(`EmitTables.cpp:384-390`) | `DirectGLES.cpp:11644` | +| B1 两条 blit 链 | 深度 / R11F 的记录臂改为只跑 `[Base+1, End)`,不再从 level 1 跑到描述符末尾(BASE_LEVEL=1 时会用 level 0 覆盖 base) | `DirectGLES.cpp:12408`、`:12425` | +| B1 死臂 | `EnsureGenerateMipmapStorageAllocated(texture)` 的 transport 臂删除:split 下 `GenerateMipmap` 在它之前已经返回 | `DirectGLES.cpp:11666` | +| B2 原生 | 三通道浮点也先走原生 `glGenerateMipmap`;错误不转给应用,改走 CPU 臂 | `DirectGLES.cpp:13114` | +| B2 CPU 臂 | 源 = 驱动上的 base 层(`ReadTextureLevelTight`,RGB/FLOAT);驱动读不回(不可附着的存储)才用 staged store,且 store 须整层覆盖;都没有则具名拒绝(`MGLOG_E_ONCE`,不 Fatal);2×2 盒滤波(monolith 同式);按驱动存储的通道数 `glTexSubImage2D`(RGB 或 RGBA,FLOAT);支持 2D 与立方体,其余目标具名拒绝 | `DirectGLES.cpp:12909`、`:12953` | +| B2 旋钮 | `MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1`,server 读(`MGITEST_MAGMA_FORCE_SHADER_MIPMAP` 先例);命中打一行 `generate-mipmap-server-cpu`(INFO,每进程一次) | `DirectGLES.cpp:12902` | +| B2 client 影子 | 任何臂都不读 client 影子 | — | + +未改:`PipeCalls.def:260` 的 `GenerateMipmap` 仍是 `kWaitApplied`。P5e 裁定 14 留着它的唯一理由(CPU 滤波读 client 影子)已消失;翻成 `kWaitNone` 是协议改动,交集成方。 + +## 3. 覆盖(`GenerateMipmapServerScenario`) + +用例:基准层分上下两半(A / B),逐层 `texelFetch` 到 RGBA8 回读;窗口外的层检查未变或仍未定义。 + +| 用例 | 形状 | 行 | +|---|---|---| +| `PackedFloat` / `Depth` × `MutableMaxLevelBelowTheChain` | 可变,定义 0..2,MAX_LEVEL=2;level 3 须未定义 | `GenerateMipmapServerScenario.cpp:294` | +| `PackedFloat` / `Depth` × `StorageWithFewerLevelsThanTheChain` | `glTexStorage2D` 3 层(整链 5) | `GenerateMipmapServerScenario.cpp:313` | +| `PackedFloat` / `Depth` × `MutableBaseLevelOneKeepsTheLevelsOutsideItsWindow` | 定义 0..3,BASE=1、MAX=2;level 0/3 不变,level 4 未定义 | `GenerateMipmapServerScenario.cpp:328` | +| `Rgb16f` / `Rgb32f` × `UploadedBaseGeneratesItsChain` | 仅上传 level 0 | `GenerateMipmapServerScenario.cpp:353` | +| `Rgb16f` / `Rgb32f` × `RenderTargetBaseGeneratesFromTheGpuWrittenLevel` | 上传哨兵色后把 level 0 当渲染目标绘制(Complementary colortex2 形状) | `GenerateMipmapServerScenario.cpp:369` | + +| 臂 | 登记 | Espryt | Magma(对照) | +|---|---|---|---| +| monolith | 环境发现 `DirectGLES.` / `DirectVulkan.` | 4 绿 + 6 按名跳过(M1、M2) | 7 绿 + 3 按名跳过(M3) | +| Split / Spawn / Tcp | `MobileGL/MG_IntegrationTest/CMakeLists.txt:3355`;Magma 门控块 `MobileGL/MG_IntegrationTest/CMakeLists.txt:4059` | 10 × 3 绿 | 10 × 3 绿 | +| 强制 CPU(Split / Spawn) | `MobileGL/MG_IntegrationTest/CMakeLists.txt:3356-3372`,标签 `integration-cpu-mip-{split,spawn}` | 4 × 2 绿,server 日志须有 `generate-mipmap-server-cpu`(`GenerateMipmapServerScenario.cpp:282`) | — | +| A/B inproc(门的 `integration-gpu[inproc]` 环境) | 同环境发现 | 10 绿 | 10 绿 | + +- 强制 CPU 臂不上 tcp:tcp 的 server 是共享的 `TcpServer.Start` 夹具(`MobileGL/MG_IntegrationTest/CMakeLists.txt:2154`),旋钮放那里会让所有 tcp 条目都走 CPU 臂、原生臂在 tcp 上失去覆盖;CPU 臂本身与传输无关(spawn 已证跨进程)。与 P9 `RemintFallback.` 同形,不需要 parity 例外行。 + +## 4. red-once + +| # | 变异 | 载体 | 观察到的红 | +|---|---|---|---| +| 1 | 基线 `8d3e2317`(只加用例) | `DirectGLES.{Split,Spawn,Tcp}.GenerateMipmapServerScenario.*` | 30/30 abort:B1 六例 `Fatal{UnmigratedEmulation, "generate-mipmap-storage"}`,B2 四例 `Fatal{UnmigratedEmulation, "generate-mipmap-cpu-filter"}`;`CpuMip.` 8/8 abort | +| 2 | CPU 臂入口放回 `cpu-filter` 闩(去掉新臂) | `DirectGLES.{Split,Spawn}.CpuMip.*` | 8/8 Subprocess aborted | +| 3 | CPU 臂跳过驱动回读、改读 store | 同上 | 4/8 Failed:两个渲染目标用例 × 两臂读到哨兵色(陈旧) | +| 4 | server 不再读旋钮 | 同上 | 8/8 Failed:像素对(原生臂),server 日志无 `generate-mipmap-server-cpu` | +| 5 | 两条 blit 链改回 `1 .. Desc.Levels` | `DirectGLES.{Split,Spawn,Tcp}.GenerateMipmapServerScenario.{PackedFloat,Depth}*` | 6/18 Failed(另 2 条为 tcp 夹具):`MutableBaseLevelOne` 两格式 × 三臂 | +| 6 | 窗口不按计划 / MAX_LEVEL 裁剪(`End = Desc.Levels`) | 同上 | 6/18 Failed:同 5 | +| 7 | `MGITEST_RUN_MONOLITH_MIP_DEFECTS=1` | `Direct{GLES,Vulkan}.GenerateMipmapServerScenario.*`,`MOBILEGL_TRANSPORT=monolith` | 9/20 Failed:M1 两例、M2 四例、M3 三例(§5) | + +恢复一律复制比对 + `touch`(`cmp` 一致),每次恢复后重建。 + +## 5. monolith 缺陷(记给 dev,ID-P8-3;用例在该臂按名跳过,旋钮 `MGITEST_RUN_MONOLITH_MIP_DEFECTS=1` 复现) + +| # | 后端 | 缺陷 | 位置 | red-once 读数 | +|---|---|---|---|---| +| M1 | Espryt | RGB16F / RGB32F CPU 滤波读前端层影子;level 0 被 GPU 写过后影子陈旧 | `DirectGLES.cpp:12812`(调用 `:13171`) | level 1..3 = 哨兵色 `rgba(255,0,255,255)`,两格式 | +| M2 | Espryt | R11F / 深度:存储生长从 level 0 算整链,定义 MAX_LEVEL 之外的层;两条 blit 链从 level 1 跑到末尾,无视 BASE_LEVEL / MAX_LEVEL | `DirectGLES.cpp:11592`、`:12465`、`:12495` | MAX_LEVEL=2 时 level 3 宽 2(应 0);BASE=1 时 level 1..3 全被 level 0 覆盖,level 4 被定义 | +| M3 | Magma | 无 BLIT 的颜色格式(lavapipe 上 `B10G11R11_UFLOAT`,VkFormat 122)直接跳过生成,只打 `MGLOG_W_ONCE`;wire 臂自 P7 有着色器臂,monolith 没有 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:12226` | 三个 PackedFloat 用例生成层读到 `rgba(0,0,0,255)` | + +- M1 修法指向:monolith 也改为回读驱动层(P9 dev `568090f0` 的 `AdoptDriverLevelIntoShadow` 同路)。M2:monolith 两条链与生长改用 `ComputeMipmapGenerationRange` 窗口。M3:monolith 复用 wire 臂的着色器 mip。都动 pull 构建的 `.text`,按 G1 在 dev 修。 + +## 6. 门 + +`gate.sh ~/w7/p8-b p8b`,头 `1bb77890`,dirty 0: + +| 项 | 读数 | +|---|---| +| G1 | `added=0 removed=0`(clang 22.1.6);`.text` a5ba13,与建树时相同 | +| census / ratchet | 79 个 abort 点、45 个族词,与基线同:`MGPipeUnmigratedEmulation` 的调用点不是 abort 点(abort 在 `PipeApply.cpp` 的漏斗里),删两个名不改计数,基线不动;ratchet 172 不变 | +| parity / protocol pin / wire-declines | 0 错;revision 5;53 行 0 未记 | +| hygiene | 12/12 OK | +| unit | 2714/2714 | +| integration-split / spawn / tcp | 437 / 356 / 358,全绿 | +| integration-magma-split / spawn / tcp / full-split | 261 / 238 / 207 / 646,全绿 | +| integration-gpu[monolith] / [inproc] | 2746 / 2746,全绿 | +| retrace(`MOBILEGL_BUILD_TRACE_REPLAY=ON`,236 条) | 230 过;6 条 `iris-iterationrp-in-world`(两后端 × monolith / SPLIT / SPAWN)为已知主机 llvmpipe JIT 崩溃;全部重放日志 0 行 `generate-mipmap-*` | + +## 7. 发现未修 + +| 项 | 说明 | +|---|---| +| `PipeCatalogue` 名单无门 | 名单与树里的调用点没有任何脚本比对,漂移只能靠人发现;`fatal_census` 数的是 abort 点,不数 `MGPipeUnmigratedEmulation` 调用 | +| CPU 臂的目标 | 2D 数组 / 3D / 立方体数组的 RGB16F / RGB32F 若驱动拒绝原生生成,CPU 臂具名拒绝(monolith 的 CPU 滤波对这些目标同样只按 2D 取址) | +| 层间精度 | CPU 臂层间以 float 传递,不在每层回量化到 half;GL 不规定滤波,与 monolith(每层从 half 影子读)可能差 1 ulp |