mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-08 02:23:23 +09:00
1728 lines
104 KiB
C++
1728 lines
104 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.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 "DirectGLESInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
namespace BufferImpl {
|
|
// Called by the application's own glMemoryBarrier: a barrier that orders storage writes
|
|
// already did the work.
|
|
static void NoteShaderStorageWritesOrdered() {
|
|
g_shaderStorageWritesUnordered = false;
|
|
}
|
|
|
|
void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
ProcessDeferredBufferReleases();
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
ProcessDeferredT0Retires(); // P11 B2
|
|
#endif
|
|
// THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it
|
|
// is why the uniform class is emitted at all: compute does NOT go through the
|
|
// per-program block remap in BindCurrentProgramWithResources, so these are the
|
|
// points a compute shader actually reads.
|
|
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform, GL_UNIFORM_BUFFER);
|
|
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, GL_SHADER_STORAGE_BUFFER);
|
|
if (includeDispatchIndirectBuffer) {
|
|
// P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb
|
|
// record's handle, never the frontend binding slot or the client allocator.
|
|
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
|
|
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
|
|
if (resource && resource->id != 0) {
|
|
BindBufferId(GL_DISPATCH_INDIRECT_BUFFER, resource->id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // namespace BufferImpl
|
|
|
|
namespace TextureImpl {
|
|
// P5e (fb): the eager glBindImageTexture funnel's HANDLE ARM, and it binds nothing.
|
|
//
|
|
// `bind_shader_image` is an unbarriered row (§2.2) whose backend entry point takes GL
|
|
// arguments and a GL NAME - the record's own MGPipeHandle (MGPImageBind::Res) stops at
|
|
// the sink - so there is no way to resolve the texture's twin from what arrives here,
|
|
// and the frontend binding this used to read is a BARRIER_PULLED row.
|
|
//
|
|
// Nothing is lost by deferring: SyncImageTextureBindings at the next validate point
|
|
// re-binds every unit up to the mark from the applier's own MGPImageView array, and the
|
|
// sink bumps the texture shutter serial immediately after this call
|
|
// (MGPipeApplierNoteTextureStateMoved), which is one of the four values the sweep's gate
|
|
// is keyed on - so the sweep that follows this bind cannot be suppressed. An image unit
|
|
// has no reader but a shader, and no shader runs between here and that validate point.
|
|
//
|
|
// WHAT MUST STILL HAPPEN HERE is the high-water mark: it is the "no draw in this context
|
|
// can be reading an image" early-out, so a unit that is given a texture without raising
|
|
// it would be skipped by every sweep afterwards.
|
|
static void NoteImageUnitBoundWithoutReadingTheFrontend(Uint unit, Bool holdsTexture) {
|
|
if (!holdsTexture) return;
|
|
if (unit + 1 > g_imageUnitHighWaterMark) {
|
|
g_imageUnitHighWaterMark = unit + 1;
|
|
}
|
|
}
|
|
} // namespace TextureImpl
|
|
|
|
void PrepareForCompute(Bool includeDispatchIndirectBuffer) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Single per-dispatch program resolve and texture-key capture, as in
|
|
// PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a
|
|
// pipeline bound this is its compute stage program, which is a whole program on its
|
|
// own - the graphics composite a draw builds carries no compute stage.
|
|
//
|
|
// P5e (pa), kimi row 92: THE DISPATCH TWIN OF THE SAME RETIREMENT, the same arm and the
|
|
// same conjunction - the three callees this value reaches are the ones PrepareForDraw
|
|
// reaches, minus the attribute-values sync (a dispatch has no vertex stage) and the
|
|
// frontend link/SPIR-V gate below, which already has a record arm of its own. These are
|
|
// the 7 strict-lane entries of GetProgramForDispatch@DispatchCompute.
|
|
const SharedPtr<MG_State::GLState::ProgramObject> noFrontendProgram;
|
|
const auto& currentProgram = noFrontendProgram;
|
|
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
|
|
|
// Read BEFORE this dispatch's own binding walk sets it again (see the flag's comment).
|
|
const Bool storageWritesUnordered = BufferImpl::g_shaderStorageWritesUnordered;
|
|
BufferImpl::g_shaderStorageWritesUnordered = false;
|
|
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
|
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
|
TextureImpl::SyncImageTextureBindings();
|
|
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
|
// Both syncs above may have re-uploaded a texture this dispatch reads or stores
|
|
// (Managers.h, NoteInPlaceTextureUpload), and the previous shader work may have left
|
|
// storage writes unordered; one barrier covers whichever of the two applies.
|
|
{
|
|
GLbitfield barriers = TextureImpl::TakeInPlaceTextureUploadBarrierBits();
|
|
if (storageWritesUnordered) barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
|
|
if (barriers != 0 && g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(barriers);
|
|
}
|
|
// P5e (pg), kimi row 92: the DISPATCH half of the same retirement -
|
|
// MGPipeApplier().DispatchProgram, which set_dispatch_program carried. A compute-only
|
|
// pipeline has no draw program at all, which is why the two applier fields are separate.
|
|
const MG_Pipe::MGPipeHandle dispatchCso = MG_Pipe::MGPipeApplier().DispatchProgram;
|
|
PrgramImpl::SyncCurrentProgramByHandle(dispatchCso, false);
|
|
|
|
// The same gate as the frontend one below, from the record: a dispatch of a program
|
|
// the frontend reports unlinked, or one whose SPIR-V never arrived, binds nothing.
|
|
const auto* const record = PrgramImpl::FindShaderCsoRecord(dispatchCso);
|
|
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
|
g_GLESFuncs.glUseProgram(0);
|
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
|
return;
|
|
}
|
|
|
|
// Compute shaders sample textures through the same unit bindings as draws
|
|
// (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on
|
|
// unit 0), so re-establish unit bindings after the content syncs above.
|
|
BindCurrentTextures(textureKeys, currentProgram);
|
|
// Compute programs need the same per-program resource sync as draws:
|
|
// uniform-block bindings and sampler units only exist through the API
|
|
// because layout(binding) is stripped from the transpiled ESSL.
|
|
BindCurrentProgramWithResources(currentProgram, textureKeys, dispatchCso);
|
|
}
|
|
|
|
Bool UpdateTextureBindingAtTarget(GLenum target) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto unit = MG_Pipe::gPipeInputs->GetActiveTextureUnit();
|
|
|
|
auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
|
if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) {
|
|
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
|
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
|
return false;
|
|
}
|
|
|
|
// P5c (hd, CONTRACT-P5C §3.4): under an active transport the destination is the handle
|
|
// the copy_framebuffer_to_texture record carried; the unit binding slot and the client
|
|
// allocator are never read (T2/T4).
|
|
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(dstHandle);
|
|
if (backendTextureSlot == nullptr) {
|
|
MGLOG_E_ONCE("%s: the verb's destination texture handle {%u, %u} is refused by "
|
|
"the twin slot table (live generation %u)",
|
|
__func__, dstHandle.Slot, dstHandle.Gen,
|
|
TextureImpl::g_backendTextureObjects.LiveGenAt(dstHandle.Slot));
|
|
return false;
|
|
}
|
|
auto& backendObj = *backendTextureSlot;
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<TextureImpl::BackendTextureObject>();
|
|
}
|
|
backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit);
|
|
return true;
|
|
}
|
|
|
|
static Bool IsDepthOnlyFormat(TextureInternalFormat format) {
|
|
return MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format);
|
|
}
|
|
|
|
static Bool IsColorOnlyFormat(TextureInternalFormat format) {
|
|
return !MG_Util::IsDepthFormatInternalFormat(format) && !MG_Util::IsStencilFormatInternalFormat(format);
|
|
}
|
|
|
|
static Bool IsIntegerColorFormat(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::RGB10A2UI:
|
|
case TextureInternalFormat::R8I:
|
|
case TextureInternalFormat::R8UI:
|
|
case TextureInternalFormat::R16I:
|
|
case TextureInternalFormat::R16UI:
|
|
case TextureInternalFormat::R32I:
|
|
case TextureInternalFormat::R32UI:
|
|
case TextureInternalFormat::RG8I:
|
|
case TextureInternalFormat::RG8UI:
|
|
case TextureInternalFormat::RG16I:
|
|
case TextureInternalFormat::RG16UI:
|
|
case TextureInternalFormat::RG32I:
|
|
case TextureInternalFormat::RG32UI:
|
|
case TextureInternalFormat::RGB8I:
|
|
case TextureInternalFormat::RGB8UI:
|
|
case TextureInternalFormat::RGB16I:
|
|
case TextureInternalFormat::RGB16UI:
|
|
case TextureInternalFormat::RGB32I:
|
|
case TextureInternalFormat::RGB32UI:
|
|
case TextureInternalFormat::RGBA8I:
|
|
case TextureInternalFormat::RGBA8UI:
|
|
case TextureInternalFormat::RGBA16I:
|
|
case TextureInternalFormat::RGBA16UI:
|
|
case TextureInternalFormat::RGBA32I:
|
|
case TextureInternalFormat::RGBA32UI:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two
|
|
// descriptor reads this file makes (Managers.cpp has the same inverse beside the storage
|
|
// sync; it is file-local there, and a header for eleven cases would be the wrong trade).
|
|
static TextureTarget TextureTargetForPipeResourceTarget(Uint8 pipeResourceTarget) {
|
|
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
|
|
case MG_Pipe::MGPipeResourceTarget::Tex1D:
|
|
return TextureTarget::Texture1D;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex2D:
|
|
return TextureTarget::Texture2D;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex3D:
|
|
return TextureTarget::Texture3D;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
|
|
return TextureTarget::Texture1DArray;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
|
|
return TextureTarget::Texture2DArray;
|
|
case MG_Pipe::MGPipeResourceTarget::TexCube:
|
|
return TextureTarget::TextureCubeMap;
|
|
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
|
|
return TextureTarget::TextureCubeMapArray;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex2DMS:
|
|
return TextureTarget::Texture2DMultisample;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray:
|
|
return TextureTarget::Texture2DMultisampleArray;
|
|
case MG_Pipe::MGPipeResourceTarget::TexRect:
|
|
return TextureTarget::TextureRectangle;
|
|
case MG_Pipe::MGPipeResourceTarget::TexBuffer:
|
|
return TextureTarget::TextureBuffer;
|
|
default:
|
|
return TextureTarget::Unknown;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
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_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, window.Base);
|
|
const Int shrinking = MG_Record::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;
|
|
}
|
|
|
|
static void AssertNoGLError(const char* operation) {
|
|
const GLenum err = g_GLESFuncs.glGetError();
|
|
MOBILEGL_ASSERT(err == GL_NO_ERROR, "%s failed: %s", operation, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
}
|
|
|
|
static ErrorCode ConvertGLESErrorToErrorCode(GLenum err) {
|
|
switch (err) {
|
|
case GL_INVALID_ENUM:
|
|
return ErrorCode::InvalidEnum;
|
|
case GL_INVALID_VALUE:
|
|
return ErrorCode::InvalidValue;
|
|
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
|
return ErrorCode::InvalidFramebufferOperation;
|
|
case GL_OUT_OF_MEMORY:
|
|
return ErrorCode::OutOfMemory;
|
|
case GL_INVALID_OPERATION:
|
|
default:
|
|
return ErrorCode::InvalidOperation;
|
|
}
|
|
}
|
|
|
|
static Bool RecordGLError(const char* operation, GLenum target, TextureInternalFormat format) {
|
|
const GLenum err = g_GLESFuncs.glGetError();
|
|
if (err == GL_NO_ERROR) {
|
|
return true;
|
|
}
|
|
|
|
MGLOG_E_ONCE("%s failed: %s. target=%s, format=%s", operation, MG_Util::ConvertGLEnumToString(err).c_str(),
|
|
MG_Util::ConvertGLEnumToString(target).c_str(),
|
|
MG_Util::ConvertTextureInternalFormatToString(format).c_str());
|
|
MG_Pipe::gPipeInputs->RecordError(
|
|
ConvertGLESErrorToErrorCode(err),
|
|
MakeUnique<GenericErrorInfo>("DirectGLES", operation, MG_Util::ConvertGLEnumToString(err)));
|
|
return false;
|
|
}
|
|
|
|
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
|
|
// scope (GenerateMipmap must respecify texture storage while no incomplete
|
|
// user FBO is bound); restores the previous bindings on exit.
|
|
class ScopedCompleteFramebufferBinding {
|
|
public:
|
|
ScopedCompleteFramebufferBinding() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) {
|
|
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, ScratchFBOImpl::EnsureCompleteTinyFramebufferId());
|
|
}
|
|
|
|
private:
|
|
ScopedFramebufferBinding m_binding;
|
|
};
|
|
|
|
class ScopedDetachedTextureFramebufferAttachments {
|
|
public:
|
|
// P5e (tx2). THE SAME SWEEP, KEYED BY THE TEXTURE'S HANDLE.
|
|
//
|
|
// The frontend constructor below answers "which framebuffers attach this texture" by
|
|
// walking every live FBO twin's frontend FramebufferObject and comparing attachment
|
|
// SharedPtrs - kimi audit row 63, an unmemoised walk of client objects. The applier
|
|
// already holds the answer: every framebuffer's record carries its eleven MGPSurfaces
|
|
// and each one names its texture BY HANDLE (`MGPSurface::Res`), so the comparison is
|
|
// {slot, gen} against {slot, gen} and the level / layered / upload-target details the
|
|
// detach needs come off the same surface.
|
|
//
|
|
// This is the same reverse question fb's texture -> FBO-slot index answers for its
|
|
// attachment sync; here it is asked over the records directly, because the walk is on a
|
|
// rare path (per glGenerateMipmap) and does not need an index to be cheap.
|
|
explicit ScopedDetachedTextureFramebufferAttachments(MG_Pipe::MGPipeHandle textureHandle) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return;
|
|
auto* twin = TextureImpl::ResolveTextureTwin(textureHandle);
|
|
if (twin == nullptr) return;
|
|
const GLuint backendTextureId = twin->GetBackendTextureId();
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
|
|
FramebufferImpl::g_backendFramebufferObjects.ForEachLive(
|
|
[&](MG_Pipe::MGPipeHandle fbo, const SharedPtr<FramebufferImpl::BackendFramebufferObject>& backendFBO) {
|
|
if (!backendFBO) return;
|
|
const auto* record = st.FramebufferRecordFor(fbo);
|
|
if (record == nullptr || record->IsDefault != 0) return;
|
|
|
|
const auto detachPoint = [&](const MG_Pipe::MGPSurface& surface,
|
|
FramebufferAttachmentType frontendType) {
|
|
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
|
|
if (surface.Res.Slot != textureHandle.Slot || surface.Res.Gen != textureHandle.Gen) {
|
|
return;
|
|
}
|
|
GLenum backendAttachment = GL_NONE;
|
|
if (frontendType >= FramebufferAttachmentType::Color0 &&
|
|
frontendType <= FramebufferAttachmentType::Color31) {
|
|
backendAttachment = backendFBO->GetBackendAttachmentType(frontendType);
|
|
} else {
|
|
backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType);
|
|
}
|
|
if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return;
|
|
|
|
GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
|
|
static_cast<TextureUploadTarget>(surface.UploadTarget));
|
|
if (textureTarget == GL_UNKNOWN_MGL) {
|
|
textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(
|
|
static_cast<TextureTarget>(surface.TextureTarget));
|
|
}
|
|
const GLuint backendFBOId = backendFBO->GetBackendFramebufferId();
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId);
|
|
if (surface.Layered != 0) {
|
|
g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0);
|
|
} else {
|
|
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment, textureTarget, 0,
|
|
0);
|
|
}
|
|
ClearGLErrors();
|
|
m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget,
|
|
backendTextureId, static_cast<GLint>(surface.Level),
|
|
surface.Layered != 0});
|
|
};
|
|
|
|
for (SizeT i = 0; i < 8; ++i) {
|
|
detachPoint(record->Color[i], static_cast<FramebufferAttachmentType>(
|
|
static_cast<SizeT>(FramebufferAttachmentType::Color0) + i));
|
|
}
|
|
detachPoint(record->Depth, FramebufferAttachmentType::Depth);
|
|
detachPoint(record->Stencil, FramebufferAttachmentType::Stencil);
|
|
});
|
|
}
|
|
|
|
~ScopedDetachedTextureFramebufferAttachments() {
|
|
for (const auto& attachment : m_detachedAttachments) {
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, attachment.framebuffer);
|
|
if (attachment.layered) {
|
|
g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.texture,
|
|
attachment.level);
|
|
} else {
|
|
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, attachment.attachment,
|
|
attachment.textureTarget, attachment.texture, attachment.level);
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
struct DetachedAttachment {
|
|
GLuint framebuffer = 0;
|
|
GLenum attachment = GL_NONE;
|
|
GLenum textureTarget = GL_TEXTURE_2D;
|
|
GLuint texture = 0;
|
|
GLint level = 0;
|
|
Bool layered = false;
|
|
};
|
|
|
|
// Declared first so its restore runs after the reattach loop in the dtor.
|
|
ScopedFramebufferBinding m_binding{/*saveRead=*/true, /*saveDraw=*/true};
|
|
Vector<DetachedAttachment> m_detachedAttachments;
|
|
};
|
|
|
|
// Binds the scratch blit READ/DRAW framebuffers with scissor forced off (blits
|
|
// are scissored) for one texture-to-texture copy; restores the bindings and the
|
|
// scissor state on exit. Attachments on the two scratch FBOs are managed by the
|
|
// blit helpers through the ScratchFBOImpl attachment shadow.
|
|
class ScopedDepthBlitState {
|
|
public:
|
|
ScopedDepthBlitState() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) {
|
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER,
|
|
ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitReadFramebuffer()));
|
|
AssertNoGLError("bind depth blit read framebuffer");
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER,
|
|
ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitDrawFramebuffer()));
|
|
AssertNoGLError("bind depth blit draw framebuffer");
|
|
}
|
|
|
|
private:
|
|
ScopedScissorDisable m_scissorOff;
|
|
ScopedFramebufferBinding m_binding;
|
|
};
|
|
|
|
static void BlitDepthTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth,
|
|
GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY,
|
|
GLsizei dstWidth, GLsizei dstHeight) {
|
|
MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures.");
|
|
MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative.");
|
|
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0,
|
|
"Depth blit dimensions must be positive.");
|
|
|
|
ClearGLErrors();
|
|
ScopedDepthBlitState state;
|
|
auto& readFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer();
|
|
ScratchFBOImpl::EnsureDepthAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel,
|
|
/*withStencil=*/false);
|
|
AssertNoGLError("attach depth blit source texture");
|
|
ScratchFBOImpl::EnsureDepthAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel,
|
|
/*withStencil=*/false);
|
|
AssertNoGLError("attach depth blit destination texture");
|
|
ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE);
|
|
AssertNoGLError("set depth blit read buffer");
|
|
ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE);
|
|
AssertNoGLError("set depth blit draw buffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Depth blit read framebuffer is incomplete.");
|
|
AssertNoGLError("check depth blit read framebuffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Depth blit draw framebuffer is incomplete.");
|
|
AssertNoGLError("check depth blit draw framebuffer");
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, dstX, dstY, dstX + dstWidth,
|
|
dstY + dstHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
|
AssertNoGLError("depth texture blit");
|
|
}
|
|
|
|
static void BlitColorTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth,
|
|
GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY,
|
|
GLsizei dstWidth, GLsizei dstHeight, GLenum filter) {
|
|
MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures.");
|
|
MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative.");
|
|
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0,
|
|
"Color blit dimensions must be positive.");
|
|
MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear.");
|
|
|
|
ClearGLErrors();
|
|
ScopedDepthBlitState state;
|
|
auto& readFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer();
|
|
ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel);
|
|
AssertNoGLError("attach color blit source texture");
|
|
ScratchFBOImpl::EnsureColorAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel);
|
|
AssertNoGLError("attach color blit destination texture");
|
|
ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0);
|
|
AssertNoGLError("set color blit read buffer");
|
|
ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0);
|
|
AssertNoGLError("set color blit draw buffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Color blit read framebuffer is incomplete.");
|
|
AssertNoGLError("check color blit read framebuffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Color blit draw framebuffer is incomplete.");
|
|
AssertNoGLError("check color blit draw framebuffer");
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(srcX, srcY, srcX + srcWidth, srcY + srcHeight, dstX, dstY, dstX + dstWidth,
|
|
dstY + dstHeight, GL_COLOR_BUFFER_BIT, filter);
|
|
AssertNoGLError("color texture blit");
|
|
}
|
|
|
|
// The layer-point siblings of the two blits above, for a texture VIEW's mip chain: those
|
|
// texels live in the storage owner's layers [MinLayer, MinLayer + NumLayers) and a 2D
|
|
// attach would address layer 0 of every one of them. One layer's extents are the whole
|
|
// rectangle, exactly as in the 2D helpers.
|
|
static void BlitDepthTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth,
|
|
GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer,
|
|
GLsizei dstWidth, GLsizei dstHeight) {
|
|
MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Depth blit requires valid backend textures.");
|
|
MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Depth blit mip levels must be non-negative.");
|
|
MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Depth blit layers must be non-negative.");
|
|
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0,
|
|
"Depth blit dimensions must be positive.");
|
|
|
|
ClearGLErrors();
|
|
ScopedDepthBlitState state;
|
|
auto& readFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer();
|
|
ScratchFBOImpl::EnsureDepthAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer);
|
|
AssertNoGLError("attach depth blit source layer");
|
|
ScratchFBOImpl::EnsureDepthAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer);
|
|
AssertNoGLError("attach depth blit destination layer");
|
|
ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE);
|
|
AssertNoGLError("set depth blit read buffer");
|
|
ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE);
|
|
AssertNoGLError("set depth blit draw buffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Depth blit read framebuffer is incomplete.");
|
|
AssertNoGLError("check depth blit read framebuffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Depth blit draw framebuffer is incomplete.");
|
|
AssertNoGLError("check depth blit draw framebuffer");
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_DEPTH_BUFFER_BIT,
|
|
GL_NEAREST);
|
|
AssertNoGLError("depth texture layer blit");
|
|
}
|
|
|
|
static void BlitColorTextureLayer(GLuint srcTexture, GLint srcLevel, GLint srcLayer, GLsizei srcWidth,
|
|
GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstLayer,
|
|
GLsizei dstWidth, GLsizei dstHeight, GLenum filter) {
|
|
MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "Color blit requires valid backend textures.");
|
|
MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "Color blit mip levels must be non-negative.");
|
|
MOBILEGL_ASSERT(srcLayer >= 0 && dstLayer >= 0, "Color blit layers must be non-negative.");
|
|
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && dstWidth > 0 && dstHeight > 0,
|
|
"Color blit dimensions must be positive.");
|
|
MOBILEGL_ASSERT(filter == GL_NEAREST || filter == GL_LINEAR, "Color blit filter must be nearest or linear.");
|
|
|
|
ClearGLErrors();
|
|
ScopedDepthBlitState state;
|
|
auto& readFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer();
|
|
ScratchFBOImpl::EnsureColorAttachmentLayer(readFB, GL_READ_FRAMEBUFFER, srcTexture, srcLevel, srcLayer);
|
|
AssertNoGLError("attach color blit source layer");
|
|
ScratchFBOImpl::EnsureColorAttachmentLayer(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, dstLevel, dstLayer);
|
|
AssertNoGLError("attach color blit destination layer");
|
|
ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0);
|
|
AssertNoGLError("set color blit read buffer");
|
|
ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0);
|
|
AssertNoGLError("set color blit draw buffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Color blit read framebuffer is incomplete.");
|
|
AssertNoGLError("check color blit read framebuffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"Color blit draw framebuffer is incomplete.");
|
|
AssertNoGLError("check color blit draw framebuffer");
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight, GL_COLOR_BUFFER_BIT,
|
|
filter);
|
|
AssertNoGLError("color texture layer blit");
|
|
}
|
|
|
|
// A mip chain generated inside a texture VIEW's window. Only the LAYER axis needs the
|
|
// manual path: BASE_LEVEL/MAX_LEVEL are the view's own parameters, so the native
|
|
// glGenerateMipmap already clips the LEVEL axis the way the view describes - but it writes
|
|
// every layer of the storage the view aliases, and a view's layer window is a subset of
|
|
// those.
|
|
//
|
|
// The levels here are the VIEW's own (level 0 aliases the storage's MinLevel), because that
|
|
// is the space a view's descriptor extents and the frontend's own level sizes are measured
|
|
// in. The blits are the one place the two systems meet, so the storage level a blit names is
|
|
// MinLevel + the view's level.
|
|
struct MipmapViewWindow {
|
|
GLuint StorageTextureId = 0;
|
|
Uint32 MinLevel = 0;
|
|
Uint32 MinLayer = 0;
|
|
Uint32 NumLayers = 1;
|
|
Uint32 LogicalBase = 0;
|
|
Uint32 LogicalEnd = 0; // view-relative, end-exclusive
|
|
Bool Depth = false;
|
|
GLenum Filter = GL_LINEAR;
|
|
};
|
|
|
|
template <typename ExtentAtLevel>
|
|
static void GenerateMipmapThroughViewWindow(const MipmapViewWindow& window, ExtentAtLevel&& extentAtLevel) {
|
|
if (window.StorageTextureId == 0 || window.NumLayers == 0) return;
|
|
for (Uint32 level = window.LogicalBase + 1; level < window.LogicalEnd; ++level) {
|
|
const IntVec3 srcSize = extentAtLevel(level - 1);
|
|
const IntVec3 dstSize = extentAtLevel(level);
|
|
MOBILEGL_ASSERT(srcSize.x() > 0 && srcSize.y() > 0 && dstSize.x() > 0 && dstSize.y() > 0,
|
|
"View mipmap generation needs non-empty source and destination extents.");
|
|
const GLint srcLevel = static_cast<GLint>(window.MinLevel + level - 1);
|
|
const GLint dstLevel = static_cast<GLint>(window.MinLevel + level);
|
|
for (Uint32 i = 0; i < window.NumLayers; ++i) {
|
|
const GLint layer = static_cast<GLint>(window.MinLayer + i);
|
|
if (window.Depth) {
|
|
BlitDepthTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast<GLsizei>(srcSize.x()),
|
|
static_cast<GLsizei>(srcSize.y()), window.StorageTextureId, dstLevel, layer,
|
|
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()));
|
|
} else {
|
|
BlitColorTextureLayer(window.StorageTextureId, srcLevel, layer, static_cast<GLsizei>(srcSize.x()),
|
|
static_cast<GLsizei>(srcSize.y()), window.StorageTextureId, dstLevel, layer,
|
|
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()),
|
|
window.Filter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CopyR32FTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei width,
|
|
GLsizei height, GLuint dstTexture, GLenum dstTarget, GLint dstLevel, GLint dstX,
|
|
GLint dstY) {
|
|
MOBILEGL_ASSERT(srcTexture != 0 && dstTexture != 0, "R32F copy requires valid backend textures.");
|
|
MOBILEGL_ASSERT(dstTarget == GL_TEXTURE_2D, "R32F copy only supports GL_TEXTURE_2D destinations.");
|
|
MOBILEGL_ASSERT(srcLevel >= 0 && dstLevel >= 0, "R32F copy mip levels must be non-negative.");
|
|
MOBILEGL_ASSERT(width > 0 && height > 0, "R32F copy dimensions must be positive.");
|
|
|
|
ClearGLErrors();
|
|
ScopedDepthBlitState state;
|
|
auto& readFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel);
|
|
AssertNoGLError("attach R32F copy source texture");
|
|
ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0);
|
|
AssertNoGLError("set R32F copy read buffer");
|
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
|
"R32F copy read framebuffer is incomplete.");
|
|
AssertNoGLError("check R32F copy read framebuffer");
|
|
|
|
Vector<Float> pixels(static_cast<SizeT>(width) * static_cast<SizeT>(height));
|
|
{
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
|
g_GLESFuncs.glReadPixels(srcX, srcY, width, height, GL_RED, GL_FLOAT, pixels.data());
|
|
AssertNoGLError("read R32F copy pixels");
|
|
}
|
|
|
|
// Upload side: the rows are tightly packed floats, which the resting driver
|
|
// UNPACK state (4/0/0/0, maintained by ScopedDefaultUnpackState) parses
|
|
// correctly; the unpack-PBO binding rests at 0 by the same discipline (the
|
|
// call below no-ops unless something diverged).
|
|
BufferImpl::BindPixelUnpackBufferId(0);
|
|
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
|
g_GLESFuncs.glBindTexture(dstTarget, dstTexture);
|
|
g_GLESFuncs.glTexSubImage2D(dstTarget, dstLevel, dstX, dstY, width, height, GL_RED, GL_FLOAT, pixels.data());
|
|
AssertNoGLError("upload R32F copy pixels");
|
|
// Re-bind what the texture-binding cache says lives on the temp unit so the
|
|
// cache stays truthful without a driver query.
|
|
auto* cachedBound =
|
|
TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
|
[static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(dstTarget))];
|
|
g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
|
}
|
|
|
|
// WHICH LAYERS A VIEW'S MIP CHAIN MAY WRITE. A generation through a view has two coordinate
|
|
// systems in it - the view's own level/layer window (the sampler view CSO) and its storage
|
|
// owner's - and the native arm below carries only part of the first one: BASE_LEVEL and
|
|
// MAX_LEVEL are the view's own parameters, so glGenerateMipmap clips the LEVEL axis the way
|
|
// the view describes, but the layer set it writes is every layer of the storage the view
|
|
// aliases. A windowed view's generation therefore rewrites owner layers outside its window,
|
|
// which the view window itself says it must not.
|
|
//
|
|
// True means this record's generation was handled here (or refuted) and the native arm must
|
|
// NOT run; false means "not this shape": no view, a view whose window covers the whole
|
|
// storage - where the native call already agrees with GL - a multisample view, which has no
|
|
// mip chain to generate, or an aspect the layer blits do not carry.
|
|
static Bool GenerateMipmapThroughViewWindowByRecord(MG_Pipe::MGPipeHandle mipRes,
|
|
const MG_Pipe::MGPipeResourceRecord& record,
|
|
TextureInternalFormat format) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) return false;
|
|
if (record.Desc.Samples != 0) return false;
|
|
const Bool depth = IsDepthOnlyFormat(format);
|
|
if (!depth && !IsColorOnlyFormat(format)) return false;
|
|
|
|
// The view record, validated as SyncTextureViewToBackendByRecord validates it: this
|
|
// window is the only place the four numbers come from, and a stale or foreign one would
|
|
// generate into another texture's levels.
|
|
const auto* viewRecord = PipeSamplerViewRecordForHandle(record.ViewCso);
|
|
if (viewRecord == nullptr || !viewRecord->Live || viewRecord->Gen != record.ViewCso.Gen) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-record\"} {%u,%u}", mipRes.Slot, mipRes.Gen);
|
|
std::abort();
|
|
}
|
|
const auto& view = viewRecord->View;
|
|
if (view.Texture != mipRes || view.NumLevels == 0 || view.NumLayers == 0) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-window\"} {%u,%u}", mipRes.Slot, mipRes.Gen);
|
|
std::abort();
|
|
}
|
|
const auto* storageRecord = PipeTextureRecordForHandle(record.Desc.ViewOf);
|
|
if (storageRecord == nullptr) {
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"mipmap-view-storage\"} {%u,%u}", mipRes.Slot, mipRes.Gen);
|
|
std::abort();
|
|
}
|
|
if (view.MinLayer == 0 && view.NumLayers >= std::max<Uint32>(storageRecord->Desc.ArrayLayers, 1u)) return false;
|
|
|
|
const auto& applier = MG_Pipe::MGPipeApplier();
|
|
MipmapViewWindow window;
|
|
window.MinLevel = view.MinLevel;
|
|
window.MinLayer = view.MinLayer;
|
|
window.NumLayers = view.NumLayers;
|
|
window.LogicalBase = applier.VerbMipBaseLevel;
|
|
// LevelCount is the logical END-EXCLUSIVE level, not a count after BaseLevel, and the
|
|
// frontend already clipped it to the object's own range; the view's window is the bound
|
|
// only this side can apply.
|
|
window.LogicalEnd = std::min({static_cast<Uint32>(applier.VerbMipLevelCount),
|
|
static_cast<Uint32>(record.Desc.Levels), static_cast<Uint32>(view.NumLevels)});
|
|
window.Depth = depth;
|
|
window.Filter = IsIntegerColorFormat(format) ? GL_NEAREST : GL_LINEAR;
|
|
if (window.LogicalEnd <= window.LogicalBase + 1) return true;
|
|
|
|
// The STORAGE's ES name is what the blits write: the view's own name aliases the same
|
|
// texels, but only the owner's name carries every level and layer this window reaches.
|
|
auto& storage = TextureImpl::SyncTextureToBackendByHandle(record.Desc.ViewOf);
|
|
if (!storage || storage->GetBackendTextureId() == 0) {
|
|
MGLOG_E_ONCE("MGPipe: generate_mipmap through view {%u, %u} has no storage texture to write; "
|
|
"the call is dropped",
|
|
mipRes.Slot, mipRes.Gen);
|
|
return true;
|
|
}
|
|
window.StorageTextureId = storage->GetBackendTextureId();
|
|
GenerateMipmapThroughViewWindow(window, [&record](Uint32 level) {
|
|
return MG_Record::StagedTextureMipExtent(record.Desc.Target, record.Desc.Width, record.Desc.Height,
|
|
record.Desc.Depth, level);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// P5e (tx2): the two CPU-blit mip chains, driven by the RECORD the verb named. What changes
|
|
// against the P5c arms below is only the resolution: those took the frontend texture and
|
|
// 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 RecordMipWindow& window) {
|
|
const auto& desc = record.Desc;
|
|
const GLuint textureId = backendTexture->GetBackendTextureId();
|
|
for (Uint32 level = window.Base + 1; level < window.End; ++level) {
|
|
const IntVec3 srcSize =
|
|
MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
|
|
const IntVec3 dstSize =
|
|
MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level);
|
|
BlitDepthTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0, static_cast<GLsizei>(srcSize.x()),
|
|
static_cast<GLsizei>(srcSize.y()), textureId, static_cast<GLint>(level), 0, 0,
|
|
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()));
|
|
}
|
|
}
|
|
|
|
static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
|
|
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 = window.Base + 1; level < window.End; ++level) {
|
|
const IntVec3 srcSize =
|
|
MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
|
|
const IntVec3 dstSize =
|
|
MG_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, level);
|
|
BlitColorTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0, static_cast<GLsizei>(srcSize.x()),
|
|
static_cast<GLsizei>(srcSize.y()), textureId, static_cast<GLint>(level), 0, 0,
|
|
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()), filter);
|
|
}
|
|
}
|
|
|
|
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
|
GLsizei height, GLint border) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DebugImpl::ErrorLopper errorLopper;
|
|
MGLOG_D("%s: Backend", __func__);
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
RenderStateImpl::SyncRenderState();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
if (!UpdateTextureBindingAtTarget(target)) return;
|
|
|
|
// Bind necessary FBO and texture
|
|
BindCurrentFBO(FramebufferTarget::Read);
|
|
Uint activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit();
|
|
TextureImpl::BackendTextureObject* dstBackendTexture = nullptr;
|
|
TextureInternalFormat mgInternalFormat{};
|
|
// P5c (hd, CONTRACT-P5C §3.4): with an active transport the destination resolves from
|
|
// the handle the copy_framebuffer_to_texture record carried (MGPCopyFromFramebuffer::
|
|
// Dst, the verb stash) and its format is the applier descriptor's - the client's
|
|
// texture-unit binding slot and the client slot allocator are never read (T2/T4).
|
|
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
|
|
const auto* dstRecord = PipeTextureRecordForHandle(dstHandle);
|
|
// P5e (tx2): SYNCED, not merely looked up. The destination of a copy is not sampled
|
|
// by the draw program, so the per-draw unit list - which is the sampler-view window
|
|
// now - never brings it across; before P5e the frontend walk over every slot of every
|
|
// touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says
|
|
// the union that still covers every texture includes "the waited texture ops", and
|
|
// this is one of them: it syncs its own endpoint by handle.
|
|
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
|
|
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
|
|
if (!backendTexture || dstRecord == nullptr) {
|
|
MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin "
|
|
"or no applier record on this side",
|
|
dstHandle.Slot, dstHandle.Gen);
|
|
return;
|
|
}
|
|
dstBackendTexture = backendTexture.get();
|
|
mgInternalFormat = static_cast<TextureInternalFormat>(dstRecord->Desc.InternalFormat);
|
|
dstBackendTexture->Bind(target, activeTextureUnit);
|
|
|
|
GLenum format = GL_DEPTH_COMPONENT;
|
|
GLenum type = GL_UNSIGNED_INT;
|
|
TextureImpl::GenerateTextureFormatInfo(mgInternalFormat, &internalformat, &format, &type,
|
|
MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
MOBILEGL_ASSERT(format != GL_NONE && type != GL_NONE,
|
|
"%s: cannot GenerateTextureFormatInfo(%s): out internalformat=%s, format=%s, type=%s",
|
|
MG_Util::ConvertTextureInternalFormatToString(mgInternalFormat).c_str(),
|
|
MG_Util::ConvertGLEnumToString(internalformat).c_str(),
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
|
|
|
Bool isDepthFormat =
|
|
MG_Util::IsDepthFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat));
|
|
Bool isStencilFormat =
|
|
MG_Util::IsStencilFormatInternalFormat(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat));
|
|
|
|
if (!isDepthFormat) {
|
|
g_GLESFuncs.glCopyTexImage2D(target, level, internalformat, x, y, width, height, border);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
} else {
|
|
MGLOG_D("%s: Backend depth", __func__);
|
|
// nullptr means "uninitialized storage" only while no unpack PBO is
|
|
// bound; enforce the resting 0 state instead of assuming it (no-op
|
|
// through the binding cache unless something diverged).
|
|
BufferImpl::BindPixelUnpackBufferId(0);
|
|
g_GLESFuncs.glTexImage2D(target, level, (GLint)internalformat, width, height, border, format, type,
|
|
nullptr);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
auto currentTex = (GLint)dstBackendTexture->GetBackendTextureId();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
TempFBOBinder tempFBOBinder(false);
|
|
ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit
|
|
ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER,
|
|
static_cast<Uint>(currentTex), target, level, isStencilFormat);
|
|
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
|
return;
|
|
}
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height,
|
|
GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0),
|
|
GL_NEAREST);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
}
|
|
}
|
|
|
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
|
GLsizei height) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DebugImpl::ErrorLopper errorLopper;
|
|
|
|
MGLOG_D("%s: Backend", __func__);
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
RenderStateImpl::SyncRenderState();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
if (!UpdateTextureBindingAtTarget(target)) return;
|
|
|
|
// Bind necessary FBO and texture
|
|
BindCurrentFBO(FramebufferTarget::Read);
|
|
auto activeTextureUnit = MG_Pipe::gPipeInputs->GetActiveTextureUnit();
|
|
TextureImpl::BackendTextureObject* dstBackendTexture = nullptr;
|
|
// P5c (hd, CONTRACT-P5C §3.4): see CopyTexImage2D - the record's Dst handle, never the
|
|
// client's unit binding slot or the client allocator (T2/T4).
|
|
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
|
|
// P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not
|
|
// in the sampler-view window, so nothing else on this path would build its twin.
|
|
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
|
|
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
|
|
if (!backendTexture) {
|
|
MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no "
|
|
"twin on this side",
|
|
dstHandle.Slot, dstHandle.Gen);
|
|
return;
|
|
}
|
|
dstBackendTexture = backendTexture.get();
|
|
dstBackendTexture->Bind(target, activeTextureUnit);
|
|
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
GLenum internalFormat;
|
|
g_GLESFuncs.glGetTexLevelParameteriv(target, level, GL_TEXTURE_INTERNAL_FORMAT, (GLint*)&internalFormat);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
auto mgInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat);
|
|
|
|
Bool isDepthFormat = MG_Util::IsDepthFormatInternalFormat(mgInternalFormat);
|
|
Bool isStencilFormat = MG_Util::IsStencilFormatInternalFormat(mgInternalFormat);
|
|
|
|
if (!isDepthFormat) {
|
|
g_GLESFuncs.glCopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
} else {
|
|
MGLOG_D("%s: Backend depth", __func__);
|
|
auto currentTex = dstBackendTexture->GetBackendTextureId();
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
TempFBOBinder tempFBOBinder(false);
|
|
ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit
|
|
ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, currentTex,
|
|
target, level, isStencilFormat);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
|
return;
|
|
}
|
|
|
|
g_GLESFuncs.glBlitFramebuffer(
|
|
x, y, x + width, y + height, xoffset, yoffset, xoffset + width, yoffset + height,
|
|
GL_DEPTH_BUFFER_BIT | (isStencilFormat ? GL_STENCIL_BUFFER_BIT : 0), GL_NEAREST);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
}
|
|
}
|
|
|
|
// P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL.
|
|
//
|
|
// ES generates a chain only for a color-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.
|
|
//
|
|
// P8-SE: THE STORE ANSWERS ONLY FOR A TEXTURE THE DRIVER NEVER WROTE (P8-E's sticky mark). A
|
|
// render target's level 0 lives on the driver; the store still holds whatever crossed before
|
|
// the draw, and filtering it would hand the chain stale texels. `driverWrittenBefore` is the
|
|
// mark as it stood BEFORE this generation noted its own write (GenerateMipmap's split arm),
|
|
// because that note is unconditional and would otherwise decline every store read. Anything
|
|
// the store cannot stand for is a named decline, `declined` says why.
|
|
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, Bool driverWrittenBefore, Vector<Float>& rgb,
|
|
const char*& declined) {
|
|
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 (!WireTextureReadbackRefusedForTesting(extent) &&
|
|
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;
|
|
}
|
|
auto& store = MG_Record::ServerStagedTexture();
|
|
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(mipRes);
|
|
const auto faceCode = static_cast<Uint16>(face);
|
|
const auto levelCode = static_cast<Uint16>(level);
|
|
const Bool half = format == TextureInternalFormat::RGB16F;
|
|
const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float);
|
|
// 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)) {
|
|
declined = "a view has no staged level of its own";
|
|
} else if (!store.Holds()) {
|
|
declined = "this arm keeps no staged store";
|
|
} else if (driverWrittenBefore) {
|
|
declined = "the driver has written the texture since its bytes crossed";
|
|
} else if (!store.IsCovered(key, faceCode, levelCode)) {
|
|
declined = "not every byte of the level crossed";
|
|
} else if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) {
|
|
declined = "the staged level is not the size of the level";
|
|
}
|
|
if (declined != nullptr) 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, Bool driverWrittenBefore, 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_Record::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, window.Base);
|
|
Vector<Float> level;
|
|
const char* declined = nullptr;
|
|
if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base,
|
|
extent, driverWrittenBefore, level, declined)) {
|
|
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u could not "
|
|
"be read back from the driver and the staged store cannot stand for it (%s); levels "
|
|
"%u..%u are left as they are - DECLINED (generate-mipmap-store-declined)",
|
|
mipRes.Slot, mipRes.Gen, window.Base, declined != nullptr ? declined : "?",
|
|
window.Base + 1, window.End - 1);
|
|
return;
|
|
}
|
|
for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) {
|
|
const IntVec3 dstExtent =
|
|
MG_Record::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'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.
|
|
//
|
|
// `driverWrittenBefore`: P8-E's driver-written mark on the texture's staged store as it stood
|
|
// before this call noted its own write - the CPU arm's store route reads it (P8-SE).
|
|
static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes,
|
|
const MG_Pipe::MGPipeResourceRecord& record,
|
|
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
|
|
Bool driverWrittenBefore) {
|
|
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;
|
|
|
|
// 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, GenerateMipmapWindowByRecord(record));
|
|
return;
|
|
}
|
|
if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) {
|
|
GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record));
|
|
return;
|
|
}
|
|
if (threeChannelFloat && ForcedServerCpuMipmap()) {
|
|
GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore,
|
|
"forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP");
|
|
return;
|
|
}
|
|
|
|
const GLenum backendTarget =
|
|
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
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 and some other drivers 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, driverWrittenBefore,
|
|
"the driver refused glGenerateMipmap");
|
|
}
|
|
|
|
void PatchParameteri(GLenum pname, GLint value) {
|
|
if (g_GLESFuncs.glPatchParameteri == nullptr) return;
|
|
g_GLESFuncs.glPatchParameteri(pname, value);
|
|
}
|
|
|
|
void GenerateMipmap(GLenum target) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
// P5e (tx2), CONTRACT-P5E §5.2 / ruling 14 (ID-92's neighbor): GENERATE_MIPMAP RESOLVES
|
|
// ITS TEXTURE FROM VerbMipRes, NEVER FROM THE ACTIVE UNIT.
|
|
//
|
|
// The verb's own record names the texture (MGPMipPlan::Res, stashed as VerbMipRes) and
|
|
// EnsureGenerateMipmapStorageAllocated's disaggregated arm has read it since P5c. What
|
|
// stood here instead was GetActiveTextureUnit + GetTextureUnitObject + the target's
|
|
// binding slot - three BARRIER_PULLED reads to re-derive a handle the record carried -
|
|
// and they are the ONLY reason this row still waits: ClientSession.cpp parks the client
|
|
// so that the unit it reads is the unit the call was made on. With the unit read gone the
|
|
// wait has nothing left to protect, which is what lets PipeCalls.def's WaitClass column
|
|
// for GenerateMipmap move to kWaitNone.
|
|
const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes;
|
|
// P8-SE: whether the staged store still stands for the texture is read BEFORE the note
|
|
// below sets the mark, or the CPU arm's store route could never answer at all.
|
|
const Bool driverWrittenBefore =
|
|
MG_Record::ServerStagedTexture().IsDriverWritten(MG_Record::StagedTextureStore::KeyForHandle(mipRes));
|
|
// Every arm GenerateMipmapByRecord takes writes the levels above the base on the
|
|
// 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);
|
|
if (!backendTexture || record == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: generate_mipmap names texture {%u, %u}, which has no applier "
|
|
"record or no driver texture; the call is dropped",
|
|
mipRes.Slot, mipRes.Gen);
|
|
return;
|
|
}
|
|
GenerateMipmapByRecord(target, mipRes, *record, backendTexture, driverWrittenBefore);
|
|
return;
|
|
}
|
|
|
|
const GLubyte* GetString(GLenum name) {
|
|
return g_GLESFuncs.glGetString(name);
|
|
}
|
|
|
|
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
PrepareForCompute(false);
|
|
g_GLESFuncs.glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
|
}
|
|
|
|
void DispatchComputeIndirect(GLintptr indirect) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
PrepareForCompute(true);
|
|
g_GLESFuncs.glDispatchComputeIndirect(indirect);
|
|
}
|
|
|
|
// An atomic counter is a shader storage block by the time it reaches the ES driver (glslang
|
|
// lowers every atomic_uint onto one), so an application that asks only for the counter
|
|
// barrier is asking about memory the driver knows as storage-buffer memory. Ordering one
|
|
// does not oblige a driver to order the other, so the counter bit implies the storage bit
|
|
// here - which is what the lowering costs and the only place it can be paid.
|
|
static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) {
|
|
if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) {
|
|
barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
|
|
}
|
|
return barriers;
|
|
}
|
|
|
|
void MemoryBarrier(GLbitfield barriers) {
|
|
barriers = LowerAtomicCounterBarrierBits(barriers);
|
|
if ((barriers & GL_SHADER_STORAGE_BARRIER_BIT) != 0) BufferImpl::NoteShaderStorageWritesOrdered();
|
|
g_GLESFuncs.glMemoryBarrier(barriers);
|
|
if (g_GLESCapabilities.IsAngleRenderer) {
|
|
g_GLESFuncs.glFlush();
|
|
}
|
|
}
|
|
|
|
void MemoryBarrierByRegion(GLbitfield barriers) {
|
|
g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers));
|
|
}
|
|
|
|
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
|
//
|
|
// The frontend hands this backend the target the APPLICATION named, and three of the
|
|
// targets core GL has do not exist in ES at all. They are not missing here either - the
|
|
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
|
|
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
|
|
// but glCopyImageSubData was the one path that never asked for that translation and passed
|
|
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
|
|
// and with the error only asserted on (asserts are compiled out of an INFO build) the
|
|
// destination silently keeps whatever it held.
|
|
//
|
|
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
|
|
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
|
|
// target.
|
|
//
|
|
// GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and
|
|
// GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while
|
|
// ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand
|
|
// the driver GL_UNKNOWN_MGL.
|
|
struct GLESCopyImageEndpoint {
|
|
GLenum target = GL_TEXTURE_2D;
|
|
// Exactly one of the two is set. The backend object is kept rather than its id, because
|
|
// the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture),
|
|
// so it is read at the point of use.
|
|
SharedPtr<TextureImpl::BackendTextureObject> texture;
|
|
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
|
|
GLint x = 0;
|
|
GLint y = 0;
|
|
GLint z = 0;
|
|
|
|
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
|
|
GLuint Name() const {
|
|
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
|
|
return texture ? texture->GetBackendTextureId() : 0u;
|
|
}
|
|
};
|
|
|
|
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
|
|
GLint z, GLESCopyImageEndpoint& out) {
|
|
if (endpoint.IsRenderbuffer()) {
|
|
if (!PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle)) return false;
|
|
auto* slot =
|
|
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreateByHandle(endpoint.RenderbufferHandle);
|
|
if (!slot) return false;
|
|
if (!*slot) *slot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
out.renderbuffer = *slot;
|
|
out.renderbuffer->SyncToBackendByHandle(endpoint.RenderbufferHandle);
|
|
if (!out.renderbuffer) return false;
|
|
out.target = GL_RENDERBUFFER;
|
|
out.x = x;
|
|
out.y = y;
|
|
out.z = z;
|
|
return true;
|
|
}
|
|
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
|
|
// slot inside the backend texture registry, and the second call mutates that very map:
|
|
// GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by
|
|
// robin-hood displacement well under the load factor), and Find drops any
|
|
// entry whose state object has expired - which, with the map open-addressed and erasing
|
|
// by shifting the probe cluster backwards, relocates entries other than the erased one.
|
|
// Either way a reference taken by the first call is stale by the time the second returns,
|
|
// and it is read four more times below. Copying the SharedPtr costs two refcount bumps on
|
|
// a path that is already doing a texture copy.
|
|
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
|
|
// reaches here - but the assertion that says so is compiled out of a release build, and
|
|
// SyncTextureObjectToBackend would register a null state object.
|
|
out.texture = TextureImpl::SyncTextureToBackendByHandle(endpoint.TextureHandle);
|
|
if (!out.texture) return false;
|
|
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
|
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
|
// No axis remap for GL_TEXTURE_1D_ARRAY. The frontend STORES a 1D array with its layers
|
|
// on y (GetBackendUploadSize moves them across to the ES 2D array's z), but this entry
|
|
// point does not ADDRESS it that way: GL 4.6 core 18.3.2 treats every array texture as a
|
|
// stack of slices on z and gives a 1D array a height of 1 - exactly the shape the ES 2D
|
|
// array has - so GL's (x, 0, layer) and the ES image's (x, 0, layer) already agree.
|
|
// Remapping y into z here fetched the wrong slice for every call that spelled the layer
|
|
// the way GL defines it.
|
|
out.x = x;
|
|
out.y = y;
|
|
out.z = z;
|
|
return true;
|
|
}
|
|
|
|
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
|
|
const auto* record = endpoint.IsRenderbuffer() ? PipeRenderbufferRecordForHandle(endpoint.RenderbufferHandle)
|
|
: PipeTextureRecordForHandle(endpoint.TextureHandle);
|
|
return record ? static_cast<TextureInternalFormat>(record->Desc.InternalFormat)
|
|
: TextureInternalFormat::Unknown;
|
|
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
|
|
return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown;
|
|
}
|
|
|
|
// Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one
|
|
// upload target (so not a cube map, whose six chains the z axis selects between) and layers on
|
|
// the z axis (GL_TEXTURE_1D_ARRAY carries them on y).
|
|
static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
|
if (!texture) return false;
|
|
if (texture->GetTarget() == TextureTarget::Texture1DArray) return false;
|
|
return texture->GetUploadTargets().size() == 1;
|
|
}
|
|
|
|
// glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU
|
|
// shadow has to stay authoritative - a packed format with redundant encodings, where a GPU
|
|
// readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) -
|
|
// the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already
|
|
// put these texels on the GPU, and flagging the level would only schedule a redundant upload
|
|
// back over them.
|
|
//
|
|
// Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot
|
|
// address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level
|
|
// whose shadow is missing or not a plain texel grid, a region outside either level, or a
|
|
// self-copy within one level, where the row copies could overlap.
|
|
static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX,
|
|
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint,
|
|
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei width,
|
|
GLsizei height, GLsizei depth) {
|
|
if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return;
|
|
if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return;
|
|
if (width <= 0 || height <= 0 || depth <= 0) return;
|
|
if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) {
|
|
return;
|
|
}
|
|
auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get());
|
|
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
|
|
if (!srcMipmap || !dstMipmap) return;
|
|
// UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling
|
|
// §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's
|
|
// original row) but in the server's staged store: CopyImageSubData's handle arm makes the
|
|
// same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY
|
|
// format, not only the redundant-encoding ones this mirror serves, and marks the store
|
|
// driver-written where it cannot. The store is what answers a split glGetTexImage the
|
|
// driver refuses to read back (DirectGLESReadback.cpp's store route); every other split
|
|
// read of the destination reads the GPU level itself.
|
|
//
|
|
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
|
|
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
|
|
// integration-gpu lanes under MOBILEGL_TRANSPORT=monolith, where this mirror is on an
|
|
// ordinary correct path and must still run.
|
|
return;
|
|
}
|
|
|
|
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX,
|
|
GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, GLenum dstTarget,
|
|
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight,
|
|
GLsizei srcDepth) {
|
|
GLESCopyImageEndpoint src{};
|
|
GLESCopyImageEndpoint dst{};
|
|
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
|
|
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
|
|
// supposed to catch it expands to nothing. The four Name() calls below are the same
|
|
// dereference. The frontend validator is what keeps this unreachable and what reports
|
|
// the error the application is owed; declining is only how a future gap up there stops
|
|
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
|
// The handle arm's endpoint names its texture by handle, and the server has no frontend
|
|
// object to note. The staged store's half of the note waits for the copy (P8-E, below).
|
|
const Bool handleArmDestination =
|
|
!dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle);
|
|
if (handleArmDestination) {
|
|
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true);
|
|
}
|
|
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
|
|
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
|
|
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
|
return;
|
|
}
|
|
// P8-E: THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
|
|
// so every byte the store holds for them is on the driver, and the copy below is a raw
|
|
// texel-block move - the store makes the same move, which keeps the destination level
|
|
// answerable from here when the driver will not read it back (ReadTextureImageWire's store
|
|
// route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level
|
|
// re-upload) from putting the pre-copy texels back. Where the store cannot follow (a
|
|
// renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose
|
|
// bytes did not all cross) it is marked driver-written instead, so it declines rather than
|
|
// answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row).
|
|
if (handleArmDestination) {
|
|
const Bool followed =
|
|
!srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) &&
|
|
TextureImpl::FollowCopyImageInStagedStore(srcEndpoint.TextureHandle, static_cast<Uint32>(srcLevel),
|
|
IntVec3{srcX, srcY, srcZ}, dstEndpoint.TextureHandle,
|
|
static_cast<Uint32>(dstLevel), IntVec3{dstX, dstY, dstZ},
|
|
IntVec3{srcWidth, srcHeight, srcDepth});
|
|
if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
|
|
}
|
|
|
|
// Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto
|
|
// wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint.
|
|
const GLsizei copyHeight = srcHeight;
|
|
const GLsizei copyDepth = srcDepth;
|
|
|
|
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
|
|
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
|
|
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
|
|
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
|
|
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
|
|
// turns it down.
|
|
const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer();
|
|
|
|
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat);
|
|
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat);
|
|
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat);
|
|
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat);
|
|
if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) {
|
|
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
|
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
|
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
|
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
|
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
|
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
|
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, dst.Name(), dstLevel, dst.x,
|
|
dst.y, srcWidth, copyHeight);
|
|
return;
|
|
}
|
|
|
|
if (!anyRenderbuffer &&
|
|
(srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
|
|
// The single glGetError below decides the fallback dispatch, and
|
|
// ErrorLopper::Clear is compiled out at the default log level - drain
|
|
// with the always-live helper so a stale flag cannot misroute a
|
|
// succeeded native copy into the 2D-only fallback.
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, dst.Name(),
|
|
dst.target, dstLevel, dst.x, dst.y, dst.z, srcWidth, copyHeight, copyDepth);
|
|
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
|
if (copyImageError == GL_NO_ERROR) {
|
|
return;
|
|
}
|
|
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
|
|
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
|
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
|
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
|
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
|
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
|
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, dst.Name(), dst.target,
|
|
dstLevel, dst.x, dst.y);
|
|
return;
|
|
}
|
|
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, dst.Name(), dst.target,
|
|
dstLevel, dst.x, dst.y, dst.z, srcWidth, copyHeight, copyDepth);
|
|
// Every error condition glCopyImageSubData has was already ruled out by the frontend
|
|
// validator, so a driver error here is an internal invariant violation, not something
|
|
// the application can provoke. Say so where an INFO build can still see it, then trap
|
|
// in the builds that trap - the previous bare assert left a release build with a
|
|
// destination that silently kept its old contents.
|
|
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
|
if (copyImageError != GL_NO_ERROR) {
|
|
MGLOG_E_ONCE(
|
|
"glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
|
|
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
|
|
MG_Util::ConvertGLEnumToString(src.target).c_str(), MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
|
|
MG_Util::ConvertGLEnumToString(dst.target).c_str(), MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
|
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
|
// P8-E: the store already made the move the driver just refused.
|
|
if (handleArmDestination) {
|
|
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
|
|
}
|
|
return;
|
|
}
|
|
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
|
|
// CPU shadow is what glGetTexImage answers from, so it has to follow the same move -
|
|
// otherwise it hands back whatever the level held before this copy.
|
|
if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) {
|
|
MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel, dstX,
|
|
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
|
}
|
|
}
|
|
|
|
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
|
GLenum format) {
|
|
(void)level;
|
|
(void)layered;
|
|
(void)layer;
|
|
(void)access;
|
|
(void)format;
|
|
// P5e (fb): see NoteImageUnitBoundWithoutReadingTheFrontend. Under a transport this
|
|
// entry point records the high-water mark and lets the next validate point's sweep do
|
|
// the bind from the record; reading the frontend image binding here is the
|
|
// GetImageTextureBinding row that rule F forbids, and `bind_shader_image` is not a
|
|
// barriered row.
|
|
TextureImpl::NoteImageUnitBoundWithoutReadingTheFrontend(unit, texture != 0);
|
|
return;
|
|
}
|
|
|
|
// Only the pnames MG_Impl/GLImpl/Getter/GL_Getter.cpp has no case for reach here. Every
|
|
// indexed pname naming FRONTEND state - the indexed buffer bindings, the per-unit
|
|
// texture/sampler bindings, the image-unit bindings, the viewport rectangles, the indexed
|
|
// capabilities - is answered there and returns before the table is consulted, so the arms
|
|
// this function used to carry for GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were
|
|
// unreachable duplicates of the frontend's, and they did not even agree with it (the
|
|
// frontend reports the range glBindBufferRange was ASKED for, verbatim and unclamped; these
|
|
// clamped it to the buffer's current storage). In practice what arrives is
|
|
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE, which the driver owns.
|
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
|
|
if (!data) return;
|
|
if (g_GLESFuncs.glGetIntegeri_v) {
|
|
g_GLESFuncs.glGetIntegeri_v(target, index, data);
|
|
} else {
|
|
*data = 0;
|
|
}
|
|
}
|
|
|
|
// NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry
|
|
// point must never be the thing that BUILDS a backend program.
|
|
//
|
|
// The frontend has already recorded the rebinding on the program object
|
|
// (SetShaderStorageBlockBinding) - that record is what GL_BUFFER_BINDING reports and what
|
|
// BackendProgramObjectImpl::SyncToBackend replays onto every driver program it builds. So
|
|
// the only work left here is an optimization: push the change straight onto a driver
|
|
// program that is ALREADY built and already current with this link, so the next draw does
|
|
// not have to be preceded by a rebuild.
|
|
//
|
|
// Calling GetBackendProgramId() instead would sync-on-demand from a non-draw entry point,
|
|
// i.e. transpile and compile the whole program while the draw-path globals that the ESSL
|
|
// is generated against (PrgramImpl::g_fragColorBroadcastCount, the snorm/unorm clamp
|
|
// masks - established by SyncCurrentProgram) still hold another program's values. That
|
|
// bakes a program against the wrong state, and under CPU load it was also observed to
|
|
// fail the driver compile outright. Deferring is spec-fine: a binding only has to take
|
|
// effect by the block's next use.
|
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
|
if (!storageBlockName) return;
|
|
const auto handle = MG_Pipe::MGPipeApplier().VerbStorageBlockProgram;
|
|
auto* slot = PrgramImpl::g_backendProgramObjects.FindByHandle(handle);
|
|
if (slot && *slot && (*slot)->GetBackendProgramId()) {
|
|
PrgramImpl::ApplyShaderStorageBlockBinding((*slot)->GetBackendProgramId(), storageBlockName,
|
|
storageBlockBinding);
|
|
}
|
|
return;
|
|
}
|
|
|
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
RenderStateImpl::SyncRenderState();
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil);
|
|
}
|
|
|
|
namespace {
|
|
// A color attachment the backend widened from three channels to four has to end up
|
|
// holding alpha 1.0 - the value GL reports for a channel the application's format does
|
|
// not have - so an explicit per-buffer clear of it writes 1.0 rather than whatever the
|
|
// application passed (SubstituteWidenedClearAlpha, in Managers.h). Draws can never move
|
|
// it again: their alpha write mask is forced off, see SyncRenderState. That pairing is
|
|
// what makes GL_DST_ALPHA blending, glReadPixels and glBlitFramebuffer all see the right
|
|
// value without any of them being intercepted.
|
|
|
|
// Whether draw buffer `drawbuffer` of the framebuffer currently bound as DRAW is such an
|
|
// attachment. Answered from the mask SyncCurrentFBO just recomputed, so it costs nothing.
|
|
Bool IsWidenedBoundDrawBuffer(GLenum buffer, GLint drawbuffer) {
|
|
return buffer == GL_COLOR && drawbuffer >= 0 && drawbuffer < 32 &&
|
|
(FramebufferImpl::g_alphaWidenedDrawBufferMask & (1u << drawbuffer)) != 0;
|
|
}
|
|
} // namespace
|
|
|
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
GLfloat widenedValue[4] = {};
|
|
g_GLESFuncs.glClearBufferfv(
|
|
buffer, drawbuffer,
|
|
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), 1.0f, widenedValue));
|
|
}
|
|
|
|
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
// SyncCurrentFBO early-outs for the default framebuffer, so without this
|
|
// bind a user-FBO -> default-FBO switch would leave the clear landing on
|
|
// the stale driver DRAW binding (the fi/fv/uiv siblings all bind too).
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
GLint widenedValue[4] = {};
|
|
g_GLESFuncs.glClearBufferiv(
|
|
buffer, drawbuffer,
|
|
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLint(1), widenedValue));
|
|
}
|
|
|
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
GLuint widenedValue[4] = {};
|
|
g_GLESFuncs.glClearBufferuiv(
|
|
buffer, drawbuffer,
|
|
SubstituteWidenedClearAlpha(value, IsWidenedBoundDrawBuffer(buffer, drawbuffer), GLuint(1), widenedValue));
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|