mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
- Layout only: token streams match up to split literals, continuations and using order - The size totals fall, so the structure baseline is lowered
1606 lines
104 KiB
C++
1606 lines
104 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp
|
|
// Copyright (c) 2025-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
|
|
|
|
#include "ManagersInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
static Bool IsAngleLlvmpipeRenderer() {
|
|
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
|
|
}
|
|
|
|
// P3b/P4b (wave 2-D, package D3). See the declaration for why a view's storage clauses are
|
|
// asked of another record, why null means "ask again" and why the deletion order cannot
|
|
// strand this walk.
|
|
const MG_Pipe::MGPipeResourceRecord* PipeTextureStorageRecordForRecord(
|
|
const MG_Pipe::MGPipeResourceRecord& record) {
|
|
// ONE hop is the shape the client emits and the second is already generous; the bound is
|
|
// here so a cyclic chain is a null answer rather than a hang, not because a real chain
|
|
// is ever this long.
|
|
constexpr Uint32 kViewChainBound = 4;
|
|
const MG_Pipe::MGPipeResourceRecord* storage = &record;
|
|
for (Uint32 hop = 0; hop < kViewChainBound; ++hop) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(storage->Desc.ViewOf)) return storage;
|
|
storage = PipeTextureRecordForHandle(storage->Desc.ViewOf);
|
|
if (storage == nullptr) return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
namespace TextureImpl {
|
|
BackendTextureObject::BackendTextureObject() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
|
m_contextGeneration = g_backendContextGeneration;
|
|
if (m_backendTextureId == 0) {
|
|
MGLOG_E_ONCE("Failed to generate texture object.");
|
|
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
|
} else {
|
|
MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId);
|
|
}
|
|
}
|
|
|
|
BackendTextureObject::~BackendTextureObject() {
|
|
if (InProcessTeardown()) {
|
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
}
|
|
if (m_backendTextureId == 0) {
|
|
return;
|
|
}
|
|
// Scrub every driver-state shadow that could false-skip when the name
|
|
// or this heap address is recycled - regardless of whether the id can
|
|
// still be deleted.
|
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
|
for (auto& unitCache : g_boundTexturesCache) {
|
|
for (auto& boundTexture : unitCache) {
|
|
if (boundTexture == this) {
|
|
boundTexture = nullptr;
|
|
}
|
|
}
|
|
}
|
|
// TEMP-EXP (leak texture deletes): /sdcard/MG/exp_leak_texture_deletes.
|
|
// Discriminator for the mali-mem-purge hiccup theory: never hand the
|
|
// driver a texture free, so the purge daemon has nothing to reclaim.
|
|
static const Bool s_expLeakTextureDeletes = [] {
|
|
FILE* f = std::fopen("/sdcard/MG/exp_leak_texture_deletes", "rb");
|
|
if (!f) return false;
|
|
std::fclose(f);
|
|
return true;
|
|
}();
|
|
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures &&
|
|
!s_expLeakTextureDeletes) {
|
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
|
if (m_bufferImageSplitViewId != 0) {
|
|
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
|
}
|
|
}
|
|
m_backendTextureId = 0;
|
|
m_bufferImageSplitViewId = 0;
|
|
}
|
|
|
|
void BackendTextureObject::Bind(GLenum target, Uint unit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (g_activeTextureUnit != unit) {
|
|
ActivateTextureUnit(unit);
|
|
}
|
|
|
|
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
if (this == g_boundTexturesCache[unit][targetN]) return;
|
|
|
|
g_GLESFuncs.glBindTexture(target, m_backendTextureId);
|
|
g_boundTexturesCache[unit][targetN] = this;
|
|
}
|
|
|
|
Uint BackendTextureObject::GetBackendTextureId() const {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
return m_backendTextureId;
|
|
}
|
|
|
|
// P5e (tx2), CONTRACT-P5E §5.2 / scout G-S2-5. See the declaration.
|
|
void BackendTextureObject::RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
|
|
const MG_Pipe::MGPipeResourceRecord& record) {
|
|
if (m_imageBindableStorageRequired) {
|
|
return;
|
|
}
|
|
// P13 W4b (defect h): monolith's record arm takes the body too; its old arm only
|
|
// cleared m_isInitialized and replayed the frontend shadow at the next sync.
|
|
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
|
|
// ES image binding needs immutable storage, but an already
|
|
// immutable core-format allocation needs no replacement. Keep
|
|
// its GPU contents and native name; normal serial/shape checks
|
|
// below still consume pending uploads and real redefinitions.
|
|
if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) {
|
|
m_imageBindableStorageRequired = true;
|
|
m_forceTextureParamsResync = true;
|
|
return;
|
|
}
|
|
// P9 W2 (docs/archive/notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
|
|
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
|
|
// generated mips, image stores) and the staged store holds what the client uploaded;
|
|
// the client is never asked, so no pull protocol exists. Reading only the store would
|
|
// be wrong: a GPU-written level's store run is stale or absent - both host corpus
|
|
// re-mints (iris-photon, iris-derivative) are render targets with no staged bytes.
|
|
const Bool hadBackendStorage = m_isInitialized;
|
|
// The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be
|
|
// produced on the host (llvmpipe renders every image format), so the test lane forces
|
|
// one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
|
|
static const Bool s_forceReadbackFailure =
|
|
std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr;
|
|
auto& store = MG_Record::ServerStagedTexture();
|
|
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
|
|
const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
|
GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE;
|
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format),
|
|
{}, nullptr, &canonicalFormat, &canonicalType);
|
|
for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) {
|
|
for (Uint32 level = 0; level < record.Desc.Levels; ++level) {
|
|
const auto targetCode = static_cast<Uint16>(uploadTarget);
|
|
const auto levelCode = static_cast<Uint16>(level);
|
|
const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode);
|
|
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue;
|
|
const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode);
|
|
const Bool covered = store.IsCovered(key, targetCode, levelCode);
|
|
const MG_Pipe::MGPBox whole{0,
|
|
0,
|
|
0,
|
|
static_cast<Uint32>(extent.x()),
|
|
static_cast<Uint32>(extent.y()),
|
|
static_cast<Uint32>(extent.z())};
|
|
// A genuinely full pending write owns every byte. A
|
|
// union box covering the level is not sufficient when
|
|
// its explicit rectangles leave GPU-written gaps.
|
|
const auto coversWhole = [&](const auto& box) {
|
|
return box.X == 0 && box.Y == 0 && box.Z == 0 && box.W == whole.W && box.H == whole.H &&
|
|
box.D == whole.D;
|
|
};
|
|
const Bool pendingWhole =
|
|
pending && (pending->Regions.empty()
|
|
? coversWhole(pending->UnionBox)
|
|
: std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole));
|
|
// W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's
|
|
// Defined-ness follows the client's respecifies, the driver's follows this
|
|
// twin's last sync, and the two differ for a level defined (null data, or null
|
|
// data plus a partial upload) after that sync: the driver has no such level, so
|
|
// the GPU cannot have written it. A covered level then replays its staged run
|
|
// (the client's whole-level shadow, pending boxes included) and an uncovered one
|
|
// is undefined content with nothing to preserve. Reading it anyway was
|
|
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
|
|
const Bool nativeLevel =
|
|
m_isInitialized && NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget,
|
|
static_cast<GLint>(level), extent);
|
|
if (nativeLevel && !(covered && pendingWhole)) {
|
|
Vector<Uint8> snapshot;
|
|
const Bool readBack =
|
|
!s_forceReadbackFailure && ReadTextureLevelTight(m_backendTextureId, target, uploadTarget,
|
|
format, static_cast<GLint>(level), extent,
|
|
/*sourceUsesImageCarrier=*/false,
|
|
canonicalFormat, canonicalType, snapshot);
|
|
if (!readBack && !covered) {
|
|
// The GPU holds this level, the driver will not hand it back, and no
|
|
// CPU byte of it exists anywhere: the one case with no honest answer.
|
|
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u",
|
|
res.Slot, res.Gen, level);
|
|
std::abort();
|
|
}
|
|
if (!readBack) {
|
|
// W2-b: the driver refused the readback (a format it cannot attach
|
|
// to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm
|
|
// without EXT_texture_norm16, float without EXT_color_buffer_float)
|
|
// but the store covers the level. Its staged run is exactly what the
|
|
// monolith re-mint replays; texels the GPU wrote since that upload
|
|
// are not preserved, which is monolith's answer too.
|
|
MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver "
|
|
"refused the readback (internal format %u); replaying the level's "
|
|
"staged bytes instead (remint-readback-fallback)",
|
|
res.Slot, res.Gen, level, record.Desc.InternalFormat);
|
|
}
|
|
if (readBack && pending) {
|
|
// Native contents include prior draws/image
|
|
// writes; the pending stage owns ONLY its boxes.
|
|
// Merge those boxes before adopting a whole-level
|
|
// snapshot, never overwrite untouched GPU texels
|
|
// with the rest of a stale client staging run.
|
|
const SizeT pixels = static_cast<SizeT>(extent.x()) * extent.y() * extent.z();
|
|
const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode);
|
|
if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) {
|
|
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}");
|
|
std::abort();
|
|
}
|
|
const SizeT bpp = stagedSize / pixels;
|
|
const Uint8* staged =
|
|
store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion");
|
|
const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
|
|
if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W || Uint64(y) + h > whole.H ||
|
|
Uint64(z) + d > whole.D) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}");
|
|
std::abort();
|
|
}
|
|
for (Uint32 slice = 0; slice < d; ++slice) {
|
|
for (Uint32 row = 0; row < h; ++row) {
|
|
const SizeT offset =
|
|
((static_cast<SizeT>(z + slice) * whole.H + y + row) * whole.W + x) * bpp;
|
|
Memcpy(snapshot.data() + offset, staged + offset, static_cast<SizeT>(w) * bpp);
|
|
}
|
|
}
|
|
};
|
|
if (pending->Regions.empty()) {
|
|
const auto& box = pending->UnionBox;
|
|
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
|
} else {
|
|
for (const auto& box : pending->Regions)
|
|
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
|
}
|
|
}
|
|
if (readBack) {
|
|
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
|
|
}
|
|
}
|
|
// Undefined contents have no byte run to preserve. A
|
|
// first allocation can still be image-bindable without
|
|
// inventing an upload or consulting the frontend.
|
|
if (!store.IsCovered(key, targetCode, levelCode)) continue;
|
|
if (!RearmPipeTextureLevelUpload(
|
|
res,
|
|
MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast<Uint32>(uploadTarget)),
|
|
levelCode, whole)) {
|
|
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}");
|
|
std::abort();
|
|
}
|
|
}
|
|
}
|
|
// W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as
|
|
// on the frontend one (which counts in RequireImageBindableStorage). The keep path
|
|
// above is not a re-mint and returns before this; a first allocation holds nothing.
|
|
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
|
|
}
|
|
// Every defined byte now belongs to the server and every required
|
|
// replay is in its pending set. The existing upload path builds and
|
|
// fills the immutable/widened carrier from those records.
|
|
m_imageBindableStorageRequired = true;
|
|
m_isInitialized = false;
|
|
// The widened carrier's swizzle override, exactly as the frontend overload's tail
|
|
// sets it and for the same reason: the parameter sync is gated on a params version
|
|
// this transition does not move.
|
|
m_forceTextureParamsResync = true;
|
|
}
|
|
|
|
// Replaces one level's shadow bytes with what the driver texture holds for it, then copies
|
|
// back the client writes the driver has not been sent yet - exactly the texels written
|
|
// (the level's dirty footprint), not the union box or the rect list, which also span texels
|
|
// the client never wrote and which only the driver's copy holds correctly. Returns false,
|
|
// leaving the shadow untouched, when the driver's copy cannot stand in for the shadow: the
|
|
// client wrote every texel of the level, the level is stored compressed, the driver has no
|
|
// image at this level and extent (defined after the last sync), or the readback cannot be
|
|
// served (a multisample image, a format the driver will not attach to a framebuffer).
|
|
//
|
|
// P11 M, `pipePending` (push builds): THE CLIENT WRITES THE DRIVER HAS NOT BEEN SENT MAY
|
|
// LIVE IN THE APPLIER'S PENDING SET INSTEAD. On the texture handle arm the client clears its
|
|
// own dirty flags when it emits a level (D-D5), so the frontend model below answers "clean"
|
|
// for a write that is still only in the shadow; the entry the applier accumulated for this
|
|
// (uploadTarget, level) names the same shadow-grid boxes. Both are merged over the
|
|
// snapshot. Only a box-only entry whose box IS the level owns every byte of it (dev's rule:
|
|
// a union box spanning the level with gaps in it is not a whole-level write); an entry with
|
|
// regions never short-circuits, since accumulated regions may overlap and their texels do
|
|
// not add up - the readback and the merge below give the same bytes when they do cover it.
|
|
// Null on the pull build's arm and the legacy arm.
|
|
static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture,
|
|
const MG_State::GLState::ITextureObject& stateTextureObject,
|
|
MG_State::GLState::TextureObjectMipmap& mipmapObject,
|
|
TextureUploadTarget uploadTarget, Uint level,
|
|
const MG_Pipe::MGPipeResourceRecord::PendingUpload* pipePending) {
|
|
const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level);
|
|
const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)};
|
|
const SizeT texelCount =
|
|
static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y()) * static_cast<SizeT>(extent.z());
|
|
Vector<MG_State::GLState::MipmapDirtyRegion> pending;
|
|
mipmapObject.GetStorageDirtyFootprint(uploadTarget, level, pending);
|
|
// Disjoint boxes, so their texels add up; a union box spanning the level is not enough.
|
|
SizeT pendingTexels = 0;
|
|
for (const auto& box : pending) {
|
|
pendingTexels += box.TexelCount();
|
|
}
|
|
if (!pending.empty() && pendingTexels >= texelCount) return false;
|
|
// Every box the entry names, checked against the level before anything is read: a box
|
|
// outside it is a corrupt record, and the answer is the pre-readback one (keep the
|
|
// shadow) rather than a copy out of bounds.
|
|
const auto pipeBoxInLevel = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
|
|
return x >= 0 && y >= 0 && z >= 0 && Uint64(x) + w <= Uint64(extent.x()) &&
|
|
Uint64(y) + h <= Uint64(extent.y()) && Uint64(z) + d <= Uint64(extent.z());
|
|
};
|
|
if (pipePending != nullptr) {
|
|
const auto& box = pipePending->UnionBox;
|
|
if (!pipeBoxInLevel(box.X, box.Y, box.Z, box.W, box.H, box.D)) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's box lies outside the level; the "
|
|
"image-bindable re-mint replays the shadow as it stands",
|
|
stateTextureObject.GetExternalIndex(), level);
|
|
return false;
|
|
}
|
|
for (const auto& region : pipePending->Regions) {
|
|
if (!pipeBoxInLevel(region.X, region.Y, region.Z, region.W, region.H, region.D)) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's region lies outside the "
|
|
"level; the image-bindable re-mint replays the shadow as it stands",
|
|
stateTextureObject.GetExternalIndex(), level);
|
|
return false;
|
|
}
|
|
}
|
|
if (pipePending->Regions.empty() && box.X == 0 && box.Y == 0 && box.Z == 0 &&
|
|
box.W == static_cast<Uint32>(extent.x()) && box.H == static_cast<Uint32>(extent.y()) &&
|
|
box.D == static_cast<Uint32>(extent.z())) {
|
|
return false;
|
|
}
|
|
}
|
|
if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false;
|
|
if (!g_GLESFuncs.glGetTexLevelParameteriv) return false;
|
|
|
|
const TextureTarget target = stateTextureObject.GetTarget();
|
|
const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(target);
|
|
const GLenum faceTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
|
const IntVec3 backendExtent = GetBackendUploadSize(target, texelSize);
|
|
GLint driverWidth = 0, driverHeight = 0, driverDepth = 0;
|
|
backendTexture.Bind(nativeTarget);
|
|
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_WIDTH, &driverWidth);
|
|
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_HEIGHT,
|
|
&driverHeight);
|
|
g_GLESFuncs.glGetTexLevelParameteriv(faceTarget, static_cast<GLint>(level), GL_TEXTURE_DEPTH, &driverDepth);
|
|
if (driverWidth != backendExtent.x() || driverHeight != backendExtent.y() ||
|
|
std::max(driverDepth, 1) != std::max(backendExtent.z(), 1)) {
|
|
return false;
|
|
}
|
|
|
|
const TextureInternalFormat format = stateTextureObject.GetFormat();
|
|
GLenum shadowFormat = GL_NONE, shadowType = GL_NONE;
|
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(MG_Util::ConvertTextureInternalFormatToGLEnum(format),
|
|
{}, nullptr, &shadowFormat, &shadowType);
|
|
Vector<Uint8> snapshot;
|
|
if (!ReadTextureLevelTight(backendTextureId, target, uploadTarget, format, static_cast<GLint>(level),
|
|
texelSize, /*sourceUsesImageCarrier=*/false, shadowFormat, shadowType,
|
|
snapshot)) {
|
|
return false;
|
|
}
|
|
const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level);
|
|
auto* shadow = static_cast<Uint8*>(mipmapObject.MapMipmapData(uploadTarget, level));
|
|
if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) {
|
|
return false;
|
|
}
|
|
|
|
if (!pending.empty()) {
|
|
// The boxes index the SHADOW's texel grid (a 1D array's layers are its rows), which
|
|
// is also the snapshot's byte order.
|
|
const SizeT bpp = shadowBytes / texelCount;
|
|
const SizeT rowBytes = static_cast<SizeT>(extent.x()) * bpp;
|
|
const SizeT sliceBytes = static_cast<SizeT>(extent.y()) * rowBytes;
|
|
for (const auto& box : pending) {
|
|
if (box.Empty()) continue;
|
|
const SizeT spanBytes = static_cast<SizeT>(box.hi.x() - box.lo.x()) * bpp;
|
|
for (Int z = box.lo.z(); z < box.hi.z(); ++z) {
|
|
for (Int y = box.lo.y(); y < box.hi.y(); ++y) {
|
|
const SizeT offset = static_cast<SizeT>(z) * sliceBytes + static_cast<SizeT>(y) * rowBytes +
|
|
static_cast<SizeT>(box.lo.x()) * bpp;
|
|
Memcpy(snapshot.data() + offset, shadow + offset, spanBytes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (pipePending != nullptr) {
|
|
// The same merge for the applier's entry: its regions when it carries a list, its
|
|
// union box when it does not (RegionCount 0 = "the box is the whole story", D-D3).
|
|
const SizeT bpp = shadowBytes / texelCount;
|
|
const SizeT rowBytes = static_cast<SizeT>(extent.x()) * bpp;
|
|
const SizeT sliceBytes = static_cast<SizeT>(extent.y()) * rowBytes;
|
|
const auto mergePipeBox = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) {
|
|
for (Uint32 slice = 0; slice < d; ++slice) {
|
|
for (Uint32 row = 0; row < h; ++row) {
|
|
const SizeT offset = static_cast<SizeT>(z + static_cast<Int32>(slice)) * sliceBytes +
|
|
static_cast<SizeT>(y + static_cast<Int32>(row)) * rowBytes +
|
|
static_cast<SizeT>(x) * bpp;
|
|
Memcpy(snapshot.data() + offset, shadow + offset, static_cast<SizeT>(w) * bpp);
|
|
}
|
|
}
|
|
};
|
|
if (pipePending->Regions.empty()) {
|
|
const auto& box = pipePending->UnionBox;
|
|
mergePipeBox(box.X, box.Y, box.Z, box.W, box.H, box.D);
|
|
} else {
|
|
for (const auto& region : pipePending->Regions) {
|
|
mergePipeBox(region.X, region.Y, region.Z, region.W, region.H, region.D);
|
|
}
|
|
}
|
|
}
|
|
Memcpy(shadow, snapshot.data(), shadowBytes);
|
|
return true;
|
|
}
|
|
|
|
void BackendTextureObject::RecreateBackendTexture() {
|
|
if (m_backendTextureId != 0) {
|
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
|
// Application FBO twins that attached the dying id memoize on FRONTEND
|
|
// attachment versions, which this backend-side re-mint does not move;
|
|
// without this bump their driver FBOs would keep the deleted name
|
|
// attached forever (see g_attachmentBackendIdGeneration).
|
|
++FramebufferImpl::g_attachmentBackendIdGeneration;
|
|
if (m_contextGeneration == g_backendContextGeneration) {
|
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
|
}
|
|
for (auto& unitCache : g_boundTexturesCache) {
|
|
for (auto& boundTexture : unitCache) {
|
|
if (boundTexture == this) {
|
|
boundTexture = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// A shared image's EGLImage lives exactly as long as the name it was bound to.
|
|
m_sharedImage = nullptr;
|
|
#endif
|
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
|
m_contextGeneration = g_backendContextGeneration;
|
|
if (m_backendTextureId == 0) {
|
|
MGLOG_E_ONCE("Failed to regenerate texture object.");
|
|
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
|
} else {
|
|
MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId);
|
|
}
|
|
m_isInitialized = false;
|
|
m_backendStorageImmutable = false;
|
|
m_prevTextureInfo = {};
|
|
// The new ES texture starts at the ES defaults, so every parameter this object had
|
|
// already pushed onto the old one is gone. The change-detection caches below would
|
|
// otherwise still claim those values are in force and SyncTextureParamsToBackend
|
|
// would skip the whole pass on the unchanged params version, leaving the driver
|
|
// texture at defaults for the rest of its life. Latent for swizzle, LOD range and
|
|
// border colour long before GL_DEPTH_STENCIL_TEXTURE_MODE joined them; the mode
|
|
// makes it visible because falling back to the default silently samples the wrong
|
|
// aspect rather than merely mis-filtering.
|
|
m_cacheLodRange = {0, 1000};
|
|
m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue,
|
|
TextureSwizzleParam::Alpha};
|
|
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
|
|
m_forceTextureParamsResync = true;
|
|
// The filter/wrap/LOD cache belongs to the name that just went away, and its gate is
|
|
// the frontend SAMPLER's version, which a backend re-mint does not move - so without
|
|
// this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync.
|
|
m_cacheSamplerParameters = SamplerParameters{};
|
|
m_forceSamplerResync = true;
|
|
}
|
|
|
|
void ExerciseDefaultUnpackScopeForTesting() {
|
|
const ScopedDefaultUnpackState scope;
|
|
}
|
|
|
|
// See Managers.h. One flag for every context, like the image-unit high-water mark: a
|
|
// dispatch on another context can consume an upload's flag before the uploading
|
|
// context's own dispatch sees it - a hole only a shared texture re-uploaded on one context
|
|
// and image-accessed on another can fall into, and GL wants a fence between those anyway.
|
|
static Bool g_inPlaceTextureUploadPending = false;
|
|
|
|
void NoteInPlaceTextureUpload() {
|
|
g_inPlaceTextureUploadPending = true;
|
|
}
|
|
|
|
GLbitfield TakeInPlaceTextureUploadBarrierBits() {
|
|
if (!g_inPlaceTextureUploadPending) return 0;
|
|
g_inPlaceTextureUploadPending = false;
|
|
return GL_TEXTURE_UPDATE_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT;
|
|
}
|
|
|
|
// Components per texel the frontend format's client data carries. Only the three-channel
|
|
// formats that can be widened to a four-channel render target need an answer (see
|
|
// PrepareChannelWidenedUpload); everything else keeps its own layout and reports 0.
|
|
Uint GetWidenableClientComponentCount(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::RGB8Snorm:
|
|
case TextureInternalFormat::RGB16Snorm:
|
|
case TextureInternalFormat::RGB16:
|
|
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
|
|
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
|
|
case TextureInternalFormat::RGB16F:
|
|
case TextureInternalFormat::RGB32F:
|
|
case TextureInternalFormat::SRGB8:
|
|
case TextureInternalFormat::RGB8I:
|
|
case TextureInternalFormat::RGB8UI:
|
|
case TextureInternalFormat::RGB16I:
|
|
case TextureInternalFormat::RGB16UI:
|
|
case TextureInternalFormat::RGB32I:
|
|
case TextureInternalFormat::RGB32UI:
|
|
return 3;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// True when the widened format's client data is integer rather than normalized. The two
|
|
// classes share every narrow component type - GL_RGB8I and GL_RGB8_SNORM are both uploaded
|
|
// as GL_BYTE - but their "1.0" differs: an integer channel's one is the integer 1, a
|
|
// normalized channel's is the saturated field. The type alone cannot tell them apart, so
|
|
// the source format has to.
|
|
Bool IsIntegerWidenableFormat(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::RGB8I:
|
|
case TextureInternalFormat::RGB8UI:
|
|
case TextureInternalFormat::RGB16I:
|
|
case TextureInternalFormat::RGB16UI:
|
|
case TextureInternalFormat::RGB32I:
|
|
case TextureInternalFormat::RGB32UI:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
// P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was
|
|
// resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the
|
|
// parameter sync and the built-in sampler sync) read Desc.Target here, through the same
|
|
// StagedTextureTargetForPipeTarget mapping the storage sync already routes through - so all
|
|
// three bodies answer the question from one carrier rather than two authorities.
|
|
#define MGB_TEXPARAM_TARGET(rec, obj) \
|
|
((rec) != nullptr ? BufferImpl::StagedTextureTargetForPipeTarget((rec)->Desc.Target) : (obj)->GetTarget())
|
|
|
|
// The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought.
|
|
// Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability
|
|
// is the extension AND the pointer, because eglGetProcAddress hands back live-looking
|
|
// stubs (see AcquireGLESFunctions).
|
|
static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() {
|
|
if (g_GLESFuncs.glTextureViewEXT != nullptr) {
|
|
return g_GLESFuncs.glTextureViewEXT;
|
|
}
|
|
return reinterpret_cast<MG_External::GLES::glTextureViewEXT_PTR>(g_GLESFuncs.glTextureViewOES);
|
|
}
|
|
|
|
// P5e (tx2): see the declaration for the split between what the record answers and what
|
|
// the wire still cannot say about a view.
|
|
void BackendTextureObject::SyncTextureViewToBackendByRecord(
|
|
MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record,
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
|
if (!g_GLESCapabilities.SupportsTextureView) {
|
|
// Unreachable through the API - the frontend refuses glTextureView without
|
|
// GL_ARB_texture_view - and kept for the reason the frontend arm keeps its twin.
|
|
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
|
|
"EXT/OES_texture_view.",
|
|
record.Desc.GlNameForDiag);
|
|
return;
|
|
}
|
|
// The storage owner by handle, through the same by-handle entry every other draw-path
|
|
// texture takes: no frontend object, no allocator probe.
|
|
const SharedPtr<BackendTextureObject> storageBackendObject =
|
|
SyncTextureToBackendByHandle(record.Desc.ViewOf, m_imageBindableStorageRequired);
|
|
if (!storageBackendObject) {
|
|
MGLOG_E_ONCE("Failed to sync the storage texture {%u, %u} of view %u on the handle arm.",
|
|
record.Desc.ViewOf.Slot, record.Desc.ViewOf.Gen, record.Desc.GlNameForDiag);
|
|
return;
|
|
}
|
|
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
|
|
if (storageBackendTextureId == 0) {
|
|
MGLOG_D("Storage texture of view %u has no ES name yet.", record.Desc.GlNameForDiag);
|
|
return;
|
|
}
|
|
// P3b/P4b D3: THE SERIAL THIS TWIN STAMPS IS THE STORAGE'S, not this view's, because
|
|
// the gate that reads it back (IsDrawSyncCleanByRecord) now asks the same record. It
|
|
// is read AFTER the storage sync above - which has just consumed the owner's pending
|
|
// uploads - and taken by VALUE, because the applier's vector may move under any later
|
|
// call and a Uint64 cannot dangle. A storage record that has gone answers 0, which
|
|
// the gate reads as "never clean": the direction that re-syncs.
|
|
const MG_Pipe::MGPipeResourceRecord* storageRecord = PipeTextureStorageRecordForRecord(record);
|
|
const Uint64 storageSerial = storageRecord != nullptr ? storageRecord->Serial : 0;
|
|
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
|
|
// The steady case, and the only one this arm has to be fast at: one record read,
|
|
// one integer compare and the serial stamp that keeps the aggregate clean gate
|
|
// (IsDrawSyncCleanByRecord) answering true for the next draw.
|
|
m_syncedResourceSerial = storageSerial;
|
|
return;
|
|
}
|
|
// The view window is a sampler-view CSO, created even when this
|
|
// texture is only image-bound or attached to an FBO. It is not part
|
|
// of the resource descriptor, and needs no frontend fallback.
|
|
(void)stateTextureObject;
|
|
const auto& applier = MG_Pipe::MGPipeApplier();
|
|
const auto handle = record.ViewCso;
|
|
if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= applier.SamplerViewCsos.size()) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-cso\"} {%u,%u}", res.Slot, res.Gen);
|
|
std::abort();
|
|
}
|
|
const auto& cso = applier.SamplerViewCsos[handle.Slot];
|
|
const auto& view = cso.View;
|
|
if (!cso.Live || cso.Gen != handle.Gen || view.Texture != res || !view.NumLevels || !view.NumLayers) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"texture-view-window\"} {%u,%u}", res.Slot, res.Gen);
|
|
std::abort();
|
|
}
|
|
const auto viewTarget = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
|
const GLenum nativeTarget = ConvertTextureTargetToBackendGLEnum(viewTarget);
|
|
GLenum internalFormat = 0, transferFormat = 0, transferType = 0;
|
|
GenerateTextureFormatInfo(static_cast<TextureInternalFormat>(view.InternalFormat), &internalFormat,
|
|
&transferFormat, &transferType, viewTarget);
|
|
// A name already bound or used as a view cannot be redefined by
|
|
// glTextureView. This also invalidates native binding/FBO caches.
|
|
RecreateBackendTexture();
|
|
DebugImpl::ErrorLopper::Clear();
|
|
ResolveTextureViewEntryPoint()(m_backendTextureId, nativeTarget, storageBackendTextureId, internalFormat,
|
|
view.MinLevel, view.NumLevels, view.MinLayer, view.NumLayers);
|
|
const GLenum error = g_GLESFuncs.glGetError();
|
|
if (error != GL_NO_ERROR) {
|
|
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"texture-view-native\"} {%u,%u} error=0x%x", res.Slot,
|
|
res.Gen, error);
|
|
std::abort();
|
|
}
|
|
m_viewSourceBackendTextureId = storageBackendTextureId;
|
|
m_isInitialized = true;
|
|
m_backendStorageImmutable = true;
|
|
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
|
|
record.Desc.Width,
|
|
record.Desc.Height,
|
|
record.Desc.Depth,
|
|
view.NumLevels,
|
|
0,
|
|
record.Desc.Samples,
|
|
record.Desc.FixedSampleLocations != 0};
|
|
// The storage's, for the steady arm's reason above (P3b/P4b D3).
|
|
m_syncedResourceSerial = storageSerial;
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). The client defined
|
|
// level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor
|
|
// still says so, but the texels are the image's, so nothing below may allocate or upload.
|
|
Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) {
|
|
const Uint64 imageId = record.SharedImageId;
|
|
if (imageId == 0) {
|
|
// Respecified away from the image. The ES name is an EGLImage sibling, which must not
|
|
// be redefined in place: the immutable flag sends the normal path through
|
|
// RecreateBackendTexture, and the EGLImage goes with the name it was bound to.
|
|
m_sharedImageId = 0;
|
|
m_isInitialized = false;
|
|
m_backendStorageImmutable = true;
|
|
return false;
|
|
}
|
|
|
|
// Steady state: bound to this image already (the serial moves with sub-data and with a
|
|
// metadata respecify, neither of which changes what the name samples). A failed bind is
|
|
// retried only once the record moves. Pending sub-data is the caller's
|
|
// (ReplaySharedImageUploads), after the image's sync state has been acquired.
|
|
if (m_sharedImageId == imageId && m_isInitialized &&
|
|
(m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) {
|
|
m_syncedResourceSerial = record.Serial;
|
|
return true;
|
|
}
|
|
|
|
// A fresh mutable name: the current one may hold immutable storage (which no EGLImage
|
|
// can replace) or another image's sibling. RecreateBackendTexture also moves the FBO
|
|
// twins' attachment generation and forces the parameter resync the new name needs.
|
|
RecreateBackendTexture();
|
|
m_sharedImage = nullptr;
|
|
m_sharedImageId = imageId;
|
|
m_isInitialized = true;
|
|
// Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm).
|
|
m_backendStorageImmutable = true;
|
|
m_syncedResourceSerial = record.Serial;
|
|
|
|
const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
|
if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) {
|
|
MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty",
|
|
record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str());
|
|
return true;
|
|
}
|
|
String why;
|
|
SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why);
|
|
if (image == nullptr) {
|
|
MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty",
|
|
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId), why.c_str());
|
|
return true;
|
|
}
|
|
Bind(GL_TEXTURE_2D);
|
|
if (SharedImageImpl::IsYuv(image)) {
|
|
// A YUV image is CONVERTED into the name's own RGBA8 level at each frame's first use
|
|
// (SharedImageImpl::ConvertYuvForSampling), never made its storage.
|
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
|
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, static_cast<GLsizei>(record.Desc.Width),
|
|
static_cast<GLsizei>(record.Desc.Height));
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
MGLOG_E_ONCE("Shared image texture %u: no %ux%u RGBA8 storage for YUV image %llu; left empty",
|
|
record.Desc.GlNameForDiag, record.Desc.Width, record.Desc.Height,
|
|
static_cast<unsigned long long>(imageId));
|
|
return true;
|
|
}
|
|
} else if (!SharedImageImpl::TargetBoundTexture2D(image)) {
|
|
MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty",
|
|
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId));
|
|
return true;
|
|
}
|
|
m_sharedImage = std::move(image);
|
|
m_sharedImageUploadsRgb = false;
|
|
// The shape the name now has: the descriptor's level 0, and that one level only.
|
|
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
|
|
record.Desc.Width,
|
|
record.Desc.Height,
|
|
record.Desc.Depth,
|
|
1,
|
|
0,
|
|
record.Desc.Samples,
|
|
record.Desc.FixedSampleLocations != 0};
|
|
return true;
|
|
}
|
|
|
|
// SUB-DATA ON A SHARED IMAGE IS WRITTEN INTO IT. An X server drawing with glamor uploads
|
|
// its clients' pixels (MIT-SHM / PutImage) into window pixmaps whose level 0 is a shared
|
|
// image, with glTexSubImage2D: the texels are in the staged store (the client defined the
|
|
// level without data, so only the uploaded boxes are covered), and they go into the
|
|
// EGLImage-backed name box by box. Every pending entry leaves the set either way, or the
|
|
// per-draw clean gate would never close. Called after AcquireForSampling, so the upload is
|
|
// ordered after the image's pending reads and writes like any other use.
|
|
void BackendTextureObject::ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record) {
|
|
if (record.PendingUploads.empty()) return;
|
|
Vector<std::pair<Uint16, Uint16>> keys;
|
|
Vector<std::pair<Uint16, MG_Pipe::MGPBox>> boxes;
|
|
keys.reserve(record.PendingUploads.size());
|
|
for (const auto& pending : record.PendingUploads) {
|
|
const Uint16 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget);
|
|
keys.emplace_back(uploadHalf, pending.Level);
|
|
if (pending.Level != 0) continue; // an image has one level
|
|
if (pending.Regions.empty()) {
|
|
boxes.emplace_back(uploadHalf, pending.UnionBox);
|
|
} else {
|
|
for (const auto& region : pending.Regions)
|
|
boxes.emplace_back(uploadHalf,
|
|
MG_Pipe::MGPBox{region.X, region.Y, region.Z, region.W, region.H, region.D});
|
|
}
|
|
}
|
|
if (m_sharedImage != nullptr && !boxes.empty()) {
|
|
auto& store = MG_Record::ServerStagedTexture();
|
|
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(record.Desc.Resource);
|
|
Bind(GL_TEXTURE_2D);
|
|
ScopedDefaultUnpackState unpackState;
|
|
BufferImpl::BindPixelUnpackBufferId(0);
|
|
for (const auto& [uploadHalf, box] : boxes) {
|
|
Int rowTexels = 0;
|
|
// A shared image is RGBA8 to every client that names it (the level the client
|
|
// defined over it): four bytes a texel.
|
|
const Uint8* bytes =
|
|
store.CoveredBoxBytes(key, uploadHalf, 0, box.X, box.Y, box.W, box.H, 4, &rowTexels);
|
|
if (bytes == nullptr) {
|
|
MGLOG_E_ONCE("Shared image texture %u: an upload of %ux%u at (%d,%d) has no staged texels; "
|
|
"not written",
|
|
record.Desc.GlNameForDiag, box.W, box.H, box.X, box.Y);
|
|
continue;
|
|
}
|
|
const Uint8* origin =
|
|
bytes +
|
|
(static_cast<SizeT>(box.Y) * static_cast<SizeT>(rowTexels) + static_cast<SizeT>(box.X)) * 4;
|
|
if (!m_sharedImageUploadsRgb) {
|
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
|
|
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowTexels);
|
|
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
|
|
static_cast<GLsizei>(box.H), GL_RGBA, GL_UNSIGNED_BYTE, origin);
|
|
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
// An image of an X format (no alpha) is an RGB8 texture to the driver, which
|
|
// takes only RGB texels: this image's uploads are converted from now on.
|
|
if (g_GLESFuncs.glGetError() != GL_INVALID_OPERATION) continue;
|
|
m_sharedImageUploadsRgb = true;
|
|
}
|
|
Vector<Uint8> rgb(static_cast<SizeT>(box.W) * box.H * 3);
|
|
for (Uint32 row = 0; row < box.H; ++row) {
|
|
const Uint8* src = origin + static_cast<SizeT>(row) * static_cast<SizeT>(rowTexels) * 4;
|
|
Uint8* dst = rgb.data() + static_cast<SizeT>(row) * box.W * 3;
|
|
for (Uint32 x = 0; x < box.W; ++x) {
|
|
dst[x * 3 + 0] = src[x * 4 + 0];
|
|
dst[x * 3 + 1] = src[x * 4 + 1];
|
|
dst[x * 3 + 2] = src[x * 4 + 2];
|
|
}
|
|
}
|
|
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
|
|
static_cast<GLsizei>(box.H), GL_RGB, GL_UNSIGNED_BYTE, rgb.data());
|
|
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
|
}
|
|
MGLOG_I_ONCE("Shared image texture %u: sub-data written into its image (%s texels)",
|
|
record.Desc.GlNameForDiag, m_sharedImageUploadsRgb ? "RGB" : "RGBA");
|
|
}
|
|
for (const auto& [uploadHalf, level] : keys)
|
|
ConsumePipeTextureUpload(record.Desc.Resource, uploadHalf, level);
|
|
}
|
|
#endif
|
|
|
|
// Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view
|
|
// that needs no work costs the same nothing per draw that any other synced texture does.
|
|
void BackendTextureObject::StampViewSyncKeys(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
|
if (MG_Pipe::gPipeInputs->IsLive()) {
|
|
m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId();
|
|
m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration();
|
|
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
|
|
}
|
|
m_syncedContentVersion = stateTextureObject->GetContentVersion();
|
|
}
|
|
|
|
const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject,
|
|
const MG_Pipe::MGPipeResourceRecord* record) {
|
|
if (record == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
|
|
"built-in sampler cannot be pushed (handle {%u, %u})",
|
|
TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot,
|
|
m_pushedSyncHandle.Gen);
|
|
return nullptr;
|
|
}
|
|
// NO set_texture_params HAS BEEN APPLIED FOR THIS TEXTURE YET, which is not the same
|
|
// thing as a malformed one and must not be read as one. ParamsSerial is the
|
|
// applier's own "have I ever stored a params record here" (it is bumped on every
|
|
// set_texture_params and starts at 0), and Params is value-initialised beside it -
|
|
// so on a texture the application has never given a parameter to, BuiltinSampler is
|
|
// null because the record was never written, not because a client wrote a null.
|
|
// There is nothing to push in that state; SyncTextureParamsToBackend's own gate
|
|
// reaches the same conclusion silently (m_syncedParamsSerial starts at 0 too and the
|
|
// compare skips), and this decline is made silent for the same reason. Found by the
|
|
// verification round's census: three cases were declining loudly here on textures
|
|
// that had simply never been glTexParameter'd.
|
|
if (record->ParamsSerial == 0) {
|
|
MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in "
|
|
"sampler to push.",
|
|
TextureDiagName(stateTextureObject, record));
|
|
return nullptr;
|
|
}
|
|
// D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption}
|
|
// territory - EVERY ITextureObject owns a SamplerObject, so a null is a malformed
|
|
// record and not "no sampler". The applier is where that verdict is raised; this
|
|
// side names it and declines, because a backend that sampled through whatever the
|
|
// driver texture last held would be the silent half of the same bug.
|
|
const MG_Pipe::MGPipeHandle builtin = record->Params.BuiltinSampler;
|
|
if (MG_Pipe::MGPipeHandleIsNull(builtin)) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u's set_texture_params names the null handle as its "
|
|
"built-in sampler CSO; every texture owns a sampler object, so this "
|
|
"record is malformed - declining the built-in sampler push",
|
|
TextureDiagName(stateTextureObject, record));
|
|
return nullptr;
|
|
}
|
|
const auto* cso = PipeSamplerCsoRecordForHandle(builtin);
|
|
if (cso == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier "
|
|
"record - declining the built-in sampler push",
|
|
TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen);
|
|
return nullptr;
|
|
}
|
|
// THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are
|
|
// content-addressed at capacity 256 (D-F1), so two textures with identical sampling
|
|
// share one CSO and a texture whose sampling diverges is re-pointed at a DIFFERENT
|
|
// CSO whose serial may well be smaller than the one it left. Keying on the serial
|
|
// alone would then skip the re-push and leave the driver texture filtering with the
|
|
// old CSO's values for ever.
|
|
//
|
|
// SamplerResync is the second half of the gate and it is the one that matters more
|
|
// than mis-filtering: ES makes a texture INCOMPLETE when its filters do not suit its
|
|
// level set, and an incomplete texture samples (0,0,0,1) rather than its contents.
|
|
// The server SETS it (RecreateBackendTexture) and the wire byte exists so the client
|
|
// can force a resync it knows about; neither side clears the other's copy (D-E2), so
|
|
// it is read here and never written back.
|
|
if (m_syncedBuiltinSampler == builtin && m_syncedBuiltinSamplerSerial == cso->Serial &&
|
|
record->Params.SamplerResync == 0 && !m_forceSamplerResync) {
|
|
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
|
return nullptr;
|
|
}
|
|
m_syncedBuiltinSampler = builtin;
|
|
m_syncedBuiltinSamplerSerial = cso->Serial;
|
|
m_forceSamplerResync = false;
|
|
return &cso->Params;
|
|
}
|
|
|
|
void BackendTextureObject::SyncBuiltinSamplerToBackend(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
|
|
// P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on
|
|
// this twin - the record and the built-in sampler's CSO answer everything below and
|
|
// the frontend object is not consulted at all. Without a handle it is still the
|
|
// caller's bug it always was. P5e (fix1): and with a handle noted on an arm that
|
|
// cannot serve it, it is Fatal{RoleViolation, "texture-handle-arm"} - see the
|
|
// refusal's own comment beside MGB_TEXTURE_HANDLE_ARM_OFF.
|
|
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncBuiltinSamplerToBackend")) {
|
|
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
|
|
return;
|
|
}
|
|
|
|
// P4a (D-E1): the fifteen sampler values are SAMPLER state (GL 4.6 table 23.18) and
|
|
// Espryt pushes them with glTexParameter* onto the TEXTURE rather than with
|
|
// glBindSampler onto the unit - behaviour P4a preserves exactly. What changes is
|
|
// where the values and the "did they move" answer come from.
|
|
//
|
|
// The whole arm choice is a preprocessor #if/#else rather than a runtime branch
|
|
// around one shared prologue, and that is a G1 requirement (D-P): the PULL build's
|
|
// text below the #else is the pre-P4a text token for token, so its object code
|
|
// cannot move. Routing the pull path through a pointer it did not have before was
|
|
// measured at -2 bytes on this function, which is a G1 failure.
|
|
const SamplerParameters* pendingSamplerParams = nullptr;
|
|
// P5e (tx2): resolved ONCE and shared with the target read below, which used to be
|
|
// the frontend `GetTarget()` this arm was not allowed to make.
|
|
const MG_Pipe::MGPipeResourceRecord* pushedDesc = nullptr;
|
|
pushedDesc = ResolveOwnRecord(stateTextureObject);
|
|
pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc);
|
|
if (pendingSamplerParams == nullptr) return;
|
|
|
|
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
|
|
m_backendTextureId, TextureDiagName(stateTextureObject, pushedDesc));
|
|
auto targetInternal = MGB_TEXPARAM_TARGET(pushedDesc, stateTextureObject);
|
|
GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal);
|
|
MGLOG_D(" Texture target for syncing is %s",
|
|
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
|
if (!IsSupportedTextureTarget(targetInternal)) {
|
|
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
|
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
|
return;
|
|
}
|
|
|
|
const SamplerParameters& samplerParams = *pendingSamplerParams;
|
|
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
|
m_cacheSamplerParameters = samplerParams;
|
|
return;
|
|
}
|
|
|
|
Bind(target);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
// Update built-in sampler parameters
|
|
MGLOG_D("Updating sampler parameters for texture with ID: %u", m_backendTextureId);
|
|
|
|
#define SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \
|
|
if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \
|
|
g_GLESFuncs.glTexParameteri(target, glName, \
|
|
MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \
|
|
m_cacheSamplerParameters.internalName = samplerParams.internalName; \
|
|
DebugImpl::ErrorLopper::Loop( \
|
|
[file = __FILE__, line = __LINE__, func = __func__, \
|
|
t = MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)](GLenum err) { \
|
|
MGLOG_D("%s(%s:%d) ES error %s, GL_TEXTURE_MIN_FILTER = %s", func, file, line, \
|
|
MG_Util::ConvertGLEnumToString(err).c_str(), MG_Util::ConvertGLEnumToString(t).c_str()); \
|
|
}); \
|
|
}
|
|
|
|
if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter ||
|
|
m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) {
|
|
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
|
|
(GLint)ResolveBackendMinFilter(samplerParams, IsAngleLlvmpipeRenderer()));
|
|
m_cacheSamplerParameters.minFilter = samplerParams.minFilter;
|
|
m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode;
|
|
}
|
|
if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) {
|
|
g_GLESFuncs.glTexParameteri(
|
|
target, GL_TEXTURE_MAG_FILTER,
|
|
(GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None));
|
|
m_cacheSamplerParameters.magFilter = samplerParams.magFilter;
|
|
}
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode)
|
|
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode)
|
|
if (SupportsWrapR(targetInternal)) {
|
|
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode)
|
|
} else {
|
|
m_cacheSamplerParameters.wrapR = samplerParams.wrapR;
|
|
}
|
|
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc)
|
|
SYNC_TEX_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode)
|
|
if (m_cacheSamplerParameters.minLod != samplerParams.minLod) {
|
|
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MIN_LOD, samplerParams.minLod);
|
|
m_cacheSamplerParameters.minLod = samplerParams.minLod;
|
|
}
|
|
if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) {
|
|
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
|
|
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
|
|
}
|
|
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
|
|
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
|
|
g_GLESFuncs.glTexParameterf(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, samplerParams.maxAnisotropy);
|
|
}
|
|
// Unsupported GLES backends intentionally treat anisotropy as a
|
|
// frontend-only no-op; remember the observed value so the cache
|
|
// remains coherent without issuing an illegal enum every sync.
|
|
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
|
|
}
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
#undef SYNC_TEX_SAMPLER_PARAM_IF_CHANGED
|
|
}
|
|
|
|
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
|
// P5e (tx2): the handle the caller adopted this twin with, and only failing that the
|
|
// client allocator - ResolveOwnRecord holds the whole of that rule.
|
|
const auto* record = ResolveOwnRecord(stateTextureObject);
|
|
if (record == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
|
|
"parameters cannot be pushed (handle {%u, %u})",
|
|
TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot,
|
|
m_pushedSyncHandle.Gen);
|
|
return nullptr;
|
|
}
|
|
// ParamsSerial + ForceResync replace m_syncedTextureParamsVersion +
|
|
// m_forceTextureParamsResync. ForceResync exists because the widened-channel
|
|
// carrier needs a swizzle override the frontend params version does not move for;
|
|
// it is SET by the server (RequireImageBindableStorage, RecreateBackendTexture set
|
|
// m_forceTextureParamsResync on the twin) and by the client on the wire, and
|
|
// NEITHER SIDE CLEARS THE OTHER'S (D-E2) - so the wire byte is read and never
|
|
// written back, while the twin's own flag is cleared here exactly as it was before.
|
|
if (m_syncedParamsSerial == record->ParamsSerial && record->Params.ForceResync == 0 &&
|
|
!m_forceTextureParamsResync) {
|
|
MGLOG_D("Texture parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
|
|
return nullptr;
|
|
}
|
|
m_syncedParamsSerial = record->ParamsSerial;
|
|
m_forceTextureParamsResync = false;
|
|
return record;
|
|
}
|
|
|
|
// P4a (D-E1): the six values SyncTextureParamsToBackend pushes, spelled once per arm.
|
|
// MACROS, for the reason MGB_LEVEL_NEEDS_UPLOAD gives one function up: in the PULL build
|
|
// each expands to the pre-P4a expression at its original site, so that build's tokens -
|
|
// and its object code, whose symbol sizes G1 pins to the byte - are unchanged. Reading
|
|
// them into locals instead was measured at +9 bytes on this function.
|
|
//
|
|
// Four of the six come from MGPTextureParams, which is per texture OBJECT and
|
|
// independent of any binding - that is what closes D-E3's gap, where a texture that is
|
|
// only a READ-framebuffer attachment reaches SyncMipmapsToBackend and nothing else. The
|
|
// FORMAT comes from the descriptor beside them, and the BORDER comes from the built-in
|
|
// sampler's CSO (D-F4), because a border colour is sampler state.
|
|
#define MGB_TEXPARAM_LEVEL_RANGE \
|
|
(pushedRecord != nullptr ? UintVec2{static_cast<Uint>(pushedRecord->Params.BaseLevel), \
|
|
static_cast<Uint>(pushedRecord->Params.MaxLevel)} \
|
|
: stateTextureObject->GetLevelRange())
|
|
#define MGB_TEXPARAM_FORMAT \
|
|
(pushedRecord != nullptr ? static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat) \
|
|
: stateTextureObject->GetFormat())
|
|
#define MGB_TEXPARAM_SWIZZLE \
|
|
(pushedRecord != nullptr \
|
|
? Vec4<TextureSwizzleParam>{static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[0]), \
|
|
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[1]), \
|
|
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[2]), \
|
|
static_cast<TextureSwizzleParam>(pushedRecord->Params.Swizzle[3])} \
|
|
: stateTextureObject->GetAllSwizzleParams())
|
|
// The contract now spells this encoding (c0c, ID-12 DV-2): kMGPipeDepthStencilModeDepth = 0 =
|
|
// GL_DEPTH_COMPONENT, kMGPipeDepthStencilModeStencil = 1 = GL_STENCIL_INDEX. v1 open-coded the
|
|
// direction as a bare `!= 0` and recorded it as a deviation; the constants are read here instead,
|
|
// so a future re-numbering moves both sides at once. ZERO MEANS THE DEFAULT is still what decides
|
|
// the direction: GL_DEPTH_COMPONENT is the GL and ES default and a texture that never asks for
|
|
// the stencil aspect never emits the call, so a zeroed record has to decode to exactly what such
|
|
// a texture already has.
|
|
#define MGB_TEXPARAM_DS_MODE \
|
|
(pushedRecord != nullptr \
|
|
? (pushedRecord->Params.DepthStencilMode == MG_Pipe::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX \
|
|
: GL_DEPTH_COMPONENT) \
|
|
: stateTextureObject->GetDepthStencilTextureMode())
|
|
#define MGB_TEXPARAM_BORDER_F \
|
|
(pushedBorder != nullptr ? pushedBorder->borderColor : stateTextureObject->GetBorderColor())
|
|
#define MGB_TEXPARAM_BORDER_I \
|
|
(pushedBorder != nullptr ? pushedBorder->borderColorI : stateTextureObject->GetBorderColorI())
|
|
#define MGB_TEXPARAM_BORDER_UI \
|
|
(pushedBorder != nullptr ? pushedBorder->borderColorUI : stateTextureObject->GetBorderColorUI())
|
|
#define MGB_TEXPARAM_BORDER_FORM \
|
|
(pushedBorder != nullptr ? pushedBorder->borderColorForm : stateTextureObject->GetBorderColorForm())
|
|
|
|
void BackendTextureObject::SyncTextureParamsToBackend(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
|
|
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
|
|
// see the twin note in SyncBuiltinSamplerToBackend.
|
|
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncTextureParamsToBackend")) {
|
|
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
|
|
return;
|
|
}
|
|
|
|
// P4a (D-E1/D-E3): where the parameter values come from, and what says they moved.
|
|
// Null on the handle arm means "nothing to do" and is NEVER a fall-back to the
|
|
// frontend; the legacy arm keeps the params-version pair verbatim, token for token,
|
|
// because the PULL build's object code may not move (D-P).
|
|
const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr;
|
|
const SamplerParameters* pushedBorder = nullptr;
|
|
// Whether the four border comparands can be READ at all this sync. On the handle arm
|
|
// a missing sampler CSO makes them unreadable and the border block below is skipped
|
|
// with the refusal already named; on the legacy arm they always come from the
|
|
// frontend object, so it is constant true and folds away.
|
|
Bool borderColorReadable = true;
|
|
pushedRecord = ResolvePushedTextureParams(stateTextureObject);
|
|
if (pushedRecord == nullptr) return;
|
|
// D-F4: the border colour is SAMPLER state (GL 4.6 table 23.18) and lives on the
|
|
// built-in sampler's CSO, not on MGPTextureParams - P4a deliberately does not
|
|
// put SamplerParameters on the wire twice. All FOUR comparands (float, int,
|
|
// uint, form) still cross and are still all compared below, because two integer
|
|
// borders differing above 2^24 share one float and a Float -> Int transition can
|
|
// leave every number unchanged while needing a different driver entry point.
|
|
const auto* borderCso = PipeSamplerCsoRecordForHandle(pushedRecord->Params.BuiltinSampler);
|
|
if (borderCso != nullptr) {
|
|
pushedBorder = &borderCso->Params;
|
|
} else {
|
|
borderColorReadable = false;
|
|
MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier "
|
|
"record, so its border colour cannot be pushed",
|
|
TextureDiagName(stateTextureObject, pushedRecord),
|
|
pushedRecord->Params.BuiltinSampler.Slot, pushedRecord->Params.BuiltinSampler.Gen);
|
|
}
|
|
|
|
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
|
|
TextureDiagName(stateTextureObject, pushedRecord));
|
|
|
|
auto targetInternal = MGB_TEXPARAM_TARGET(pushedRecord, stateTextureObject);
|
|
GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal);
|
|
MGLOG_D(" Texture target for syncing is %s",
|
|
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
|
if (!IsSupportedTextureTarget(targetInternal)) {
|
|
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
|
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
|
return;
|
|
}
|
|
|
|
// Multisample targets reject the *sampler* parameters (LOD range, border color) but
|
|
// GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them.
|
|
// Bailing out entirely used to drop every swizzle write on the floor, which is what the
|
|
// frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which
|
|
// deliberately omits the swizzle enums). Note the caches for the skipped parameters are
|
|
// still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the
|
|
// change detection below would swallow the very writes we came here to emit.
|
|
const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal);
|
|
if (isMultisampleTarget) {
|
|
m_cacheLodRange = MGB_TEXPARAM_LEVEL_RANGE;
|
|
if (borderColorReadable) m_cacheBorderColor = MGB_TEXPARAM_BORDER_F;
|
|
}
|
|
|
|
Bind(target);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
// Update texture parameters
|
|
MGLOG_D("Updating texture parameters for texture with ID: %u", m_backendTextureId);
|
|
|
|
const auto& levelRange = MGB_TEXPARAM_LEVEL_RANGE;
|
|
|
|
if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) {
|
|
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
|
|
m_cacheLodRange.x() = levelRange.x();
|
|
}
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) {
|
|
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
|
|
m_cacheLodRange.y() = levelRange.y();
|
|
}
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
// A three-channel format widened to four to keep the image colour-renderable (see
|
|
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
|
|
// whatever the draw that filled it wrote there is not what GL would report: a format
|
|
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
|
|
// promotion stays invisible, composed with the swizzle the application asked for.
|
|
Vec4<TextureSwizzleParam> swizzleParams = MGB_TEXPARAM_SWIZZLE;
|
|
if (TextureImpl::BackendTextureFormatAddsAlpha(MGB_TEXPARAM_FORMAT, targetInternal)) {
|
|
for (SizeT channel = 0; channel < 4; ++channel) {
|
|
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
|
|
swizzleParams[channel] = TextureSwizzleParam::One;
|
|
}
|
|
}
|
|
}
|
|
// The same composition for the image-format widening, which can add TWO or THREE
|
|
// channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the
|
|
// format does not have as 0, except alpha, which reads as 1 - so a sampler must see
|
|
// those constants and not whatever the widened storage holds. The upload and the
|
|
// shader's own store mask already keep them at exactly these values; this covers
|
|
// storage nothing has written yet (glTexStorage with no upload), whose surplus
|
|
// channels are undefined. Composed with the application's own swizzle for the same
|
|
// reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the
|
|
// logical texel, so it is the source that is substituted, never the destination.
|
|
if (const auto imageWidening = m_imageBindableStorageRequired
|
|
? TextureImpl::GetImageBindableStorageWidening(MGB_TEXPARAM_FORMAT)
|
|
: TextureImpl::ImageBindableStorageWidening{}) {
|
|
for (SizeT channel = 0; channel < 4; ++channel) {
|
|
switch (swizzleParams[channel]) {
|
|
case TextureSwizzleParam::Green:
|
|
if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero;
|
|
break;
|
|
case TextureSwizzleParam::Blue:
|
|
if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero;
|
|
break;
|
|
case TextureSwizzleParam::Alpha:
|
|
if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (swizzleParams != m_cacheSwizzleParams) {
|
|
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
|
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
|
g_GLESFuncs.glTexParameteri(target, glEnum, MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams.func)); \
|
|
m_cacheSwizzleParams.func = swizzleParams.func; \
|
|
}
|
|
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(r(), GL_TEXTURE_SWIZZLE_R);
|
|
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(g(), GL_TEXTURE_SWIZZLE_G);
|
|
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(b(), GL_TEXTURE_SWIZZLE_B);
|
|
SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(a(), GL_TEXTURE_SWIZZLE_A);
|
|
#undef SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED
|
|
m_cacheSwizzleParams = swizzleParams;
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
}
|
|
|
|
// GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver
|
|
// without it every such call is INVALID_ENUM, so the parameter is simply not synced.
|
|
//
|
|
// The FORM has to be forwarded along with the value. A border colour set through
|
|
// glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the
|
|
// border returns whatever the driver's integer border register holds - so pushing it
|
|
// through glTexParameterfv handed the driver float 255.0 and the shader read back
|
|
// 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core
|
|
// beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate;
|
|
// the entry-point null check covers a driver that advertises the extension without them.
|
|
// The redundancy filter has to look at the AUTHORITATIVE representation, not just the
|
|
// float one: two integer borders that differ above 2^24 (16777216 and 16777217, say)
|
|
// collapse onto the same float, so a float-only comparison would skip the second sync and
|
|
// leave the driver holding the first value forever.
|
|
// m-3: GATED, because on the handle arm an unreadable border means the CSO record is
|
|
// missing and the block below is skipped with the refusal already named - so v1's
|
|
// unconditional initialiser was a live frontend read on a switched-over path whose
|
|
// value nothing then used. The value a skipped block sees is the cache's own, which
|
|
// cannot make the condition true.
|
|
const auto borderColorForm = borderColorReadable ? MGB_TEXPARAM_BORDER_FORM : m_cacheBorderColorForm;
|
|
if (!isMultisampleTarget && borderColorReadable && g_GLESCapabilities.SupportsTextureBorderClamp &&
|
|
(m_cacheBorderColor != MGB_TEXPARAM_BORDER_F || m_cacheBorderColorI != MGB_TEXPARAM_BORDER_I ||
|
|
m_cacheBorderColorUI != MGB_TEXPARAM_BORDER_UI || m_cacheBorderColorForm != borderColorForm)) {
|
|
if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) {
|
|
const auto& borderColorI = MGB_TEXPARAM_BORDER_I;
|
|
const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(),
|
|
borderColorI.w()};
|
|
g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
|
} else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) {
|
|
const auto& borderColorUI = MGB_TEXPARAM_BORDER_UI;
|
|
const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(),
|
|
borderColorUI.w()};
|
|
g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
|
} else {
|
|
const auto& borderColor = MGB_TEXPARAM_BORDER_F;
|
|
const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(),
|
|
borderColor.w()};
|
|
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
|
}
|
|
m_cacheBorderColor = MGB_TEXPARAM_BORDER_F;
|
|
m_cacheBorderColorI = MGB_TEXPARAM_BORDER_I;
|
|
m_cacheBorderColorUI = MGB_TEXPARAM_BORDER_UI;
|
|
m_cacheBorderColorForm = borderColorForm;
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
}
|
|
|
|
// GL_DEPTH_STENCIL_TEXTURE_MODE (GL_ARB_stencil_texturing / ES 3.1 core): which aspect
|
|
// of a packed depth/stencil image a sampler reads. Until this was forwarded the
|
|
// frontend kept the mode as a pure shadow - glGetTexParameter answered it, sampling
|
|
// ignored it - so a usampler2D bound to a D24S8 texture in STENCIL_INDEX mode read the
|
|
// depth aspect. Texture state rather than sampler state, so multisample targets take
|
|
// it too (ES 3.1 8.10 lists it among the three pnames they accept). It is only sent
|
|
// when it has moved, which for the overwhelming majority of textures is never.
|
|
const Bool supportsStencilTextureMode =
|
|
g_GLESCapabilities.GLESVersion.Major > 3 ||
|
|
(g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1);
|
|
if (supportsStencilTextureMode) {
|
|
const GLenum depthStencilTextureMode = MGB_TEXPARAM_DS_MODE;
|
|
if (m_cacheDepthStencilTextureMode != depthStencilTextureMode) {
|
|
g_GLESFuncs.glTexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE,
|
|
static_cast<GLint>(depthStencilTextureMode));
|
|
m_cacheDepthStencilTextureMode = depthStencilTextureMode;
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
#undef MGB_TEXPARAM_LEVEL_RANGE
|
|
#undef MGB_TEXPARAM_FORMAT
|
|
#undef MGB_TEXPARAM_SWIZZLE
|
|
#undef MGB_TEXPARAM_DS_MODE
|
|
#undef MGB_TEXPARAM_BORDER_F
|
|
#undef MGB_TEXPARAM_BORDER_I
|
|
#undef MGB_TEXPARAM_BORDER_UI
|
|
#undef MGB_TEXPARAM_BORDER_FORM
|
|
|
|
void ActivateTextureUnit(Uint unit) {
|
|
if (unit == g_activeTextureUnit) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glActiveTexture(GL_TEXTURE0 + unit);
|
|
g_activeTextureUnit = unit;
|
|
}
|
|
|
|
void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued
|
|
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
if (g_boundTexturesCache[unit][targetN] == nullptr) return;
|
|
|
|
ActivateTextureUnit(unit);
|
|
g_GLESFuncs.glBindTexture(target, 0);
|
|
g_boundTexturesCache[unit][targetN] = nullptr;
|
|
}
|
|
|
|
Uint g_activeTextureUnit = 0;
|
|
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_boundTexturesCache;
|
|
TwinRegistry<MG_State::GLState::ITextureObject, BackendTextureObject, MG_Pipe::MGPipeKind::Texture>
|
|
g_backendTextureObjects;
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
void WritePendingSharedImageUploads() {
|
|
auto& applier = MG_Pipe::MGPipeApplier();
|
|
for (SizeT slot = 0; slot < applier.TextureResources.size(); ++slot) {
|
|
const auto& record = applier.TextureResources[slot];
|
|
if (!record.Live || record.SharedImageId == 0 || record.PendingUploads.empty()) continue;
|
|
const MG_Pipe::MGPipeHandle handle{static_cast<Uint32>(slot), record.Gen};
|
|
auto* twin = g_backendTextureObjects.FindByHandle(handle);
|
|
if (twin == nullptr || !*twin) continue;
|
|
(*twin)->SyncMipmapsToBackendByHandle(handle);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract.
|
|
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
|
|
// THE RECORD FIRST: a texture handle with no resource record is a seam defect - the
|
|
// client named a texture it never created, or one whose resource_destroy has
|
|
// already applied while a binding still names it - and a twin adopted for it would
|
|
// own a driver texture nothing ever uploads to.
|
|
if (PipeTextureRecordForHandle(res) == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle "
|
|
"arm, so no driver texture can be built for it and the unit keeps what "
|
|
"it holds",
|
|
res.Slot, res.Gen);
|
|
return nullptr;
|
|
}
|
|
auto* slot = AdoptTwinByHandle(g_backendTextureObjects, res, "texture");
|
|
if (slot == nullptr) return nullptr;
|
|
if (!*slot) *slot = MakeShared<BackendTextureObject>();
|
|
return slot->get();
|
|
}
|
|
|
|
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite) {
|
|
// The storage owner's handle, one hop per view like the frontend walk above (the
|
|
// client composes a view of a view onto its root, so one hop is the real shape).
|
|
MG_Pipe::MGPipeHandle storage = res;
|
|
for (Uint hop = 0; hop < 4 && !MG_Pipe::MGPipeHandleIsNull(storage); ++hop) {
|
|
const auto* record = PipeTextureRecordForHandle(storage);
|
|
if (record == nullptr || MG_Pipe::MGPipeHandleIsNull(record->Desc.ViewOf)) break;
|
|
storage = record->Desc.ViewOf;
|
|
}
|
|
if (auto* twin = ResolveTextureTwin(storage)) twin->NoteDriverSideWrite();
|
|
// P8-E: the staged store stops standing for this texture's content (a no-op on a
|
|
// monolith arm, where the store does not copy).
|
|
if (!storeFollowsTheWrite && !MG_Pipe::MGPipeHandleIsNull(storage)) {
|
|
MG_Record::ServerStagedTexture().MarkDriverWritten(
|
|
MG_Record::StagedTextureStore::KeyForHandle(storage));
|
|
}
|
|
}
|
|
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
|
|
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
|
|
const IntVec3& destinationOrigin, const IntVec3& size) {
|
|
auto& store = MG_Record::ServerStagedTexture();
|
|
// P13 W4b: an aliasing store (monolith's record arm) follows too; see FollowCopy.
|
|
if (!store.Holds()) return false;
|
|
// The store's level key for an endpoint copy-image addresses as ONE image with its
|
|
// layers on z: exactly one upload target (not a cube map, whose faces are six store
|
|
// levels) and not a 1D array (the store keeps its layers as rows). A view's own levels
|
|
// are byteless, so FollowCopy answers false for it by itself.
|
|
const auto storeTarget = [](MG_Pipe::MGPipeHandle handle, Uint16& target) {
|
|
const auto* record = PipeTextureRecordForHandle(handle);
|
|
if (record == nullptr ||
|
|
record->Desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex1DArray)) {
|
|
return false;
|
|
}
|
|
const auto& targets = BufferImpl::StagedUploadTargetsForPipeTarget(record->Desc.Target);
|
|
if (targets.size() != 1) return false;
|
|
target = static_cast<Uint16>(targets[0]);
|
|
return true;
|
|
};
|
|
Uint16 sourceTarget = 0, destinationTarget = 0;
|
|
if (!storeTarget(source, sourceTarget) || !storeTarget(destination, destinationTarget)) return false;
|
|
// ONE FORMAT BOTH ENDS. Copy-image moves the GL texel bits; the store holds each
|
|
// format's canonical shadow, which for most formats IS those bits and for some is not
|
|
// (the 16-bit packed formats are kept as UNorm8, RGB10 / RGB12 as UNorm16). Between two
|
|
// levels of one format the canonical move and the GL move agree whatever the format;
|
|
// across two compatible formats they need not, so that copy is not followed.
|
|
if (PipeTextureRecordForHandle(source)->Desc.InternalFormat !=
|
|
PipeTextureRecordForHandle(destination)->Desc.InternalFormat) {
|
|
return false;
|
|
}
|
|
return store.FollowCopy(MG_Record::StagedTextureStore::KeyForHandle(source), sourceTarget,
|
|
static_cast<Uint16>(sourceLevel), sourceOrigin,
|
|
MG_Record::StagedTextureStore::KeyForHandle(destination), destinationTarget,
|
|
static_cast<Uint16>(destinationLevel), destinationOrigin, size);
|
|
}
|
|
} // namespace TextureImpl
|
|
|
|
// =====================================================================================
|
|
// P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES
|
|
// (docs/archive/contracts/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature
|
|
// in c0e so both sides compile from day one")
|
|
// =====================================================================================
|
|
//
|
|
// WHY THEY ARE ALL IN ONE BLOCK AND WHY THE BODIES ABORT. P5e lands as eight packages in
|
|
// eight parallel worktrees, and five of them are on opposite sides of these calls: tx2
|
|
// writes SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle while fb's
|
|
// attachment sync and image sweep CALL them; fb writes the two SyncToBackendByHandle
|
|
// overloads while tx2's detach walk needs the reverse index behind them; id rekeys the
|
|
// registries the three resolvers read. A package that had to add its own declaration would
|
|
// collide with the package that added the other half, which is the merge trap P4a's
|
|
// contract package was written to avoid.
|
|
//
|
|
// So the signature is fixed HERE, once, and each family's commit replaces a body. The body
|
|
// ABORTS BY NAME rather than returning null or doing nothing: a resolver that answered null
|
|
// would render a blank draw and a sync that did nothing would render stale pixels, and both
|
|
// would be green lanes. Nothing calls any of them at this commit - every call site is still
|
|
// on the frontend overload beside it - so the abort is a link-time seam, not a runtime one.
|
|
//
|
|
// A package REPLACES the body in place and deletes the matching comment; it does not add a
|
|
// second definition elsewhere, or the linker's answer depends on link order.
|
|
|
|
namespace TextureImpl {
|
|
// ---- P5e (tx2): THE TEXTURE FAMILY'S BY-HANDLE ENTRY, CONTRACT-P5E §5.2 --------------
|
|
//
|
|
// SamplerImpl::ResolveSamplerCsoTwin is the shape (Managers.cpp, the sampler half was
|
|
// already run-ahead clean on its record arm and this is that shape brought to the texture
|
|
// half): RECORD FIRST, twin by GetOrCreateByHandle, three syncs driven from the record,
|
|
// no frontend touch and no allocator probe anywhere on the path.
|
|
//
|
|
// WHAT THE FRONTEND ENTRY PAYS AND THIS ONE DOES NOT. SyncTextureObjectToBackend takes a
|
|
// by-VALUE copy of the twin and does a second Find at the tail, because its registry's
|
|
// Find returns a reference INTO an open-addressed map that a NESTED sync (a view syncing
|
|
// its storage texture) can rehash out from under it. A slot-table entry is an ARRAY
|
|
// ELEMENT: a nested GetOrCreateByHandle can only grow the vector, so the answer is
|
|
// re-INDEXED at the tail rather than pinned by a refcount, and the copy is a refcount
|
|
// this arm does not pay.
|
|
//
|
|
// A null return is a NAMED decline and never a mint: no record (the client named a
|
|
// texture it never created, or one whose resource_destroy has already applied), or a slot
|
|
// whose live generation is ahead of the handle's. A caller binds nothing and the unit
|
|
// keeps what it holds.
|
|
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture,
|
|
Bool imageBindableStorageRequired) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// The one storage every decline arm can return a reference to. Never written.
|
|
static SharedPtr<BackendTextureObject> s_noTwin{};
|
|
s_noTwin = nullptr;
|
|
|
|
const auto* record = PipeTextureRecordForHandle(texture);
|
|
if (record == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle "
|
|
"arm, so no driver texture can be built for it and the caller keeps "
|
|
"what it holds",
|
|
texture.Slot, texture.Gen);
|
|
return s_noTwin;
|
|
}
|
|
auto* slot = g_backendTextureObjects.GetOrCreateByHandle(texture);
|
|
if (slot == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: texture {%u, %u} cannot be adopted on the handle arm (the slot's "
|
|
"live generation is %u), so no driver texture is built for it",
|
|
texture.Slot, texture.Gen, g_backendTextureObjects.LiveGenAt(texture.Slot));
|
|
return s_noTwin;
|
|
}
|
|
if (!*slot) *slot = MakeShared<BackendTextureObject>();
|
|
// THE HANDLE IS NOTED BEFORE ANY SYNC RUNS. It is what the three prologues read
|
|
// instead of probing the client allocator, and what selects the by-handle arm inside
|
|
// them, so it has to be in place before the first of them is called.
|
|
(*slot)->NotePushedSyncHandle(texture);
|
|
|
|
const SharedPtr<BackendTextureObject> twin = *slot;
|
|
if (imageBindableStorageRequired) {
|
|
twin->RequireImageBindableStorageByHandle(texture, *record);
|
|
}
|
|
static const SharedPtr<MG_State::GLState::ITextureObject> kNoFrontendTexture{};
|
|
twin->SyncTextureParamsToBackend(kNoFrontendTexture);
|
|
twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture);
|
|
twin->SyncMipmapsToBackend(kNoFrontendTexture);
|
|
// The storage sync may RE-MINT the driver texture, which discards every parameter the
|
|
// two calls above just pushed - the frontend entry's argument, unchanged.
|
|
if (twin->NeedsParameterResync()) {
|
|
twin->SyncTextureParamsToBackend(kNoFrontendTexture);
|
|
twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture);
|
|
}
|
|
// ONE re-index, and only because a nested adopt may have GROWN the vector; nothing on
|
|
// this arm erases a live slot, so the entry itself cannot have gone.
|
|
auto* refreshed = g_backendTextureObjects.FindByHandle(texture);
|
|
if (refreshed != nullptr && *refreshed) {
|
|
return *refreshed;
|
|
}
|
|
s_noTwin = twin;
|
|
return s_noTwin;
|
|
}
|
|
|
|
void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) {
|
|
// The STORAGE half alone, for fb's attachment sync and the image sweep: an attachment
|
|
// needs its levels on the driver, not its sampler parameters (§5.4). The handle note
|
|
// is what routes the body below onto the record.
|
|
NotePushedSyncHandle(texture);
|
|
static const SharedPtr<MG_State::GLState::ITextureObject> kNoFrontendTexture{};
|
|
SyncMipmapsToBackend(kNoFrontendTexture);
|
|
}
|
|
|
|
// P5e (tx2), CONTRACT-P5E §5.2. See the declaration for the clause-by-clause derivation
|
|
// and for why BOTH halves of each resync pair are read here.
|
|
Bool BackendTextureObject::IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res,
|
|
const MG_Pipe::MGPipeResourceRecord& record) const {
|
|
(void)res;
|
|
// P3b/P4b D3: THE TWO STORAGE CLAUSES ARE ASKED OF THE STORAGE RECORD, which is this
|
|
// record for every texture that owns its texels and the record named by Desc.ViewOf
|
|
// for a glTextureView. A view's own Serial is moved by nothing an upload does - the
|
|
// client keys every resource_subdata on the storage owner, and the applier moves that
|
|
// record's Serial and PendingUploads alone - so reading the view's here answered
|
|
// "clean" for every owner write after the view's first sample. The three PARAMETER
|
|
// clauses below stay this record's: a view has its own texture parameters and its own
|
|
// built-in sampler, which is the whole point of the name existing. See
|
|
// PipeTextureStorageRecordForRecord and CONTRACT-P5E.md §5.2.
|
|
const MG_Pipe::MGPipeResourceRecord* storage = PipeTextureStorageRecordForRecord(record);
|
|
if (storage == nullptr) return false;
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// Never clean while bound to a shared image: see IsDrawSyncClean.
|
|
if (m_sharedImage != nullptr) return false;
|
|
#endif
|
|
// SyncMipmapsToBackend's own early-out, verbatim.
|
|
if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != storage->Serial ||
|
|
!storage->PendingUploads.empty()) {
|
|
return false;
|
|
}
|
|
// SyncTextureParamsToBackend's (ResolvePushedTextureParams' gate).
|
|
if (m_syncedParamsSerial != record.ParamsSerial || record.Params.ForceResync != 0 ||
|
|
m_forceTextureParamsResync) {
|
|
return false;
|
|
}
|
|
// SyncBuiltinSamplerToBackend's (ResolvePushedBuiltinSampler's gate). A texture whose
|
|
// params have never been set has no built-in sampler to push and that prologue
|
|
// declines SILENTLY - so a zero ParamsSerial is clean here for the same reason, and
|
|
// the CSO lookup below is only made when there is a CSO to look up.
|
|
if (record.ParamsSerial != 0) {
|
|
const MG_Pipe::MGPipeHandle builtin = record.Params.BuiltinSampler;
|
|
if (m_syncedBuiltinSampler != builtin) return false;
|
|
const auto* cso = PipeSamplerCsoRecordForHandle(builtin);
|
|
if (cso == nullptr || m_syncedBuiltinSamplerSerial != cso->Serial) return false;
|
|
if (record.Params.SamplerResync != 0 || m_forceSamplerResync) return false;
|
|
} else if (m_forceSamplerResync) {
|
|
return false;
|
|
}
|
|
// The storage-kind restriction the frontend gate carries, from the descriptor: a
|
|
// buffer texture's backing store can move without any serial above noticing.
|
|
return static_cast<TextureStorageType>(record.Desc.StorageKind) == TextureStorageType::Mipmap;
|
|
}
|
|
} // namespace TextureImpl
|
|
|
|
// FramebufferImpl::BackendFramebufferObject::SyncToBackendByHandle and
|
|
// RenderbufferImpl::BackendRenderbufferObject::SyncToBackendByHandle LANDED with package fb
|
|
// (P5e); their bodies are beside the object forms they overload, where the attachment walk
|
|
// and the storage allocation they share can be read side by side.
|
|
|
|
// PrgramImpl's seam is GONE, not stubbed: package pg wrote
|
|
// BackendProgramObjectImpl::SyncToBackendByHandle's real body beside the frontend overload
|
|
// it is the twin of (above, with the two ProgramArchiveSource heads). A second definition
|
|
// here would make the linker's answer depend on link order, which is the one failure this
|
|
// block's own comment was written to prevent.
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|