mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Espryt, P8-B): glGenerateMipmap on the server - the record's plan is the generated window (B1) and RGB16F/RGB32F generate natively or on the server's CPU from its own level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm
This commit is contained in:
@@ -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<MG_Pipe::MGPipeResourceTarget>(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<Uint32>(applier.VerbMipLevelCount, desc.Levels);
|
||||
if (record.ParamsSerial) end = std::min<Uint32>(end, static_cast<Uint32>(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<Int>(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<MG_State::GLState::ITextureObject>& 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<MG_State::GLState::TextureObjectMipmap*>(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<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& 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<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
const RecordMipWindow& window) {
|
||||
const auto& desc = record.Desc;
|
||||
const GLenum filter =
|
||||
IsIntegerColorFormat(static_cast<TextureInternalFormat>(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<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
TextureTarget textureTarget, TextureUploadTarget face, Uint32 level,
|
||||
const IntVec3& extent, Vector<Float>& rgb) {
|
||||
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
||||
const SizeT texels = static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y());
|
||||
Vector<Uint8> bytes;
|
||||
if (ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format,
|
||||
static_cast<GLint>(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<Uint16>(face);
|
||||
const auto levelCode = static_cast<Uint16>(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<TextureImpl::BackendTextureObject>& backendTexture,
|
||||
const char* why) {
|
||||
const auto& desc = record.Desc;
|
||||
const auto format = static_cast<TextureInternalFormat>(desc.InternalFormat);
|
||||
const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target);
|
||||
const RecordMipWindow window = GenerateMipmapWindowByRecord(record);
|
||||
if (window.End <= window.Base + 1) return;
|
||||
Vector<TextureUploadTarget> 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<TextureUploadTarget>(
|
||||
static_cast<Int>(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<Float> 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<Float> next(static_cast<SizeT>(dstExtent.x()) * static_cast<SizeT>(dstExtent.y()) * 3);
|
||||
const auto at = [&](Int x, Int y, Int c) {
|
||||
return level[(static_cast<SizeT>(y) * static_cast<SizeT>(extent.x()) + static_cast<SizeT>(x)) * 3 +
|
||||
static_cast<SizeT>(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<SizeT>(y) * static_cast<SizeT>(dstExtent.x()) + static_cast<SizeT>(x)) *
|
||||
3 + static_cast<SizeT>(c)] =
|
||||
0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c));
|
||||
}
|
||||
}
|
||||
Vector<Float> 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<GLint>(dst), 0, 0, static_cast<GLsizei>(dstExtent.x()),
|
||||
static_cast<GLsizei>(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<SizeT>(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<TextureImpl::BackendTextureObject>& backendTexture) {
|
||||
const auto format = static_cast<TextureInternalFormat>(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 "
|
||||
|
||||
@@ -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
|
||||
@@ -3340,6 +3343,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.<arm>.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
|
||||
@@ -4025,6 +4056,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
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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 <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#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<float, 3>;
|
||||
// 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<int>(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<float> 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<float> texels(static_cast<std::size_t>(edge * edge * channels));
|
||||
for (int y = 0; y < edge; ++y) {
|
||||
for (int x = 0; x < edge; ++x) {
|
||||
float* texel = texels.data() + static_cast<std::size_t>((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<float> 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<float>& texels) {
|
||||
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(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<float>& 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
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user