mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
10683 lines
601 KiB
C++
10683 lines
601 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLES.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 "DirectGLES.h"
|
|
#include "EGL/egl.h"
|
|
#include "MG_Util/Types.h"
|
|
#include "Utils.h"
|
|
#include "Managers.h"
|
|
#include "MultiDraw.h"
|
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
|
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
|
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
|
#include <MG_State/GLState/Core.h>
|
|
#include <MG_State/GLState/ErrorState/Error.h>
|
|
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
|
#include <MG_Util/SelfTest/DriverBugProbes.h>
|
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
|
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
|
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
|
#include <MG_Util/Math/HalfFloat.h>
|
|
#include <MG_Util/Metrics/BufferMetrics.h>
|
|
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
|
#include <Config.h>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <thread>
|
|
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<X11/Xlib.h>)
|
|
#pragma push_macro("Bool")
|
|
#pragma push_macro("None")
|
|
#include <X11/Xlib.h>
|
|
#pragma pop_macro("None")
|
|
#pragma pop_macro("Bool")
|
|
#endif
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
MG_External::EGLFunctionsTable g_EGLFuncs;
|
|
MG_External::GLESFunctionsTable g_GLESFuncs;
|
|
MG_External::GLESCapabilities g_GLESCapabilities;
|
|
|
|
static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight);
|
|
static SharedPtr<MG_State::GLState::SamplerObject> g_rawDepthFetchSamplerState;
|
|
static SharedPtr<SamplerImpl::BackendSamplerObject> g_rawDepthFetchSamplerBackend;
|
|
|
|
// Two objects are the same binding iff they share a control block. Raw addresses lie
|
|
// (a freed object's heap slot is reused), but a held weak_ptr pins the control block,
|
|
// so no later object can ever owner-equal a snapshot of its predecessor.
|
|
template <typename T>
|
|
static Bool OwnerEquals(const WeakPtr<T>& snapshot, const SharedPtr<T>& current) {
|
|
return !snapshot.owner_before(current) && !current.owner_before(snapshot);
|
|
}
|
|
|
|
// Owner-keyed direct-mapped memo over a StateBackendObjectRegistry, for the object
|
|
// kinds the draw path re-Finds every draw (VAO, program). A hit replaces the
|
|
// registry's hash-map Find with one array index plus an owner-equality compare.
|
|
//
|
|
// Why the raw twin pointer is safe to hand back: the twin is owned by the registry
|
|
// entry's SharedPtr, and a LIVE state object's entry is never erased nor has its
|
|
// twin replaced once set — Find and CollectGarbage erase only expired entries,
|
|
// GetOrCreate resets the twin only when the previous owner at that address expired,
|
|
// and the sync paths create a twin only when the slot is null. Owner-equality of
|
|
// the weak snapshot with the live frontend therefore proves the memoed pointer is
|
|
// still that frontend's registered twin. A recycled heap address fails the
|
|
// owner-equality (the weak snapshot pins the predecessor's control block), and a
|
|
// slot collision merely falls back to the registry Find. Nothing here needs an
|
|
// explicit invalidation hook: registry entries survive ES-context loss (twins
|
|
// re-sync via their own generation gates), matching the Find they replace.
|
|
template <typename StateObject, typename BackendObject, SizeT kIndexBits>
|
|
class TwinLookupMemo {
|
|
public:
|
|
BackendObject* Lookup(const SharedPtr<StateObject>& stateObj) const {
|
|
const Slot& slot = m_slots[IndexFor(stateObj.get())];
|
|
if (slot.key == stateObj.get() && slot.twin != nullptr && OwnerEquals(slot.owner, stateObj)) {
|
|
return slot.twin;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Store(const SharedPtr<StateObject>& stateObj, BackendObject* twin) {
|
|
Slot& slot = m_slots[IndexFor(stateObj.get())];
|
|
slot.key = stateObj.get();
|
|
slot.owner = stateObj;
|
|
slot.twin = twin;
|
|
}
|
|
|
|
private:
|
|
struct Slot {
|
|
StateObject* key = nullptr;
|
|
WeakPtr<StateObject> owner;
|
|
BackendObject* twin = nullptr;
|
|
};
|
|
|
|
static SizeT IndexFor(const StateObject* ptr) {
|
|
// Fibonacci hashing: heap addresses share alignment zeros and arena locality;
|
|
// the multiply spreads them before the top bits pick the slot.
|
|
const Uint64 h = reinterpret_cast<std::uintptr_t>(ptr) * 0x9E3779B97F4A7C15ull;
|
|
return static_cast<SizeT>(h >> (64u - kIndexBits));
|
|
}
|
|
|
|
Array<Slot, (SizeT(1) << kIndexBits)> m_slots;
|
|
};
|
|
|
|
// 4096 slots (128 KiB, sparse-touched): Minecraft-shaped workloads cycle hundreds
|
|
// of section VAOs per frame (the driver bench cycles 512), and a colliding pair
|
|
// ping-pongs back onto the hash Find every frame — at 512 keys the expected
|
|
// collided fraction is ~12% here vs ~22% at 2048. Programs are far fewer; 256
|
|
// slots is plenty.
|
|
static TwinLookupMemo<MG_State::GLState::VertexArrayObject, VertexArrayImpl::BackendVertexArrayObject, 12>
|
|
g_vaoTwinLookupMemo;
|
|
static TwinLookupMemo<MG_State::GLState::ProgramObject, PrgramImpl::BackendProgramObjectImpl, 8>
|
|
g_programTwinLookupMemo;
|
|
// Framebuffers qualify for the same memo (see the class comment's registry
|
|
// invariants: g_backendFramebufferObjects only GetOrCreate-resets an expired
|
|
// owner's twin and never erases/replaces a live one). Apps bind a handful of
|
|
// FBOs; 64 slots is plenty.
|
|
static TwinLookupMemo<MG_State::GLState::FramebufferObject, FramebufferImpl::BackendFramebufferObject, 6>
|
|
g_fboTwinLookupMemo;
|
|
|
|
// Cached addresses of the frontend's framebuffer binding slots. The frontend
|
|
// getter linear-scans its slot array per call and the draw path asks for these
|
|
// slots several times per draw (FBO sync, FBO texture-attachment sync, the
|
|
// draw-buffer broadcast memo, the draw-time bind). The slots are by-value
|
|
// members of the GLContext, so their addresses are a pure function of the
|
|
// context's address: the cache is keyed on the raw context pointer, and a
|
|
// recreated context landing on the SAME address has its slots at the same
|
|
// addresses again - the cached pointers cannot go stale. Invalidation is
|
|
// exactly the pointer compare below.
|
|
using FbBindingSlot =
|
|
std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
|
|
static const MG_State::GLState::GLContext* g_fbSlotCacheContext = nullptr;
|
|
static Array<FbBindingSlot*, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fbSlotCache = {};
|
|
static inline FbBindingSlot& GetFramebufferBindingSlotFast(FramebufferTarget target) {
|
|
MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get();
|
|
if (ctx != g_fbSlotCacheContext) {
|
|
for (SizeT i = 0; i < g_fbSlotCache.size(); ++i) {
|
|
g_fbSlotCache[i] = &ctx->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
|
|
}
|
|
g_fbSlotCacheContext = ctx;
|
|
}
|
|
return *g_fbSlotCache[SizeT(target)];
|
|
}
|
|
|
|
static Bool IsDualSourceBlendFactor(BlendFactor v) {
|
|
switch (v) {
|
|
case BlendFactor::Src1Color:
|
|
case BlendFactor::OneMinusSrc1Color:
|
|
case BlendFactor::Src1Alpha:
|
|
case BlendFactor::OneMinusSrc1Alpha:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SamplerImpl::BackendSamplerObject* GetRawDepthFetchSampler() {
|
|
if (!g_rawDepthFetchSamplerState) {
|
|
g_rawDepthFetchSamplerState = MakeShared<MG_State::GLState::SamplerObject>(0);
|
|
g_rawDepthFetchSamplerState->SetMinFilter(SamplerFilterMode::Nearest);
|
|
g_rawDepthFetchSamplerState->SetMagFilter(SamplerFilterMode::Nearest);
|
|
g_rawDepthFetchSamplerState->SetMipmapMode(SamplerMipmapMode::None);
|
|
g_rawDepthFetchSamplerState->SetCompareMode(SamplerCompareMode::None);
|
|
g_rawDepthFetchSamplerState->SetSamplerCompareFunc(SamplerCompareFunc::Always);
|
|
g_rawDepthFetchSamplerBackend = MakeShared<SamplerImpl::BackendSamplerObject>();
|
|
}
|
|
g_rawDepthFetchSamplerBackend->SyncToBackend(g_rawDepthFetchSamplerState);
|
|
return g_rawDepthFetchSamplerBackend.get();
|
|
}
|
|
|
|
Bool NeedsRawDepthFetchSampler(const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject,
|
|
TextureInternalFormat textureFormat) {
|
|
if (!MG_Util::IsDepthFormatInternalFormat(textureFormat) || !samplerObject) {
|
|
return false;
|
|
}
|
|
|
|
const auto& samplerParams = samplerObject->GetAllSamplerParameters();
|
|
if (samplerParams.compareMode != SamplerCompareMode::None) {
|
|
return false;
|
|
}
|
|
|
|
return samplerParams.minFilter != SamplerFilterMode::Nearest ||
|
|
samplerParams.mipmapMode != SamplerMipmapMode::None ||
|
|
samplerParams.magFilter != SamplerFilterMode::Nearest;
|
|
}
|
|
|
|
// Frontend texture target a GLSL sampler uniform samples from. Only used to find
|
|
// which of a unit's bindings carries the GL_TEXTURE_LOD_BIAS the shader needs;
|
|
// targets with no mip chain map to Unknown so the lookup falls back to no bias.
|
|
TextureTarget SamplerUniformTextureTarget(GLenum uniformType) {
|
|
switch (uniformType) {
|
|
case GL_SAMPLER_1D:
|
|
case GL_INT_SAMPLER_1D:
|
|
case GL_UNSIGNED_INT_SAMPLER_1D:
|
|
case GL_SAMPLER_1D_SHADOW:
|
|
return TextureTarget::Texture1D;
|
|
case GL_SAMPLER_2D:
|
|
case GL_INT_SAMPLER_2D:
|
|
case GL_UNSIGNED_INT_SAMPLER_2D:
|
|
case GL_SAMPLER_2D_SHADOW:
|
|
return TextureTarget::Texture2D;
|
|
case GL_SAMPLER_3D:
|
|
case GL_INT_SAMPLER_3D:
|
|
case GL_UNSIGNED_INT_SAMPLER_3D:
|
|
return TextureTarget::Texture3D;
|
|
case GL_SAMPLER_CUBE:
|
|
case GL_INT_SAMPLER_CUBE:
|
|
case GL_UNSIGNED_INT_SAMPLER_CUBE:
|
|
case GL_SAMPLER_CUBE_SHADOW:
|
|
return TextureTarget::TextureCubeMap;
|
|
case GL_SAMPLER_1D_ARRAY:
|
|
case GL_INT_SAMPLER_1D_ARRAY:
|
|
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
|
|
case GL_SAMPLER_1D_ARRAY_SHADOW:
|
|
return TextureTarget::Texture1DArray;
|
|
case GL_SAMPLER_2D_ARRAY:
|
|
case GL_INT_SAMPLER_2D_ARRAY:
|
|
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
|
|
case GL_SAMPLER_2D_ARRAY_SHADOW:
|
|
return TextureTarget::Texture2DArray;
|
|
case GL_SAMPLER_CUBE_MAP_ARRAY:
|
|
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
|
|
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
|
|
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
|
|
return TextureTarget::TextureCubeMapArray;
|
|
case GL_SAMPLER_2D_RECT:
|
|
case GL_INT_SAMPLER_2D_RECT:
|
|
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
|
|
case GL_SAMPLER_2D_RECT_SHADOW:
|
|
return TextureTarget::TextureRectangle;
|
|
case GL_SAMPLER_2D_MULTISAMPLE:
|
|
case GL_INT_SAMPLER_2D_MULTISAMPLE:
|
|
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
|
|
return TextureTarget::Texture2DMultisample;
|
|
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
|
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
|
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
|
return TextureTarget::Texture2DMultisampleArray;
|
|
case GL_SAMPLER_BUFFER:
|
|
case GL_INT_SAMPLER_BUFFER:
|
|
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
|
|
return TextureTarget::TextureBuffer;
|
|
default:
|
|
return TextureTarget::Unknown;
|
|
}
|
|
}
|
|
|
|
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
if (drawBuffer) {
|
|
drawBuffer->SyncPersistentMappedRange();
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
if (commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
|
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
|
return nullptr;
|
|
}
|
|
return drawBuffer->MappedData() + commandOffset;
|
|
}
|
|
|
|
if (!indirect) {
|
|
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
|
return nullptr;
|
|
}
|
|
|
|
return reinterpret_cast<const Uint8*>(indirect);
|
|
}
|
|
|
|
namespace DebugImpl {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {
|
|
GLenum err = g_GLESFuncs.glGetError();
|
|
while (err != GL_NO_ERROR) {
|
|
func(err);
|
|
err = g_GLESFuncs.glGetError();
|
|
}
|
|
}
|
|
|
|
void ErrorLopper::Clear() {
|
|
GLenum err = g_GLESFuncs.glGetError();
|
|
while (err != GL_NO_ERROR) {
|
|
MGLOG_D("Stray GL Error cleared: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
|
err = g_GLESFuncs.glGetError();
|
|
}
|
|
}
|
|
|
|
ErrorLopper::ErrorLopper() {
|
|
Clear();
|
|
}
|
|
ErrorLopper::~ErrorLopper() {
|
|
Clear();
|
|
}
|
|
#else
|
|
// Error HYGIENE is not a debugging feature: every site that brackets a risky ES call with
|
|
// Clear()/Loop() relied on these to empty the driver's queue, and compiling them to
|
|
// nothing left whatever the driver raised sitting there for an unrelated later
|
|
// `glGetError() == GL_NO_ERROR` probe to read as its own failure. The callback stays
|
|
// unused because MGLOG_D is compiled out at this level, but the queue still gets drained.
|
|
// Bounded like DrainESErrors: a driver that never returns GL_NO_ERROR (a lost context is
|
|
// the usual way) must not spin here.
|
|
constexpr Int kMaxDrainedESErrors = 32;
|
|
|
|
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {
|
|
static_cast<void>(func);
|
|
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
}
|
|
|
|
void ErrorLopper::Clear() {
|
|
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
}
|
|
|
|
ErrorLopper::ErrorLopper() {
|
|
Clear();
|
|
}
|
|
ErrorLopper::~ErrorLopper() {
|
|
Clear();
|
|
}
|
|
#endif
|
|
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
OpenGLScopeMarker::OpenGLScopeMarker(const String& scopeName) {
|
|
g_GLESFuncs.glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, scopeName.c_str());
|
|
}
|
|
|
|
OpenGLScopeMarker::~OpenGLScopeMarker() {
|
|
g_GLESFuncs.glPopDebugGroup();
|
|
}
|
|
#else
|
|
OpenGLScopeMarker::OpenGLScopeMarker(const String& scopeName) {}
|
|
|
|
OpenGLScopeMarker::~OpenGLScopeMarker() {}
|
|
#endif
|
|
} // namespace DebugImpl
|
|
|
|
// TODO: deletion for deleted objects
|
|
|
|
namespace BufferImpl {
|
|
void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Only sync up to the high-water mark of app-touched points; the fixed array is 84
|
|
// deep but apps bind a handful, so the never-touched tail is already at GL default 0.
|
|
auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);
|
|
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
|
|
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
|
|
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
|
|
// by stopping: this frontend-indexed pass exists for the compute path, and the
|
|
// per-program rebind in BindCurrentProgramWithResources - which is what actually feeds
|
|
// a shader - remaps every block a program declares onto a compacted ES point, so a
|
|
// block bound at GL point 83 still reaches its shader.
|
|
if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) {
|
|
bindingPointCnt = std::min(bindingPointCnt,
|
|
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
|
|
}
|
|
for (SizeT i = 0; i < bindingPointCnt; ++i) {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
|
|
auto& obj = point.GetBoundObject();
|
|
if (!obj) {
|
|
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), 0);
|
|
continue;
|
|
}
|
|
|
|
auto* backendResource = EnsureBufferResource(obj);
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer found for %s binding point %zu.",
|
|
MG_Util::ConvertGLEnumToString(glTarget).c_str(), i);
|
|
continue;
|
|
}
|
|
|
|
const auto& range = point.GetRange();
|
|
auto backendBufferId = backendResource->id;
|
|
if (range.start == 0 && range.end >= obj->GetSize()) {
|
|
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
|
|
} else {
|
|
const auto start = std::min(range.start, obj->GetSize());
|
|
const auto end = std::min(range.end, obj->GetSize());
|
|
BindBufferRangeCached(glTarget, static_cast<GLuint>(i), backendBufferId,
|
|
static_cast<GLintptr>(start), static_cast<GLsizeiptr>(end - start));
|
|
}
|
|
}
|
|
}
|
|
|
|
// The capture points the CAPTURE PROGRAM uses, and nothing else.
|
|
//
|
|
// This used to go through SyncBufferBindingPoints, which walks the application's
|
|
// GLOBAL touched-binding-point high-water mark and binds 0 to every point with no
|
|
// frontend buffer. deqp/glcts permanently raises that mark to
|
|
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS by clearing all of them after each test
|
|
// case, so every capture using fewer points than that - i.e. every INTERLEAVED_ATTRIBS
|
|
// capture - had glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, 0) issued for the
|
|
// unused tail immediately before glBeginTransformFeedback. The Mali G1-Ultra driver
|
|
// then recorded NOTHING: no GL error, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0, the
|
|
// application's buffer left holding its pre-draw bytes. Confirmed on device - the
|
|
// separate/interleaved split in KHR-GL46.transform_feedback follows exactly whether
|
|
// all four points were left bound.
|
|
//
|
|
// Those binds were never needed for correctness either. A capture only writes the
|
|
// points the program's buffer mode uses (GL 4.6 core 13.2.2), so a point past
|
|
// bufferCount cannot be written whatever is left bound there, and a point the program
|
|
// DOES use with no buffer bound is already an error the frontend raised at
|
|
// glBeginTransformFeedback. The rule this encodes: never issue a capture-point bind
|
|
// the application did not ask for.
|
|
//
|
|
// Scoping it to the program (rather than skipping redundant binds behind the shadow)
|
|
// is what makes it ORDER-INDEPENDENT: the shadow has to drop to unknown whenever a
|
|
// transform feedback OBJECT is bound, since the points belong to the object, and the
|
|
// clears came straight back for the next capture in the process.
|
|
void SyncTransformFeedbackBindingPoints(SizeT bufferCount) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const SizeT pointCount = std::min<SizeT>(
|
|
bufferCount, MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
|
|
for (SizeT i = 0; i < pointCount; ++i) {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
|
|
const auto& obj = point.GetBoundObject();
|
|
// A stride-0 slot (two consecutive gl_NextBuffer entries) captures nothing and
|
|
// needs no binding; anything else with no buffer never got past the frontend.
|
|
if (!obj) continue;
|
|
|
|
auto* backendResource = EnsureBufferResource(obj);
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer for GL_TRANSFORM_FEEDBACK_BUFFER capture point %zu; the capture "
|
|
"will not reach the application's buffer.",
|
|
i);
|
|
continue;
|
|
}
|
|
|
|
const auto& range = point.GetRange();
|
|
const auto backendBufferId = backendResource->id;
|
|
if (range.start == 0 && range.end >= obj->GetSize()) {
|
|
BindBufferBaseCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId);
|
|
} else {
|
|
const auto start = std::min(range.start, obj->GetSize());
|
|
const auto end = std::min(range.end, obj->GetSize());
|
|
BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), backendBufferId,
|
|
static_cast<GLintptr>(start), static_cast<GLsizeiptr>(end - start));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called once the storage-buffer points are bound and the draw/dispatch is about to
|
|
// go out: whatever the shader writes there lands in the ES driver's buffers, behind
|
|
// the frontend's CPU shadow. Flagging them makes the next MapBuffer/GetBufferSubData
|
|
// pull the real contents back (BufferObject::SyncGpuWrites).
|
|
void MarkShaderStorageBuffersGpuWritten() {
|
|
const SizeT bindingPointCnt =
|
|
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
|
|
for (SizeT i = 0; i < bindingPointCnt; ++i) {
|
|
const auto& obj =
|
|
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
|
|
if (obj) obj->MarkGpuWritten();
|
|
}
|
|
}
|
|
|
|
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
|
|
for (const Int glBinding : glBindings) {
|
|
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
|
|
const Int esslBinding = esslBindingTop - glBinding;
|
|
// Already diagnosed once when the block was transpiled; nothing was bound to it
|
|
// there either, so there is nothing to unbind here.
|
|
if (esslBinding < 0) continue;
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
|
|
static_cast<Uint>(glBinding));
|
|
auto& obj = point.GetBoundObject();
|
|
if (!obj) {
|
|
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
|
|
continue;
|
|
}
|
|
|
|
auto* backendResource = EnsureBufferResource(obj);
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
|
|
continue;
|
|
}
|
|
|
|
const auto& range = point.GetRange();
|
|
if (range.start == 0 && range.end >= obj->GetSize()) {
|
|
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
|
|
backendResource->id);
|
|
} else {
|
|
const auto start = std::min(range.start, obj->GetSize());
|
|
const auto end = std::min(range.end, obj->GetSize());
|
|
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
|
|
backendResource->id, static_cast<GLintptr>(start),
|
|
static_cast<GLsizeiptr>(end - start));
|
|
}
|
|
// The whole point of a counter is that the shader INCREMENTS it, and every
|
|
// conformance case reads the result back with glMapBufferRange or
|
|
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
|
|
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
|
|
obj->MarkGpuWritten();
|
|
}
|
|
}
|
|
|
|
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(target).GetBoundObject();
|
|
if (!bufferObject) {
|
|
g_GLESFuncs.glBindBuffer(glTarget, 0);
|
|
return;
|
|
}
|
|
|
|
auto* backendResource = EnsureBufferResource(bufferObject);
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str());
|
|
return;
|
|
}
|
|
BindBufferId(glTarget, backendResource->id);
|
|
}
|
|
|
|
// `vaoConfigVersion` is the caller's early read of currentVAOObject->GetConfigVersion():
|
|
// the VAO's config fields live on a cache line the draw path touches nowhere else, and
|
|
// cycling section VAOs makes that a guaranteed miss - reading it at the top of
|
|
// PrepareForDraw overlaps the miss with the program/texture-key work instead of
|
|
// stalling the memo check below. Nothing between the read and here can move it
|
|
// (only frontend GL entry points mutate VAO config, none run inside a preparation).
|
|
void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion,
|
|
Bool includeIBO = false, Bool includeIndirectBuffer = false) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
ProcessDeferredBufferReleases();
|
|
|
|
// All buffers we need are:
|
|
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
|
// PBO is not needed since it should be handled in frontend
|
|
|
|
if (!currentVAOObject) {
|
|
MGLOG_E_ONCE("No VAO is currently bound, cannot sync necessary buffers.");
|
|
return;
|
|
}
|
|
|
|
// VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling
|
|
// hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw
|
|
// only to rediscover the same one or two static buffers. While the config
|
|
// version holds (every attach/enable/disable bumps it, and it pins each
|
|
// memoed frontend pointer via the attribute SharedPtrs), the walk reduces to
|
|
// an IsBufferDrawClean probe per distinct buffer; only dirty entries take
|
|
// EnsureBufferResource, re-fetched through their attribute index.
|
|
// Pre-pass epoch read (acquire), stamped into the memo only after a pass in
|
|
// which EVERY probe came up clean: while the stamp still equals the current
|
|
// epoch, no path that can dirty any buffer has run (see the mutation-site
|
|
// enumeration at CurrentBufferMutationEpoch's declaration in Managers.h),
|
|
// so the probes themselves are skipped. A mid-pass bump lands after this
|
|
// read, makes the stamp stale, and re-runs the probes next draw.
|
|
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
|
|
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
|
const Uint32 configVersion = vaoConfigVersion;
|
|
if (memo && memo->valid && memo->configVersion == configVersion) {
|
|
if (memo->vboCleanEpoch != bufferEpoch) {
|
|
Bool allClean = true;
|
|
for (Uint i = 0; i < memo->count; ++i) {
|
|
auto& entry = memo->entries[i];
|
|
if (IsBufferDrawClean(entry.frontend, entry.resource)) continue;
|
|
allClean = false;
|
|
// Same object the entry was built from: config version unchanged.
|
|
entry.resource =
|
|
EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
|
|
}
|
|
// Entries EnsureBufferResource repaired are not re-probed here; the
|
|
// next pass's clean probes stamp them (one extra pass, never a skip
|
|
// of work).
|
|
memo->vboCleanEpoch = allClean ? bufferEpoch : 0;
|
|
}
|
|
} else {
|
|
// Full walk, once per distinct buffer (an interleaved Minecraft-shaped VAO
|
|
// feeds all attributes out of one VBO), rebuilding the memo as it goes.
|
|
// The twin can only be absent on the error path that has no VAO twin at
|
|
// all; the dedupe scratch keeps this branch correct even then.
|
|
MG_State::GLState::BufferObject*
|
|
syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
|
|
Uint syncedBufferCount = 0;
|
|
const auto& allAttributes = currentVAOObject->GetAllAttributes();
|
|
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
|
const auto& attrib = allAttributes[attribIndex];
|
|
if (!attrib.Enabled) continue;
|
|
const auto& bufferObject = attrib.Buffer;
|
|
if (!bufferObject) continue;
|
|
|
|
auto* const bufferKey = bufferObject.get();
|
|
Bool alreadySynced = false;
|
|
for (Uint i = 0; i < syncedBufferCount; ++i) {
|
|
if (syncedBuffers[i] == bufferKey) {
|
|
alreadySynced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (alreadySynced) continue;
|
|
|
|
auto* resource = EnsureBufferResource(bufferObject);
|
|
if (memo) {
|
|
auto& entry = memo->entries[syncedBufferCount];
|
|
entry.frontend = bufferKey;
|
|
entry.attribIndex = static_cast<Uint8>(attribIndex);
|
|
entry.resource = resource;
|
|
}
|
|
syncedBuffers[syncedBufferCount++] = bufferKey;
|
|
}
|
|
if (memo) {
|
|
memo->count = syncedBufferCount;
|
|
memo->configVersion = configVersion;
|
|
memo->valid = true;
|
|
// Rebuilt via EnsureBufferResource, not probed clean: the next
|
|
// probe pass stamps the epoch.
|
|
memo->vboCleanEpoch = 0;
|
|
}
|
|
}
|
|
|
|
// IBO, memoed by bound-object identity (its slot version is not covered by
|
|
// the config version). A stale identity hit is impossible in effect: the
|
|
// clean probe re-validates the resource against the LIVE bound object.
|
|
if (includeIBO) {
|
|
const auto& possibleIBO = currentVAOObject->GetIndexBufferBindingSlot().GetBoundObject();
|
|
if (possibleIBO) {
|
|
// The epoch stamp alone is NOT enough here: the index slot can
|
|
// rebind another buffer with no epoch (and no config-version) move,
|
|
// so the identity compare always runs; only the clean PROBE is
|
|
// elided while the stamp holds.
|
|
if (memo && memo->iboFrontend == possibleIBO.get() && memo->iboCleanEpoch == bufferEpoch) {
|
|
// probed fully clean at this epoch; nothing can have dirtied it
|
|
} else if (memo && memo->iboFrontend == possibleIBO.get() &&
|
|
IsBufferDrawClean(memo->iboFrontend, memo->iboResource)) {
|
|
memo->iboCleanEpoch = bufferEpoch;
|
|
} else {
|
|
auto* resource = EnsureBufferResource(possibleIBO);
|
|
if (memo) {
|
|
memo->iboFrontend = possibleIBO.get();
|
|
memo->iboResource = resource;
|
|
// Repaired, not probed clean: stamp on the next clean probe.
|
|
memo->iboCleanEpoch = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES
|
|
// context since indirect draws now execute natively on the GPU.
|
|
if (includeIndirectBuffer) {
|
|
auto& possibleIndirectBuffer =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
if (possibleIndirectBuffer) {
|
|
SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER);
|
|
}
|
|
}
|
|
|
|
// UBO binding points are (re)established per draw by BindCurrentProgramWithResources at
|
|
// their compacted link-time points: CacheResourceLocations glUniformBlockBinding's the
|
|
// transpiled ESSL blocks to points 0,1,2,... (layout(binding=N) is stripped from the
|
|
// ESSL), so those compacted points are the only ones the shader reads. A frontend-indexed
|
|
// sync here would bind points the shader never reads and is unconditionally overwritten by
|
|
// the program rebind that always follows in PrepareForDraw - i.e. redundant for draws - so
|
|
// it is intentionally omitted (see SyncComputeBuffers for the compute path, which needs it).
|
|
// SSBOs are different: their block bindings are baked into the ESSL at compile time and
|
|
// BindCurrentProgramWithResources binds no SSBO points, so this is their sole draw-path
|
|
// binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers).
|
|
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
|
|
MarkShaderStorageBuffersGpuWritten();
|
|
}
|
|
|
|
void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
ProcessDeferredBufferReleases();
|
|
SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER);
|
|
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
|
|
MarkShaderStorageBuffersGpuWritten();
|
|
if (includeDispatchIndirectBuffer) {
|
|
SyncBoundBuffer(BufferTarget::DispatchIndirect, GL_DISPATCH_INDIRECT_BUFFER);
|
|
}
|
|
}
|
|
} // namespace BufferImpl
|
|
|
|
// Transform feedback is captured by the real ES driver: the backend program
|
|
// declares the capture set at link time (see BackendProgramObjectImpl::SyncToBackend)
|
|
// and the span below wraps the driver's own glBeginTransformFeedback/glEndTransformFeedback.
|
|
//
|
|
// The driver-side Begin is deferred from the frontend's glBeginTransformFeedback to
|
|
// the first draw of the span: ES requires the capturing program to be current and
|
|
// the capture buffers bound when Begin is issued, and both of those only become true
|
|
// once PrepareForDraw has run. A span that never draws therefore never touches the
|
|
// driver at all, which is also what the GL semantics amount to.
|
|
//
|
|
// Transform feedback objects (ARB_transform_feedback2) are ES 3.0 core, so each
|
|
// frontend object gets one of the driver's: a paused span lives inside the ES object,
|
|
// which is the only way several of them can be paused at once - and the only reason
|
|
// the default object alone would not do.
|
|
namespace XfbImpl {
|
|
namespace {
|
|
struct XfbCaptureTarget {
|
|
SharedPtr<MG_State::GLState::BufferObject> buffer;
|
|
Uint backendId = 0;
|
|
SizeT start = 0;
|
|
SizeT end = 0;
|
|
// WHICH capture buffer of the program this is. The list is COMPACTED - a
|
|
// capture buffer with no bound buffer object contributes no entry - so the
|
|
// position in the vector is not the program's buffer index, and everything
|
|
// that asks the program about a target (its stride, which varyings land in
|
|
// it) has to ask about this index instead. A capture list beginning with
|
|
// gl_NextBuffer is the shape that makes them differ: buffer 0 has stride 0
|
|
// and nothing bound, so target 0 describes buffer 1.
|
|
SizeT bufferIndex = 0;
|
|
};
|
|
|
|
// Per frontend transform feedback object. The default object (name 0) maps to
|
|
// the driver's default object (id 0) and is always present.
|
|
struct XfbObjectState {
|
|
GLuint esId = 0;
|
|
Bool pending = false; // frontend Begin seen, driver capture not started yet
|
|
Bool started = false; // driver capture running
|
|
Bool paused = false; // frontend Pause seen and not yet resumed
|
|
GLenum primitiveMode = GL_POINTS;
|
|
Vector<XfbCaptureTarget> targets;
|
|
// Set for a layout ES cannot express (gl_SkipComponents / gl_NextBuffer):
|
|
// the driver captures gap-free records into the scratch buffer below and
|
|
// End scatters them into `targets`.
|
|
Bool scattered = false;
|
|
SharedPtr<MG_State::GLState::ProgramObject> scatterProgram;
|
|
SizeT scatterCapacityVertices = 0;
|
|
};
|
|
|
|
// One scratch ES buffer serves every scattered capture: only one span can be
|
|
// recording at a time (the driver would reject a second Begin), so its contents
|
|
// are consumed by the End that follows.
|
|
GLuint g_scatterBufferId = 0;
|
|
SizeT g_scatterBufferSize = 0;
|
|
|
|
UnorderedMap<GLuint, XfbObjectState> g_xfbObjects;
|
|
GLuint g_currentXfbName = 0;
|
|
// Cached address of g_xfbObjects[g_currentXfbName]: PrepareForDraw consults
|
|
// CurrentXfb on EVERY draw (StartPendingTransformFeedback) and the map
|
|
// lookup was pure per-draw overhead for the overwhelmingly common no-capture
|
|
// case. Open addressing keeps values in the bucket array, so ANY insert can
|
|
// rehash and move them - and erase moves them too, by shifting the rest of the
|
|
// probe cluster into the hole, which reaches entries other than the erased one.
|
|
// Every site that mutates the map or rebinds the current name resets this to
|
|
// null instead of reasoning about stability, and CurrentXfb re-resolves lazily.
|
|
XfbObjectState* g_currentXfbState = nullptr;
|
|
|
|
XfbObjectState& CurrentXfb() {
|
|
if (g_currentXfbState == nullptr) {
|
|
g_currentXfbState = &g_xfbObjects[g_currentXfbName];
|
|
}
|
|
return *g_currentXfbState;
|
|
}
|
|
|
|
// EVERY way this path can lose a capture used to be silent: three unlogged early
|
|
// returns before the driver Begin, an unchecked glBeginTransformFeedback, and two
|
|
// `continue`s in the readback. The application sees a buffer that kept its
|
|
// pre-draw bytes, GL_NO_ERROR, and GL_LINK_STATUS true - which is how one defect
|
|
// reached ~320 conformance bodies across four families before anyone could say
|
|
// which of the branches fired. Nothing below changes what MobileGL DOES on a
|
|
// healthy capture; it only makes a lost one name itself in /sdcard/MG/latest.log.
|
|
//
|
|
// MGLOG_E_ONCE (not _D) on purpose: these have to be readable in an INFO-level
|
|
// artifact, the same reason the backend link failure at Managers.cpp is MGLOG_E.
|
|
constexpr Int kMaxDrainedXfbErrors = 32;
|
|
|
|
// The ES error raised by the call just issued, GL_NO_ERROR if it succeeded. Drains
|
|
// the rest of the queue so the next probe cannot read this one as its own.
|
|
GLenum TakeXfbDriverError() {
|
|
const GLenum first = g_GLESFuncs.glGetError();
|
|
if (first == GL_NO_ERROR) return GL_NO_ERROR;
|
|
for (Int i = 0; i < kMaxDrainedXfbErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
return first;
|
|
}
|
|
|
|
Bool AreTransformFeedbackObjectsSupported() {
|
|
return g_GLESFuncs.glGenTransformFeedbacks != nullptr &&
|
|
g_GLESFuncs.glBindTransformFeedback != nullptr &&
|
|
g_GLESFuncs.glDeleteTransformFeedbacks != nullptr &&
|
|
g_GLESFuncs.glPauseTransformFeedback != nullptr &&
|
|
g_GLESFuncs.glResumeTransformFeedback != nullptr;
|
|
}
|
|
|
|
// Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote
|
|
// the capture buffers behind the frontend's back, so the shadows that back
|
|
// MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage
|
|
// the backend already owns (coherent persistent map) need nothing: reads resolve
|
|
// against that storage directly.
|
|
void ReadbackCapturedRanges(Vector<XfbCaptureTarget>& targets) {
|
|
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) {
|
|
MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so "
|
|
"captured data can never reach the application's buffers");
|
|
}
|
|
if (targets.empty()) {
|
|
// The span closed with nothing to mirror back. Either the deferred Begin
|
|
// never ran (a span with no draw - legal) or it ran and found no bound
|
|
// capture buffer, which is not.
|
|
MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets");
|
|
}
|
|
if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) {
|
|
for (const auto& target : targets) {
|
|
if (!target.buffer || target.buffer->IsBackendPersistentMapped()) continue;
|
|
const SizeT size = target.end - target.start;
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
|
|
void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget,
|
|
static_cast<GLintptr>(target.start),
|
|
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
|
|
if (mapped == nullptr) {
|
|
// Silent before: the capture landed in the ES buffer and the
|
|
// application's next glMapBuffer read the untouched shadow, which
|
|
// is indistinguishable from "the draw wrote nothing".
|
|
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for "
|
|
"capture readback (ES error %s); the captured data will NOT be visible to "
|
|
"the application",
|
|
target.backendId, target.start, target.end,
|
|
MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str());
|
|
continue;
|
|
}
|
|
target.buffer->WritebackFromBackend({mapped, size}, target.start);
|
|
// WritebackFromBackend bumps the frontend change serial with no
|
|
// backend op, leaving the buffer draw-dirty behind the epoch's
|
|
// back; re-open the draw-clean memos.
|
|
BufferImpl::BumpBufferMutationEpoch();
|
|
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
|
|
}
|
|
}
|
|
targets.clear();
|
|
}
|
|
|
|
// Binds a scratch buffer, sized for `capacityVertices` gap-free records, to
|
|
// capture point 0 in place of the application's buffers. Returns false when the
|
|
// scratch storage cannot be provided, in which case the caller falls back to the
|
|
// direct binding (which produces a wrong layout, but is what happened before).
|
|
Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) {
|
|
if (packedStride == 0 || capacityVertices == 0) return false;
|
|
if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false;
|
|
const SizeT required = packedStride * capacityVertices;
|
|
if (g_scatterBufferId == 0) {
|
|
g_GLESFuncs.glGenBuffers(1, &g_scatterBufferId);
|
|
if (g_scatterBufferId == 0) return false;
|
|
g_scatterBufferSize = 0;
|
|
}
|
|
if (g_scatterBufferSize < required) {
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
|
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(required), nullptr,
|
|
GL_DYNAMIC_COPY);
|
|
g_scatterBufferSize = required;
|
|
}
|
|
// Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents
|
|
// entries are consumed at link time and never reach the driver, so the ES
|
|
// program is declared INTERLEAVED over a single buffer and point 0 is the only
|
|
// point it can write (GL 4.6 core 13.2.2).
|
|
//
|
|
// The other points are therefore left exactly as they are. Clearing them - which
|
|
// this used to do, across the application's whole touched high-water mark - is
|
|
// both unnecessary (the ES program cannot write an unused point) and the precise
|
|
// trigger for the Mali G1-Ultra capture loss: see
|
|
// SyncTransformFeedbackBindingPoints for the mechanism and the device evidence.
|
|
// KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that
|
|
// reaches this path.
|
|
BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0,
|
|
static_cast<GLsizeiptr>(required));
|
|
return true;
|
|
}
|
|
|
|
// Distributes the gap-free records the driver captured into the application's
|
|
// buffers at the offsets the GL layout asks for. Only the bytes a varying actually
|
|
// occupies are written, so the holes gl_SkipComponents asks for keep whatever the
|
|
// application had put there - which is the whole point of the feature.
|
|
void ScatterCapturedRecords(XfbObjectState& xfb) {
|
|
const auto& program = xfb.scatterProgram;
|
|
if (!program || xfb.targets.empty()) return;
|
|
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return;
|
|
|
|
const SizeT packedStride = program->GetTransformFeedbackPackedStride();
|
|
const SizeT modelledVertices =
|
|
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());
|
|
const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
|
|
if (packedStride == 0 || vertices == 0) {
|
|
// The scatter path redirected the DRIVER's capture into the scratch buffer,
|
|
// so bailing here leaves the application's buffers holding their pre-draw
|
|
// bytes - a total data loss, not a no-op. The vertex count is the CPU model
|
|
// (AccountTransformFeedbackPrimitives), which is 0 for any draw mode
|
|
// CountPrimitivesForDraw does not know and for the instanced/indirect entry
|
|
// points that never call it.
|
|
MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, "
|
|
"CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep "
|
|
"their pre-draw contents.",
|
|
packedStride, modelledVertices, xfb.scatterCapacityVertices);
|
|
return;
|
|
}
|
|
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
|
|
const void* packed = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget, 0,
|
|
static_cast<GLsizeiptr>(packedStride * vertices),
|
|
GL_MAP_READ_BIT);
|
|
if (packed == nullptr) {
|
|
MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer");
|
|
return;
|
|
}
|
|
|
|
// One staged copy per destination buffer: start from what the application had
|
|
// (the shadow is authoritative - uploads go shadow -> ES, and previous captures
|
|
// were mirrored back into it), patch the captured varyings in, then push the
|
|
// whole range down once.
|
|
for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) {
|
|
const auto& target = xfb.targets[targetIndex];
|
|
if (!target.buffer) continue;
|
|
// By BUFFER index, not by position in the compacted list - see XfbCaptureTarget.
|
|
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
|
|
if (stride == 0) continue;
|
|
const SizeT rangeBytes = target.end - target.start;
|
|
Vector<Uint8> staged(rangeBytes);
|
|
Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes);
|
|
|
|
for (const auto& varying : program->GetTransformFeedbackVaryings()) {
|
|
if (varying.bufferIndex != target.bufferIndex) continue;
|
|
for (SizeT v = 0; v < vertices; ++v) {
|
|
const SizeT dstOffset = v * stride + varying.offsetBytes;
|
|
if (dstOffset + varying.byteSize > rangeBytes) break;
|
|
Memcpy(staged.data() + dstOffset,
|
|
static_cast<const Uint8*>(packed) + v * packedStride + varying.packedOffsetBytes,
|
|
varying.byteSize);
|
|
}
|
|
}
|
|
|
|
target.buffer->WritebackFromBackend({staged.data(), rangeBytes}, target.start);
|
|
// Serial bumped with no backend op (see ReadbackCapturedRanges).
|
|
BufferImpl::BumpBufferMutationEpoch();
|
|
if (g_GLESFuncs.glBufferSubData != nullptr) {
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
|
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget,
|
|
static_cast<GLintptr>(target.start),
|
|
static_cast<GLsizeiptr>(rangeBytes), staged.data());
|
|
}
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
|
|
}
|
|
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
|
|
xfb.targets.clear();
|
|
}
|
|
} // namespace
|
|
|
|
Bool AreTransformFeedbacksSupported() {
|
|
return g_GLESFuncs.glBeginTransformFeedback != nullptr &&
|
|
g_GLESFuncs.glEndTransformFeedback != nullptr &&
|
|
g_GLESFuncs.glTransformFeedbackVaryings != nullptr;
|
|
}
|
|
|
|
Bool IsCaptureSpanOpen() {
|
|
const auto& xfb = CurrentXfb();
|
|
return (xfb.pending || xfb.started) && !xfb.paused;
|
|
}
|
|
|
|
void BeginTransformFeedback(GLenum primitiveMode) {
|
|
if (!AreTransformFeedbacksSupported()) return;
|
|
auto& xfb = CurrentXfb();
|
|
xfb.primitiveMode = primitiveMode;
|
|
xfb.pending = true;
|
|
xfb.started = false;
|
|
xfb.paused = false;
|
|
xfb.targets.clear();
|
|
}
|
|
|
|
// Tail of PrepareForDraw: the program is bound and every buffer the draw needs
|
|
// is up to date, so the capture buffers can be bound and the span opened.
|
|
void StartPendingTransformFeedback() {
|
|
auto& xfb = CurrentXfb();
|
|
// A span that was paused before its first draw must not open here: the draw is
|
|
// not captured, and opening the span would also subject it to the capture
|
|
// primitive-mode rule the paused draw is exempt from.
|
|
if (!xfb.pending || xfb.paused) return;
|
|
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
|
if (!program) {
|
|
// The pending flag is deliberately NOT consumed here. It used to be cleared
|
|
// before this check, so a single draw that could not see the capture program
|
|
// retired the span permanently: every later draw of the same span found
|
|
// pending==false, the driver Begin never happened, and End found started==false
|
|
// and skipped the readback - a whole capture lost with no GL error anywhere.
|
|
// The frontend only reaches a draw with an active span after glBeginTransformFeedback
|
|
// stored a program, so this is a "cannot happen" that must stay recoverable.
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the "
|
|
"driver span stays closed and this draw is not captured");
|
|
return;
|
|
}
|
|
xfb.pending = false;
|
|
|
|
// Snapshot what the driver is about to capture into. GL forbids rebinding the
|
|
// capture buffers while the span is open, so this stays valid until End, and
|
|
// recording it here keeps End independent of the frontend capture state.
|
|
const SizeT bufferCount = program->GetTransformFeedbackBufferCount();
|
|
for (SizeT i = 0; i < bufferCount; ++i) {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
|
static_cast<Uint>(i));
|
|
const auto& bufferObject = point.GetBoundObject();
|
|
if (!bufferObject) continue;
|
|
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
|
|
if (!backendResource || backendResource->id == 0) continue;
|
|
const Range1D range = point.GetRange();
|
|
const SizeT start = std::min(range.start, bufferObject->GetSize());
|
|
const SizeT end = std::min(range.end, bufferObject->GetSize());
|
|
if (end <= start) continue;
|
|
xfb.targets.push_back({bufferObject, backendResource->id, start, end, i});
|
|
}
|
|
|
|
BufferImpl::SyncTransformFeedbackBindingPoints(bufferCount);
|
|
|
|
// A layout with holes or several interleaved buffers is not expressible on ES:
|
|
// capture gap-free into scratch storage and place the records at End instead.
|
|
xfb.scattered = false;
|
|
xfb.scatterProgram.reset();
|
|
xfb.scatterCapacityVertices = 0;
|
|
if (program->NeedsScatteredTransformFeedbackCapture()) {
|
|
SizeT capacityVertices = ~SizeT(0);
|
|
for (const auto& target : xfb.targets) {
|
|
// By BUFFER index. Reading the stride at the target's POSITION made a
|
|
// capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and
|
|
// nothing bound, so target 0 describes buffer 1 - read stride 0, skip every
|
|
// target, and leave the capacity at zero.
|
|
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
|
|
if (stride == 0) continue;
|
|
capacityVertices = std::min<SizeT>(capacityVertices, (target.end - target.start) / stride);
|
|
}
|
|
if (capacityVertices == ~SizeT(0)) capacityVertices = 0;
|
|
if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) {
|
|
xfb.scattered = true;
|
|
xfb.scatterProgram = program;
|
|
xfb.scatterCapacityVertices = capacityVertices;
|
|
} else {
|
|
// NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a
|
|
// scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer /
|
|
// gl_SkipComponents entries are consumed at link time and never reach the
|
|
// driver), so it writes capture point 0 and nothing else. Point 0 here is
|
|
// either unbound or - the dangerous case - still holds whatever an earlier
|
|
// capture in this process bound there, because the frontend's own
|
|
// glBindBufferBase is state-only and nothing else in the backend touches the
|
|
// indexed points. Opening the span would then have the driver capture over an
|
|
// application buffer that has nothing to do with this draw, and the frontend
|
|
// shadow would never learn of it.
|
|
//
|
|
// Leaving the span closed reproduces exactly what the old high-water clear
|
|
// loop achieved by binding 0 here and letting the driver refuse the Begin -
|
|
// the capture records nothing - without issuing a capture-point bind the
|
|
// application did not ask for, which is the thing that loses captures whole
|
|
// on Mali (see SyncTransformFeedbackBindingPoints).
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture "
|
|
"(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the "
|
|
"capture cannot land in a stale binding. Nothing will be captured.",
|
|
program->GetTransformFeedbackPackedStride(), capacityVertices);
|
|
xfb.targets.clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// A capture program with buffers bound must have produced at least one target;
|
|
// an empty list means End has nothing to mirror back and the application will
|
|
// read its buffer's pre-draw bytes however well the GPU captured.
|
|
if (xfb.targets.empty()) {
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets "
|
|
"(program declares %zu capture buffer(s), none of them resolved to a bound backend "
|
|
"buffer with a non-empty range); nothing will be read back",
|
|
bufferCount);
|
|
}
|
|
|
|
g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode);
|
|
// Unchecked before. Every ES error condition here (already active, a current
|
|
// program with no capture set, a capture point the program uses with no buffer)
|
|
// ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
|
// reads 0 and the application sees no error at all - MobileGL's own error state is
|
|
// separate from the driver's, so a driver rejection here is invisible to it.
|
|
if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) {
|
|
// The mode is printed as a number as well as a name: GL_POINTS is 0, which the
|
|
// enum converter spells "GL_FALSE", and a reader chasing a lost capture should
|
|
// not have to know that.
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED "
|
|
"glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend "
|
|
"program %u, %zu capture buffer(s), %zu target(s), mode=%s.",
|
|
MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(),
|
|
static_cast<unsigned>(xfb.primitiveMode),
|
|
MG_Util::ConvertGLEnumToString(beginError).c_str(),
|
|
PrgramImpl::g_lastUsedBackendProgramId, bufferCount,
|
|
xfb.targets.size(),
|
|
MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str());
|
|
}
|
|
xfb.started = true;
|
|
}
|
|
|
|
void EndTransformFeedback() {
|
|
auto& xfb = CurrentXfb();
|
|
const Bool wasPending = xfb.pending;
|
|
xfb.pending = false;
|
|
xfb.paused = false;
|
|
if (!xfb.started) {
|
|
// A span that never drew is legal and captures nothing by definition; one that
|
|
// is STILL pending here drew nothing the backend saw, which for a span the
|
|
// application expected data from is the whole bug in one line.
|
|
MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)",
|
|
wasPending ? 1 : 0);
|
|
return;
|
|
}
|
|
xfb.started = false;
|
|
g_GLESFuncs.glEndTransformFeedback();
|
|
if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) {
|
|
MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the "
|
|
"driver's capture state and MobileGL's have diverged",
|
|
MG_Util::ConvertGLEnumToString(endError).c_str());
|
|
}
|
|
if (xfb.scattered) {
|
|
ScatterCapturedRecords(xfb);
|
|
xfb.scattered = false;
|
|
xfb.scatterProgram.reset();
|
|
} else {
|
|
ReadbackCapturedRanges(xfb.targets);
|
|
}
|
|
}
|
|
|
|
void PauseTransformFeedback() {
|
|
auto& xfb = CurrentXfb();
|
|
xfb.paused = true;
|
|
// A span the driver never opened (paused before the first draw) has nothing to
|
|
// pause; the flag above is what holds the deferred Begin back until the resume.
|
|
if (!xfb.started || g_GLESFuncs.glPauseTransformFeedback == nullptr) return;
|
|
g_GLESFuncs.glPauseTransformFeedback();
|
|
}
|
|
|
|
void ResumeTransformFeedback() {
|
|
auto& xfb = CurrentXfb();
|
|
xfb.paused = false;
|
|
if (!xfb.started || g_GLESFuncs.glResumeTransformFeedback == nullptr) return;
|
|
g_GLESFuncs.glResumeTransformFeedback();
|
|
}
|
|
|
|
void BindTransformFeedback(GLuint name) {
|
|
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
|
|
// The capture buffer bindings are the OBJECT's, not the context's: the bind below
|
|
// swaps all of them for whatever the target object holds, which the redundant-bind
|
|
// shadow has never seen.
|
|
BufferImpl::InvalidateTransformFeedbackBindingShadows();
|
|
if (!AreTransformFeedbackObjectsSupported()) {
|
|
// Without driver objects there is only the default span; keep the frontend
|
|
// name so the bookkeeping below stays consistent.
|
|
g_currentXfbName = name;
|
|
return;
|
|
}
|
|
auto& xfb = g_xfbObjects[name];
|
|
if (name != 0 && xfb.esId == 0) {
|
|
g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId);
|
|
}
|
|
g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId);
|
|
g_currentXfbName = name;
|
|
}
|
|
|
|
void DeleteTransformFeedback(GLuint name) {
|
|
const auto it = g_xfbObjects.find(name);
|
|
if (it == g_xfbObjects.end()) return;
|
|
if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) {
|
|
g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId);
|
|
}
|
|
g_currentXfbState = nullptr; // erase shifts the probe cluster, moving other entries
|
|
g_xfbObjects.erase(it);
|
|
// The frontend reverts to the default object when the bound one is deleted.
|
|
if (g_currentXfbName == name) {
|
|
BindTransformFeedback(0);
|
|
}
|
|
}
|
|
|
|
// The ES context went away (or is being torn down): the spans, their buffer ids, the
|
|
// driver objects and the frontend objects they pinned all belonged to it.
|
|
void OnBackendContextDestroyed() {
|
|
g_currentXfbState = nullptr;
|
|
g_xfbObjects.clear();
|
|
g_currentXfbName = 0;
|
|
g_scatterBufferId = 0;
|
|
g_scatterBufferSize = 0;
|
|
BufferImpl::InvalidateTransformFeedbackBindingShadows();
|
|
}
|
|
} // namespace XfbImpl
|
|
|
|
namespace VertexArrayImpl {
|
|
// Resolve-or-create the VAO's backend twin, once per draw: PrepareForDraw passes
|
|
// the result to the buffer sync (resolved-buffers memo host), the VAO sync and
|
|
// the draw-time bind, which each used to run their own registry Find. The raw
|
|
// pointer stays valid for the whole draw: the frontend VAO is pinned by the
|
|
// context binding, and a live object's registry entry is never erased nor its
|
|
// twin replaced (see TwinLookupMemo's contract).
|
|
BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (auto* twin = g_vaoTwinLookupMemo.Lookup(vao)) {
|
|
return twin;
|
|
}
|
|
auto* backendVAOSlot = g_backendVertexArrayObjects.Find(vao.get());
|
|
auto& backendObj = backendVAOSlot ? *backendVAOSlot : g_backendVertexArrayObjects.GetOrCreate(vao);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<BackendVertexArrayObject>();
|
|
}
|
|
g_vaoTwinLookupMemo.Store(vao, backendObj.get());
|
|
return backendObj.get();
|
|
}
|
|
|
|
void SyncCurrentVAO(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
|
BackendVertexArrayObject* vaoTwin) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
|
|
|
|
if (!currentVAOObject || !vaoTwin) {
|
|
MGLOG_E_ONCE("No VAO is currently bound, cannot sync current VAO.");
|
|
return;
|
|
}
|
|
|
|
vaoTwin->SyncToBackend(currentVAOObject);
|
|
}
|
|
|
|
// GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current
|
|
// value* (context state set by glVertexAttrib*, default (0,0,0,1)) rather than any buffer.
|
|
// MobileGL stores those values in MG_State only, so without this step the ES driver would feed
|
|
// the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1).
|
|
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
|
|
// current value is what the shader will actually read.
|
|
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin,
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (!program) return;
|
|
|
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
|
if (!vao || !vaoTwin) return;
|
|
|
|
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
|
if (activeAttribMask == 0) return;
|
|
|
|
// Which of the program's attributes lack an enabled array cannot change without
|
|
// the VAO's config version moving (Enable/DisableAttribute bump it) or the
|
|
// program's active mask changing, so the per-draw Enabled probes reduce to two
|
|
// compares. The memo lives on the twin (1:1 with the VAO, no identity key
|
|
// needed): a function-static single entry missed on every draw once the app
|
|
// cycled section VAOs, re-reading the cold attribute slots each time; here a
|
|
// cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations.
|
|
auto& memo = vaoTwin->GetPendingAttribValueMaskMemo();
|
|
const Uint32 configVersion = vao->GetConfigVersion();
|
|
if (!memo.valid || configVersion != memo.configVersion || activeAttribMask != memo.activeMask) {
|
|
Uint32 pending = 0;
|
|
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
if (!vao->GetAttribute(location).Enabled) pending |= (1u << location);
|
|
}
|
|
memo.configVersion = configVersion;
|
|
memo.activeMask = activeAttribMask;
|
|
memo.pendingMask = pending;
|
|
memo.valid = true;
|
|
}
|
|
if (memo.pendingMask == 0) return;
|
|
|
|
for (Uint32 remaining = memo.pendingMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
|
|
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
|
|
const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location));
|
|
switch (typeInfo.baseType) {
|
|
case MG_State::GLState::VertexAttribBaseType::Float:
|
|
g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Int:
|
|
g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Uint:
|
|
g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Unsupported:
|
|
MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
|
|
"shader input type 0x%x is not supported as a current generic vertex attribute",
|
|
program->GetExternalIndex(), location, program->GetAttribType(location));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} // namespace VertexArrayImpl
|
|
|
|
namespace TextureImpl {
|
|
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
|
Bool imageBindableStorageRequired) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get());
|
|
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
|
|
: g_backendTextureObjects.GetOrCreate(textureObject);
|
|
if (!backendSlot) {
|
|
backendSlot = MakeShared<BackendTextureObject>();
|
|
}
|
|
|
|
// A by-VALUE copy of the twin for the duration of the syncs below. `backendSlot` is a
|
|
// reference INTO the open-addressed registry, and syncing can RE-ENTER this function:
|
|
// a texture created by glTextureView has to sync the texture whose storage it views
|
|
// first (SyncTextureViewToBackend), and that nested call may insert, grow the map and
|
|
// relocate every entry - leaving the reference dangling. Holding the object itself
|
|
// keeps the calls below working on the right twin regardless; the slot is re-resolved
|
|
// at the end for the reference this function returns.
|
|
const SharedPtr<BackendTextureObject> backendObj = backendSlot;
|
|
|
|
if (imageBindableStorageRequired) {
|
|
backendObj->RequireImageBindableStorage(textureObject);
|
|
}
|
|
backendObj->SyncTextureParamsToBackend(textureObject);
|
|
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
|
backendObj->SyncMipmapsToBackend(textureObject);
|
|
// The storage sync may RE-MINT the driver texture - a fresh glTexStorage after a
|
|
// shape change, an image-bindable widening, or the glTextureView that an
|
|
// ARB_texture_view view is created on - which discards every parameter the two calls
|
|
// above just pushed. Re-push them here rather than leaving it to the next sync: the
|
|
// very next thing that happens is usually the draw this sync was run for, and until
|
|
// the filters land the new texture is at the ES defaults, which for a single-level or
|
|
// integer texture is not merely mis-filtered but INCOMPLETE, i.e. it samples zero.
|
|
if (backendObj->NeedsParameterResync()) {
|
|
backendObj->SyncTextureParamsToBackend(textureObject);
|
|
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
|
}
|
|
|
|
auto* refreshedSlot = g_backendTextureObjects.Find(textureObject.get());
|
|
auto& refreshedBackendObj = refreshedSlot ? *refreshedSlot
|
|
: g_backendTextureObjects.GetOrCreate(textureObject);
|
|
if (!refreshedBackendObj) {
|
|
// A collection ran during the nested sync and took this slot with it; put the
|
|
// twin the caller is about to use back, rather than handing back an empty one.
|
|
refreshedBackendObj = backendObj;
|
|
}
|
|
return refreshedBackendObj;
|
|
}
|
|
|
|
// Identity snapshot of what one texture unit has bound: the object in every binding
|
|
// slot plus the unit's sampler object. This - not the context's texture bind
|
|
// generation - is what the per-draw texture memos key on: the generation also bumps
|
|
// on REDUNDANT re-binds (glBindSampler of the sampler the unit already carries, which
|
|
// 26.2 issues around every texture-unit switch), so a generation-keyed memo re-derives
|
|
// everything on draws that changed nothing. The snapshot compares the bindings
|
|
// themselves, so only a real change can invalidate it. What it deliberately does NOT
|
|
// cover, and its users must key on separately:
|
|
// * membership/completeness flips with no binding moving - a default texture's image
|
|
// appearing or vanishing, any texture shape or sampler parameter change - all bump
|
|
// the sampling-resolution generation (BumpShapeVersion / SamplerObject::BumpVersion
|
|
// are their only writers and bump it unconditionally);
|
|
// * everything unit bindings say nothing about: the touched-unit high-water mark,
|
|
// the frontend context identity, the backend ES context generation, and (for the
|
|
// resolution memo) the program keys that arbitrate aliased targets.
|
|
struct UnitBindingsSnapshot {
|
|
Array<WeakPtr<MG_State::GLState::ITextureObject>, (SizeT)TextureTarget::TextureTargetCount>
|
|
slotObjects{};
|
|
WeakPtr<MG_State::GLState::SamplerObject> samplerObject{};
|
|
};
|
|
|
|
static void CaptureUnitBindings(Int maxTouchedUnit, Vector<UnitBindingsSnapshot>& out) {
|
|
out.resize(static_cast<SizeT>(maxTouchedUnit + 1));
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
auto& snapshot = out[static_cast<SizeT>(unit)];
|
|
const auto& slots = textureUnit.GetAllBindingSlots();
|
|
for (SizeT i = 0; i < slots.size(); ++i) {
|
|
snapshot.slotObjects[i] = slots[i].GetBoundObject();
|
|
}
|
|
snapshot.samplerObject = textureUnit.GetSamplerObject();
|
|
}
|
|
}
|
|
|
|
static Bool UnitBindingsUnchanged(Int maxTouchedUnit, const Vector<UnitBindingsSnapshot>& snapshots) {
|
|
if (snapshots.size() != static_cast<SizeT>(maxTouchedUnit + 1)) return false;
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
const auto& snapshot = snapshots[static_cast<SizeT>(unit)];
|
|
const auto& slots = textureUnit.GetAllBindingSlots();
|
|
for (SizeT i = 0; i < slots.size(); ++i) {
|
|
if (!OwnerEquals(snapshot.slotObjects[i], slots[i].GetBoundObject())) return false;
|
|
}
|
|
if (!OwnerEquals(snapshot.samplerObject, textureUnit.GetSamplerObject())) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static Vector<UnitBindingsSnapshot> g_observedUnitBindings;
|
|
static Uint64 g_observedUnitBindingsContextId = 0;
|
|
static Uint64 g_observedUnitBindingsGeneration = 0;
|
|
static Int g_observedUnitBindingsMaxUnit = -1;
|
|
static Uint64 g_unitBindingsEpoch = 0;
|
|
|
|
// Epoch of the touched units' bindings: moves exactly when WHAT is bound changes,
|
|
// never on a redundant re-bind. Memos snapshot the returned value instead of holding
|
|
// their own UnitBindingsSnapshot, so the owner-compare walk runs at most once per
|
|
// draw no matter how many memos key on it. The observation triple gates the walk:
|
|
// while (context id, bind generation, high-water mark) are unchanged nothing can
|
|
// have moved, because every path that changes a binding bumps the generation. A
|
|
// given epoch value names one observed (context, high-water mark, bindings) state -
|
|
// a context switch, a high-water-mark move or a bindings change each recapture and
|
|
// bump - so epoch equality alone proves the bindings a consumer resolved against
|
|
// are the bindings on the units now.
|
|
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
|
|
const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();
|
|
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
|
if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit &&
|
|
g_observedUnitBindingsGeneration == bindGeneration) {
|
|
return g_unitBindingsEpoch;
|
|
}
|
|
if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit ||
|
|
!UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) {
|
|
CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings);
|
|
++g_unitBindingsEpoch;
|
|
}
|
|
g_observedUnitBindingsContextId = contextId;
|
|
g_observedUnitBindingsMaxUnit = maxTouchedUnit;
|
|
g_observedUnitBindingsGeneration = bindGeneration;
|
|
return g_unitBindingsEpoch;
|
|
}
|
|
|
|
// Work list behind SyncNeccessaryTextures' per-draw unit walk. WHICH textures the
|
|
// touched units hold is a pure function of the unit bindings, so the GLContext identity
|
|
// (a never-reused id, not the heap address a recreated context can land on again), the
|
|
// unit-bindings snapshot and the touched-unit high-water mark are a complete key -
|
|
// WHAT each entry then has to do is still decided per draw by the version compares
|
|
// inside the sync calls, which is why texture content, shape and parameter changes need
|
|
// no key here.
|
|
//
|
|
// Entries borrow, they never own. `slot` points at the binding slot's shared_ptr, whose
|
|
// address is fixed for the context's lifetime (TextureState holds the unit array by
|
|
// value) and whose VALUE cannot change without bumping the bind generation. `backend` is
|
|
// the registry's object for the texture in that slot; the registry only erases an entry
|
|
// once the frontend texture has expired, and a frontend texture cannot expire while a
|
|
// slot the key covers still holds a reference to it. Holding either side by shared_ptr
|
|
// instead would keep dead frontend textures alive and defeat the registry's
|
|
// weak-reference GC.
|
|
//
|
|
// `texture` records WHICH frontend object `backend` was paired with when the entry was
|
|
// built, and PairingsIntact re-checks it before any replay. The keys above are the
|
|
// primary guard, but they are all derived state: a slot swap that never reaches the
|
|
// bind generation (the DSA by-name emulation used to swap a slot silently) would leave
|
|
// every key matching while the borrowed slot pointed at a different texture, and the
|
|
// replay would then drive texture A's backend twin from texture B's frontend state -
|
|
// re-specifying A's backend storage with B's shape and destroying A's contents. A raw
|
|
// pointer compare per entry is far cheaper than the walk it guards, and a stale pairing
|
|
// costs only a list rebuild, so this stays as the structural net under the keys.
|
|
struct UnitTextureSyncEntry {
|
|
const SharedPtr<MG_State::GLState::ITextureObject>* slot = nullptr;
|
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
BackendTextureObject* backend = nullptr;
|
|
};
|
|
// True while every entry's borrowed slot still holds the texture the entry was paired
|
|
// with. Callers put it LAST in the key conjunction so it only runs on a key hit.
|
|
static Bool PairingsIntact(const Vector<UnitTextureSyncEntry>& list) {
|
|
for (const auto& entry : list) {
|
|
if (entry.slot->get() != entry.texture) return false;
|
|
}
|
|
return true;
|
|
}
|
|
static Vector<UnitTextureSyncEntry> g_unitTextureSyncList;
|
|
static Bool g_unitTextureSyncListValid = false;
|
|
static Uint64 g_unitTextureSyncListContextId = 0;
|
|
static Int g_unitTextureSyncListMaxUnit = -1;
|
|
static Uint g_unitTextureSyncListContextGeneration = 0;
|
|
static Uint64 g_unitTextureSyncListEpoch = 0;
|
|
static Uint64 g_unitTextureSyncListSamplingGeneration = 0;
|
|
|
|
// Sibling memo for the draw FBO's texture attachments (see the use site in
|
|
// SyncNeccessaryTextures for the key derivation and the borrow rules, which are the
|
|
// unit list's). A null FBO pointer means "not stamped".
|
|
static Vector<UnitTextureSyncEntry> g_fboTextureSyncList;
|
|
static MG_State::GLState::FramebufferObject* g_fboTextureSyncListFbo = nullptr;
|
|
static Uint16 g_fboTextureSyncListSlotVersion = 0;
|
|
static Uint16 g_fboTextureSyncListObjectVersion = 0;
|
|
static Uint64 g_fboTextureSyncListContextId = 0;
|
|
static Uint g_fboTextureSyncListContextGeneration = 0;
|
|
|
|
// The frontend texture-state keys the per-draw texture stages
|
|
// (SyncNeccessaryTextures, then BindCurrentTextures) both consume. Captured
|
|
// ONCE per draw/dispatch preparation and passed to both: nothing between the
|
|
// two stages can move any of these - only frontend GL entry points mutate
|
|
// them, and none run inside a backend preparation (the FBO/program syncs in
|
|
// between only read frontend state). The no-arg wrappers keep capturing at
|
|
// the call for every non-draw call site (Clear, readbacks).
|
|
struct DrawTextureSyncKeys {
|
|
Uint64 contextId = 0;
|
|
Uint64 samplingGeneration = 0;
|
|
Uint64 unitBindingsEpoch = 0;
|
|
Int maxTouchedUnit = -1;
|
|
};
|
|
|
|
DrawTextureSyncKeys CaptureDrawTextureSyncKeys() {
|
|
DrawTextureSyncKeys keys;
|
|
keys.contextId = MG_State::pGLContext->GetTextureContextId();
|
|
// Units past the frontend's high-water mark have provably-empty slots.
|
|
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
|
|
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
|
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
|
|
return keys;
|
|
}
|
|
|
|
void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_backendTextureObjects.CollectGarbageIfNeeded();
|
|
|
|
// All textures we need are:
|
|
// 1. textures bound to texture units (TODO: only sync ones that are used in current program)
|
|
// 2. textures used in current FBO
|
|
// 3. textures bound to image units (TODO)
|
|
|
|
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
|
const Uint64 samplingGeneration = keys.samplingGeneration;
|
|
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
|
|
// The epoch survives redundant re-binds; the sampling-resolution generation
|
|
// covers the one membership input the epoch cannot see - a default texture's
|
|
// image appearing or vanishing flips IsUndefinedDefaultTexture with no binding
|
|
// moving. Its cost is a spare rebuild whenever any texture shape or sampler
|
|
// parameter actually changes, which real frames do at load time, not per draw.
|
|
if (g_unitTextureSyncListValid &&
|
|
g_unitTextureSyncListContextId == keys.contextId &&
|
|
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
|
g_unitTextureSyncListContextGeneration == g_backendContextGeneration &&
|
|
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
|
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
|
PairingsIntact(g_unitTextureSyncList)) {
|
|
for (const auto& entry : g_unitTextureSyncList) {
|
|
// Aggregate gate == the conjunction of the three callees' own
|
|
// early-outs (see IsDrawSyncClean); skipping on true is
|
|
// behavior-identical, false falls through to the calls.
|
|
if (entry.backend->IsDrawSyncClean(entry.slot->get(), keys.contextId, samplingGeneration)) {
|
|
continue;
|
|
}
|
|
entry.backend->SyncTextureParamsToBackend(*entry.slot);
|
|
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
|
|
entry.backend->SyncMipmapsToBackend(*entry.slot);
|
|
}
|
|
} else {
|
|
g_unitTextureSyncListValid = false;
|
|
g_unitTextureSyncList.clear();
|
|
for (Int index = 0; index <= maxTouchedUnit; ++index) {
|
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);
|
|
for (const auto& bindingSlot : unit.GetAllBindingSlots()) {
|
|
auto& textureObject = bindingSlot.GetBoundObject();
|
|
// An image-less default texture (name 0) is the slot's initial / "unbound"
|
|
// state; it has nothing to sync, so skip it as cheaply as the old null slot.
|
|
if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) {
|
|
g_unitTextureSyncList.push_back({&textureObject, textureObject.get(),
|
|
SyncTextureObjectToBackend(textureObject).get()});
|
|
}
|
|
}
|
|
}
|
|
g_unitTextureSyncListContextId = keys.contextId;
|
|
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
|
g_unitTextureSyncListContextGeneration = g_backendContextGeneration;
|
|
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
|
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
|
g_unitTextureSyncListValid = true;
|
|
}
|
|
|
|
// Texture attachments of the draw FBO, memoised like the unit list above: WHICH
|
|
// textures hang off the FBO only changes with a rebind (slot version), an
|
|
// attachment/draw-buffer edit (object version - the documented invariant
|
|
// SyncCurrentFBO's memo already leans on), a different FBO landing on a recycled
|
|
// heap address (pointer + slot version together, the StampSyncedFBO trio), another
|
|
// frontend context (context id) or a rebuilt ES context (backend generation). WHAT
|
|
// each texture then needs is still decided per draw by the version gates inside the
|
|
// three sync calls. Entry lifetime mirrors the unit list: the attachment holds the
|
|
// texture's SharedPtr at a stable address while the FBO is unchanged, and the
|
|
// registry keeps a backend object alive until its frontend texture expires, which an
|
|
// attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame -
|
|
// reduces to the key compare and an empty loop.
|
|
const auto& drawSlot = GetFramebufferBindingSlotFast(FramebufferTarget::Draw);
|
|
const auto& currentFBO = drawSlot.GetBoundObject();
|
|
if (currentFBO) {
|
|
const Uint16 fboSlotVersion = drawSlot.GetVersion();
|
|
const Uint16 fboObjectVersion = currentFBO->GetObjectVersion();
|
|
const Bool fboListValid =
|
|
g_fboTextureSyncListFbo == currentFBO.get() &&
|
|
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
|
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
|
g_fboTextureSyncListContextId == keys.contextId &&
|
|
g_fboTextureSyncListContextGeneration == g_backendContextGeneration &&
|
|
PairingsIntact(g_fboTextureSyncList);
|
|
if (fboListValid) {
|
|
for (const auto& entry : g_fboTextureSyncList) {
|
|
// Same aggregate gate as the unit list above.
|
|
if (entry.backend->IsDrawSyncClean(entry.slot->get(), keys.contextId,
|
|
samplingGeneration)) {
|
|
continue;
|
|
}
|
|
entry.backend->SyncTextureParamsToBackend(*entry.slot);
|
|
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
|
|
entry.backend->SyncMipmapsToBackend(*entry.slot);
|
|
}
|
|
} else {
|
|
g_fboTextureSyncListFbo = nullptr;
|
|
g_fboTextureSyncList.clear();
|
|
for (const auto& attachment : currentFBO->GetAllAttachmentObjects()) {
|
|
if (!attachment.IsTexture()) continue;
|
|
auto& textureObject = attachment.GetTexture();
|
|
if (textureObject) {
|
|
g_fboTextureSyncList.push_back({&textureObject, textureObject.get(),
|
|
SyncTextureObjectToBackend(textureObject).get()});
|
|
}
|
|
}
|
|
g_fboTextureSyncListFbo = currentFBO.get();
|
|
g_fboTextureSyncListSlotVersion = fboSlotVersion;
|
|
g_fboTextureSyncListObjectVersion = fboObjectVersion;
|
|
g_fboTextureSyncListContextId = keys.contextId;
|
|
g_fboTextureSyncListContextGeneration = g_backendContextGeneration;
|
|
}
|
|
} else {
|
|
g_fboTextureSyncListFbo = nullptr;
|
|
g_fboTextureSyncList.clear();
|
|
}
|
|
}
|
|
|
|
void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); }
|
|
|
|
// Whether glBindImageTexture's `layered` means anything for this target - asked of the
|
|
// target the DRIVER will see, not the one the application named. A GL_TEXTURE_1D_ARRAY
|
|
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
|
|
// the state target instead answered "no" for it and pinned every 1D-array image binding
|
|
// to layer 0, whatever the application passed.
|
|
//
|
|
// `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for
|
|
// word) makes them one rule: "If the texture identified by texture does not have
|
|
// multiple layers or faces, the entire texture level is bound, regardless of the values
|
|
// of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so
|
|
// the driver must not be handed a layer index the texture has no room for. Adreno takes
|
|
// such a request literally and leaves the image unit reading zero, which is what failed
|
|
// KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the
|
|
// GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow
|
|
// is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed.
|
|
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
|
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
|
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
|
|
backendTarget == TextureTarget::Texture2DArray ||
|
|
backendTarget == TextureTarget::TextureCubeMapArray ||
|
|
backendTarget == TextureTarget::Texture2DMultisampleArray;
|
|
}
|
|
|
|
// Which image units currently hold a WRITABLE buffer texture, and how many. Kept here
|
|
// rather than recomputed per draw because the frontend tracks 192 image units and
|
|
// almost every program uses none of them: the draw path pays one integer test.
|
|
//
|
|
// Maintained by SyncImageTextureBinding, which is the single funnel for an image-unit
|
|
// change on this backend - glBindImageTextures is a frontend loop over
|
|
// glBindImageTexture, and the whole-sweep SyncImageTextureBindings goes through it too.
|
|
// Anything that clears a binding WITHOUT coming through here (a deleted texture, a
|
|
// recreated context) can only leave a bit set for a unit that no longer has one; the
|
|
// sweep re-reads the binding, finds nothing to mark, and CLEARS the bit on its way past.
|
|
// So the error is self-healing, and in the direction that costs one wasted look rather
|
|
// than one missed write.
|
|
static Array<Bool, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_writableImageBufferUnits{};
|
|
static Uint g_writableImageBufferUnitCount = 0;
|
|
|
|
static Bool IsWritableImageBufferTexture(const MG_State::GLState::ImageTextureBinding& binding) {
|
|
return binding.Texture != nullptr && binding.Access != GL_READ_ONLY &&
|
|
binding.Texture->GetStorageType() == TextureStorageType::Buffer;
|
|
}
|
|
|
|
static void TrackWritableImageBufferUnit(Uint unit, Bool writableBufferTexture) {
|
|
Bool& tracked = g_writableImageBufferUnits[unit];
|
|
if (tracked == writableBufferTexture) return;
|
|
tracked = writableBufferTexture;
|
|
// Written as two guarded steps rather than one signed add: the count is unsigned,
|
|
// and a decrement that ever ran one time too many would not saturate at zero, it
|
|
// would wrap to four billion and defeat the early-out for the rest of the process.
|
|
if (writableBufferTexture) {
|
|
++g_writableImageBufferUnitCount;
|
|
} else if (g_writableImageBufferUnitCount > 0) {
|
|
--g_writableImageBufferUnitCount;
|
|
}
|
|
}
|
|
|
|
// Highest image unit that has ever been given a texture, plus one. Maintained by the
|
|
// single funnel below, so it is a sound "no draw in this context can be reading an image"
|
|
// test: nothing reaches an image unit without going through SyncImageTextureBinding.
|
|
// Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the
|
|
// draw-path staleness check below at one integer test.
|
|
static Uint g_imageUnitHighWaterMark = 0;
|
|
|
|
void SyncImageTextureBinding(Uint unit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
|
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
|
|
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
|
|
g_imageUnitHighWaterMark = unit + 1;
|
|
}
|
|
if (!imageBinding.Texture) {
|
|
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
|
return;
|
|
}
|
|
|
|
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
|
|
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
|
|
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
|
|
const GLint layer = layerable ? imageBinding.Layer : 0;
|
|
// The bind half of the image-format widening. SyncTextureObjectToBackend has just
|
|
// allocated this texture's storage in the core carrier of its format (the call above
|
|
// is the one that marks it image-bindable), and glBindImageTexture's `format` has to
|
|
// name the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on
|
|
// Adreno for nineteen of the twenty-six non-core formats and on both Malis for
|
|
// twenty-five, and every driver that DOES accept a narrow texture through a wide
|
|
// image accepts it silently, reading and writing out of bounds. The frontend's own
|
|
// ImageTextureBinding keeps the application's format untouched, so
|
|
// GL_IMAGE_BINDING_FORMAT still answers what was passed in.
|
|
//
|
|
// Widened from the format the APPLICATION named rather than from the texture's own,
|
|
// because GL lets the two differ inside one format class and the shader was widened
|
|
// from the class the application named too (an r32ui view of an r32f image is a legal
|
|
// reinterpretation). The two carriers always have the same texel size - every format
|
|
// in a class widens to the four-channel form of that same class - so the storage
|
|
// still describes what the bind claims. Gated on the TEXTURE having been widened, so
|
|
// a bind format that names a class the storage does not have is left alone: GL
|
|
// already calls that undefined, and inventing a carrier for it would only make the
|
|
// out-of-class read wider.
|
|
//
|
|
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
|
|
// its own to widen (its texels are the application's buffer object), so
|
|
// WidenImageFormatsPass declines to widen every buffer image and the bind must decline
|
|
// with it, or the driver would be handed a carrier the shader never addressed. See the
|
|
// Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it.
|
|
//
|
|
// What a buffer image takes instead is the SPLIT, which is the same three-layer move
|
|
// through a different door: a private glTexBuffer view names the single-channel base
|
|
// format, the bind below names it too, and the shader subscripts it two components per
|
|
// original texel. Same gate on all three, so they cannot disagree.
|
|
//
|
|
// The split view is a SEPARATE texture name over the same buffer, and the bind has to
|
|
// name it rather than the application's own: the application's texture keeps the
|
|
// format it asked for so that a samplerBuffer reading the same buffer texture - which
|
|
// is NOT subscript-rewritten - still sees whole texels. See
|
|
// BackendTextureObject::m_bufferImageSplitViewId.
|
|
GLenum bindFormat = imageBinding.Format;
|
|
GLuint bindTextureId = backendTexture->GetBackendTextureId();
|
|
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
|
|
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
|
|
GL_UNKNOWN_MGL) {
|
|
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
|
|
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
|
boundFormatSplit != GL_UNKNOWN_MGL) {
|
|
bindFormat = boundFormatSplit;
|
|
if (const Uint splitViewId = backendTexture->GetBufferImageSplitViewId();
|
|
splitViewId != 0) {
|
|
bindTextureId = splitViewId;
|
|
}
|
|
}
|
|
}
|
|
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
|
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
|
|
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
|
if (boundFormatWidening) {
|
|
bindFormat = boundFormatWidening.InternalFormat;
|
|
}
|
|
}
|
|
g_GLESFuncs.glBindImageTexture(unit, bindTextureId, imageBinding.Level,
|
|
layered, layer, imageBinding.Access, bindFormat);
|
|
}
|
|
|
|
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
|
// write, and those writes land in the ES driver's buffer object - behind the frontend's
|
|
// CPU shadow, which is what MapBuffer and GetBufferSubData read. Same flag, and for the
|
|
// same reason, as MarkShaderStorageBuffersGpuWritten does for a storage block; the
|
|
// difference is only which binding the shader reaches the buffer through. A GL_READ_ONLY
|
|
// binding is left alone: marking it would make the next map wait on - and then re-read -
|
|
// a dispatch that could not have changed a byte of it.
|
|
//
|
|
// Called from the draw and dispatch preparations rather than from the eager sync
|
|
// glBindImageTexture performs: that one runs before any shader has touched the buffer,
|
|
// and flagging there would pull the driver's copy over a shadow the application may
|
|
// still be writing into.
|
|
void MarkWritableImageBufferTexturesGpuWritten() {
|
|
if (g_writableImageBufferUnitCount == 0) return;
|
|
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
|
|
if (!g_writableImageBufferUnits[unit]) continue;
|
|
const auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
|
if (!IsWritableImageBufferTexture(imageBinding)) {
|
|
TrackWritableImageBufferUnit(unit, false);
|
|
continue;
|
|
}
|
|
auto* textureBuffer =
|
|
static_cast<MG_State::GLState::TextureObjectBuffer*>(imageBinding.Texture.get());
|
|
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
|
if (bufferObject) bufferObject->MarkGpuWritten();
|
|
}
|
|
}
|
|
|
|
void SyncImageTextureBindings() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// The frontend tracks more image units than ES exposes; binding past the device
|
|
// limit raises GL_INVALID_VALUE on every dispatch.
|
|
const Uint unitCount = std::min<Uint>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS,
|
|
static_cast<Uint>(std::max(g_GLESCapabilities.MaxImageUnits, 0)));
|
|
for (Uint unit = 0; unit < unitCount; ++unit) {
|
|
SyncImageTextureBinding(unit);
|
|
}
|
|
}
|
|
|
|
// What the draw path last swept the image units against. A draw never swept them at all:
|
|
// an image unit was established once, eagerly, by glBindImageTexture and never revisited.
|
|
// That is stale the moment the texture behind it is re-specified with a new size or
|
|
// format, because ES 3.1 only allows IMMUTABLE storage on an image unit
|
|
// (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be
|
|
// redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at
|
|
// the deleted one and imageSize() reporting the old dimensions
|
|
// (KHR-GL43.shader_image_size.advanced-changeSize).
|
|
static Uint64 g_imageSweepContextId = 0;
|
|
static Uint64 g_imageSweepSamplingGeneration = 0;
|
|
static Uint g_imageSweepBackendContextGeneration = 0;
|
|
static Bool g_imageSweepValid = false;
|
|
|
|
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
|
|
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
|
|
// moves on exactly the shape and format changes that can force the re-mint. Deliberately
|
|
// NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be
|
|
// the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside
|
|
// this very sweep, so a backend-side trigger would be bumped after the gate had already
|
|
// declined to run it.
|
|
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
|
|
if (g_imageUnitHighWaterMark == 0) return;
|
|
if (g_imageSweepValid && g_imageSweepContextId == keys.contextId &&
|
|
g_imageSweepSamplingGeneration == keys.samplingGeneration &&
|
|
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
|
|
return;
|
|
}
|
|
SyncImageTextureBindings();
|
|
g_imageSweepContextId = keys.contextId;
|
|
g_imageSweepSamplingGeneration = keys.samplingGeneration;
|
|
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
|
|
g_imageSweepValid = true;
|
|
}
|
|
} // namespace TextureImpl
|
|
|
|
namespace FramebufferImpl {
|
|
// Record that `target` now reflects this exact (binding, object, revision) triple.
|
|
// Every one of the three has to move together: stamping a subset leaves an early-out
|
|
// that either never fires or fires on state it never actually pushed.
|
|
static void StampSyncedFBO(FramebufferTarget target, Uint16 slotVersion, Uint16 objectVersion,
|
|
MG_State::GLState::FramebufferObject* fbo) {
|
|
g_fboSyncedSlotVersions[SizeT(target)] = slotVersion;
|
|
g_fboSyncedObjectVersions[SizeT(target)] = objectVersion;
|
|
g_fboSyncedObjects[SizeT(target)] = fbo;
|
|
g_fboSyncedBackendIdGenerations[SizeT(target)] = g_attachmentBackendIdGeneration;
|
|
}
|
|
|
|
void SyncCurrentFBO() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_backendFramebufferObjects.CollectGarbageIfNeeded();
|
|
TextureImpl::g_backendTextureObjects.CollectGarbageIfNeeded();
|
|
RenderbufferImpl::g_backendRenderbufferObjects.CollectGarbageIfNeeded();
|
|
|
|
const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read};
|
|
|
|
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
|
|
|
|
for (auto& target : fboTargets) {
|
|
auto& slot = GetFramebufferBindingSlotFast(target);
|
|
auto& currentFBO = slot.GetBoundObject();
|
|
|
|
// The three memos together say "this target is already synced": which object is
|
|
// bound (pointer), that it is still the same binding and not a recycled address
|
|
// (slot version, bumped by every real rebind), and that the object has not been
|
|
// edited since (object version, bumped by every attachment/draw-buffer/read-buffer
|
|
// change). All three are stamped by every path below that leaves the target synced
|
|
// - including the ones that decide there is nothing to do. Leaving the slot version
|
|
// to ForceBindCurrentFBO alone made it a permanent mismatch for any app that never
|
|
// hits a blit or a DSA clear, so this early-out never fired and a Minecraft-shaped
|
|
// frame re-ran the whole attachment walk on all 5495 draws.
|
|
const Uint16 slotVersion = slot.GetVersion();
|
|
const Uint16 objectVersion = currentFBO ? currentFBO->GetObjectVersion() : 0;
|
|
auto* currentPtr = currentFBO.get();
|
|
// The backend-id generation joins the triple: a backend texture re-mint
|
|
// (RecreateBackendTexture) moves no frontend version, so without it the
|
|
// early-out would keep the driver FBO on the deleted texture name.
|
|
if (slotVersion == g_fboSyncedSlotVersions[SizeT(target)] &&
|
|
objectVersion == g_fboSyncedObjectVersions[SizeT(target)] &&
|
|
currentPtr == g_fboSyncedObjects[SizeT(target)] &&
|
|
g_fboSyncedBackendIdGenerations[SizeT(target)] == g_attachmentBackendIdGeneration) {
|
|
lastUpdatedFBO = currentPtr;
|
|
continue;
|
|
}
|
|
|
|
if (!currentFBO) {
|
|
MGLOG_E_ONCE("No FBO is currently bound, cannot sync current FBO.");
|
|
continue;
|
|
}
|
|
|
|
if (currentFBO == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
|
// Default FBO, nothing to sync - except the widened-attachment mask, which is
|
|
// only ever WRITTEN by SyncToBackend and would otherwise still describe the
|
|
// user FBO that was draw-bound before. The window surface is a real RGBA
|
|
// buffer, so nothing here is ever widened.
|
|
if (target == FramebufferTarget::Draw) {
|
|
g_alphaWidenedDrawBufferMask = 0;
|
|
g_integerColorDrawBufferMask = 0;
|
|
}
|
|
StampSyncedFBO(target, slotVersion, objectVersion, currentPtr);
|
|
continue;
|
|
}
|
|
|
|
if (currentFBO.get() == lastUpdatedFBO) {
|
|
MGLOG_D("Draw FBO and read FBO are the same, skipping sync.");
|
|
// The attachment/draw-buffer work was already done for this GL FBO as the DRAW
|
|
// target, but the read buffer (glReadBuffer) is READ-target-specific and would
|
|
// be dropped by this skip. Apply it so reads target the right attachment (e.g.
|
|
// KHR-GL33.draw_buffers reads each COLOR_ATTACHMENT while the FBO stays bound as
|
|
// GL_FRAMEBUFFER — without this every glReadBuffer is a no-op and all reads hit
|
|
// COLOR_ATTACHMENT0).
|
|
if (target == FramebufferTarget::Read) {
|
|
auto* syncedFBOSlot = g_backendFramebufferObjects.Find(currentFBO.get());
|
|
if (syncedFBOSlot && *syncedFBOSlot) {
|
|
(*syncedFBOSlot)->SyncReadBufferToBackend(currentFBO);
|
|
}
|
|
}
|
|
StampSyncedFBO(target, slotVersion, objectVersion, currentPtr);
|
|
continue;
|
|
}
|
|
|
|
auto* backendFBOSlot = g_backendFramebufferObjects.Find(currentFBO.get());
|
|
auto& backendObj =
|
|
backendFBOSlot ? *backendFBOSlot : g_backendFramebufferObjects.GetOrCreate(currentFBO);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<BackendFramebufferObject>();
|
|
}
|
|
backendObj->SyncToBackend(currentFBO, target);
|
|
|
|
StampSyncedFBO(target, slotVersion, objectVersion, currentPtr);
|
|
lastUpdatedFBO = currentFBO.get();
|
|
}
|
|
}
|
|
} // namespace FramebufferImpl
|
|
|
|
namespace RenderStateImpl {
|
|
static Uint16 g_syncedRenderStateVersion = 0;
|
|
static Bool g_hasSyncedRenderState = false;
|
|
static RenderStateParameters g_syncedRenderStateParameters;
|
|
static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
|
|
// The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState);
|
|
// an impossible value so the first sync always pushes.
|
|
static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
|
|
// GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the
|
|
// frontend's (desktop-GL default) disabled state down.
|
|
static Bool g_syncedSrgbFramebufferWrites = true;
|
|
// Set when the shadow below stops describing the real ES context. The ES context
|
|
// OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the
|
|
// previous context's enable state while the frontend's parameter block AND its
|
|
// version counter both restart from defaults. Every "differs from what I last pushed"
|
|
// test in SyncRenderState would then agree that nothing needs pushing, and the
|
|
// leftover state silently applies to the new context - the class of bug the CTS
|
|
// caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into
|
|
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole
|
|
// block rather than the one cap that happened to be noticed.
|
|
static Bool g_forceFullRenderStateResync = true;
|
|
// Which draw buffers' alpha channel the colour mask last pushed to the driver had forced
|
|
// OFF - i.e. the value of `appliedWidenMask` in the last SyncRenderState that reached the
|
|
// colour-mask block. NOT derivable from the frontend parameter block: it depends on the
|
|
// bound DRAW framebuffer's attachment formats and on whether the caller is a draw or a
|
|
// clear, neither of which bumps the frontend render-state version. Without it a
|
|
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
|
|
// the clear's undoctored mask.
|
|
static Uint32 g_syncedColorMaskAlphaWidenMask = 0;
|
|
// Scratch for the dual-source-blend decline path in the blend block below. File-scope
|
|
// rather than a local so the ordinary draw pays nothing for it: it is written only on a
|
|
// driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and
|
|
// SyncRenderState runs on the GL thread only.
|
|
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
|
|
g_dualSourceDeclinedBlendStates;
|
|
void InvalidateSyncedRenderState() {
|
|
g_forceFullRenderStateResync = true;
|
|
g_hasSyncedRenderState = false;
|
|
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
|
|
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
|
|
}
|
|
void SyncRenderState(Bool forColorClear) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
|
const Bool forceFullPush = g_forceFullRenderStateResync;
|
|
g_forceFullRenderStateResync = false;
|
|
// The alpha discipline for widened colour attachments (see the header comment on
|
|
// SyncRenderState): a DRAW must not be able to move the stored alpha off 1.0, a CLEAR
|
|
// is what puts it there. So the draw path masks alpha off on every widened draw
|
|
// buffer and the clear path masks nothing.
|
|
const Uint32 appliedWidenMask = forColorClear ? 0u : FramebufferImpl::g_alphaWidenedDrawBufferMask;
|
|
const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask;
|
|
if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState &&
|
|
currentRenderStateVersion == g_syncedRenderStateVersion) {
|
|
return;
|
|
}
|
|
|
|
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
|
|
|
// The frontend has ONE version for the whole parameter block, so a per-draw blend
|
|
// toggle used to re-diff all ~40 pieces of state field by field on every draw
|
|
// (Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), making this the
|
|
// hottest thing mc_state_toggle did). Split the struct into three contiguous byte
|
|
// spans - the head (viewport/point/line/polygon-offset scalars), the blend array,
|
|
// and everything after it - and let one memcmp per span decide whether its blocks
|
|
// run at all. memcmp can false-DIFFER on padding bytes (harmless: the field-wise
|
|
// block runs and finds nothing) but can never false-match, and after the first full
|
|
// sync the tail memcpy below makes the shadow byte-identical, padding included, so
|
|
// in the steady state a span memcmp is exact. Blocks whose inputs are NOT in the
|
|
// parameter struct (the surface-size viewport fallback, the sRGB context
|
|
// capability) stay outside the gates.
|
|
static_assert(std::is_trivially_copyable_v<RenderStateParameters>,
|
|
"span memcmp/memcpy below treats the parameter block as raw bytes");
|
|
constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates);
|
|
constexpr SizeT kBlendSpanEnd = offsetof(RenderStateParameters, LogicOp);
|
|
const auto* currentBytes = reinterpret_cast<const unsigned char*>(¶meters);
|
|
const auto* syncedBytes = reinterpret_cast<const unsigned char*>(&g_syncedRenderStateParameters);
|
|
const Bool headSpanDirty =
|
|
!g_hasSyncedRenderState || std::memcmp(currentBytes, syncedBytes, kBlendSpanBegin) != 0;
|
|
const Bool blendSpanDirty =
|
|
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanBegin, syncedBytes + kBlendSpanBegin,
|
|
kBlendSpanEnd - kBlendSpanBegin) != 0;
|
|
const Bool tailSpanDirty =
|
|
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
|
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
|
|
|
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
|
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
|
Int surfaceWidth = 0;
|
|
Int surfaceHeight = 0;
|
|
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
|
backendViewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
}
|
|
if (backendViewport != g_syncedBackendViewport) {
|
|
g_GLESFuncs.glViewport(backendViewport.x(), backendViewport.y(), backendViewport.z(),
|
|
backendViewport.w());
|
|
g_syncedBackendViewport = backendViewport;
|
|
}
|
|
|
|
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
|
|
// struct, i.e. in the tail span.
|
|
if (tailSpanDirty) {
|
|
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
|
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
|
if (parameters.cap_mg##Enabled) { \
|
|
g_GLESFuncs.glEnable(cap_gl); \
|
|
} else { \
|
|
g_GLESFuncs.glDisable(cap_gl); \
|
|
} \
|
|
}
|
|
SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST);
|
|
SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP);
|
|
SYNC_CAPABILITY(Dither, GL_DITHER);
|
|
SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE);
|
|
SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE);
|
|
SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE);
|
|
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
|
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
|
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
|
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
|
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
|
|
|
#undef SYNC_CAPABILITY
|
|
|
|
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
|
|
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
|
|
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
|
|
// index every ES draw rasterizes against - can be forwarded; a program that
|
|
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
|
|
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
|
|
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
|
|
{
|
|
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
|
|
const Bool syncedScissorTest =
|
|
(g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
|
if (forceFullPush || scissorTest != syncedScissorTest) {
|
|
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
|
// gl_ClipDistance clipping is per-distance enable state in GL, and ES reaches it
|
|
// only through GL_EXT_clip_cull_distance. That extension reuses the desktop enum
|
|
// values for CLIP_DISTANCE0_EXT..7_EXT, but the token is absent from the ES
|
|
// headers this file compiles against, hence the local name. Without the
|
|
// extension there is nowhere to put the state and the shader could not have
|
|
// compiled either, so the whole block is gated rather than silently no-op'ing.
|
|
constexpr GLenum kClipDistance0 = 0x3000;
|
|
constexpr Uint kClipDistanceCount = 8;
|
|
const Uint32 mask = parameters.ClipDistanceEnabledMask;
|
|
const Uint32 syncedMask = g_syncedRenderStateParameters.ClipDistanceEnabledMask;
|
|
if (forceFullPush || mask != syncedMask) {
|
|
const Uint32 changed = forceFullPush ? ~0u : (mask ^ syncedMask);
|
|
for (Uint i = 0; i < kClipDistanceCount; ++i) {
|
|
const Uint32 bit = 1u << i;
|
|
if ((changed & bit) == 0) continue;
|
|
const GLenum cap = static_cast<GLenum>(kClipDistance0 + i);
|
|
(mask & bit) ? g_GLESFuncs.glEnable(cap) : g_GLESFuncs.glDisable(cap);
|
|
}
|
|
}
|
|
}
|
|
|
|
{ // sRGB framebuffer writes. GLES core always encodes a write into an sRGB attachment,
|
|
// while GL_FRAMEBUFFER_SRGB is disabled by default in desktop GL and the frontend
|
|
// never turns it on, so the driver has to be told to write raw. Without this a render
|
|
// into an sRGB colour buffer comes back encoded once too often (the shader's own
|
|
// decode on the next fetch then leaves the value one conversion short).
|
|
const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
|
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
|
|
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
|
|
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB)
|
|
: g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB);
|
|
g_syncedSrgbFramebufferWrites = srgbWrites;
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed
|
|
// all-ones value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to
|
|
// it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements).
|
|
const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled;
|
|
const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled ||
|
|
g_syncedRenderStateParameters.PrimitiveRestartEnabled;
|
|
if (forceFullPush || restart != syncedRestart) {
|
|
restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX)
|
|
: g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
|
}
|
|
}
|
|
|
|
const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; };
|
|
|
|
// Which draw buffers the blend block below DECLINED (see it for why). Needed again at
|
|
// the shadow write-back at the end of this function: the span memcpy there clones the
|
|
// FRONTEND block, which for a declined draw buffer is not what the driver was handed.
|
|
Uint32 dualSourceDeclinedMask = 0;
|
|
|
|
if (blendSpanDirty) { // Blend State
|
|
using FBO = MG_State::GLState::FramebufferObject;
|
|
auto& syncedStates = g_syncedRenderStateParameters.BlendStates;
|
|
|
|
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
|
// glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none.
|
|
// Detected at load and surfaced in the POST. There is no fallback that BLENDS
|
|
// correctly, so a draw that asks for a SRC1 factor on a driver without the extension
|
|
// gets the blend DECLINED: that draw buffer is pushed with blending off and neutral
|
|
// One/Zero factors, and the loss is logged once. The two rejected alternatives are
|
|
// both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise
|
|
// GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend
|
|
// against stale state), and throwing, which is what this did until now, takes the
|
|
// whole process down over one unsupported blend factor. Declining is defined,
|
|
// survivable and visible in the log.
|
|
//
|
|
// NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated
|
|
// on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR
|
|
// ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate
|
|
// serves every draw buffer when they agree, and the per-index arm diffs factors
|
|
// alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any
|
|
// draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers
|
|
// GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal
|
|
// no-error probe to read as its own, and leaving this shadow recording factors the
|
|
// ES context rejected. Blending being off makes the picture unaffected; it does not
|
|
// make the enum acceptable.
|
|
const auto* effectiveBlendStates = ¶meters.BlendStates;
|
|
if (!g_GLESCapabilities.SupportsDualSourceBlend) {
|
|
Uint32 declinedWithBlendingOnMask = 0;
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto& s = parameters.BlendStates[i];
|
|
if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
|
|
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) {
|
|
dualSourceDeclinedMask |= 1u << i;
|
|
if (s.Enabled) declinedWithBlendingOnMask |= 1u << i;
|
|
}
|
|
}
|
|
if (dualSourceDeclinedMask != 0) {
|
|
// Two masks in the message because they mean different things to whoever
|
|
// reads the log: the second one is where a PICTURE was lost. A draw buffer
|
|
// in the first mask but not the second had blending off anyway, so nothing
|
|
// was blended and nothing was dropped - only the unusable enum was kept out
|
|
// of the driver.
|
|
MGLOG_E_ONCE(
|
|
"SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer "
|
|
"mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see "
|
|
"the dual-source blend row in the driver POST). Those draw buffers are pushed "
|
|
"with neutral One/Zero factors instead. Blending was actually ENABLED on mask "
|
|
"0x%x, and only there is anything lost: the fragment's first output is written "
|
|
"unblended and the second source is dropped.",
|
|
dualSourceDeclinedMask, declinedWithBlendingOnMask);
|
|
g_dualSourceDeclinedBlendStates = parameters.BlendStates;
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
|
auto& s = g_dualSourceDeclinedBlendStates[i];
|
|
// Both halves, for the same reason the Vulkan arm neutralises both: the
|
|
// enable so nothing blends against a source the driver cannot produce,
|
|
// the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled
|
|
// on a buffer that was already off is a no-op, which is what makes one
|
|
// ungated rule serve both cases.
|
|
s.Enabled = false;
|
|
s.SrcFactorRGB = BlendFactor::One;
|
|
s.DstFactorRGB = BlendFactor::Zero;
|
|
s.SrcFactorAlpha = BlendFactor::One;
|
|
s.DstFactorAlpha = BlendFactor::Zero;
|
|
}
|
|
effectiveBlendStates = &g_dualSourceDeclinedBlendStates;
|
|
}
|
|
}
|
|
// The rest of the block reads the EFFECTIVE state. The per-field writes it makes
|
|
// into `syncedStates` are provisional - the span memcpy at the end of this function
|
|
// overwrites the whole blend span with the frontend's own bytes - so the declined
|
|
// draw buffers are put back there, see the write-back below.
|
|
const auto& targetStates = *effectiveBlendStates;
|
|
|
|
Bool allEnabled = true;
|
|
Bool allDisabled = true;
|
|
Bool anyCapDirty = forceFullPush;
|
|
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
Bool enabled = targetStates[i].Enabled;
|
|
if (enabled)
|
|
allDisabled = false;
|
|
else
|
|
allEnabled = false;
|
|
|
|
if (enabled != syncedStates[i].Enabled) {
|
|
anyCapDirty = true;
|
|
}
|
|
}
|
|
|
|
if (anyCapDirty) {
|
|
if (allEnabled) {
|
|
g_GLESFuncs.glEnable(GL_BLEND);
|
|
for (auto& s : syncedStates)
|
|
s.Enabled = true;
|
|
} else if (allDisabled) {
|
|
g_GLESFuncs.glDisable(GL_BLEND);
|
|
for (auto& s : syncedStates)
|
|
s.Enabled = false;
|
|
} else {
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) {
|
|
syncedStates[i].Enabled = targetStates[i].Enabled;
|
|
syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i)
|
|
: g_GLESFuncs.glDisablei(GL_BLEND, i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Bool allFuncsSame = true;
|
|
Bool anyFuncDirty = forceFullPush;
|
|
const auto& first = targetStates[0];
|
|
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto& cur = targetStates[i];
|
|
const auto& syn = syncedStates[i];
|
|
|
|
Bool isDiffFromSyn =
|
|
(cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB ||
|
|
cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha);
|
|
|
|
if (isDiffFromSyn) anyFuncDirty = true;
|
|
|
|
if (allFuncsSame && i > 0) {
|
|
if (cur.SrcFactorRGB != first.SrcFactorRGB || cur.DstFactorRGB != first.DstFactorRGB ||
|
|
cur.SrcFactorAlpha != first.SrcFactorAlpha || cur.DstFactorAlpha != first.DstFactorAlpha) {
|
|
allFuncsSame = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyFuncDirty) {
|
|
if (allFuncsSame) {
|
|
g_GLESFuncs.glBlendFuncSeparate(MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorRGB),
|
|
MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorRGB),
|
|
MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorAlpha),
|
|
MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorAlpha));
|
|
|
|
for (auto& syn : syncedStates) {
|
|
syn.SrcFactorRGB = first.SrcFactorRGB;
|
|
syn.DstFactorRGB = first.DstFactorRGB;
|
|
syn.SrcFactorAlpha = first.SrcFactorAlpha;
|
|
syn.DstFactorAlpha = first.DstFactorAlpha;
|
|
}
|
|
} else {
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto& cur = targetStates[i];
|
|
auto& syn = syncedStates[i];
|
|
|
|
if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB ||
|
|
cur.DstFactorRGB != syn.DstFactorRGB ||
|
|
cur.SrcFactorAlpha != syn.SrcFactorAlpha ||
|
|
cur.DstFactorAlpha != syn.DstFactorAlpha) {
|
|
syn.SrcFactorRGB = cur.SrcFactorRGB;
|
|
syn.DstFactorRGB = cur.DstFactorRGB;
|
|
syn.SrcFactorAlpha = cur.SrcFactorAlpha;
|
|
syn.DstFactorAlpha = cur.DstFactorAlpha;
|
|
|
|
g_GLESFuncs.glBlendFuncSeparatei(
|
|
i, MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorRGB),
|
|
MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorRGB),
|
|
MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorAlpha),
|
|
MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorAlpha));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Bool allEquationsSame = true;
|
|
Bool anyEquationDirty = forceFullPush;
|
|
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto& cur = targetStates[i];
|
|
const auto& syn = syncedStates[i];
|
|
|
|
if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) {
|
|
anyEquationDirty = true;
|
|
}
|
|
|
|
if (allEquationsSame && i > 0) {
|
|
if (cur.ColorEquation != first.ColorEquation || cur.AlphaEquation != first.AlphaEquation) {
|
|
allEquationsSame = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyEquationDirty) {
|
|
if (allEquationsSame) {
|
|
g_GLESFuncs.glBlendEquationSeparate(MG_Util::ConvertBlendEquationToGLEnum(first.ColorEquation),
|
|
MG_Util::ConvertBlendEquationToGLEnum(first.AlphaEquation));
|
|
|
|
for (auto& syn : syncedStates) {
|
|
syn.ColorEquation = first.ColorEquation;
|
|
syn.AlphaEquation = first.AlphaEquation;
|
|
}
|
|
} else {
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
const auto& cur = targetStates[i];
|
|
auto& syn = syncedStates[i];
|
|
|
|
if (forceFullPush || cur.ColorEquation != syn.ColorEquation ||
|
|
cur.AlphaEquation != syn.AlphaEquation) {
|
|
syn.ColorEquation = cur.ColorEquation;
|
|
syn.AlphaEquation = cur.AlphaEquation;
|
|
|
|
g_GLESFuncs.glBlendEquationSeparatei(
|
|
i, MG_Util::ConvertBlendEquationToGLEnum(cur.ColorEquation),
|
|
MG_Util::ConvertBlendEquationToGLEnum(cur.AlphaEquation));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Depth state
|
|
if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
|
|
g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc));
|
|
}
|
|
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
|
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
|
}
|
|
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
|
|
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Stencil state
|
|
for (SizeT faceIndex = 0; faceIndex < parameters.StencilStates.size(); ++faceIndex) {
|
|
const StencilFaceState& current = parameters.StencilStates[faceIndex];
|
|
const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex];
|
|
const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK;
|
|
|
|
if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref ||
|
|
current.ValueMask != synced.ValueMask) {
|
|
g_GLESFuncs.glStencilFuncSeparate(
|
|
glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func), current.Ref,
|
|
current.ValueMask);
|
|
}
|
|
if (forceFullPush || current.WriteMask != synced.WriteMask) {
|
|
g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask);
|
|
}
|
|
if (forceFullPush || current.FailOp != synced.FailOp ||
|
|
current.PassDepthFailOp != synced.PassDepthFailOp ||
|
|
current.PassDepthPassOp != synced.PassDepthPassOp) {
|
|
g_GLESFuncs.glStencilOpSeparate(
|
|
glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp),
|
|
MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthFailOp),
|
|
MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthPassOp));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask
|
|
// (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when
|
|
// draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0.
|
|
// Mirrors the blend block.
|
|
using FBO = MG_State::GLState::FramebufferObject;
|
|
const auto& targetMasks = parameters.ColorMasks;
|
|
const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks;
|
|
|
|
// What the DRIVER is told for draw buffer i. Identical to the application's mask
|
|
// except on a widened attachment during a draw, where alpha is forced off; the
|
|
// frontend's own array is never written, so glGet(GL_COLOR_WRITEMASK) keeps
|
|
// answering with the application's value.
|
|
const auto driverMask = [&](Uint i) -> BoolVec4 {
|
|
BoolVec4 m = targetMasks[i];
|
|
if (i < 32 && (appliedWidenMask & (1u << i)) != 0) {
|
|
m.w() = false;
|
|
}
|
|
return m;
|
|
};
|
|
|
|
Bool anyDirty = forceFullPush || colorMaskWidenDirty;
|
|
Bool allSame = true;
|
|
const BoolVec4 driverMask0 = driverMask(0);
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
if (targetMasks[i] != syncedMasks[i]) anyDirty = true;
|
|
if (i > 0 && driverMask(i) != driverMask0) allSame = false;
|
|
}
|
|
|
|
if (anyDirty) {
|
|
if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) {
|
|
// Without draw_buffers_indexed there is only one mask for the whole
|
|
// framebuffer, so a widened draw buffer 0 costs every other buffer its
|
|
// alpha writes. ES 3.2 makes glColorMaski core and ES 3.1 has it as
|
|
// EXT/OES; the only devices that reach this line are ES 3.0-class, where
|
|
// MRT with a mixed widened/native colour attachment set is already rare.
|
|
const BoolVec4& m = driverMask0;
|
|
g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
|
|
ToGLBoolean(m.w()));
|
|
} else {
|
|
const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski
|
|
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
|
|
: g_GLESFuncs.glColorMaskiOES;
|
|
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
|
// colorMaskWidenDirty forces every slot: the previous push may have
|
|
// been the non-indexed glColorMask above (which set all of them), and
|
|
// the per-slot diff below only knows about the application's array.
|
|
if (forceFullPush || colorMaskWidenDirty || targetMasks[i] != syncedMasks[i]) {
|
|
const BoolVec4 m = driverMask(i);
|
|
colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
|
|
ToGLBoolean(m.w()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
g_syncedColorMaskAlphaWidenMask = appliedWidenMask;
|
|
}
|
|
|
|
if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when present.
|
|
// Without the extension the mode stays FILL and non-FILL requests are dropped.
|
|
if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) &&
|
|
g_GLESCapabilities.SupportsPolygonMode) {
|
|
const auto polygonModeFn =
|
|
g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE;
|
|
polygonModeFn(GL_FRONT_AND_BACK, parameters.PolygonModeFront);
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Clear values
|
|
if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
|
|
const FloatVec4& clearCol = parameters.ClearColor;
|
|
g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w());
|
|
}
|
|
if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
|
|
g_GLESFuncs.glClearDepthf(parameters.ClearDepth);
|
|
}
|
|
if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
|
|
g_GLESFuncs.glClearStencil(static_cast<GLint>(parameters.ClearStencil));
|
|
}
|
|
if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
|
|
const FloatVec4& blendColor = parameters.BlendColor;
|
|
g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w());
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Cull face mode
|
|
if (forceFullPush || parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
|
|
const CullFaceMode& cfm = parameters.CullFaceModeSetting;
|
|
g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm));
|
|
}
|
|
if (forceFullPush || parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
|
|
const FrontFaceMode& ffm = parameters.FrontFaceModeSetting;
|
|
g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm));
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and
|
|
// for the same reason: what has to reach the driver is NOT simply the parameter
|
|
// field. RenderStateParameters::ScissorBoxes starts all-zero, which means "the
|
|
// application has never called glScissor" - it is not a GL scissor box. GL's
|
|
// initial box is the whole window, which the frontend has no way to spell before a
|
|
// surface exists. The pre-resync code got away with pushing the field verbatim only
|
|
// by accident: the shadow held the same default, the field never compared unequal,
|
|
// and the ES context kept its own correct default. Under the forced full push that
|
|
// accident is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle,
|
|
// and everything drawn with GL_SCISSOR_TEST enabled before the app's first
|
|
// glScissor is clipped away - Minecraft 26.2 keeps only its unscissored sky and
|
|
// hand and loses the terrain and the whole GUI.
|
|
//
|
|
// The condition is the WRITTEN FLAG, not the extent. An empty rectangle is a
|
|
// perfectly legal thing to ask for - glScissor(0,0,0,0) means "the scissor test
|
|
// rejects every fragment" - so testing `width <= 0 || height <= 0` substituted the
|
|
// whole surface for a deliberately empty box and inverted the request into "accept
|
|
// every fragment", no matter how many times the application had already called
|
|
// glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is exactly that: all 16
|
|
// boxes zero-sized with the test enabled, requiring the draw to be clipped away
|
|
// entirely. Reading the flag preserves the Minecraft protection bit-for-bit - before
|
|
// the first glScissor the bit is clear and the surface size is still substituted -
|
|
// while an explicit empty box now reaches the driver verbatim. Negative extents
|
|
// cannot arrive here at all: all three entry points reject them with
|
|
// GL_INVALID_VALUE before storing (GL_RenderState.cpp's ValidateNonNegativeExtent).
|
|
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
|
|
if ((parameters.ScissorBoxWrittenMask & 1u) == 0) {
|
|
Int surfaceWidth = 0;
|
|
Int surfaceHeight = 0;
|
|
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
|
backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
}
|
|
// Compared against what was actually PUSHED, not against the parameter field, so
|
|
// the resolved value and the diff can never disagree.
|
|
if (backendScissorBox != g_syncedBackendScissorBox) {
|
|
g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(),
|
|
backendScissorBox.w());
|
|
g_syncedBackendScissorBox = backendScissorBox;
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Logic op (first field of the tail span)
|
|
// glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a
|
|
// plain ES 3.x driver. Before the forced resync it was reached only when an app
|
|
// actually set a logic op; now every MakeCurrent would call it, so the null check
|
|
// is mandatory rather than defensive.
|
|
if (g_GLESFuncs.glLogicOp &&
|
|
(forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) {
|
|
g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp));
|
|
}
|
|
}
|
|
|
|
if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below)
|
|
if (forceFullPush || parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
|
|
parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits) {
|
|
g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits);
|
|
}
|
|
}
|
|
|
|
if (headSpanDirty) { // Line width
|
|
if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
|
|
g_GLESFuncs.glLineWidth(parameters.LineWidth);
|
|
}
|
|
}
|
|
|
|
if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize,
|
|
// so the entry point is absent on most drivers; see the glLogicOp note above)
|
|
if (g_GLESFuncs.glPointSize &&
|
|
(forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) {
|
|
g_GLESFuncs.glPointSize(parameters.PointSize);
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Sample coverage
|
|
if (forceFullPush || parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
|
|
parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) {
|
|
g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue,
|
|
ToGLBoolean(parameters.SampleCoverageInvert));
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Sample mask
|
|
if (g_GLESFuncs.glSampleMaski &&
|
|
(forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) {
|
|
g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue);
|
|
}
|
|
}
|
|
|
|
if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core)
|
|
// Both halves are gated on the same entry point rather than on a version check:
|
|
// GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core /
|
|
// OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would
|
|
// only push an INVALID_ENUM into the driver's queue. This is NOT part of the
|
|
// SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to
|
|
// put a guard.
|
|
if (g_GLESFuncs.glMinSampleShading) {
|
|
if (forceFullPush ||
|
|
parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) {
|
|
if (parameters.SampleShadingEnabled) {
|
|
g_GLESFuncs.glEnable(GL_SAMPLE_SHADING);
|
|
} else {
|
|
g_GLESFuncs.glDisable(GL_SAMPLE_SHADING);
|
|
}
|
|
}
|
|
if (forceFullPush || parameters.MinSampleShadingValue !=
|
|
g_syncedRenderStateParameters.MinSampleShadingValue) {
|
|
g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
g_syncedRenderStateVersion = currentRenderStateVersion;
|
|
// Byte copy, not member copy: it also clones the frontend struct's padding bytes,
|
|
// which is what lets the span memcmps above answer "unchanged" exactly instead of
|
|
// tripping on indeterminate padding every draw. Only the dirty spans need copying -
|
|
// a clean span's bytes are already identical by the very memcmp that skipped it.
|
|
auto* syncedBytesMut = reinterpret_cast<unsigned char*>(&g_syncedRenderStateParameters);
|
|
if (headSpanDirty) {
|
|
std::memcpy(syncedBytesMut, currentBytes, kBlendSpanBegin);
|
|
}
|
|
if (blendSpanDirty) {
|
|
std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin,
|
|
kBlendSpanEnd - kBlendSpanBegin);
|
|
// ...except for a draw buffer whose dual-source blend was DECLINED, where the
|
|
// frontend block is precisely what did NOT reach the driver. The shadow has to hold
|
|
// what was pushed or the next diff compares against state the ES context never got:
|
|
// going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides,
|
|
// the enable block finds nothing to do, and blending stays off from the decline.
|
|
// The span stays permanently "dirty" against the frontend as a result, which costs
|
|
// one memcmp plus this block per render-state VERSION change - the top-of-function
|
|
// version early-out still skips repeat draws entirely.
|
|
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
|
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
|
g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i];
|
|
}
|
|
}
|
|
if (tailSpanDirty) {
|
|
std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd,
|
|
sizeof(RenderStateParameters) - kBlendSpanEnd);
|
|
}
|
|
g_hasSyncedRenderState = true;
|
|
}
|
|
} // namespace RenderStateImpl
|
|
|
|
namespace PrgramImpl {
|
|
// The twin SyncCurrentProgram resolved for the draw/dispatch being prepared,
|
|
// consumed by BindCurrentProgramWithResources and the post-draw draw-parameter
|
|
// updates in the same GL entry point — they all used to repeat the registry
|
|
// Find. Valid only while the keyed frontend program is the current draw
|
|
// program: every consumer compares the raw key against GetProgramForDraw()
|
|
// before trusting the twin, and SyncCurrentProgram rewrites the pair at the
|
|
// top of every PrepareForDraw/PrepareForCompute, so a recycled address can
|
|
// never be consumed (the stale pair is overwritten before any consumer runs).
|
|
static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr;
|
|
static BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr;
|
|
|
|
// Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount:
|
|
// the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so
|
|
// it is keyed exactly like SyncCurrentFBO's synced trio - object pointer (identity),
|
|
// slot version (a recycled heap address cannot re-match: every real rebind bumps
|
|
// it), and object version (every attachment/draw-buffer/read-buffer edit bumps it,
|
|
// the same documented invariant the FBO sync memo already leans on). A null FBO
|
|
// keys on (nullptr, slot version, 0).
|
|
static const MG_State::GLState::FramebufferObject* g_broadcastMemoFbo = nullptr;
|
|
static Uint16 g_broadcastMemoSlotVersion = 0;
|
|
static Uint16 g_broadcastMemoObjectVersion = 0;
|
|
static Bool g_broadcastMemoValid = false;
|
|
static Uint g_broadcastMemoCount = 1;
|
|
|
|
// The identity+version key above is only monotonic WITHIN one GLContext: a
|
|
// library teardown + re-init frees every FramebufferObject and restarts the
|
|
// draw slot's counter at zero, so a recycled FBO address with coinciding
|
|
// fresh versions would false-hit. Cleared at the same boundaries as the
|
|
// structurally identical SyncCurrentFBO trio (InvalidateFramebufferBindingCache).
|
|
void InvalidateBroadcastMemo() {
|
|
g_broadcastMemoValid = false;
|
|
}
|
|
|
|
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
g_backendProgramObjects.CollectGarbageIfNeeded();
|
|
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
|
|
|
g_currentDrawFrontendProgram = nullptr;
|
|
g_currentDrawBackendProgram = nullptr;
|
|
|
|
// ... || !GetSpirvStatus(): see BackendProgramObjectImpl::SyncToBackend - a
|
|
// program whose SPIR-V never arrived is linked but not drawable.
|
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
|
g_GLESFuncs.glUseProgram(0);
|
|
g_lastUsedBackendProgramId = 0;
|
|
return;
|
|
}
|
|
// Read from the frontend rather than from the backend framebuffer sync, which
|
|
// only runs later in PrepareForDraw: a program compiled against a stale count
|
|
// would not be relinked until the draw after the one that needed it.
|
|
{
|
|
const auto& drawSlot = GetFramebufferBindingSlotFast(FramebufferTarget::Draw);
|
|
const auto& drawFBO = drawSlot.GetBoundObject();
|
|
const Uint16 slotVersion = drawSlot.GetVersion();
|
|
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
|
|
if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() ||
|
|
g_broadcastMemoSlotVersion != slotVersion || g_broadcastMemoObjectVersion != objectVersion) {
|
|
Uint enabledDrawBuffers = 0;
|
|
if (drawFBO) {
|
|
const auto& drawBuffers = drawFBO->GetDrawBuffers();
|
|
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
|
if (drawBuffers[i] != FramebufferAttachmentType::None) {
|
|
enabledDrawBuffers = i + 1;
|
|
}
|
|
}
|
|
}
|
|
g_broadcastMemoFbo = drawFBO.get();
|
|
g_broadcastMemoSlotVersion = slotVersion;
|
|
g_broadcastMemoObjectVersion = objectVersion;
|
|
g_broadcastMemoCount = std::max<Uint>(enabledDrawBuffers, 1);
|
|
g_broadcastMemoValid = true;
|
|
}
|
|
g_fragColorBroadcastCount = g_broadcastMemoCount;
|
|
}
|
|
|
|
BackendProgramObjectImpl* twin = g_programTwinLookupMemo.Lookup(currentProgram);
|
|
if (!twin) {
|
|
auto* backendProgramSlot = g_backendProgramObjects.Find(currentProgram.get());
|
|
auto& backendObj =
|
|
backendProgramSlot ? *backendProgramSlot : g_backendProgramObjects.GetOrCreate(currentProgram);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<BackendProgramObjectImpl>();
|
|
}
|
|
g_programTwinLookupMemo.Store(currentProgram, backendObj.get());
|
|
twin = backendObj.get();
|
|
}
|
|
// A link-version mismatch means the program was relinked: the backend
|
|
// shaders and every cache built by CacheResourceLocations (block
|
|
// indices, sampler locations, UBO upload gate) are stale.
|
|
//
|
|
// The storage-block signature is the same shape of condition: ES cannot move a
|
|
// storage block's binding after link, so glShaderStorageBlockBinding is honoured by
|
|
// baking the effective binding into the generated ESSL - which makes a program built
|
|
// against a different override set stale. It is compared HERE rather than acted on in
|
|
// the entry point because that one must never trigger a build (see
|
|
// ShaderStorageBlockBinding below). The signature is over the values, so an
|
|
// application that re-sets the same bindings every frame rebuilds nothing.
|
|
//
|
|
// The image-unit generation is a third of the same shape, and it used to be
|
|
// carried by accident: glUniform1i on an image uniform bumped the program's backend
|
|
// state version, which was in the program-pipeline composite's cache key, so a
|
|
// pipeline draw got a whole NEW composite object and therefore a fresh twin. Keying
|
|
// that cache on the link version instead (ProgramPipelineObject) removed the
|
|
// accident - and it never covered the monolithic glUseProgram path at all - so the
|
|
// dependency is stated here instead.
|
|
if (!twin->GetBackendProgramId() ||
|
|
twin->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
|
|
twin->GetSyncedImageUnitVersion() != currentProgram->GetImageUnitVersion() ||
|
|
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
|
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
|
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
|
twin->GetShaderStorageBlockBindingSignature() !=
|
|
ComputeShaderStorageBlockBindingSignature(*currentProgram) ||
|
|
// A fourth of the same shape, and the reason glBindImageTexture itself does
|
|
// nothing: GLSL ES demands a format layout qualifier on an image where desktop
|
|
// GLSL lets a writeonly declaration omit one, so a format-less declaration is
|
|
// compiled against the format the application BOUND, and a rebind to a
|
|
// different one makes what was built wrong. Asked of the twin because only it
|
|
// knows which units its own images address - and answered by an empty-vector
|
|
// test for every program that declares its formats, which is nearly all of them.
|
|
!twin->ImageUnitFormatsStillMatch() ||
|
|
// A fifth of the same shape, for the programs ES will not link at all: one whose
|
|
// tessellation evaluation stage has no control stage gets a synthesized
|
|
// pass-through one, and GL_PATCH_VERTICES is compiled INTO it as
|
|
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
|
|
// built program wrong. -1 is "this program needed no such stage", which compares
|
|
// equal to itself and costs every other program one integer test.
|
|
//
|
|
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL are baked into the same stage for the same
|
|
// reason (ES has neither the state nor an entry point), so glPatchParameterfv
|
|
// makes it stale too. Both level comparisons sit INSIDE the >= 0 guard: a program
|
|
// with a control stage of its own - which is nearly all of them - still pays only
|
|
// the one integer test.
|
|
//
|
|
// Compared by BIT PATTERN, matching what DirectVulkan hashes into its module key.
|
|
// A float compare here would never settle for a NaN level - NaN != NaN - and every
|
|
// draw of that program would re-transpile, re-compile and re-link a byte-identical
|
|
// shader. glPatchParameterfv accepts NaN by design.
|
|
//
|
|
// The gl_PerVertex MEMBER SET needs no clause of its own here, and that asymmetry
|
|
// with DirectVulkan is deliberate rather than an omission. It can only change with
|
|
// the evaluation stage, i.e. across a relink - which the link-version test at the
|
|
// top of this condition already catches - and this backend never invents the shape
|
|
// in the first place: AttachPassthroughTessControlStage extracts the member text
|
|
// out of the neighbouring stages' emitted ESSL on every rebuild
|
|
// (ExtractPerVertexBlockMembers, "mirrored, never invented"). DirectVulkan needs
|
|
// the mask in its key precisely because it does NOT mirror - it redeclares from a
|
|
// member set it has to be told.
|
|
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
|
(twin->GetPassthroughTessControlPatchVertices() !=
|
|
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
|
|
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
|
|
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
|
|
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
|
|
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
|
|
twin->SyncToBackend(currentProgram);
|
|
}
|
|
g_currentDrawFrontendProgram = currentProgram.get();
|
|
g_currentDrawBackendProgram = twin;
|
|
}
|
|
} // namespace PrgramImpl
|
|
|
|
void BindCurrentFBO(FramebufferTarget target) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto& slot = GetFramebufferBindingSlotFast(target);
|
|
// No fast path on the binding slot's version. It is a 16-bit counter that only
|
|
// ForceBindCurrentFBO ever stamps here, so the comparison was against an arbitrarily old
|
|
// snapshot and any later slot version that happened to land on it - one wrap of the
|
|
// counter, or simply enough rebinds - read as "already bound" and left the driver on a
|
|
// completely different framebuffer. KHR-GL32.packed_pixels then read its gradient back
|
|
// out of the previous subtest's framebuffer.
|
|
//
|
|
// Skipping the work is BindFramebufferId's job anyway: it shadows the driver's own
|
|
// draw/read bindings and drops the glBindFramebuffer when the target already holds the id,
|
|
// which is where the cost actually is. What is left here is one registry lookup,
|
|
// and the twin memo replaces even that with an array probe on the steady path.
|
|
const auto& currentFBO = slot.GetBoundObject();
|
|
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
|
FramebufferImpl::BackendFramebufferObject* twin = g_fboTwinLookupMemo.Lookup(currentFBO);
|
|
if (!twin) {
|
|
auto* backendFBOSlot = FramebufferImpl::g_backendFramebufferObjects.Find(currentFBO.get());
|
|
if (backendFBOSlot && *backendFBOSlot) {
|
|
twin = backendFBOSlot->get();
|
|
g_fboTwinLookupMemo.Store(currentFBO, twin);
|
|
}
|
|
}
|
|
if (twin) {
|
|
twin->Bind(target);
|
|
} else {
|
|
MGLOG_E_ONCE("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
|
|
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
|
}
|
|
} else {
|
|
MGLOG_D("Binding default framebuffer as %s FBO", (target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
|
// Through the shadow: a raw bind here would leave the shadow claiming
|
|
// the previous user FBO, false-skipping its next re-bind.
|
|
FramebufferImpl::BindFramebufferId(
|
|
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
|
}
|
|
}
|
|
|
|
void SyncAndBindFramebufferObject(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
FramebufferTarget target, Bool forceSync = false) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (!framebuffer || framebuffer == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
|
// Same reset as SyncCurrentFBO's default-framebuffer branch: SyncToBackend is the
|
|
// only writer of the widened-attachment mask, so a path that skips it has to say so
|
|
// explicitly. It matters here because the DSA clears and glBlitFramebuffer briefly
|
|
// sync a DIFFERENT framebuffer as DRAW and then restore the application's through
|
|
// ForceBindCurrentFBO - which lands right here when that one is the default.
|
|
if (target == FramebufferTarget::Draw) {
|
|
FramebufferImpl::g_alphaWidenedDrawBufferMask = 0;
|
|
FramebufferImpl::g_integerColorDrawBufferMask = 0;
|
|
}
|
|
FramebufferImpl::BindFramebufferId(
|
|
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
|
return;
|
|
}
|
|
|
|
auto& registry = FramebufferImpl::g_backendFramebufferObjects;
|
|
auto* backendFBOSlot = registry.Find(framebuffer.get());
|
|
auto& backendObj = backendFBOSlot ? *backendFBOSlot : registry.GetOrCreate(framebuffer);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<FramebufferImpl::BackendFramebufferObject>();
|
|
}
|
|
if (forceSync) {
|
|
backendObj->InvalidateSyncedState();
|
|
}
|
|
|
|
backendObj->SyncToBackend(framebuffer, target);
|
|
backendObj->Bind(target);
|
|
}
|
|
|
|
void ForceBindCurrentFBO(FramebufferTarget target) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto& slot = GetFramebufferBindingSlotFast(target);
|
|
const auto& fbo = slot.GetBoundObject();
|
|
SyncAndBindFramebufferObject(fbo, target);
|
|
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
|
|
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
|
|
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
|
|
FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] =
|
|
FramebufferImpl::g_attachmentBackendIdGeneration;
|
|
}
|
|
|
|
static void BindCurrentProgramWithResources(
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
|
const TextureImpl::DrawTextureSyncKeys& keys);
|
|
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
|
|
|
void PrepareForDraw(DrawSyncFlags syncBit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// One twin resolve serves the whole draw: the buffer sync (which hosts the
|
|
// resolved-buffers memo on the twin), the VAO sync and the draw-time bind
|
|
// below. Nothing in between can invalidate it — the bound VAO is pinned by
|
|
// the context, and no step here erases or replaces a live VAO's twin.
|
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
|
|
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
|
|
// Early config-version read: see the note on SyncNeccessaryBuffers - issuing
|
|
// the (cold-line) load here overlaps its miss with the resolves below.
|
|
const Uint32 vaoConfigVersion = currentVAO ? currentVAO->GetConfigVersion() : 0;
|
|
// One program resolve and one texture-key capture serve the whole draw, for
|
|
// the same reason the twin resolve does: only frontend GL entry points move
|
|
// either, and none can run inside this preparation. GetProgramForDraw is a
|
|
// cross-TU call with a guarded static inside - repeating it per stage showed
|
|
// up in draw-loop profiles.
|
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
|
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
|
|
|
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion,
|
|
syncBit & DrawSyncBit::IndexBuffer,
|
|
syncBit & DrawSyncBit::IndirectBuffer);
|
|
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
|
// A draw reads and writes through its image units too, so the unit bindings have to be
|
|
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
|
|
// pays one integer test, and one with images re-issues them only when a texture shape
|
|
// moved under them.
|
|
TextureImpl::SyncImageTextureBindingsForDraw(textureKeys);
|
|
// A draw writes through its image units too - the conformance case that found this
|
|
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
|
|
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
PrgramImpl::SyncCurrentProgram(currentProgram);
|
|
RenderStateImpl::SyncRenderState();
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (vaoTwin) {
|
|
vaoTwin->Bind();
|
|
} else {
|
|
VertexArrayImpl::BindBackendVAOId(0);
|
|
}
|
|
}
|
|
|
|
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
|
|
|
|
BindCurrentTextures(textureKeys, currentProgram);
|
|
BindCurrentProgramWithResources(currentProgram, textureKeys);
|
|
|
|
// Last: opening the capture span needs the program current and the capture
|
|
// buffers bound, and ES rejects most binding changes once it is open.
|
|
XfbImpl::StartPendingTransformFeedback();
|
|
}
|
|
|
|
// Resolves every frontend texture unit's textures onto the backend context. Returns
|
|
// false when the resolution could not be completed from the state it read - a bound
|
|
// texture that has no backend object yet is skipped, and a later draw would bind it
|
|
// without any of the memo keys below moving - so the caller must not memoise it.
|
|
static Bool ResolveAndBindUnitTextures(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
|
Int maxTouchedUnit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
Bool fullyResolved = true;
|
|
// Frontend target the current program samples at a given unit; resolves an
|
|
// aliased native binding when two real textures compete for it (see below).
|
|
// Only consulted on a conflict, so the ordinary unit costs nothing.
|
|
const auto sampledTargetForUnit = [¤tProgram](Int unit) {
|
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
|
return TextureTarget::Unknown;
|
|
}
|
|
const Uint maxUniformLocation = currentProgram->GetMaxUniformLocation();
|
|
for (Uint location = 0; location <= maxUniformLocation; ++location) {
|
|
if (currentProgram->GetUniformSamplerOrImageUnitIndex(location) != unit) continue;
|
|
const auto target = SamplerUniformTextureTarget(currentProgram->GetUniformType(location));
|
|
if (target != TextureTarget::Unknown) {
|
|
return target;
|
|
}
|
|
}
|
|
return TextureTarget::Unknown;
|
|
};
|
|
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
|
|
Array<TextureTarget, (SizeT)TextureTarget::TextureTargetCount> claimedByFrontendTarget{};
|
|
claimedByFrontendTarget.fill(TextureTarget::Unknown);
|
|
|
|
// Two passes over the slots, because desktop 1D/1D-array targets alias ES
|
|
// 2D/2D-array targets: a unit can hold a real texture on one of an aliased
|
|
// pair and a default (name 0) object on the other, and one native binding
|
|
// has to serve both. A default object only carries the app's bind-0 state,
|
|
// so a real texture always wins the alias - binding it second would leave
|
|
// the shader sampling an empty texture (an app that gives name 0 an image
|
|
// then still keeps the default's binding when nothing else claims the
|
|
// native target). Pass 0 places the real textures, pass 1 fills in the
|
|
// defaults for native targets that are still unclaimed.
|
|
for (Int pass = 0; pass < 2; ++pass) {
|
|
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
|
|
const auto& textureObject = bindingSlot.GetBoundObject();
|
|
if (!textureObject) continue;
|
|
|
|
// An image-less default texture is the frontend's bind-0 state. Defer native
|
|
// unbinding until all slots have been considered: a default alias must not
|
|
// clear a real binding either.
|
|
if (MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) continue;
|
|
|
|
const Bool isDefaultObject = textureObject->GetExternalIndex() == 0;
|
|
if (isDefaultObject != (pass == 1)) continue;
|
|
|
|
auto target = textureObject->GetTarget();
|
|
if (!TextureImpl::IsSupportedTextureTarget(target)) {
|
|
MGLOG_D(" Texture target %s is not supported, skipping.",
|
|
MG_Util::ConvertTextureTargetToString(target).c_str());
|
|
continue;
|
|
}
|
|
const auto backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
|
const SizeT backendTargetIndex = static_cast<SizeT>(backendTarget);
|
|
if (isDefaultObject && boundBackendTargets[backendTargetIndex]) continue;
|
|
|
|
// Two REAL textures can want the same native target as well - an app is
|
|
// free to keep a 1D texture and a 2D texture bound to one unit, and GL
|
|
// resolves which one is sampled from the shader's sampler type. Ask the
|
|
// program; without an answer the first binding placed stands rather than
|
|
// being silently overwritten by whichever slot comes last.
|
|
if (!isDefaultObject && boundBackendTargets[backendTargetIndex] &&
|
|
claimedByFrontendTarget[backendTargetIndex] != target) {
|
|
if (sampledTargetForUnit(unit) != target) continue;
|
|
}
|
|
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
|
|
|
// A texture whose mip chain does not satisfy the filter's completeness
|
|
// rules samples as (0, 0, 0, 1). The ES driver cannot work that out for
|
|
// itself here: the backend texture is immutable storage, so a level the
|
|
// application redefined at the wrong size never reached it. Leaving the
|
|
// native target unbound produces exactly the incomplete-texture result.
|
|
const auto& effectiveSampler = textureUnit.GetSamplerObject()
|
|
? textureUnit.GetSamplerObject()
|
|
: textureObject->GetSamplerObject();
|
|
if (MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(),
|
|
effectiveSampler.get())) {
|
|
continue;
|
|
}
|
|
|
|
// Bind texture object
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
|
if (!backendTextureSlot || !*backendTextureSlot) {
|
|
fullyResolved = false;
|
|
continue;
|
|
}
|
|
|
|
(*backendTextureSlot)->Bind(targetGL, unit);
|
|
boundBackendTargets[backendTargetIndex] = true;
|
|
claimedByFrontendTarget[backendTargetIndex] = target;
|
|
}
|
|
}
|
|
|
|
// Clear each native target that has no resolved frontend binding. This is the backend
|
|
// half of glBindTexture(..., 0); skipping image-less default objects would otherwise
|
|
// leave the previously sampled ES texture resident. Deduplicate mapped desktop targets
|
|
// so 1D/2D and 1D-array/2D-array aliases do not cause redundant binds.
|
|
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> visitedBackendTargets{};
|
|
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
|
|
const auto target = bindingSlot.GetTarget();
|
|
if (!TextureImpl::IsSupportedTextureTarget(target)) continue;
|
|
|
|
const auto backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
|
const auto backendTargetIndex = static_cast<SizeT>(backendTarget);
|
|
if (visitedBackendTargets[backendTargetIndex]) continue;
|
|
visitedBackendTargets[backendTargetIndex] = true;
|
|
|
|
if (!boundBackendTargets[backendTargetIndex]) {
|
|
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
|
TextureImpl::UnbindTexture(unit, targetGL);
|
|
}
|
|
}
|
|
}
|
|
return fullyResolved;
|
|
}
|
|
|
|
// Per-unit memo of the sampler-registry lookup, NOT of the resulting unit binding
|
|
// (BackendSamplerObject::Bind already dedups against g_boundSamplersCache). The registry
|
|
// pairs a live frontend sampler with one backend object for the frontend's whole
|
|
// lifetime: GetOrCreate replaces a pairing only when the frontend expired, entries are
|
|
// erased only when the frontend expired, and a unit's sampler cannot expire while the
|
|
// unit holds it - so owner-equality of the unit's current sampler against the snapshot
|
|
// proves the cached pointer is exactly what Find would return. A miss result is never
|
|
// cached: the backend object may not exist yet when the unit pass runs (the program
|
|
// pass creates it later in the same draw), and a cached miss would keep skipping the
|
|
// bind after it appears.
|
|
struct UnitSamplerLookupMemo {
|
|
WeakPtr<MG_State::GLState::SamplerObject> frontend{};
|
|
SamplerImpl::BackendSamplerObject* backend = nullptr;
|
|
};
|
|
static Array<UnitSamplerLookupMemo, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_unitSamplerLookupMemos;
|
|
|
|
static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend(
|
|
Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {
|
|
auto& memo = g_unitSamplerLookupMemos[static_cast<SizeT>(unit)];
|
|
if (memo.backend && OwnerEquals(memo.frontend, samplerObject)) {
|
|
return memo.backend;
|
|
}
|
|
auto* backendSamplerSlot = SamplerImpl::g_backendSamplerObjects.Find(samplerObject.get());
|
|
if (backendSamplerSlot && *backendSamplerSlot) {
|
|
memo.frontend = samplerObject;
|
|
memo.backend = backendSamplerSlot->get();
|
|
return memo.backend;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Puts each touched unit's frontend sampler object on the backend unit. Not part
|
|
// of the texture memo below (BindCurrentProgramWithResources may rewrite sampled
|
|
// units' samplers right after this runs - raw-depth-fetch substitution,
|
|
// per-program sampler objects); instead the walk carries its own memo:
|
|
//
|
|
// The walk is a pure function of WHICH sampler object each touched unit holds -
|
|
// covered by (context id, unit-bindings epoch, high-water mark; the epoch
|
|
// snapshot includes each unit's sampler object and bumps when the mark moves) -
|
|
// and replaying it as "do nothing" additionally requires the sampler bindings it
|
|
// left to still be on the driver, which the row compare against
|
|
// g_boundSamplersCache (the shadow every sampler bind routes through) proves.
|
|
// Any later writer - the program pass substituting a raw-depth sampler, a
|
|
// frontend sampler bind/unbind (epoch), the ES-context generation moving - lands
|
|
// in one of those inputs. A unit whose sampler had no backend object yet leaves
|
|
// its row untouched; the object is only created by the program pass, whose Bind
|
|
// moves the row and thereby re-opens this memo, so the miss cannot be latched.
|
|
static Uint64 g_unitSamplerWalkContextId = 0;
|
|
static Uint64 g_unitSamplerWalkEpoch = 0;
|
|
static Int g_unitSamplerWalkMaxUnit = -1;
|
|
static Uint g_unitSamplerWalkContextGeneration = 0;
|
|
static Bool g_unitSamplerWalkValid = false;
|
|
static decltype(SamplerImpl::g_boundSamplersCache) g_unitSamplerWalkRows{};
|
|
|
|
static void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys) {
|
|
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
|
const SizeT rowBytes =
|
|
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
|
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
|
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
|
g_unitSamplerWalkContextGeneration == g_backendContextGeneration &&
|
|
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
|
return;
|
|
}
|
|
g_unitSamplerWalkValid = false;
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
|
if (samplerObject) {
|
|
if (auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject)) {
|
|
backendSampler->Bind(unit);
|
|
}
|
|
} else {
|
|
// Symmetric with the bind above: a sampler object left on the unit by an earlier
|
|
// draw keeps being applied, and on a multisample texture - which takes no sampler
|
|
// object at all - the draw is rejected outright.
|
|
SamplerImpl::UnbindSampler(unit);
|
|
}
|
|
}
|
|
g_unitSamplerWalkContextId = keys.contextId;
|
|
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
|
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
|
g_unitSamplerWalkContextGeneration = g_backendContextGeneration;
|
|
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
|
g_unitSamplerWalkValid = true;
|
|
}
|
|
|
|
// Memo of the resolved per-unit TEXTURE bindings, so a steady-state draw loop stops
|
|
// re-deriving an answer nothing has invalidated (a Minecraft frame issues thousands of
|
|
// draws that touch none of the inputs below, and the resolution walks every binding slot
|
|
// of every touched unit three times).
|
|
//
|
|
// ResolveAndBindUnitTextures is a pure function of:
|
|
// * the GLContext identity - a never-reused id, since the counters below restart at 0 in
|
|
// a new context and a recreated one can land on the old heap address;
|
|
// * the per-unit bindings - which texture object every slot holds and which sampler
|
|
// object each unit carries, keyed as TextureImpl::CurrentUnitBindingsEpoch rather
|
|
// than as the texture bind generation: the generation also bumps on redundant
|
|
// re-binds (26.2 re-binds the unit's own sampler around every texture-unit switch),
|
|
// which would force a full re-resolution per draw for an answer that cannot have
|
|
// moved. The one input the old generation key covered that the epoch does not - a
|
|
// default texture gaining or losing an image with no bind moving - bumps the
|
|
// sampling-resolution generation below (SetInternalFormat -> BumpShapeVersion);
|
|
// * the touched-unit high-water mark (units above it have provably-empty slots);
|
|
// * the program that arbitrates two real textures aliased onto one native target -
|
|
// identity, plus the lifetime id because a freed program can be replaced at the same
|
|
// address, plus the backend-state version which moves on every relink and on every
|
|
// sampler-uniform unit assignment, plus the link status;
|
|
// * the sampling-resolution generation - any texture shape change or any sampler
|
|
// parameter change, i.e. everything mipmap-completeness is computed from, and
|
|
// completeness is what decides whether a texture is bound at all;
|
|
// * the ES context generation, because the backend texture ids and the driver's own
|
|
// binding state die with the context.
|
|
//
|
|
// A matching key only says the ANSWER is unchanged; replaying it as "do nothing" also
|
|
// requires the bindings it established to still be on the driver. Rather than enumerate
|
|
// every writer, the memo keeps the binding shadow it left and compares it: that shadow is
|
|
// already the authority every redundant-bind filter in this backend trusts, and every path
|
|
// that puts a texture on a unit behind this function's back maintains it - the upload
|
|
// path's scratch bind on the temp unit (BackendTextureObject::Bind out of
|
|
// SyncMipmapsToBackend), CopyTexSubImage2D and GenerateMipmap binding on the active unit,
|
|
// the glBindTextures fast path, and the self-scrub a BackendTextureObject performs when it
|
|
// is destroyed or respecified.
|
|
struct ResolvedTextureBindingMemo {
|
|
Bool valid = false;
|
|
Uint64 glContextId = 0;
|
|
Int maxTouchedUnit = -1;
|
|
Uint64 unitBindingsEpoch = 0;
|
|
Uint64 samplingResolutionGeneration = 0;
|
|
const void* program = nullptr;
|
|
Uint64 programLifetimeId = 0;
|
|
Uint32 programBackendStateVersion = 0;
|
|
Bool programLinked = false;
|
|
Uint contextGeneration = 0;
|
|
decltype(TextureImpl::g_boundTexturesCache) boundTextures{};
|
|
};
|
|
// Small per-PROGRAM memo set, not one global: the program is part of the key
|
|
// (it arbitrates aliased native targets), so a frame that cycles a handful of
|
|
// programs over UNCHANGED unit bindings - Sodium issues 62 glUseProgram per
|
|
// frame - invalidated a single memo on every switch and re-ran the full
|
|
// three-pass slot walk for an answer that had not moved. Entries are selected
|
|
// by raw program pointer; everything a recycled address could confuse is
|
|
// caught by the full key compare (lifetime id) exactly as before. Round-robin
|
|
// eviction; 4 entries cover the shader set a Minecraft-shaped frame cycles.
|
|
static Array<ResolvedTextureBindingMemo, 4> g_resolvedTextureBindingMemos;
|
|
static Uint32 g_resolvedTextureBindingMemoCursor = 0;
|
|
|
|
// Rebinds every frontend texture unit's textures (and sampler objects) on the
|
|
// backend context. Needed before draws AND compute dispatches: content syncs
|
|
// (SyncTextureObjectToBackend) bind scratch textures on the active unit as a
|
|
// side effect, so unit bindings must be re-established afterwards or shaders
|
|
// sample whatever texture the last sync left behind (e.g. Flywheel's depth
|
|
// pyramid downsample reading a stale unit-0 binding instead of the depth
|
|
// attachment).
|
|
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Units past the frontend's high-water mark have provably-empty slots.
|
|
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
|
|
|
// Entry selection by program pointer; a missing program takes the round-robin
|
|
// victim. WHICH entry is used is only a performance choice - correctness sits
|
|
// entirely in the full key + shadow compare below, unchanged from the single
|
|
// memo this set replaces.
|
|
const void* programKey = static_cast<const void*>(currentProgram.get());
|
|
ResolvedTextureBindingMemo* memoSlot = nullptr;
|
|
for (auto& candidate : g_resolvedTextureBindingMemos) {
|
|
if (candidate.valid && candidate.program == programKey) {
|
|
memoSlot = &candidate;
|
|
break;
|
|
}
|
|
}
|
|
if (!memoSlot) {
|
|
g_resolvedTextureBindingMemoCursor =
|
|
(g_resolvedTextureBindingMemoCursor + 1) % g_resolvedTextureBindingMemos.size();
|
|
memoSlot = &g_resolvedTextureBindingMemos[g_resolvedTextureBindingMemoCursor];
|
|
}
|
|
auto& memo = *memoSlot;
|
|
const SizeT shadowBytes =
|
|
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
|
|
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
|
|
const Bool keysMatch = memo.valid && memo.glContextId == keys.contextId &&
|
|
memo.maxTouchedUnit == maxTouchedUnit &&
|
|
memo.unitBindingsEpoch == unitBindingsEpoch &&
|
|
memo.samplingResolutionGeneration == keys.samplingGeneration &&
|
|
memo.program == programKey &&
|
|
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
|
|
memo.programBackendStateVersion ==
|
|
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
|
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
|
memo.contextGeneration == g_backendContextGeneration;
|
|
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
|
// snapshotted row count) matches.
|
|
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
|
|
shadowBytes) != 0) {
|
|
memo.valid = false;
|
|
if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) {
|
|
memo.glContextId = keys.contextId;
|
|
memo.maxTouchedUnit = maxTouchedUnit;
|
|
memo.unitBindingsEpoch = unitBindingsEpoch;
|
|
memo.samplingResolutionGeneration = keys.samplingGeneration;
|
|
memo.program = programKey;
|
|
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
|
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
|
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
|
memo.contextGeneration = g_backendContextGeneration;
|
|
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
|
memo.valid = true;
|
|
}
|
|
}
|
|
|
|
BindCurrentUnitSamplers(keys);
|
|
}
|
|
|
|
void BindCurrentTextures() {
|
|
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());
|
|
}
|
|
|
|
// Binds the current program's backend object and re-establishes its per-program
|
|
// resources: global UBO contents, uniform-block bindings, and sampler uniform
|
|
// units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the
|
|
// association must be rebuilt through the API). Compute dispatches depend on
|
|
// this as much as draws do — e.g. Flywheel's cull shader reads the
|
|
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
|
static void BindCurrentProgramWithResources(
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
|
const TextureImpl::DrawTextureSyncKeys& keys) {
|
|
if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// The twin SyncCurrentProgram just resolved for this draw; the registry
|
|
// Find only runs if the stash somehow does not match (defensive fallback).
|
|
PrgramImpl::BackendProgramObjectImpl* twin =
|
|
PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()
|
|
? PrgramImpl::g_currentDrawBackendProgram
|
|
: nullptr;
|
|
if (!twin) {
|
|
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
|
if (backendProgramSlot) {
|
|
twin = backendProgramSlot->get();
|
|
}
|
|
}
|
|
if (twin) {
|
|
auto& backendProgram = *twin;
|
|
backendProgram.Use();
|
|
|
|
// Global UBO: block index and binding-point assignment are cached at
|
|
// link time (CacheResourceLocations); re-upload only when the CPU shadow
|
|
// actually changed since the last upload for this program.
|
|
if (currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const Uint32 uboContentVersion = currentProgram->GetUBOContentVersion();
|
|
const SizeT uboSize = static_cast<SizeT>(currentProgram->GetUBOSize());
|
|
// Preferred path: write changed contents into a fresh slot of the
|
|
// shared persistent-mapped ring and bind it as a range. The GPU
|
|
// never reads bytes the CPU is writing, so the driver has no
|
|
// write-after-read hazard to resolve — the in-place glBufferSubData
|
|
// below forced Adreno into a ghost/stall on every uniform-dirtying
|
|
// draw (MC dirties uniforms every draw), which dominated frame time.
|
|
Bool ringBound = false;
|
|
if (BufferImpl::UboRingAvailable()) {
|
|
const SizeT bindSize =
|
|
std::max(uboSize, static_cast<SizeT>(backendProgram.GetGlobalUboBackendBlockSize()));
|
|
const Uint64 frameSerial = CurrentFrameSerial();
|
|
auto& ringSlot = backendProgram.GetGlobalUboRingAllocation();
|
|
Bool slotValid = ringSlot.ringGeneration == BufferImpl::UboRingGeneration() &&
|
|
ringSlot.frameSerial == frameSerial &&
|
|
ringSlot.contentVersion == uboContentVersion;
|
|
if (!slotValid) {
|
|
SizeT offset = 0;
|
|
if (BufferImpl::UboRingAllocate(bindSize, offset)) {
|
|
std::memcpy(static_cast<Uint8*>(BufferImpl::UboRingMappedPtr()) + offset,
|
|
currentProgram->MapUBO(), uboSize);
|
|
ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial,
|
|
offset};
|
|
slotValid = true;
|
|
}
|
|
}
|
|
if (slotValid) {
|
|
BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, BufferImpl::UboRingBufferId(),
|
|
static_cast<GLintptr>(ringSlot.offset),
|
|
static_cast<GLsizeiptr>(bindSize));
|
|
ringBound = true;
|
|
}
|
|
}
|
|
if (!ringBound) {
|
|
// Fallback (no EXT_buffer_storage / fences, or ring creation
|
|
// failed): the original in-place upload.
|
|
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
|
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
|
currentProgram->MapUBO());
|
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
|
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
|
}
|
|
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0,
|
|
backendProgram.GetBackendGlobalUBOId());
|
|
}
|
|
}
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("UpdateUBO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Normal UBOs: backend block indices and glUniformBlockBinding
|
|
// assignments are cached at link time; per draw only the buffer
|
|
// bindings are re-established (they follow frontend binding points).
|
|
const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices();
|
|
const auto uboCount = static_cast<Int>(blockBackendIndices.size());
|
|
// Pre-loop epoch read; the per-resource drawCleanEpoch stamps below
|
|
// follow the same stamp-the-pre-read-value rule as the VAO memo
|
|
// (Managers.h documents the contract and the mutation-site list).
|
|
const Uint64 bufferEpoch =
|
|
uboCount > 0 ? BufferImpl::CurrentBufferMutationEpoch() : 0;
|
|
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
|
|
for (Int i = 0; i < uboCount; ++i) {
|
|
++lastUBOBinding;
|
|
if (blockBackendIndices[i] < 0) {
|
|
continue;
|
|
}
|
|
|
|
// Connect buffer to backend binding point
|
|
auto binding = currentProgram->GetUniformBlockBinding(i);
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);
|
|
auto& bufferObj = point.GetBoundObject();
|
|
auto range = point.GetRange();
|
|
|
|
if (bufferObj) {
|
|
// Clean-probe (or epoch-stamped) fast path before the full
|
|
// EnsureBufferResource: an unchanged static UBO needs no
|
|
// storage work, only its binding re-established below.
|
|
auto* backendResource = BufferImpl::GetBufferResource(bufferObj.get());
|
|
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
|
|
if (backendResource &&
|
|
BufferImpl::IsBufferDrawClean(bufferObj.get(), backendResource)) {
|
|
backendResource->drawCleanEpoch = bufferEpoch;
|
|
} else {
|
|
backendResource = BufferImpl::EnsureBufferResource(bufferObj);
|
|
}
|
|
}
|
|
if (backendResource && backendResource->id != 0) {
|
|
// glBindBufferBase/Range set the generic GL_UNIFORM_BUFFER binding
|
|
// as a side effect, so no separate BindBufferId is needed here.
|
|
if (range.end == 0) {
|
|
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding,
|
|
backendResource->id);
|
|
} else {
|
|
BufferImpl::BindBufferRangeCached(
|
|
GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id,
|
|
(GLintptr)range.start, (GLintptr)(range.end - range.start));
|
|
}
|
|
} else {
|
|
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
|
|
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
|
|
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
|
|
// program declares, and no other program's blocks live there. Both the draw and
|
|
// the dispatch path reach this, which is what a compute-shader counter needs.
|
|
if (!backendProgram.GetAtomicCounterBindings().empty()) {
|
|
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
|
|
backendProgram.GetAtomicCounterEsslBindingTop());
|
|
}
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Sampler unit binding: backend locations are cached at link time;
|
|
// glUniform1i is program state, so it is only re-issued when the
|
|
// frontend-assigned unit differs from what this program last saw.
|
|
//
|
|
// The whole pass sits behind the per-twin SamplerPassMemo (see its
|
|
// declaration for the invalidation enumeration): while the keys hold
|
|
// and every previously touched unit's sampler-shadow row is exactly
|
|
// what this pass last left there, re-running it is a provable no-op.
|
|
auto& samplerPassMemo = backendProgram.GetSamplerPassMemo();
|
|
const Uint32 programBackendStateVersion = currentProgram->GetBackendStateVersion();
|
|
Bool samplerPassClean =
|
|
samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId &&
|
|
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
|
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
|
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
|
samplerPassMemo.textureContextGeneration == g_backendContextGeneration;
|
|
if (samplerPassClean) {
|
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
|
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
|
samplerPassMemo.rows[i]) {
|
|
samplerPassClean = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!samplerPassClean) {
|
|
constexpr SizeT kMaxMemoEntries =
|
|
PrgramImpl::BackendProgramObjectImpl::SamplerPassMemo::kMaxEntries;
|
|
samplerPassMemo.valid = false;
|
|
samplerPassMemo.count = 0;
|
|
Bool memoisable = true;
|
|
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
|
|
const auto unit =
|
|
currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation);
|
|
if (unit == -1) continue;
|
|
// Record the touched unit for the memo; a pass touching more
|
|
// units than the memo can carry (or an out-of-range unit)
|
|
// simply never memoises.
|
|
if (memoisable && samplerPassMemo.count < kMaxMemoEntries && unit >= 0 &&
|
|
unit < static_cast<Int>(SamplerImpl::g_boundSamplersCache.size())) {
|
|
samplerPassMemo.units[samplerPassMemo.count++] = static_cast<Uint8>(unit);
|
|
} else {
|
|
memoisable = false;
|
|
}
|
|
if (samplerBinding.lastAssignedUnit != unit) {
|
|
g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit);
|
|
samplerBinding.lastAssignedUnit = unit;
|
|
}
|
|
|
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
auto& samplerObject = textureUnit.GetSamplerObject();
|
|
const auto& texture2D =
|
|
textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
|
|
|
// ES has no per-texture/sampler LOD bias, so the transpiled ESSL folds
|
|
// it in from a uniform (PrgramImpl::EmulateTextureLodBias). A bound
|
|
// sampler object overrides the texture's own sampler state, as in GL.
|
|
if (samplerBinding.lodBiasLocation >= 0) {
|
|
Float lodBias = 0.0f;
|
|
if (samplerObject) {
|
|
lodBias = samplerObject->GetLodBias();
|
|
} else if (const auto sampledTarget =
|
|
SamplerUniformTextureTarget(samplerBinding.uniformType);
|
|
sampledTarget != TextureTarget::Unknown) {
|
|
const auto& boundTexture =
|
|
textureUnit.GetBindingSlot(sampledTarget).GetBoundObject();
|
|
if (boundTexture && boundTexture->GetSamplerObject()) {
|
|
lodBias = boundTexture->GetSamplerObject()->GetLodBias();
|
|
}
|
|
}
|
|
if (lodBias != samplerBinding.lastAssignedLodBias) {
|
|
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
|
|
samplerBinding.lastAssignedLodBias = lodBias;
|
|
}
|
|
}
|
|
const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject =
|
|
&samplerObject;
|
|
if (!*rawDepthSamplerObject && texture2D) {
|
|
rawDepthSamplerObject = &texture2D->GetSamplerObject();
|
|
}
|
|
|
|
if (samplerBinding.uniformType == GL_SAMPLER_2D && texture2D &&
|
|
NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) {
|
|
GetRawDepthFetchSampler()->Bind(unit);
|
|
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
|
|
} else if (samplerObject) {
|
|
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
|
|
if (!backendSampler) {
|
|
auto& backendObj =
|
|
SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
|
|
}
|
|
backendSampler = backendObj.get();
|
|
}
|
|
backendSampler->SyncToBackend(samplerObject);
|
|
// Syncing the object's parameters is not the same as putting it on the
|
|
// unit: without this the driver kept sampling with the texture's own
|
|
// parameters and every sampler object was inert.
|
|
backendSampler->Bind(unit);
|
|
} else {
|
|
SamplerImpl::UnbindSampler(unit);
|
|
}
|
|
}
|
|
if (memoisable) {
|
|
// Snapshot the rows AFTER the pass: they are exactly what a
|
|
// clean replay must find untouched.
|
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
|
samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]];
|
|
}
|
|
samplerPassMemo.contextId = keys.contextId;
|
|
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
|
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
|
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
|
samplerPassMemo.textureContextGeneration = g_backendContextGeneration;
|
|
samplerPassMemo.valid = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
g_GLESFuncs.glUseProgram(0);
|
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
|
MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Raw pointer: only ever used inside one GL entry point after PrepareForDraw/
|
|
// PrepareForCompute, where the current program (and therefore its registry twin)
|
|
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
|
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
|
return nullptr;
|
|
}
|
|
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
|
return PrgramImpl::g_currentDrawBackendProgram;
|
|
}
|
|
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
|
|
return backendProgramSlot->get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void SetCurrentBaseInstance(Uint32 baseInstance) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetBaseInstance(baseInstance);
|
|
}
|
|
}
|
|
|
|
void SetCurrentDrawID(Uint32 drawId) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetDrawID(drawId);
|
|
}
|
|
}
|
|
|
|
void SetCurrentBaseVertex(Int32 baseVertex) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetBaseVertex(baseVertex);
|
|
}
|
|
}
|
|
|
|
Bool CurrentProgramReadsDrawID() {
|
|
const auto program = GetCurrentBackendProgram();
|
|
return program != nullptr && program->ReadsDrawID();
|
|
}
|
|
|
|
Bool CurrentProgramReadsBaseVertex() {
|
|
const auto program = GetCurrentBackendProgram();
|
|
return program != nullptr && program->ReadsBaseVertex();
|
|
}
|
|
|
|
// The two questions above, asked from BEFORE PrepareForDraw - where neither can be
|
|
// answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw
|
|
// already synced, and a twin from before a relink still carries the previous link's
|
|
// uniform locations, so "no" there means "not known yet" at least as often as it
|
|
// means no. The multi-draw compute tier has to decide whether to flatten a batch
|
|
// before PrepareForDraw runs (its dispatch cannot come after the draw state), and
|
|
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
|
|
// so an unanswerable program counts as needing them.
|
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
|
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
|
const auto program = GetCurrentBackendProgram();
|
|
if (!currentProgram || program == nullptr ||
|
|
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
|
return true;
|
|
}
|
|
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
|
}
|
|
|
|
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
|
// See the block comment in Managers.h for what this is and why. Here is the state half: one
|
|
// replay pass per DISTINCT viewport state, each pushing that state onto the ES context's one
|
|
// viewport / one scissor / one depth range and telling the fragment gate which indices it
|
|
// serves.
|
|
namespace ViewportRoutingImpl {
|
|
// One replay pass: the state to push, and the set of gl_ViewportIndex values whose
|
|
// fragments this pass is allowed to keep.
|
|
struct RoutingPass {
|
|
IntVec4 viewport{};
|
|
IntVec4 scissorBox{};
|
|
FloatVec2 depthRange{};
|
|
Bool scissorTest = false;
|
|
Uint32 indexMask = 0;
|
|
};
|
|
|
|
static constexpr Uint32 kAllViewportsMask =
|
|
(RenderStateParameters::MAX_VIEWPORTS >= 32)
|
|
? 0xFFFFFFFFu
|
|
: ((1u << RenderStateParameters::MAX_VIEWPORTS) - 1u);
|
|
|
|
// The plan for the draw currently being issued. A file-scope buffer rather than a return
|
|
// value because Begin/Apply/End are three calls around a draw the caller writes, and a
|
|
// fixed array of 16 keeps it allocation-free on a path that is per draw. NOT re-entrant,
|
|
// which is a property of the call sites and not an accident: every wrap in this file and
|
|
// in MultiDraw.cpp is around the innermost native glDraw*, so no replay can begin inside
|
|
// another - and a multi-draw tier that replayed its whole loop would be nesting.
|
|
static Array<RoutingPass, RenderStateParameters::MAX_VIEWPORTS> g_passes{};
|
|
static Uint g_passCount = 0;
|
|
static PrgramImpl::BackendProgramObjectImpl* g_routedProgram = nullptr;
|
|
|
|
// What index `i` actually rasterizes against, resolved exactly the way SyncRenderState
|
|
// resolves index 0 - including both substitutions it makes, which are not cosmetic:
|
|
//
|
|
// * a viewport of zero extent means "the application has never called glViewport", and
|
|
// GL's initial viewport is the whole surface, which the frontend cannot spell before
|
|
// a surface exists;
|
|
// * a scissor rectangle is read through the WRITTEN flag and not through its extent,
|
|
// because glScissor(0, 0, 0, 0) is a legal request meaning "reject every fragment"
|
|
// and is byte-identical to the never-written default that means the opposite.
|
|
//
|
|
// Resolving them here rather than deferring to SyncRenderState is what makes the grouping
|
|
// below correct: two indices that differ only in a field that resolves to the same
|
|
// rectangle really do rasterize identically and must share one pass.
|
|
static RoutingPass ResolveIndexState(const RenderStateParameters& parameters, Uint index,
|
|
Int surfaceWidth, Int surfaceHeight) {
|
|
RoutingPass pass;
|
|
const FloatVec4& viewport = parameters.Viewports[index];
|
|
pass.viewport = IntVec4(static_cast<Int>(std::lround(viewport.x())),
|
|
static_cast<Int>(std::lround(viewport.y())),
|
|
static_cast<Int>(std::lround(viewport.z())),
|
|
static_cast<Int>(std::lround(viewport.w())));
|
|
if ((pass.viewport.z() <= 0 || pass.viewport.w() <= 0) && surfaceWidth > 0 && surfaceHeight > 0) {
|
|
pass.viewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
pass.scissorBox = parameters.ScissorBoxes[index];
|
|
if ((parameters.ScissorBoxWrittenMask & (1u << index)) == 0 && surfaceWidth > 0 &&
|
|
surfaceHeight > 0) {
|
|
pass.scissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
|
|
}
|
|
pass.depthRange = parameters.DepthRanges[index];
|
|
pass.scissorTest = (parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
|
return pass;
|
|
}
|
|
|
|
static Bool SameState(const RoutingPass& a, const RoutingPass& b) {
|
|
return a.viewport == b.viewport && a.scissorBox == b.scissorBox &&
|
|
a.depthRange == b.depthRange && a.scissorTest == b.scissorTest;
|
|
}
|
|
} // namespace ViewportRoutingImpl
|
|
|
|
Uint BeginViewportRoutingPasses() {
|
|
using namespace ViewportRoutingImpl;
|
|
g_passCount = 1;
|
|
g_routedProgram = nullptr;
|
|
|
|
// The whole emulation behind one static load, for every application that has never built
|
|
// a program writing gl_ViewportIndex - which is all of them but the conformance suite.
|
|
// Without it every draw in the process would pay GetCurrentBackendProgram's chain of
|
|
// frontend lookups for an answer that cannot change.
|
|
if (!g_anyProgramRoutesViewportIndex) {
|
|
return 1;
|
|
}
|
|
|
|
auto* program = GetCurrentBackendProgram();
|
|
if (program == nullptr || !program->RoutesViewportIndex()) {
|
|
return 1;
|
|
}
|
|
g_routedProgram = program;
|
|
// The gate reads zero until something writes it, and a zero mask discards every fragment.
|
|
// So this is not an optimization that can be skipped in the one-pass case - it is what
|
|
// keeps a routing program drawing at all.
|
|
program->SetViewportPassMask(kAllViewportsMask);
|
|
|
|
// Replaying multiplies every side effect the vertex and geometry stages have, and the
|
|
// fragment gate can only undo the ones that happen in the FRAGMENT stage. Transform
|
|
// feedback records per emitted primitive, so a replayed draw would write its vertices N
|
|
// times; rasterizer discard means there are no fragments to gate at all, so replaying
|
|
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
|
|
// open gate, i.e. to the pre-emulation behaviour, rather than to wrong data.
|
|
if (MG_State::pGLContext->IsTransformFeedbackActive() ||
|
|
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
|
return 1;
|
|
}
|
|
|
|
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
|
Int surfaceWidth = 0;
|
|
Int surfaceHeight = 0;
|
|
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
|
|
surfaceWidth = 0;
|
|
surfaceHeight = 0;
|
|
}
|
|
|
|
Uint count = 0;
|
|
for (Uint index = 0; index < RenderStateParameters::MAX_VIEWPORTS; ++index) {
|
|
const RoutingPass resolved =
|
|
ResolveIndexState(parameters, index, surfaceWidth, surfaceHeight);
|
|
Uint existing = 0;
|
|
for (; existing < count; ++existing) {
|
|
if (SameState(g_passes[existing], resolved)) break;
|
|
}
|
|
if (existing == count) {
|
|
g_passes[count] = resolved;
|
|
++count;
|
|
}
|
|
g_passes[existing].indexMask |= (1u << index);
|
|
}
|
|
|
|
// One group is the overwhelmingly common case - it is what glViewport, glScissor and
|
|
// glDepthRange leave behind, because ARB_viewport_array defines all three as writing
|
|
// EVERY index. The mask is already open and index 0's state is what SyncRenderState
|
|
// pushed, so there is nothing to replay and nothing to restore.
|
|
if (count <= 1) {
|
|
g_passCount = 1;
|
|
return 1;
|
|
}
|
|
g_passCount = count;
|
|
return count;
|
|
}
|
|
|
|
void ApplyViewportRoutingPass(Uint pass) {
|
|
using namespace ViewportRoutingImpl;
|
|
if (pass >= g_passCount || g_routedProgram == nullptr) {
|
|
return;
|
|
}
|
|
const RoutingPass& entry = g_passes[pass];
|
|
g_GLESFuncs.glViewport(entry.viewport.x(), entry.viewport.y(), entry.viewport.z(),
|
|
entry.viewport.w());
|
|
g_GLESFuncs.glScissor(entry.scissorBox.x(), entry.scissorBox.y(), entry.scissorBox.z(),
|
|
entry.scissorBox.w());
|
|
// ES has one scissor-test enable where GL has sixteen, so the per-index bit becomes a
|
|
// per-pass glEnable/glDisable. This is the half DirectVulkan cannot do at all (Vulkan has
|
|
// no per-viewport scissor toggle either and has to widen a disabled index's rectangle to
|
|
// the whole framebuffer instead); here the rectangle stays honest.
|
|
entry.scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
|
g_GLESFuncs.glDepthRangef(entry.depthRange.x(), entry.depthRange.y());
|
|
g_routedProgram->SetViewportPassMask(entry.indexMask);
|
|
}
|
|
|
|
void EndViewportRoutingPasses(Uint passCount) {
|
|
using namespace ViewportRoutingImpl;
|
|
if (passCount <= 1) {
|
|
// Nothing was pushed and the mask is already open; leaving the shadow alone here is
|
|
// what keeps a non-routing draw at exactly its previous cost.
|
|
g_routedProgram = nullptr;
|
|
return;
|
|
}
|
|
if (g_routedProgram != nullptr) {
|
|
// Any draw that reaches the driver without going through a replay - an internal blit,
|
|
// or a path this emulation has not been taught about - must not inherit the last
|
|
// pass's mask and paint nothing.
|
|
g_routedProgram->SetViewportPassMask(kAllViewportsMask);
|
|
}
|
|
g_routedProgram = nullptr;
|
|
g_passCount = 0;
|
|
// The viewport, scissor, scissor-test enable and depth range now on the ES context belong
|
|
// to the last replay pass, and the shadow SyncRenderState diffs against does not know it.
|
|
// A full resync is the honest repair and costs one state push on the next draw, which
|
|
// only a viewport-routing workload ever pays.
|
|
RenderStateImpl::InvalidateSyncedRenderState();
|
|
}
|
|
|
|
static Bool SupportsNativeIndirectDraws() {
|
|
return g_GLESCapabilities.SupportsDrawIndirect;
|
|
}
|
|
|
|
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
|
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
|
|
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
|
|
// for the per-command baseInstance, which is CPU-authored, to feed the mg_BaseInstance
|
|
// shader emulation. Without GL_EXT_base_instance a non-zero baseInstance in the command is
|
|
// technically undefined in ES; mobile drivers ignore the reserved word, instanced-array
|
|
// fetches were never baseInstance-offset here anyway, and the CPU fallback cannot see
|
|
// GPU-written command fields at all - so native is never worse. ANGLE-on-Vulkan instead
|
|
// leaks the word into gl_InstanceID (it becomes vkCmdDrawIndexedIndirect's firstInstance);
|
|
// the shader rewrite compensates by rebasing gl_InstanceID during these draws when
|
|
// IndirectDrawInstanceIdIncludesBaseInstance is set (PromoteDrawParameterGlobalsToUniforms).
|
|
// Only client-memory commands take the CPU per-command loop.
|
|
static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes,
|
|
SizeT commandOffset,
|
|
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
|
GLsizei drawcount, GLsizei stride, const char* label) {
|
|
(void)label;
|
|
// An indirect command's firstIndex/count live in GPU memory, so the substitution has
|
|
// to rewrite the whole element array buffer rather than this draw's range - which is
|
|
// exactly what it does when no CPU-known count is handed to it. Held for the whole
|
|
// command loop so every command in the batch reads the rewritten copy.
|
|
//
|
|
// firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not
|
|
// survive is the type and the element size, which are re-taken from the substitution
|
|
// below for both the native and the CPU-unrolled path.
|
|
const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr);
|
|
if (!restart.DrawIsValid()) return;
|
|
type = restart.IndexType();
|
|
indexSize = MG_Util::GetGLTypeSize(type);
|
|
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
|
if (useNative) {
|
|
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
|
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
|
|
const auto backendProgram = GetCurrentBackendProgram();
|
|
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
|
if (paramsBinding >= 0) {
|
|
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
|
|
if (resource && resource->id != 0) {
|
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
|
resource->id);
|
|
}
|
|
}
|
|
// gl_BaseVertex has no SSBO view of its own: the command's baseVertex word is read
|
|
// from the CPU shadow, so a command whose baseVertex a compute shader wrote this
|
|
// frame is not observable here (baseInstance is, through the view above). Feeding
|
|
// the stale-but-usually-correct shadow beats leaving the uniform at the previous
|
|
// draw's value, which is what a program reading gl_BaseVertex saw before.
|
|
const Bool feedBaseVertex = CurrentProgramReadsBaseVertex();
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
if (paramsBinding >= 0 && backendProgram) {
|
|
// baseInstance is the 5th word of DrawElementsIndirectCommand.
|
|
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 16) / 4));
|
|
if (feedBaseVertex) {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
}
|
|
} else {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
}
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
|
|
});
|
|
}
|
|
} else {
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
DrawElementsIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
if (cmd.count == 0 || cmd.instanceCount == 0) {
|
|
continue;
|
|
}
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
SetCurrentBaseVertex(cmd.baseVertex);
|
|
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
|
|
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
|
|
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
|
|
});
|
|
}
|
|
}
|
|
SetCurrentDrawID(0);
|
|
SetCurrentBaseInstance(0);
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset,
|
|
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
|
GLsizei drawcount, GLsizei stride, const char* label) {
|
|
(void)label;
|
|
// DrawArraysIndirectCommand has no baseVertex word, so gl_BaseVertex is zero for every
|
|
// command here. Written BEFORE the draws, not merely restored after them: the previous
|
|
// draw is what leaves a stale value, and restoring afterwards would only protect the
|
|
// NEXT draw while these commands ran with the stale one.
|
|
SetCurrentBaseVertex(0);
|
|
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
|
if (useNative) {
|
|
const auto backendProgram = GetCurrentBackendProgram();
|
|
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
|
|
if (paramsBinding >= 0) {
|
|
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
|
|
if (resource && resource->id != 0) {
|
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
|
|
resource->id);
|
|
}
|
|
}
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
const SizeT cmdByteOffset = commandOffset + static_cast<SizeT>(i) * stride;
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
if (paramsBinding >= 0 && backendProgram) {
|
|
// baseInstance is the 4th word of DrawArraysIndirectCommand.
|
|
backendProgram->SetBaseInstanceWordIndex(static_cast<Int32>((cmdByteOffset + 12) / 4));
|
|
} else {
|
|
DrawArraysIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
}
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast<const void*>(cmdByteOffset));
|
|
});
|
|
}
|
|
} else {
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
DrawArraysIndirectCommand cmd{};
|
|
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
|
|
if (cmd.count == 0 || cmd.instanceCount == 0) {
|
|
continue;
|
|
}
|
|
SetCurrentDrawID(static_cast<Uint32>(i));
|
|
SetCurrentBaseInstance(cmd.baseInstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
|
|
static_cast<GLsizei>(cmd.count),
|
|
static_cast<GLsizei>(cmd.instanceCount));
|
|
});
|
|
}
|
|
}
|
|
SetCurrentDrawID(0);
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
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.
|
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
|
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
|
|
|
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
|
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
|
TextureImpl::SyncImageTextureBindings();
|
|
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
|
PrgramImpl::SyncCurrentProgram(currentProgram);
|
|
|
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
|
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);
|
|
}
|
|
|
|
GLuint GetBackendProgramId(GLuint program) {
|
|
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
|
MGLOG_E_ONCE("Invalid frontend program object: %u", program);
|
|
return 0;
|
|
}
|
|
|
|
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
|
if (!programObject) {
|
|
MGLOG_E_ONCE("Program object %u is null.", program);
|
|
return 0;
|
|
}
|
|
|
|
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
|
|
auto& backendObj =
|
|
backendProgramSlot ? *backendProgramSlot : PrgramImpl::g_backendProgramObjects.GetOrCreate(programObject);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<PrgramImpl::BackendProgramObjectImpl>();
|
|
}
|
|
if (!backendObj->GetBackendProgramId()) {
|
|
backendObj->SyncToBackend(programObject);
|
|
}
|
|
return backendObj->GetBackendProgramId();
|
|
}
|
|
|
|
void Clear(GLbitfield mask) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
// A colour clear is exactly the operation that is allowed to write a widened
|
|
// attachment's alpha - it is what puts the 1.0 there that every later draw is masked
|
|
// away from. SyncCurrentFBO ran first, so g_alphaWidenedDrawBufferMask already describes
|
|
// the framebuffer this clear will land on.
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/(mask & GL_COLOR_BUFFER_BIT) != 0);
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
// Debug-only diagnostics: is each offscreen clear complete and unscissored?
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
{
|
|
static int diagCount = 0;
|
|
GLint fboId = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fboId);
|
|
if (fboId != 0 && diagCount++ < 900) {
|
|
GLint color0 = 0, depthName = 0, box[4] = {0};
|
|
GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
|
|
GLboolean cmask[4] = {0};
|
|
GLfloat cc[4] = {0};
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &color0);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &depthName);
|
|
g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, box);
|
|
g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, cmask);
|
|
g_GLESFuncs.glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
|
|
MGLOG_D("CLEAR fbo=%d color0=%d depth=%d mask=0x%x scissor=%d box=(%d,%d,%d,%d) cmask=%d%d%d%d cc=(%g,%g,%g,%g)",
|
|
fboId, color0, depthName, mask, (int)scissor, box[0], box[1], box[2], box[3], (int)cmask[0],
|
|
(int)cmask[1], (int)cmask[2], (int)cmask[3], cc[0], cc[1], cc[2], cc[3]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// GLES clamps the glClearColor state to [0,1] (desktop GL keeps it unclamped
|
|
// for float color buffers). Minecraft's OIT clears its depth-bounds RGBA32F
|
|
// target to -FLT_MAX as the MAX-blend identity, so an out-of-range clear
|
|
// color must go through glClearBufferfv, which GLES does not clamp.
|
|
GLbitfield remainingMask = mask;
|
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;
|
|
const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f ||
|
|
cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f;
|
|
// A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE
|
|
// clear colour to every draw buffer - so a framebuffer that mixes a widened
|
|
// attachment with a native one cannot be served by doctoring glClearColor. Take the
|
|
// same per-draw-buffer glClearBufferfv route the out-of-range case already uses and
|
|
// substitute the alpha only where it belongs. Scissor and the colour write mask apply
|
|
// to glClearBufferfv exactly as they do to glClear, so a scissored clear stays
|
|
// scissored and an application that masked alpha off still gets its way (the storage
|
|
// then keeps the 1.0 an earlier clear left, which is the same answer).
|
|
//
|
|
// glClearBufferfv on an INTEGER colour buffer is GL_INVALID_OPERATION, so a
|
|
// framebuffer with one of those as a draw buffer keeps plain glClear - which ES
|
|
// leaves undefined for integer colour buffers anyway, and which an application that
|
|
// wants a defined answer must replace with glClearBufferuiv/iv (those DO substitute
|
|
// the widened alpha). The out-of-range trigger is left exactly as it was.
|
|
const Uint32 widenedDrawBuffers = FramebufferImpl::g_alphaWidenedDrawBufferMask;
|
|
const Bool widenedColorClear =
|
|
widenedDrawBuffers != 0 && FramebufferImpl::g_integerColorDrawBufferMask == 0;
|
|
GLint clearDrawFbo = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &clearDrawFbo);
|
|
if ((outOfRange || widenedColorClear) && clearDrawFbo != 0) {
|
|
GLint maxDrawBuffers = 0;
|
|
GLint clearedCount = 0;
|
|
GLint firstDb = -1;
|
|
g_GLESFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
|
for (GLint i = 0; i < maxDrawBuffers; ++i) {
|
|
GLint db = GL_NONE;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_BUFFER0 + static_cast<GLenum>(i), &db);
|
|
if (i == 0) firstDb = db;
|
|
if (db != GL_NONE) {
|
|
const Bool widened = i < 32 && (widenedDrawBuffers & (1u << i)) != 0;
|
|
const GLfloat value[4] = {cc.x(), cc.y(), cc.z(), widened ? 1.0f : cc.w()};
|
|
g_GLESFuncs.glClearBufferfv(GL_COLOR, i, value);
|
|
++clearedCount;
|
|
}
|
|
}
|
|
remainingMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
|
(void)clearedCount;
|
|
(void)firstDb;
|
|
// Debug-only diagnostics: verify the unclamped clear actually landed.
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
{
|
|
static int diagCount = 0;
|
|
if (diagCount++ < 20) {
|
|
const GLenum clrErr = g_GLESFuncs.glGetError();
|
|
GLint prevRead = 0, prevPbo = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead);
|
|
g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPbo);
|
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)clearDrawFbo);
|
|
GLint prevReadBuf = GL_COLOR_ATTACHMENT0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &prevReadBuf);
|
|
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
GLfloat rb[4] = {0};
|
|
g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb);
|
|
const GLenum rbErr = g_GLESFuncs.glGetError();
|
|
const auto& feFbo =
|
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
|
int feDb0 = -1, feDb1 = -1;
|
|
Uint feIdx = 0, feVer = 0;
|
|
if (feFbo) {
|
|
feIdx = feFbo->GetExternalIndex();
|
|
feVer = feFbo->GetObjectVersion();
|
|
feDb0 = (int)feFbo->GetDrawBuffers()[0];
|
|
feDb1 = (int)feFbo->GetDrawBuffers()[1];
|
|
}
|
|
MGLOG_D("CLEARV fbo=%d clrErr=0x%x rbErr=0x%x cc.x=%g cleared=%d firstDb=0x%x prevReadBuf=0x%x "
|
|
"feFbo=%u feVer=%u feDb=[%d,%d] stored=(%g,%g,%g,%g)",
|
|
clearDrawFbo, clrErr, rbErr, cc.x(), clearedCount, firstDb, prevReadBuf, feIdx, feVer,
|
|
feDb0, feDb1, rb[0], rb[1], rb[2], rb[3]);
|
|
g_GLESFuncs.glReadBuffer(static_cast<GLenum>(prevReadBuf));
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevRead);
|
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, (GLuint)prevPbo);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
if (remainingMask != 0) {
|
|
g_GLESFuncs.glClear(remainingMask);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Arbitrary-index primitive restart
|
|
//
|
|
// Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only
|
|
// ever restarts on the all-ones value of the index type. When the two agree - which
|
|
// includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at
|
|
// SyncRenderState is the whole implementation and nothing here does any work. When they
|
|
// disagree the index DATA is rewritten into a scratch element array buffer.
|
|
//
|
|
// This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI
|
|
// and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp
|
|
// already call out - so an application that merely asked for a legal desktop feature
|
|
// died rather than got an error.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
struct RestartScratchBuffer {
|
|
Uint id = 0;
|
|
SizeT capacity = 0;
|
|
};
|
|
|
|
RestartScratchBuffer g_restartIndices;
|
|
Vector<Uint8> g_restartStaging;
|
|
|
|
// Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY
|
|
// draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of
|
|
// trying: a draw that renders nothing is recoverable, a stall of that size is not.
|
|
constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB
|
|
|
|
// The index type one step wider than this one, or 0 when there is none. Widening is how
|
|
// an all-ones value that is a REAL vertex index keeps its meaning while the all-ones
|
|
// value of the destination type serves as the restart sentinel: a source that cannot
|
|
// spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16.
|
|
GLenum WiderIndexType(GLenum indexType) {
|
|
switch (indexType) {
|
|
case GL_UNSIGNED_BYTE: return GL_UNSIGNED_SHORT;
|
|
case GL_UNSIGNED_SHORT: return GL_UNSIGNED_INT;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) {
|
|
switch (indexSize) {
|
|
case 1: return source[i];
|
|
case 2: {
|
|
Uint16 narrow = 0;
|
|
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
|
return narrow;
|
|
}
|
|
default: {
|
|
Uint32 wide = 0;
|
|
std::memcpy(&wide, source + i * 4, sizeof(wide));
|
|
return wide;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) {
|
|
switch (indexSize) {
|
|
case 1: destination[i] = static_cast<Uint8>(value); break;
|
|
case 2: {
|
|
const Uint16 narrow = static_cast<Uint16>(value);
|
|
std::memcpy(destination + i * 2, &narrow, sizeof(narrow));
|
|
break;
|
|
}
|
|
default: std::memcpy(destination + i * 4, &value, sizeof(value)); break;
|
|
}
|
|
}
|
|
|
|
// True when any index in the range already holds the type's all-ones value, i.e. when
|
|
// that value is doing double duty as a real vertex index and so cannot also be the
|
|
// restart sentinel. Only asked on the rare substitution path.
|
|
Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize,
|
|
Uint32 fixedMax) {
|
|
for (SizeT i = 0; i < indexCount; ++i) {
|
|
if (ReadIndex(source, i, indexSize) == fixedMax) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Copies index data, replacing every occurrence of the application's restart index with
|
|
// the all-ones value of the DESTINATION type - the only one GLES restarts on. The
|
|
// destination may be wider than the source, which is what makes the copy lossless: a
|
|
// source index equal to the source's all-ones value zero-extends to something the wider
|
|
// sentinel can never equal, so it stays the vertex it was.
|
|
//
|
|
// Same width in and out is the degenerate case, used when the source contains no
|
|
// all-ones index at all (nothing to protect) or when there is no wider type to move to.
|
|
// In that last case only - a GL_UNSIGNED_INT stream that really does use index
|
|
// 0xFFFFFFFF while asking to restart on a different one - a legal index has to be
|
|
// nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it;
|
|
// it is the one input this feature cannot represent.
|
|
void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize,
|
|
SizeT destinationIndexSize, Uint32 applicationRestartIndex,
|
|
Uint32 destinationFixedMax, Vector<Uint8>& output) {
|
|
output.resize(indexCount * destinationIndexSize);
|
|
for (SizeT i = 0; i < indexCount; ++i) {
|
|
Uint32 value = ReadIndex(source, i, sourceIndexSize);
|
|
if (value == applicationRestartIndex) {
|
|
value = destinationFixedMax;
|
|
} else if (value == destinationFixedMax) {
|
|
// Only reachable when no widening was possible; see above.
|
|
value = destinationFixedMax - 1;
|
|
}
|
|
WriteIndex(output.data(), i, destinationIndexSize, value);
|
|
}
|
|
}
|
|
|
|
// Whole-buffer respecify through the manager-wide staging target, so binding it
|
|
// disturbs no VAO state. glBufferData orphans the previous store, so the upload
|
|
// never waits on a draw still reading the old contents out of the same name.
|
|
Bool UploadRestartScratch(SizeT bytes, const void* data) {
|
|
if (g_restartIndices.id == 0) {
|
|
GLuint id = 0;
|
|
g_GLESFuncs.glGenBuffers(1, &id);
|
|
if (id == 0) return false;
|
|
g_restartIndices.id = id;
|
|
g_restartIndices.capacity = 0;
|
|
}
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id);
|
|
SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity;
|
|
while (capacity < bytes) capacity *= 2;
|
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
|
GL_STREAM_DRAW);
|
|
g_restartIndices.capacity = capacity;
|
|
if (data != nullptr && bytes != 0) {
|
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
|
|
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
|
if (!vao) return none;
|
|
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
|
}
|
|
|
|
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what the
|
|
// substitution has to put back.
|
|
Uint BoundElementArrayBufferId() {
|
|
const auto& ibo = BoundElementArrayBuffer();
|
|
if (!ibo) return 0;
|
|
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
|
return resource ? resource->id : 0;
|
|
}
|
|
} // namespace
|
|
|
|
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
|
|
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
|
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
|
return RestartSubstitutionKind::None;
|
|
}
|
|
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
|
if (fixedMax == 0) return RestartSubstitutionKind::None;
|
|
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
|
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
|
|
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
|
|
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
|
|
// nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data
|
|
// becoming "restart on 0" - turns the most common index in any mesh into a restart.
|
|
if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart;
|
|
return RestartSubstitutionKind::RewriteIndices;
|
|
}
|
|
|
|
void OnRestartSubstitutionContextDestroyed() {
|
|
g_restartIndices = {};
|
|
g_restartStaging.clear();
|
|
g_restartStaging.shrink_to_fit();
|
|
}
|
|
|
|
ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) {
|
|
if (kind != RestartSubstitutionKind::SuppressRestart) return;
|
|
// SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART
|
|
// is enabled; for this draw's index type it would restart on a value the application
|
|
// never named. Toggled directly rather than through the render-state shadow, and put back
|
|
// in the destructor, so the shadow stays true and the next draw pays nothing.
|
|
g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
|
m_suppressed = true;
|
|
}
|
|
|
|
ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() {
|
|
if (!m_suppressed) return;
|
|
g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
|
}
|
|
|
|
ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count,
|
|
const void* indices)
|
|
: m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices),
|
|
m_indexType(indexType) {
|
|
if (m_kind != RestartSubstitutionKind::RewriteIndices) {
|
|
return;
|
|
}
|
|
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
|
|
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
|
const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
|
const auto& indexBuffer = BoundElementArrayBuffer();
|
|
|
|
const Uint8* source = nullptr;
|
|
SizeT indexCount = 0;
|
|
SizeT sourceByteOffset = 0;
|
|
|
|
if (indexBuffer) {
|
|
// The WHOLE buffer is rewritten, not just this draw's range, so that every index
|
|
// keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be
|
|
// adjusted from here. It is an ELEMENT index, so it survives widening unchanged.
|
|
const SizeT sizeBytes = indexBuffer->GetSize();
|
|
if (sizeBytes < sourceIndexSize) {
|
|
return; // Nothing to restart on; let the driver see the draw unchanged.
|
|
}
|
|
if (sizeBytes > kMaxRestartRewriteBytes) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element "
|
|
"array buffer rewritten every draw, which is past the %zu-byte ceiling. Use "
|
|
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones "
|
|
"value of the index type.",
|
|
applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
// The shadow is the source of truth for CPU reads, but a persistent map or a
|
|
// shader write may have moved past it since the last sync.
|
|
indexBuffer->SyncPersistentMappedRange();
|
|
indexBuffer->SyncGpuWrites();
|
|
source = indexBuffer->MappedData();
|
|
if (source == nullptr) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
|
|
"the bound element array buffer and none is available.",
|
|
applicationRestartIndex);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
indexCount = sizeBytes / sourceIndexSize;
|
|
sourceByteOffset = reinterpret_cast<SizeT>(indices);
|
|
} else {
|
|
// No element array buffer: `indices` is a client pointer, so only the draw's own
|
|
// range is readable and an indirect draw has nothing to read at all.
|
|
if (count <= 0 || indices == nullptr || sourceIndexSize == 0) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element "
|
|
"array buffer or a client index array with a CPU-known count.",
|
|
applicationRestartIndex);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
if (static_cast<SizeT>(count) * sourceIndexSize > kMaxRestartRewriteBytes) {
|
|
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte "
|
|
"ceiling.",
|
|
static_cast<SizeT>(count) * sourceIndexSize, kMaxRestartRewriteBytes);
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
source = static_cast<const Uint8*>(indices);
|
|
indexCount = static_cast<SizeT>(count);
|
|
}
|
|
|
|
// Widen only when the source really does use the all-ones value as a vertex index -
|
|
// otherwise the sentinel is free and the copy stays the caller's width, which keeps the
|
|
// common substitution allocation-for-allocation identical to the narrow form.
|
|
GLenum destinationType = indexType;
|
|
SizeT destinationIndexSize = sourceIndexSize;
|
|
if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) {
|
|
const GLenum wider = WiderIndexType(indexType);
|
|
// An element-array offset that is not a whole number of indices cannot be rescaled
|
|
// into the widened copy, so such a draw keeps the narrow (lossy) form.
|
|
const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0;
|
|
if (wider != 0 && offsetIsWholeIndices &&
|
|
indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) {
|
|
destinationType = wider;
|
|
destinationIndexSize = MG_Util::GetGLTypeSize(wider);
|
|
} else {
|
|
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the "
|
|
"all-ones index %u: this index type cannot spell both, so every all-ones index is "
|
|
"drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones "
|
|
"value out of the index data.",
|
|
applicationRestartIndex, fixedMax, fixedMax - 1);
|
|
}
|
|
}
|
|
|
|
const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType);
|
|
RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex,
|
|
destinationFixedMax, g_restartStaging);
|
|
|
|
if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) {
|
|
MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART "
|
|
"index substitution.");
|
|
m_valid = false;
|
|
return;
|
|
}
|
|
m_previousBinding = BoundElementArrayBufferId();
|
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
|
|
m_substituted = true;
|
|
m_indexType = destinationType;
|
|
// The rewritten copy starts at byte 0 of the scratch buffer and holds one
|
|
// destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT
|
|
// offset (rescaled to the new width) and a client-memory draw reads from the front.
|
|
m_indices = indexBuffer
|
|
? reinterpret_cast<const void*>((sourceByteOffset / sourceIndexSize) * destinationIndexSize)
|
|
: nullptr;
|
|
}
|
|
|
|
ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() {
|
|
if (!m_substituted) return;
|
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding);
|
|
}
|
|
|
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices());
|
|
});
|
|
}
|
|
|
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
|
PrepareForDraw(syncBit);
|
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
|
if (currentVAO) {
|
|
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
|
|
if (backendVAOSlot && *backendVAOSlot) {
|
|
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first, count);
|
|
}
|
|
}
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawArrays(mode, first, count);
|
|
});
|
|
}
|
|
|
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
DrawSyncFlags syncBit = DrawSyncBit::None;
|
|
PrepareForDraw(syncBit);
|
|
|
|
// This loop IS the emulation - there is no batched tier for the non-indexed form -
|
|
// so each sub-draw has to be given its own gl_DrawID here, exactly as the indexed
|
|
// ladder and the indirect executors do. Without it every sub-draw of a
|
|
// glMultiDrawArrays read draw index 0.
|
|
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
|
|
if (currentVAO) {
|
|
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
|
|
if (backendVAOSlot && *backendVAOSlot) {
|
|
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first[i], count[i]);
|
|
}
|
|
}
|
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawArrays(mode, first[i], count[i]);
|
|
});
|
|
}
|
|
if (feedDrawID) SetCurrentDrawID(0);
|
|
}
|
|
|
|
// Both glMultiDrawElements entry points are emulated - ES has neither in core - by the
|
|
// tier ladder in MultiDraw.cpp, which owns the draw preparation too (its compute tier
|
|
// has to dispatch before the draw state is established). The only difference between
|
|
// them is whether the batch carries per-sub-draw base vertices.
|
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
|
GLsizei drawcount) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, nullptr);
|
|
}
|
|
|
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
|
GLsizei drawcount, const GLint* basevertex) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
MultiDrawImpl::DrawElementsBatch(mode, count, type, indices, drawcount, basevertex);
|
|
}
|
|
|
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (drawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawElementsIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu",
|
|
stride, sizeof(DrawElementsIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
const auto* commandBytes = ResolveIndirectCommandBytes(
|
|
indirect,
|
|
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + sizeof(DrawElementsIndirectCommand),
|
|
"MultiDrawElementsIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
|
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
|
|
}
|
|
|
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
|
GLsizei maxdrawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (maxdrawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawElementsIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu",
|
|
stride, sizeof(DrawElementsIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
|
if (!drawBuffer) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
|
return;
|
|
}
|
|
if (!parameterBuffer) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
|
return;
|
|
}
|
|
|
|
drawBuffer->SyncPersistentMappedRange();
|
|
parameterBuffer->SyncPersistentMappedRange();
|
|
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
|
sizeof(DrawElementsIndirectCommand);
|
|
if (commandBytes > drawBuffer->GetSize()) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
|
return;
|
|
}
|
|
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
|
return;
|
|
}
|
|
|
|
// Both counts are read from the CPU shadow, which a buffer with no shadow does not
|
|
// have - MappedData() is null there and the reads below would be a null dereference,
|
|
// not a wrong picture. The DirectVulkan twin declines the same way.
|
|
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
|
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
|
|
"draw-indirect buffer");
|
|
return;
|
|
}
|
|
|
|
Uint32 actualDrawCount = 0;
|
|
std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount));
|
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBuffer->MappedData() + commandOffset, commandOffset,
|
|
drawBuffer, static_cast<GLsizei>(actualDrawCount), stride,
|
|
"MultiDrawElementsIndirectCount");
|
|
}
|
|
|
|
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (drawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawArraysIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
|
stride, sizeof(DrawArraysIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const auto* commandBytes = ResolveIndirectCommandBytes(
|
|
indirect,
|
|
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + sizeof(DrawArraysIndirectCommand),
|
|
"MultiDrawArraysIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
|
|
drawcount, stride, "MultiDrawArraysIndirect");
|
|
}
|
|
|
|
// The non-indexed twin of MultiDrawElementsIndirectCount, and structurally identical to it:
|
|
// ES has no GL_PARAMETER_BUFFER at all, so the draw count is read from the CPU shadow of the
|
|
// bound one and the batch degenerates into an ordinary indirect multi-draw of that many
|
|
// commands. Missing from the backend table until now, which made every
|
|
// glMultiDrawArraysIndirectCount an INVALID_OPERATION ("backend does not support
|
|
// indirect-parameter array draws") on DirectGLES while the extension was advertised.
|
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
|
GLsizei stride) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
if (maxdrawcount <= 0) {
|
|
return;
|
|
}
|
|
if (stride == 0) {
|
|
stride = sizeof(DrawArraysIndirectCommand);
|
|
}
|
|
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
|
stride, sizeof(DrawArraysIndirectCommand));
|
|
return;
|
|
}
|
|
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
|
if (!drawBuffer) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
|
return;
|
|
}
|
|
if (!parameterBuffer) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
|
return;
|
|
}
|
|
|
|
drawBuffer->SyncPersistentMappedRange();
|
|
parameterBuffer->SyncPersistentMappedRange();
|
|
|
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
|
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
|
sizeof(DrawArraysIndirectCommand);
|
|
if (commandBytes > drawBuffer->GetSize()) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
|
return;
|
|
}
|
|
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
|
return;
|
|
}
|
|
|
|
// See the indexed twin: no CPU shadow means no count to read, not a wrong one.
|
|
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
|
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
|
|
"draw-indirect buffer");
|
|
return;
|
|
}
|
|
|
|
Uint32 actualDrawCount = 0;
|
|
std::memcpy(&actualDrawCount, parameterBuffer->MappedData() + drawcount, sizeof(actualDrawCount));
|
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
|
ExecuteArraysIndirectCommands(mode, drawBuffer->MappedData() + commandOffset, commandOffset, drawBuffer,
|
|
static_cast<GLsizei>(actualDrawCount), stride, "MultiDrawArraysIndirectCount");
|
|
}
|
|
|
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
|
const void* indices, GLint basevertex) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(),
|
|
basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices());
|
|
});
|
|
}
|
|
|
|
// True when the driver will apply baseInstance to the vertex fetch itself, in which case the
|
|
// attribute-offset emulation must stay out of the way. SetCurrentBaseInstance is orthogonal
|
|
// and runs either way - it feeds the shader's gl_BaseInstance, not the fetch.
|
|
inline Bool UseNativeBaseInstance() {
|
|
return g_GLESCapabilities.SupportsBaseInstance;
|
|
}
|
|
|
|
// The emulated shift has to be in place before PrepareForDraw, because that is what syncs the
|
|
// VAO; a zero here is what un-shifts the arrays for the next ordinary draw.
|
|
inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) {
|
|
return UseNativeBaseInstance() ? 0u : static_cast<Uint32>(baseinstance);
|
|
}
|
|
|
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseInstance(baseinstance);
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(),
|
|
restart.Indices(), instancecount,
|
|
basevertex, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(),
|
|
instancecount, basevertex);
|
|
}
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLint basevertex) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseVertex(basevertex);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount,
|
|
basevertex);
|
|
});
|
|
SetCurrentBaseVertex(0);
|
|
}
|
|
|
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
|
GLsizei instancecount, GLuint baseinstance) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
SetCurrentBaseInstance(baseinstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(),
|
|
instancecount, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
|
}
|
|
});
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
|
if (!restart.DrawIsValid()) return;
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
|
});
|
|
}
|
|
|
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
|
if (indexSize == 0) {
|
|
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
|
return;
|
|
}
|
|
|
|
const auto* commandBytes =
|
|
ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand), "DrawElementsIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
|
|
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
|
|
"DrawElementsIndirect");
|
|
}
|
|
|
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
|
GLuint baseinstance) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
|
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
|
PrepareForDraw(syncBit);
|
|
SetCurrentBaseInstance(baseinstance);
|
|
ForEachViewportRoutingPass([&] {
|
|
if (UseNativeBaseInstance()) {
|
|
g_GLESFuncs.glDrawArraysInstancedBaseInstanceEXT(mode, first, count, instancecount, baseinstance);
|
|
} else {
|
|
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
|
}
|
|
});
|
|
SetCurrentBaseInstance(0);
|
|
}
|
|
|
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
ForEachViewportRoutingPass([&] {
|
|
g_GLESFuncs.glDrawArraysInstanced(mode, first, count, instancecount);
|
|
});
|
|
}
|
|
|
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
|
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
|
|
PrepareForDraw(syncBit);
|
|
|
|
const auto* commandBytes =
|
|
ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
|
if (!commandBytes) {
|
|
return;
|
|
}
|
|
|
|
const auto& drawIndirectBuffer =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
|
|
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
|
}
|
|
|
|
// Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a
|
|
// lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would
|
|
// otherwise spin an unbounded drain forever inside whichever GL entry point happened to be
|
|
// cleaning up, which is how a GPU reset reads as an unkillable process whose log simply
|
|
// stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the
|
|
// diagnostic. Every drain in this backend goes through here so the bound cannot drift apart
|
|
// between them.
|
|
static constexpr Int kMaxDrainedGLErrors = 32;
|
|
|
|
static void DrainDriverErrors(const char* site) {
|
|
Int drained = 0;
|
|
while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
++drained;
|
|
}
|
|
if (drained == kMaxDrainedGLErrors) {
|
|
MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost",
|
|
site, kMaxDrainedGLErrors);
|
|
}
|
|
}
|
|
|
|
static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); }
|
|
|
|
// Sized internal format of the currently bound READ framebuffer's read colour
|
|
// attachment, 0 when it cannot be determined.
|
|
static GLenum QueryReadColorAttachmentInternalFormat() {
|
|
GLint attachmentType = 0;
|
|
GLint attachmentName = 0;
|
|
// Ask the point the backend read buffer actually names, not COLOR_ATTACHMENT0. The colour map
|
|
// in BackendFramebufferObject is a permutation, so the read attachment's image only sits at
|
|
// CA0 when that map is identity; querying CA0 unconditionally would size the resolve
|
|
// renderbuffer from a different attachment's format and either convert wrongly or fail the
|
|
// blit outright.
|
|
GLint readBuffer = GL_COLOR_ATTACHMENT0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
|
|
if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) {
|
|
readBuffer = GL_COLOR_ATTACHMENT0;
|
|
}
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &attachmentType);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &attachmentName);
|
|
if (attachmentName == 0) {
|
|
return 0;
|
|
}
|
|
GLint internalFormat = 0;
|
|
if (attachmentType == GL_RENDERBUFFER) {
|
|
if (!g_GLESFuncs.glGetRenderbufferParameteriv) return 0;
|
|
GLint previous = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous);
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(attachmentName));
|
|
g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT,
|
|
&internalFormat);
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(previous));
|
|
} else if (attachmentType == GL_TEXTURE) {
|
|
if (!g_GLESFuncs.glGetTexLevelParameteriv) return 0;
|
|
// The resolve source of interest is always a multisample 2D texture; a
|
|
// single-sample source would not have taken the fallback in the first place.
|
|
GLint previous = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &previous);
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast<GLuint>(attachmentName));
|
|
g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D_MULTISAMPLE, 0, GL_TEXTURE_INTERNAL_FORMAT,
|
|
&internalFormat);
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, static_cast<GLuint>(previous));
|
|
}
|
|
return static_cast<GLenum>(internalFormat);
|
|
}
|
|
|
|
// ES rejects any blit out of a multisample read framebuffer whose format differs
|
|
// from the draw framebuffer's. Desktop GL only requires identical formats when BOTH
|
|
// framebuffers are multisampled, so a multisample resolve is allowed to convert
|
|
// format on the way out (KHR-GL3x.framebuffer_blit resolves an R8/R16F multisample
|
|
// texture straight into an RGBA8 target). Emulate it in two steps: resolve into a
|
|
// scratch buffer of the source's own format, then run the caller's blit from there -
|
|
// that second one is single-sample on both sides, where ES does allow conversion.
|
|
static Bool ResolveThenBlit(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
|
GLint dstX1, GLint dstY1, GLenum filter) {
|
|
static Uint s_resolveFramebuffer = 0;
|
|
static Uint s_resolveRenderbuffer = 0;
|
|
static GLenum s_resolveFormat = 0;
|
|
static GLsizei s_resolveWidth = 0;
|
|
static GLsizei s_resolveHeight = 0;
|
|
static Uint s_resolveContextGeneration = ~0u;
|
|
|
|
if (!g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glGenRenderbuffers ||
|
|
!g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glFramebufferRenderbuffer) {
|
|
return false;
|
|
}
|
|
const GLenum sourceFormat = QueryReadColorAttachmentInternalFormat();
|
|
if (sourceFormat == 0) {
|
|
return false;
|
|
}
|
|
|
|
const GLint left = std::min(srcX0, srcX1);
|
|
const GLint right = std::max(srcX0, srcX1);
|
|
const GLint bottom = std::min(srcY0, srcY1);
|
|
const GLint top = std::max(srcY0, srcY1);
|
|
const GLsizei width = static_cast<GLsizei>(right - left);
|
|
const GLsizei height = static_cast<GLsizei>(top - bottom);
|
|
if (width <= 0 || height <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (s_resolveContextGeneration != g_backendContextGeneration) {
|
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
|
s_resolveFramebuffer = 0;
|
|
s_resolveRenderbuffer = 0;
|
|
s_resolveFormat = 0;
|
|
s_resolveContextGeneration = g_backendContextGeneration;
|
|
}
|
|
if (s_resolveFramebuffer == 0) {
|
|
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
|
|
g_GLESFuncs.glGenRenderbuffers(1, &s_resolveRenderbuffer);
|
|
if (s_resolveFramebuffer == 0 || s_resolveRenderbuffer == 0) return false;
|
|
s_resolveFormat = 0;
|
|
}
|
|
|
|
GLint previousRenderbuffer = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previousRenderbuffer);
|
|
if (s_resolveFormat != sourceFormat || s_resolveWidth < width || s_resolveHeight < height) {
|
|
s_resolveWidth = std::max(s_resolveWidth, width);
|
|
s_resolveHeight = std::max(s_resolveHeight, height);
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, s_resolveRenderbuffer);
|
|
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, sourceFormat, s_resolveWidth, s_resolveHeight);
|
|
s_resolveFormat = sourceFormat;
|
|
}
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(previousRenderbuffer));
|
|
|
|
GLint previousDraw = 0;
|
|
GLint previousRead = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw);
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead);
|
|
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_resolveFramebuffer);
|
|
g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
|
s_resolveRenderbuffer);
|
|
Bool resolved = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
|
if (resolved) {
|
|
DrainBlitErrors();
|
|
// A blit is scissored like a draw (the replicate path's guard documents the
|
|
// same rule): the application's box would clip this resolve into the
|
|
// scratch, and the second blit would then copy never-written scratch texels
|
|
// into the destination - silently, since scissor clipping raises no GL
|
|
// error. Disable for the staging blit only; the caller-visible blit below
|
|
// keeps the blit's native scissor semantics. Tracked via the render-state
|
|
// shadow, exactly like ScopedScissorDisable.
|
|
const Bool scissorWasEnabled =
|
|
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
|
if (scissorWasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
|
g_GLESFuncs.glBlitFramebuffer(left, bottom, right, top, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
|
|
GL_NEAREST);
|
|
if (scissorWasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
|
resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
}
|
|
if (resolved) {
|
|
// Mirror the caller's orientation into the scratch-relative source rect so a
|
|
// flipped blit stays flipped.
|
|
const GLint blitX0 = srcX0 <= srcX1 ? 0 : width;
|
|
const GLint blitX1 = srcX0 <= srcX1 ? width : 0;
|
|
const GLint blitY0 = srcY0 <= srcY1 ? 0 : height;
|
|
const GLint blitY1 = srcY0 <= srcY1 ? height : 0;
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_resolveFramebuffer);
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glBlitFramebuffer(blitX0, blitY0, blitX1, blitY1, dstX0, dstY0, dstX1, dstY1,
|
|
GL_COLOR_BUFFER_BIT, filter);
|
|
resolved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
}
|
|
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(previousRead));
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
if (!resolved) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed");
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Shared guard for the emulation passes that have to DRAW to get their work done: the
|
|
// single-sample -> multisample blit replicate below, and the depth/stencil readback
|
|
// emulation further down. Both borrow the application's ES context for a full-screen
|
|
// pass, so everything they disturb is captured here and put back on the way out - the
|
|
// sync layer's shadow of the driver state has to stay true, and one leaked binding
|
|
// regresses every draw that follows.
|
|
//
|
|
// Three members of that set are not obvious:
|
|
//
|
|
// - The borrowed texture unit. Both passes bind their scratch texture to
|
|
// TextureImpl::TempTextureUnit, and the unit's binding is only restored correctly by
|
|
// asking TextureImpl::g_boundTexturesCache what is supposed to be there: the raw
|
|
// glBindTexture the passes issue never moves that cache, so restoring a *queried*
|
|
// id leaves the driver and the cache disagreeing and the per-draw binding memo
|
|
// false-skips the re-bind - the borrowed-slot failure mode that showed up as
|
|
// process-wide glyph death under Iris. Reading GL_TEXTURE_BINDING_2D was doubly
|
|
// wrong here because the replicate path may already have switched units and bound
|
|
// its own scratch texture by the time it asked.
|
|
//
|
|
// - The sampler object on that unit. It would override the scratch texture's own
|
|
// filter and compare parameters. Unbinding through SamplerImpl::UnbindSampler moves
|
|
// the sampler cache, which is exactly what re-opens BindCurrentUnitSamplers' memo,
|
|
// so the application's sampler comes back on the next draw with no explicit restore.
|
|
//
|
|
// - An active transform feedback capture. GL rejects a draw issued with a program
|
|
// other than the one that began the capture, and would otherwise append the
|
|
// emulation's vertices to the application's buffers.
|
|
class ScopedEmulationDrawState {
|
|
public:
|
|
ScopedEmulationDrawState() {
|
|
g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &m_program);
|
|
g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_vertexArray);
|
|
g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, m_viewport);
|
|
g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, m_scissorBox);
|
|
g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, m_colorMask);
|
|
g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &m_depthFunc);
|
|
g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &m_depthMask);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &m_stencilFunc[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &m_stencilFunc[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &m_stencilRef[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &m_stencilRef[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &m_stencilValueMask[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &m_stencilValueMask[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &m_stencilWriteMask[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &m_stencilWriteMask[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &m_stencilFail[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &m_stencilFail[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &m_stencilDepthFail[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &m_stencilDepthFail[1]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &m_stencilPass[0]);
|
|
g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &m_stencilPass[1]);
|
|
if (g_GLESCapabilities.SupportsClipDistance) {
|
|
m_capabilityCount = kBaseCapabilityCount + kClipDistanceCapabilityCount;
|
|
}
|
|
for (Uint i = 0; i < m_capabilityCount; ++i) {
|
|
m_capabilities[i].enabled = g_GLESFuncs.glIsEnabled(m_capabilities[i].cap);
|
|
}
|
|
// GL_SAMPLE_MASK is ES 3.1; on an older driver the query above just raised
|
|
// GL_INVALID_ENUM and answered GL_FALSE, which is also the right thing to
|
|
// restore. Drop the flag so it is not misattributed to the emulation's own work.
|
|
DrainBlitErrors();
|
|
|
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
|
!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
|
|
g_GLESFuncs.glPauseTransformFeedback();
|
|
m_pausedTransformFeedback = true;
|
|
DrainBlitErrors();
|
|
}
|
|
|
|
m_activeTextureUnit = TextureImpl::g_activeTextureUnit;
|
|
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
|
SamplerImpl::UnbindSampler(TextureImpl::TempTextureUnit);
|
|
|
|
// The neutral baseline every emulation pass wants: nothing culled, nothing
|
|
// clipped, nothing tested, no coverage games, and colour writes open. Callers
|
|
// turn back on only what they need (the replicate pass wants the depth and
|
|
// stencil tests, and masks colour off because it writes neither).
|
|
for (Uint i = 0; i < m_capabilityCount; ++i) {
|
|
g_GLESFuncs.glDisable(m_capabilities[i].cap);
|
|
}
|
|
g_GLESFuncs.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
|
g_GLESFuncs.glDepthMask(GL_FALSE);
|
|
DrainBlitErrors();
|
|
}
|
|
|
|
~ScopedEmulationDrawState() {
|
|
g_GLESFuncs.glUseProgram(static_cast<GLuint>(m_program));
|
|
// Put back whatever the binding cache says lives on the borrowed unit, not what
|
|
// the driver happened to hold: see the class comment.
|
|
auto* cachedBound =
|
|
TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
|
[static_cast<SizeT>(TextureTarget::Texture2D)];
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
|
TextureImpl::ActivateTextureUnit(m_activeTextureUnit);
|
|
VertexArrayImpl::BindBackendVAOId(static_cast<GLuint>(m_vertexArray));
|
|
g_GLESFuncs.glViewport(m_viewport[0], m_viewport[1], m_viewport[2], m_viewport[3]);
|
|
g_GLESFuncs.glScissor(m_scissorBox[0], m_scissorBox[1], m_scissorBox[2], m_scissorBox[3]);
|
|
g_GLESFuncs.glColorMask(m_colorMask[0], m_colorMask[1], m_colorMask[2], m_colorMask[3]);
|
|
g_GLESFuncs.glDepthFunc(static_cast<GLenum>(m_depthFunc));
|
|
g_GLESFuncs.glDepthMask(m_depthMask);
|
|
const GLenum faces[2] = {GL_FRONT, GL_BACK};
|
|
for (SizeT face = 0; face < 2; ++face) {
|
|
g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast<GLenum>(m_stencilFunc[face]),
|
|
m_stencilRef[face],
|
|
static_cast<GLuint>(m_stencilValueMask[face]));
|
|
g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast<GLenum>(m_stencilFail[face]),
|
|
static_cast<GLenum>(m_stencilDepthFail[face]),
|
|
static_cast<GLenum>(m_stencilPass[face]));
|
|
g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast<GLuint>(m_stencilWriteMask[face]));
|
|
}
|
|
for (Uint i = 0; i < m_capabilityCount; ++i) {
|
|
if (m_capabilities[i].enabled) {
|
|
g_GLESFuncs.glEnable(m_capabilities[i].cap);
|
|
} else {
|
|
g_GLESFuncs.glDisable(m_capabilities[i].cap);
|
|
}
|
|
}
|
|
// The per-draw-buffer colour masks are not covered by the non-indexed
|
|
// glColorMask above. Restore what the SYNC actually pushed, not the raw
|
|
// application masks: a widened attachment's alpha write is forced off by
|
|
// SyncRenderState and memoized in g_syncedColorMaskAlphaWidenMask, and the
|
|
// next sync early-outs on an unchanged version - restoring the undoctored
|
|
// mask here would leave alpha writes enabled on the widened buffer with
|
|
// nothing left to repair it. Same three-way pointer fallback as
|
|
// SyncRenderState's push: gating on the core name alone left EXT/OES-only
|
|
// devices holding buffer 0's mask broadcast across every buffer.
|
|
const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski
|
|
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
|
|
: g_GLESFuncs.glColorMaskiOES;
|
|
if (colorMaskiFn) {
|
|
for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) {
|
|
BoolVec4 colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index];
|
|
if (index < 32 && (RenderStateImpl::g_syncedColorMaskAlphaWidenMask & (1u << index)) != 0) {
|
|
colorMask.w() = false;
|
|
}
|
|
colorMaskiFn(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE,
|
|
colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE);
|
|
}
|
|
}
|
|
if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) {
|
|
g_GLESFuncs.glResumeTransformFeedback();
|
|
}
|
|
DrainBlitErrors();
|
|
}
|
|
|
|
ScopedEmulationDrawState(const ScopedEmulationDrawState&) = delete;
|
|
ScopedEmulationDrawState& operator=(const ScopedEmulationDrawState&) = delete;
|
|
|
|
private:
|
|
struct CapabilityState {
|
|
GLenum cap;
|
|
GLboolean enabled;
|
|
};
|
|
|
|
GLint m_program = 0;
|
|
GLint m_vertexArray = 0;
|
|
GLint m_viewport[4] = {0, 0, 0, 0};
|
|
GLint m_scissorBox[4] = {0, 0, 0, 0};
|
|
GLboolean m_colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
|
GLint m_depthFunc = GL_LESS;
|
|
GLboolean m_depthMask = GL_TRUE;
|
|
GLint m_stencilFunc[2] = {GL_ALWAYS, GL_ALWAYS};
|
|
GLint m_stencilRef[2] = {0, 0};
|
|
GLint m_stencilValueMask[2] = {~0, ~0};
|
|
GLint m_stencilWriteMask[2] = {~0, ~0};
|
|
GLint m_stencilFail[2] = {GL_KEEP, GL_KEEP};
|
|
GLint m_stencilDepthFail[2] = {GL_KEEP, GL_KEEP};
|
|
GLint m_stencilPass[2] = {GL_KEEP, GL_KEEP};
|
|
Uint m_activeTextureUnit = 0;
|
|
Bool m_pausedTransformFeedback = false;
|
|
// The eight GL_CLIP_DISTANCE0_EXT..7_EXT entries are last so that a driver without
|
|
// GL_EXT_clip_cull_distance can be served by shortening the count instead of asking
|
|
// it about tokens it does not know. They belong here at all because an emulation pass
|
|
// draws its full-screen triangle with its OWN program, which writes no gl_ClipDistance:
|
|
// leaving the app's enables on would clip that triangle by undefined distances.
|
|
static constexpr Uint kBaseCapabilityCount = 10;
|
|
static constexpr Uint kClipDistanceCapabilityCount = 8;
|
|
Uint m_capabilityCount = kBaseCapabilityCount;
|
|
CapabilityState m_capabilities[kBaseCapabilityCount + kClipDistanceCapabilityCount] = {
|
|
{GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE},
|
|
{GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE},
|
|
{GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE},
|
|
{GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE},
|
|
{GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE},
|
|
{0x3000, GL_FALSE}, {0x3001, GL_FALSE},
|
|
{0x3002, GL_FALSE}, {0x3003, GL_FALSE},
|
|
{0x3004, GL_FALSE}, {0x3005, GL_FALSE},
|
|
{0x3006, GL_FALSE}, {0x3007, GL_FALSE},
|
|
};
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Single-sample -> multisample blit ("replicate")
|
|
//
|
|
// Desktop GL replicates the source sample into every destination sample when the read
|
|
// framebuffer is single-sampled and the draw framebuffer is not. ES forbids the whole
|
|
// call ("INVALID_OPERATION if SAMPLE_BUFFERS for the draw framebuffer is greater than
|
|
// zero"), so the blit silently did nothing - KHR-GL3x.packed_depth_stencil.blit's
|
|
// second loop then read a destination that still held its clear values.
|
|
//
|
|
// Emulated by drawing a full-screen triangle into the multisample framebuffer: every
|
|
// pixel is fully covered, so every sample of it receives the same value, which is
|
|
// exactly what the replicate rule asks for. Depth comes from gl_FragDepth; stencil has
|
|
// no shader output on ES, so it is written one bit plane at a time with REPLACE and a
|
|
// discard for the pixels whose source bit is clear.
|
|
// Defined further down with the other small GL helpers; the attachment-format probe below
|
|
// needs it to tell a rejected bind apart from a successful one.
|
|
static void ClearGLErrors();
|
|
|
|
namespace ReplicateBlitImpl {
|
|
static Uint s_contextGeneration = ~0u;
|
|
static GLuint s_framebuffer = 0;
|
|
static GLuint s_texture = 0;
|
|
static GLenum s_textureFormat = 0;
|
|
static GLsizei s_textureWidth = 0;
|
|
static GLsizei s_textureHeight = 0;
|
|
static GLuint s_vertexArray = 0;
|
|
static GLuint s_depthProgram = 0;
|
|
static GLuint s_stencilProgram = 0;
|
|
static GLint s_depthUvTransform = -1;
|
|
static GLint s_stencilUvTransform = -1;
|
|
static GLint s_stencilBit = -1;
|
|
static Bool s_programsFailed = false;
|
|
|
|
static const char* const kVertexSource =
|
|
"#version 300 es\n"
|
|
"uniform vec4 uUvTransform;\n"
|
|
"out vec2 vUv;\n"
|
|
"void main() {\n"
|
|
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
|
|
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
|
|
" vUv = p * uUvTransform.xy + uUvTransform.zw;\n"
|
|
"}\n";
|
|
|
|
static const char* const kDepthFragmentSource =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp sampler2D;\n"
|
|
"uniform sampler2D uSource;\n"
|
|
"in vec2 vUv;\n"
|
|
"void main() {\n"
|
|
" gl_FragDepth = texture(uSource, vUv).r;\n"
|
|
"}\n";
|
|
|
|
static const char* const kStencilFragmentSource =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp usampler2D;\n"
|
|
"uniform usampler2D uSource;\n"
|
|
"uniform uint uBit;\n"
|
|
"in vec2 vUv;\n"
|
|
"void main() {\n"
|
|
" if ((texture(uSource, vUv).r & uBit) == 0u) discard;\n"
|
|
"}\n";
|
|
|
|
static GLuint BuildProgram(const char* fragmentSource) {
|
|
const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER);
|
|
const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER);
|
|
if (vertexShader == 0 || fragmentShader == 0) {
|
|
return 0;
|
|
}
|
|
g_GLESFuncs.glShaderSource(vertexShader, 1, &kVertexSource, nullptr);
|
|
g_GLESFuncs.glCompileShader(vertexShader);
|
|
g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
|
|
g_GLESFuncs.glCompileShader(fragmentShader);
|
|
|
|
const GLuint program = g_GLESFuncs.glCreateProgram();
|
|
GLint linked = GL_FALSE;
|
|
if (program != 0) {
|
|
g_GLESFuncs.glAttachShader(program, vertexShader);
|
|
g_GLESFuncs.glAttachShader(program, fragmentShader);
|
|
g_GLESFuncs.glLinkProgram(program);
|
|
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
}
|
|
g_GLESFuncs.glDeleteShader(vertexShader);
|
|
g_GLESFuncs.glDeleteShader(fragmentShader);
|
|
if (linked != GL_TRUE) {
|
|
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
static Bool EnsureResources() {
|
|
if (s_contextGeneration != g_backendContextGeneration) {
|
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
|
s_framebuffer = 0;
|
|
s_texture = 0;
|
|
s_textureFormat = 0;
|
|
s_textureWidth = 0;
|
|
s_textureHeight = 0;
|
|
s_vertexArray = 0;
|
|
s_depthProgram = 0;
|
|
s_stencilProgram = 0;
|
|
s_programsFailed = false;
|
|
s_contextGeneration = g_backendContextGeneration;
|
|
}
|
|
if (s_programsFailed) {
|
|
return false;
|
|
}
|
|
if (s_depthProgram == 0) {
|
|
s_depthProgram = BuildProgram(kDepthFragmentSource);
|
|
s_stencilProgram = BuildProgram(kStencilFragmentSource);
|
|
if (s_depthProgram == 0 || s_stencilProgram == 0) {
|
|
s_programsFailed = true;
|
|
MGLOG_E_ONCE("BlitFramebuffer: could not build the multisample replicate programs");
|
|
return false;
|
|
}
|
|
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
|
|
s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform");
|
|
s_stencilBit = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uBit");
|
|
}
|
|
if (s_framebuffer == 0) {
|
|
g_GLESFuncs.glGenFramebuffers(1, &s_framebuffer);
|
|
if (s_framebuffer == 0) return false;
|
|
}
|
|
if (s_vertexArray == 0) {
|
|
g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray);
|
|
if (s_vertexArray == 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Sized internal format of ONE attachment point of the bound READ framebuffer, or 0
|
|
// when there is no object there to ask (the default framebuffer's buffers have no
|
|
// queryable format at all). The scratch copy has to use the very same one: ES rejects
|
|
// a depth/stencil blit between differing formats even when both sides are
|
|
// single-sampled.
|
|
//
|
|
// The texture branch can only ask about a GL_TEXTURE_2D, and a name whose target is
|
|
// something else (an array or cube texture attached by glFramebufferTextureLayer /
|
|
// glFramebufferTexture) makes glBindTexture answer GL_INVALID_OPERATION and change
|
|
// nothing. Reading glGetTexLevelParameteriv after that failed bind does NOT return 0 -
|
|
// it truthfully describes whatever texture was already on GL_TEXTURE_2D, which on this
|
|
// path is the emulation's own staging scratch. That is a wrong answer that looks like a
|
|
// right one, so the bind has to be error-checked rather than trusted.
|
|
static GLenum QueryAttachmentSizedFormat(GLenum attachment) {
|
|
GLint objectType = 0;
|
|
GLint objectName = 0;
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName);
|
|
if (objectName == 0) {
|
|
return 0;
|
|
}
|
|
GLint internalFormat = 0;
|
|
if (objectType == GL_RENDERBUFFER) {
|
|
GLint previous = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(objectName));
|
|
const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
if (bound) {
|
|
g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT,
|
|
&internalFormat);
|
|
}
|
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(previous));
|
|
ClearGLErrors();
|
|
} else if (objectType == GL_TEXTURE) {
|
|
GLint previous = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(objectName));
|
|
const Bool bound = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
if (bound) {
|
|
g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT,
|
|
&internalFormat);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
internalFormat = 0;
|
|
}
|
|
}
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous));
|
|
ClearGLErrors();
|
|
}
|
|
return static_cast<GLenum>(internalFormat);
|
|
}
|
|
|
|
// The read framebuffer's depth format, or - when it has no depth - its stencil one.
|
|
static GLenum QueryReadDepthStencilFormat(GLenum* outAttachment) {
|
|
const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
|
|
for (const GLenum attachment : attachments) {
|
|
const GLenum internalFormat = QueryAttachmentSizedFormat(attachment);
|
|
if (internalFormat != 0) {
|
|
if (outAttachment) *outAttachment = attachment;
|
|
return internalFormat;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static Bool FormatHasDepth(GLenum internalFormat) {
|
|
return internalFormat == GL_DEPTH_COMPONENT16 || internalFormat == GL_DEPTH_COMPONENT24 ||
|
|
internalFormat == GL_DEPTH_COMPONENT32F || internalFormat == GL_DEPTH24_STENCIL8 ||
|
|
internalFormat == GL_DEPTH32F_STENCIL8;
|
|
}
|
|
|
|
static Bool FormatHasStencil(GLenum internalFormat) {
|
|
return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 ||
|
|
internalFormat == GL_STENCIL_INDEX8;
|
|
}
|
|
|
|
static GLenum ScratchAttachmentFor(GLenum internalFormat) {
|
|
if (FormatHasDepth(internalFormat) && FormatHasStencil(internalFormat)) {
|
|
return GL_DEPTH_STENCIL_ATTACHMENT;
|
|
}
|
|
return FormatHasDepth(internalFormat) ? GL_DEPTH_ATTACHMENT : GL_STENCIL_ATTACHMENT;
|
|
}
|
|
} // namespace ReplicateBlitImpl
|
|
|
|
// Returns true when the request was serviced (or is not this fallback's business).
|
|
static Bool ReplicateBlitIntoMultisampleDraw(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
|
|
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) {
|
|
using namespace ReplicateBlitImpl;
|
|
if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) {
|
|
return false;
|
|
}
|
|
if (!EnsureResources()) {
|
|
return false;
|
|
}
|
|
|
|
const GLsizei srcWidth = static_cast<GLsizei>(std::abs(srcX1 - srcX0));
|
|
const GLsizei srcHeight = static_cast<GLsizei>(std::abs(srcY1 - srcY0));
|
|
const GLsizei dstWidth = static_cast<GLsizei>(std::abs(dstX1 - dstX0));
|
|
const GLsizei dstHeight = static_cast<GLsizei>(std::abs(dstY1 - dstY0));
|
|
if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) {
|
|
return false;
|
|
}
|
|
|
|
GLenum readAttachment = GL_DEPTH_ATTACHMENT;
|
|
const GLenum sourceFormat = QueryReadDepthStencilFormat(&readAttachment);
|
|
if (sourceFormat == 0) {
|
|
return false;
|
|
}
|
|
const Bool wantDepth = (mask & GL_DEPTH_BUFFER_BIT) != 0 && FormatHasDepth(sourceFormat);
|
|
const Bool wantStencil = (mask & GL_STENCIL_BUFFER_BIT) != 0 && FormatHasStencil(sourceFormat);
|
|
if (!wantDepth && !wantStencil) {
|
|
return false;
|
|
}
|
|
// Sampling the stencil half of a packed texture goes through
|
|
// GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the pname
|
|
// would just raise GL_INVALID_ENUM and the shader would read depth bits as stencil.
|
|
const Bool supportsStencilTextureMode = g_GLESCapabilities.GLESVersion.Major > 3 ||
|
|
(g_GLESCapabilities.GLESVersion.Major == 3 &&
|
|
g_GLESCapabilities.GLESVersion.Minor >= 1);
|
|
if (wantStencil && !supportsStencilTextureMode) {
|
|
return false;
|
|
}
|
|
|
|
GLint previousDraw = 0;
|
|
GLint previousRead = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw);
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead);
|
|
|
|
// Everything below borrows the application's context - a texture unit for the
|
|
// scratch sampling and the whole rasterization pipeline for the replicate passes -
|
|
// so the guard is taken before the first of those, not just before the draws. It
|
|
// also puts the scissor test where the staging blit below needs it: a blit is
|
|
// scissored like a draw, and the application's box would otherwise clip the copy
|
|
// into the scratch texture.
|
|
ScopedEmulationDrawState emulationState;
|
|
|
|
// Copy the source rectangle into a scratch texture of its own format: both sides of
|
|
// that blit are single-sampled, which ES does allow.
|
|
Bool ok = true;
|
|
if (s_texture == 0 || s_textureFormat != sourceFormat || s_textureWidth < srcWidth ||
|
|
s_textureHeight < srcHeight) {
|
|
if (s_texture != 0) {
|
|
g_GLESFuncs.glDeleteTextures(1, &s_texture); // immutable storage cannot be resized
|
|
s_texture = 0;
|
|
}
|
|
s_textureWidth = std::max(s_textureWidth, srcWidth);
|
|
s_textureHeight = std::max(s_textureHeight, srcHeight);
|
|
g_GLESFuncs.glGenTextures(1, &s_texture);
|
|
if (s_texture == 0) {
|
|
ok = false;
|
|
} else {
|
|
// The guard already activated TempTextureUnit and cleared its sampler.
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture);
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, sourceFormat, s_textureWidth, s_textureHeight);
|
|
ok = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
s_textureFormat = ok ? sourceFormat : 0;
|
|
}
|
|
}
|
|
|
|
if (ok) {
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_framebuffer);
|
|
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, ScratchAttachmentFor(sourceFormat), GL_TEXTURE_2D,
|
|
s_texture, 0);
|
|
ok = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
|
}
|
|
if (ok) {
|
|
const GLint left = std::min(srcX0, srcX1);
|
|
const GLint bottom = std::min(srcY0, srcY1);
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glBlitFramebuffer(left, bottom, left + srcWidth, bottom + srcHeight, 0, 0, srcWidth, srcHeight,
|
|
mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT), GL_NEAREST);
|
|
ok = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
}
|
|
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(previousRead));
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
if (!ok) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate");
|
|
return false;
|
|
}
|
|
|
|
const GLint dstLeft = std::min(dstX0, dstX1);
|
|
const GLint dstBottom = std::min(dstY0, dstY1);
|
|
const Bool mirrorX = (srcX1 > srcX0) != (dstX1 > dstX0);
|
|
const Bool mirrorY = (srcY1 > srcY0) != (dstY1 > dstY0);
|
|
// The scratch holds the source rectangle at its origin, so the texture is larger than
|
|
// the copied region: scale the [0,1] quad coordinates down to the region it occupies.
|
|
const Float uvScaleX = static_cast<Float>(srcWidth) / static_cast<Float>(s_textureWidth);
|
|
const Float uvScaleY = static_cast<Float>(srcHeight) / static_cast<Float>(s_textureHeight);
|
|
const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY,
|
|
mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f};
|
|
|
|
// The guard already left the pipeline neutral (nothing culled, tested, blended or
|
|
// coverage-masked) on the borrowed texture unit; what is left is this pass's own
|
|
// choices - the destination rectangle, and colour writes off because it writes only
|
|
// depth and stencil.
|
|
VertexArrayImpl::BindBackendVAOId(s_vertexArray);
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture);
|
|
g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight);
|
|
g_GLESFuncs.glScissor(dstLeft, dstBottom, dstWidth, dstHeight);
|
|
g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
|
g_GLESFuncs.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
|
|
DrainBlitErrors();
|
|
|
|
if (wantDepth) {
|
|
if (FormatHasStencil(sourceFormat)) {
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
|
}
|
|
g_GLESFuncs.glUseProgram(s_depthProgram);
|
|
g_GLESFuncs.glUniform4f(s_depthUvTransform, uvTransform[0], uvTransform[1], uvTransform[2],
|
|
uvTransform[3]);
|
|
g_GLESFuncs.glEnable(GL_DEPTH_TEST);
|
|
g_GLESFuncs.glDepthFunc(GL_ALWAYS);
|
|
g_GLESFuncs.glDepthMask(GL_TRUE);
|
|
g_GLESFuncs.glDisable(GL_STENCIL_TEST);
|
|
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
|
|
if (wantStencil) {
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
|
g_GLESFuncs.glUseProgram(s_stencilProgram);
|
|
g_GLESFuncs.glUniform4f(s_stencilUvTransform, uvTransform[0], uvTransform[1], uvTransform[2],
|
|
uvTransform[3]);
|
|
g_GLESFuncs.glDisable(GL_DEPTH_TEST);
|
|
g_GLESFuncs.glDepthMask(GL_FALSE);
|
|
g_GLESFuncs.glEnable(GL_STENCIL_TEST);
|
|
// The bit planes are written by ORing in the set bits, so the destination has to
|
|
// start from zero. The blit overwrites the whole rectangle anyway, and the scissor
|
|
// keeps the clear inside it.
|
|
const GLint zero = 0;
|
|
g_GLESFuncs.glStencilMask(0xFFu);
|
|
g_GLESFuncs.glClearBufferiv(GL_STENCIL, 0, &zero);
|
|
g_GLESFuncs.glStencilFunc(GL_ALWAYS, 0xFF, 0xFFu);
|
|
g_GLESFuncs.glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
|
for (Uint bit = 0; bit < 8; ++bit) {
|
|
g_GLESFuncs.glStencilMask(1u << bit);
|
|
g_GLESFuncs.glUniform1ui(s_stencilBit, 1u << bit);
|
|
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
|
}
|
|
|
|
const Bool replicated = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
|
|
// Everything the pass disturbed goes back through emulationState's destructor.
|
|
if (!replicated) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: multisample replicate fallback failed");
|
|
}
|
|
return replicated;
|
|
}
|
|
|
|
static void IssueBlitWithResolveFallback(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
|
|
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
|
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
|
|
return;
|
|
}
|
|
// Two ES restrictions desktop GL does not have are worth a second attempt: a
|
|
// multisample resolve that also converts colour format, and any blit into a
|
|
// multisample draw framebuffer. Both need to know how the two sides are sampled.
|
|
GLint readSamples = 0;
|
|
GLint drawSamples = 0;
|
|
{
|
|
GLint previousRead = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead);
|
|
GLint previousDraw = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw);
|
|
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousRead));
|
|
g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &readSamples);
|
|
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
|
g_GLESFuncs.glGetIntegerv(GL_SAMPLES, &drawSamples);
|
|
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(previousRead));
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
}
|
|
if (readSamples <= 0 && drawSamples > 0) {
|
|
// Single-sample source into a multisample destination: ES rejects the call
|
|
// outright, desktop GL replicates the source sample into every destination one.
|
|
if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) {
|
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH
|
|
// aspect groups still owe their copy, and each has to be retried on its own. Re-issuing
|
|
// the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it
|
|
// silently whenever the colour half could not be emulated - and on a framebuffer whose
|
|
// only attachment is depth it never can, because the colour emulation has no attachment
|
|
// to take a format from (KHR-GL33.framebuffer_blit's depth config test blits
|
|
// COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value).
|
|
const GLbitfield colourBit = mask & static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
|
const GLbitfield dsBits = mask & static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
// The colour group's one emulation is the multisample resolve that also converts format,
|
|
// which is the shape this names. It used to double as an early-out for the whole
|
|
// function, which is what cost a depth-only mask its single-aspect retry.
|
|
const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0;
|
|
if (colourBit != 0) {
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
const Bool emulated =
|
|
multisampleResolve &&
|
|
ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter);
|
|
if (!emulated) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its "
|
|
"own and no emulation applies");
|
|
}
|
|
}
|
|
}
|
|
if (dsBits != 0) {
|
|
DrainBlitErrors();
|
|
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
// Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate
|
|
// pass runs in the opposite direction, so a driver that declines a multisample
|
|
// depth/stencil resolve leaves the destination holding its clear value. The log
|
|
// is the whole diagnostic - the frontend performs no validation of its own, so
|
|
// this never reaches the application as a GL error.
|
|
MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on "
|
|
"its own and no emulation applies");
|
|
}
|
|
}
|
|
DrainBlitErrors();
|
|
}
|
|
|
|
// ---- glBlitFramebuffer onto a non-zero array layer -------------------------------------
|
|
//
|
|
// Some drivers write to layer 0 whatever layer the DRAW framebuffer's
|
|
// glFramebufferTextureLayer attachment names, and raise no error doing it (Adreno 830;
|
|
// SelfTest::ProbeBlitIgnoresDestinationArrayLayer measures it, with the destination-layer-0
|
|
// case as the control). glCopyImageSubData takes the destination layer as an argument rather
|
|
// than reading it off an attachment, and honours it on the same driver - so a blit that is a
|
|
// plain 1:1 copy is issued that way instead.
|
|
//
|
|
// ONLY a plain 1:1 copy. glCopyImageSubData cannot scale, flip, convert format or resolve
|
|
// samples, and it is not clipped by the scissor, so every one of those is a reason to hand
|
|
// the call back to the driver rather than quietly perform a different operation. Those blits
|
|
// still land on the wrong layer; a once-per-process line says so rather than leaving it to be
|
|
// rediscovered.
|
|
//
|
|
// Per aspect, not all-or-nothing: the returned mask is the bits this performed itself, and
|
|
// the caller passes the rest to the driver. A COLOR|DEPTH blit whose colour half scales and
|
|
// whose depth half does not still gets its depth half repaired.
|
|
static GLbitfield BlitLayeredDestinationAspects(
|
|
const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
|
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer, GLint srcX0, GLint srcY0,
|
|
GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) {
|
|
if (mask == 0 || !readFramebuffer || !drawFramebuffer) return 0;
|
|
if (!g_GLESFuncs.glCopyImageSubData) return 0;
|
|
if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0;
|
|
|
|
// The default framebuffer has no layers to get wrong, and a blit between the two halves
|
|
// of the same framebuffer object is not a shape this substitutes for.
|
|
const Int width = srcX1 - srcX0;
|
|
const Int height = srcY1 - srcY0;
|
|
const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height;
|
|
// The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two
|
|
// different operations no matter how the rectangles line up.
|
|
const Bool scissorEnabled =
|
|
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
|
|
|
using MobileGL::FramebufferAttachmentType;
|
|
struct AspectPlan {
|
|
GLbitfield bit;
|
|
FramebufferAttachmentType source;
|
|
FramebufferAttachmentType destination;
|
|
};
|
|
// The colour aspect follows glReadBuffer on the read side and draw buffer 0 on the write
|
|
// side, which is the only draw buffer a blit onto a layered destination can be pinned to
|
|
// here: a blit writes EVERY enabled draw buffer, so a framebuffer with more than one is
|
|
// left to the driver rather than half-repaired.
|
|
const auto& drawBuffers = drawFramebuffer->GetDrawBuffers();
|
|
// GL_NONE is what an unwritten draw-buffer slot holds, and it is a different value from
|
|
// the "no such attachment" one - counting it as enabled made every framebuffer look like
|
|
// it had eight and sent every colour blit to the driver.
|
|
//
|
|
// The buffer is found rather than assumed to be slot 0: a blit writes every ENABLED draw
|
|
// buffer, and glDrawBuffers(GL_NONE, GL_NONE, GL_NONE, GL_COLOR_ATTACHMENT0) leaves slot
|
|
// 0 empty while still naming exactly one destination.
|
|
Int enabledDrawBuffers = 0;
|
|
FramebufferAttachmentType colorDestination = FramebufferAttachmentType::None;
|
|
for (const FramebufferAttachmentType buffer : drawBuffers) {
|
|
if (buffer != FramebufferAttachmentType::Unknown && buffer != FramebufferAttachmentType::None) {
|
|
++enabledDrawBuffers;
|
|
if (enabledDrawBuffers == 1) colorDestination = buffer;
|
|
}
|
|
}
|
|
const AspectPlan plans[] = {
|
|
{GL_COLOR_BUFFER_BIT, readFramebuffer->GetReadBuffer(), colorDestination},
|
|
{GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth},
|
|
{GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil},
|
|
};
|
|
|
|
GLbitfield handled = 0;
|
|
for (const AspectPlan& plan : plans) {
|
|
if ((mask & plan.bit) == 0) continue;
|
|
if (plan.source == FramebufferAttachmentType::Unknown ||
|
|
plan.destination == FramebufferAttachmentType::Unknown ||
|
|
plan.source == FramebufferAttachmentType::None ||
|
|
plan.destination == FramebufferAttachmentType::None) {
|
|
continue;
|
|
}
|
|
const auto& sourceAttachment = readFramebuffer->GetAttachment(plan.source);
|
|
const auto& destinationAttachment = drawFramebuffer->GetAttachment(plan.destination);
|
|
// Renderbuffers have no layers, so a destination that is one cannot be hitting this.
|
|
if (!sourceAttachment.IsTexture() || !destinationAttachment.IsTexture()) continue;
|
|
// Layer 0 is the case the driver gets right, and a LAYERED attachment (glFramebufferTexture
|
|
// with no layer) blits its layer 0 by spec - neither is this defect.
|
|
if (destinationAttachment.GetTextureLayer() == 0) continue;
|
|
if (destinationAttachment.IsLayered() || sourceAttachment.IsLayered()) continue;
|
|
|
|
const auto& sourceTexture = sourceAttachment.GetTexture();
|
|
const auto& destinationTexture = destinationAttachment.GetTexture();
|
|
if (!sourceTexture || !destinationTexture) continue;
|
|
// glCopyImageSubData moves texel blocks: same format both ends, or it is a different
|
|
// operation. Multisample endpoints would additionally have to agree on sample count,
|
|
// which is a resolve the driver still owns.
|
|
if (sourceTexture->GetFormat() != destinationTexture->GetFormat()) continue;
|
|
if (sourceTexture->GetSamples() > 0 || destinationTexture->GetSamples() > 0) continue;
|
|
// Copying an image region onto itself is undefined for glCopyImageSubData, and a blit
|
|
// whose source and destination overlap is undefined for GL too - so this is not a
|
|
// shape to substitute FOR, it is one to leave exactly as the application wrote it.
|
|
if (sourceTexture == destinationTexture &&
|
|
sourceAttachment.GetTextureLevel() == destinationAttachment.GetTextureLevel() &&
|
|
sourceAttachment.GetTextureLayer() == destinationAttachment.GetTextureLayer()) {
|
|
continue;
|
|
}
|
|
|
|
// A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both
|
|
// aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT
|
|
// on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to
|
|
// keep, so the copy is only allowed when the mask covers everything the format holds.
|
|
const TextureInternalFormat format = destinationTexture->GetFormat();
|
|
const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format);
|
|
const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format);
|
|
if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue;
|
|
if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue;
|
|
// ... and having carried both, it must be credited with both, or the caller hands the
|
|
// stencil half to the driver and it lands on layer 0 after all.
|
|
const GLbitfield aspectBits =
|
|
hasDepth || hasStencil
|
|
? static_cast<GLbitfield>((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) |
|
|
(hasStencil ? GL_STENCIL_BUFFER_BIT : 0))
|
|
: static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
|
if ((handled & aspectBits) == aspectBits) continue;
|
|
|
|
if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) {
|
|
MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this "
|
|
"blit cannot be expressed as a copy (%s), so it will land on layer 0",
|
|
!oneToOne ? "it scales or flips"
|
|
: scissorEnabled ? "the scissor test is enabled"
|
|
: "the destination has more than one draw buffer");
|
|
continue;
|
|
}
|
|
|
|
auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture);
|
|
auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture);
|
|
if (!backendSource || !backendDestination) continue;
|
|
const GLuint sourceName = backendSource->GetBackendTextureId();
|
|
const GLuint destinationName = backendDestination->GetBackendTextureId();
|
|
if (sourceName == 0 || destinationName == 0) continue;
|
|
const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(sourceTexture->GetTarget());
|
|
const GLenum destinationTarget =
|
|
TextureImpl::ConvertTextureTargetToBackendGLEnum(destinationTexture->GetTarget());
|
|
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, sourceAttachment.GetTextureLevel(), srcX0, srcY0,
|
|
sourceAttachment.GetTextureLayer(), destinationName, destinationTarget,
|
|
destinationAttachment.GetTextureLevel(), dstX0, dstY0,
|
|
destinationAttachment.GetTextureLayer(), width, height, 1);
|
|
if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) {
|
|
// The driver blit still runs for this aspect - onto the wrong layer, but the
|
|
// substitute has to leave the call no worse off than it found it.
|
|
MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the "
|
|
"driver blit will run instead and land on layer 0",
|
|
MG_Util::ConvertGLEnumToString(error).c_str());
|
|
continue;
|
|
}
|
|
handled |= aspectBits;
|
|
}
|
|
return handled & mask;
|
|
}
|
|
|
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
|
GLint dstY1, GLbitfield mask, GLenum filter) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
|
|
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());
|
|
});
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
BindCurrentFBO(FramebufferTarget::Read);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0,
|
|
dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
|
// A no-op on every driver that honours a non-zero destination array layer, which is all
|
|
// of them but the probed one. Whatever it performs itself is taken out of the mask.
|
|
mask &= ~BlitLayeredDestinationAspects(
|
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
|
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
|
|
srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask);
|
|
if (mask != 0) {
|
|
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
|
}
|
|
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 BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
|
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
|
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
|
GLbitfield mask, GLenum filter) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
RenderStateImpl::SyncRenderState();
|
|
|
|
SyncAndBindFramebufferObject(readFramebuffer, FramebufferTarget::Read, true);
|
|
SyncAndBindFramebufferObject(drawFramebuffer, FramebufferTarget::Draw, true);
|
|
|
|
MGLOG_D("ES %s(%d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", __func__, srcX0, srcY0, srcX1, srcY1,
|
|
dstX0, dstY0, dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
|
|
// See the DSA-free entry point above: only the probed defect makes this do anything.
|
|
mask &= ~BlitLayeredDestinationAspects(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0,
|
|
dstY0, dstX1, dstY1, mask);
|
|
if (mask != 0) {
|
|
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
|
}
|
|
// Debug-only diagnostics: which GLES depth texture did this blit write?
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
|
if (mask & GL_DEPTH_BUFFER_BIT) {
|
|
static int diagCount = 0;
|
|
if ((diagCount++ % 600) < 4) {
|
|
GLint readFbo = 0, drawFbo = 0, readDepth = 0, drawDepth = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFbo);
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &readDepth);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &drawDepth);
|
|
MGLOG_D("DBLIT readFbo=%d(depth=%d) -> drawFbo=%d(depth=%d) rect=(%d,%d,%d,%d)->(%d,%d,%d,%d)",
|
|
readFbo, readDepth, drawFbo, drawDepth, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
|
dstY1);
|
|
}
|
|
}
|
|
#endif
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
ForceBindCurrentFBO(FramebufferTarget::Read);
|
|
ForceBindCurrentFBO(FramebufferTarget::Draw);
|
|
}
|
|
|
|
Bool UpdateTextureBindingAtTarget(GLenum target) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
auto unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
|
|
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;
|
|
}
|
|
|
|
const auto& bindingSlot = textureUnit.GetBindingSlot(textureTarget);
|
|
{
|
|
const auto& textureObject = bindingSlot.GetBoundObject();
|
|
if (!textureObject) {
|
|
MGLOG_D("%s: Texture target %s does not have texture bound.", __func__,
|
|
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
|
}
|
|
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
|
auto& backendObj = backendTextureSlot
|
|
? *backendTextureSlot
|
|
: TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
|
|
if (!backendObj) {
|
|
backendObj = MakeShared<TextureImpl::BackendTextureObject>();
|
|
}
|
|
backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---- Scoped driver-state guards for the readback/copy/blit emulation paths --------------------
|
|
// These paths borrow driver state (FBO bindings, scratch-FBO attachments, PACK
|
|
// pixel-store, the pack-PBO binding, scissor) that the app never asked to
|
|
// change; every mutation is scoped by an RAII guard so no exit path can leak
|
|
// it. Saves/restores go through the DirectGLES driver-state shadows
|
|
// (Managers.h) instead of glGetIntegerv - no driver round-trips, and redundant
|
|
// rebinds/resets no-op.
|
|
|
|
// Saves the driver READ/DRAW framebuffer binding(s) and restores them on exit.
|
|
// Per-instance state: nesting-safe.
|
|
class ScopedFramebufferBinding {
|
|
public:
|
|
ScopedFramebufferBinding(Bool saveRead, Bool saveDraw) : m_saveRead(saveRead), m_saveDraw(saveDraw) {
|
|
if (m_saveRead) m_prevRead = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Read);
|
|
if (m_saveDraw) m_prevDraw = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Draw);
|
|
}
|
|
~ScopedFramebufferBinding() {
|
|
if (m_saveRead) FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, m_prevRead);
|
|
if (m_saveDraw) FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_prevDraw);
|
|
}
|
|
ScopedFramebufferBinding(const ScopedFramebufferBinding&) = delete;
|
|
ScopedFramebufferBinding& operator=(const ScopedFramebufferBinding&) = delete;
|
|
|
|
private:
|
|
const Bool m_saveRead;
|
|
const Bool m_saveDraw;
|
|
GLuint m_prevRead = 0;
|
|
GLuint m_prevDraw = 0;
|
|
};
|
|
|
|
// Applies a PACK pixel-store configuration and restores the previous one on exit.
|
|
class ScopedPackState {
|
|
public:
|
|
explicit ScopedPackState(const PixelStoreImpl::PackState& desired)
|
|
: m_prev(PixelStoreImpl::CurrentPackState()) {
|
|
PixelStoreImpl::ApplyPackState(desired);
|
|
}
|
|
~ScopedPackState() { PixelStoreImpl::ApplyPackState(m_prev); }
|
|
ScopedPackState(const ScopedPackState&) = delete;
|
|
ScopedPackState& operator=(const ScopedPackState&) = delete;
|
|
|
|
private:
|
|
const PixelStoreImpl::PackState m_prev;
|
|
};
|
|
|
|
// The frontend's current PACK parameters, for readbacks the ES driver serves
|
|
// directly with the client's layout.
|
|
static PixelStoreImpl::PackState PackStateFromContext() {
|
|
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
|
return {static_cast<GLint>(packParams.Alignment), static_cast<GLint>(packParams.RowLength),
|
|
static_cast<GLint>(packParams.SkipRows), static_cast<GLint>(packParams.SkipPixels)};
|
|
}
|
|
|
|
// Binds a pixel PACK buffer (0 = client memory) for the scope and returns the
|
|
// binding to the resting 0 state on exit, so no later readback can accidentally
|
|
// capture into a stale PBO.
|
|
class ScopedPixelPackBuffer {
|
|
public:
|
|
explicit ScopedPixelPackBuffer(GLuint id) { BufferImpl::BindPixelPackBufferId(id); }
|
|
~ScopedPixelPackBuffer() { BufferImpl::BindPixelPackBufferId(0); }
|
|
ScopedPixelPackBuffer(const ScopedPixelPackBuffer&) = delete;
|
|
ScopedPixelPackBuffer& operator=(const ScopedPixelPackBuffer&) = delete;
|
|
};
|
|
|
|
// Force-disables GL_SCISSOR_TEST for the scope (emulation blits and clears are
|
|
// scissored; readback copies must not be clipped by app scissor state) and
|
|
// restores the app state on exit, tracked via the render-state shadow.
|
|
class ScopedScissorDisable {
|
|
public:
|
|
ScopedScissorDisable()
|
|
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
|
|
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
|
}
|
|
~ScopedScissorDisable() {
|
|
if (m_wasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
|
}
|
|
ScopedScissorDisable(const ScopedScissorDisable&) = delete;
|
|
ScopedScissorDisable& operator=(const ScopedScissorDisable&) = delete;
|
|
|
|
private:
|
|
const Bool m_wasEnabled;
|
|
};
|
|
|
|
// Binds the shared scratch FBO at READ (isRead) or DRAW for one temp operation,
|
|
// restoring the previous binding on exit. Attachments are managed through the
|
|
// ScratchFBOImpl attachment shadow by the caller (see Framebuffer()).
|
|
class TempFBOBinder {
|
|
public:
|
|
explicit TempFBOBinder(Bool isRead)
|
|
: m_binding(/*saveRead=*/isRead, /*saveDraw=*/!isRead),
|
|
m_target(isRead ? GL_READ_FRAMEBUFFER : GL_DRAW_FRAMEBUFFER) {
|
|
FramebufferImpl::BindFramebufferId(m_target, ScratchFBOImpl::EnsureId(Framebuffer()));
|
|
}
|
|
ScratchFBOImpl::ScratchFramebuffer& Framebuffer() const { return ScratchFBOImpl::TempFramebuffer(); }
|
|
GLenum Target() const { return m_target; }
|
|
|
|
private:
|
|
ScopedFramebufferBinding m_binding;
|
|
const GLenum m_target;
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
static Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
|
|
Int maxDimension = std::max<Int>(
|
|
baseTexelSize.x(),
|
|
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
|
|
Uint mipLevelCount = 1;
|
|
while (maxDimension > 1) {
|
|
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
|
++mipLevelCount;
|
|
}
|
|
return mipLevelCount;
|
|
}
|
|
|
|
static IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
|
|
return {
|
|
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
|
|
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
|
|
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
|
|
};
|
|
}
|
|
|
|
static Bool EnsureGenerateMipmapStorageAllocated(MG_State::GLState::TextureObjectMipmap& texture,
|
|
TextureUploadTarget uploadTarget, Bool& allocatedStorage) {
|
|
const Uint existingLevelCount = texture.GetMipmapLevelCount();
|
|
if (existingLevelCount == 0) {
|
|
return false;
|
|
}
|
|
|
|
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, 0);
|
|
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, 0);
|
|
const SizeT baseTexelCount = static_cast<SizeT>(baseTexelSize.x()) *
|
|
static_cast<SizeT>(baseTexelSize.y()) *
|
|
static_cast<SizeT>(baseTexelSize.z());
|
|
if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 ||
|
|
baseByteSize == 0 || baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) {
|
|
return false;
|
|
}
|
|
|
|
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
|
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
|
|
if (existingLevelCount < requiredLevelCount) {
|
|
allocatedStorage = true;
|
|
}
|
|
for (Uint level = existingLevelCount; level < requiredLevelCount; ++level) {
|
|
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level);
|
|
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
|
static_cast<SizeT>(levelTexelSize.y()) *
|
|
static_cast<SizeT>(levelTexelSize.z());
|
|
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
|
texture.MarkStorageDirty(uploadTarget, level, false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
|
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
|
MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage.");
|
|
Bool allocatedStorage = false;
|
|
for (const TextureUploadTarget uploadTarget : texture->GetUploadTargets()) {
|
|
const Bool allocated = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, uploadTarget, allocatedStorage);
|
|
MOBILEGL_ASSERT(allocated, "GenerateMipmap could not allocate generated mipmap storage.");
|
|
}
|
|
return allocatedStorage;
|
|
}
|
|
|
|
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_State::pGLContext->RecordError(
|
|
ConvertGLESErrorToErrorCode(err),
|
|
MakeUnique<GenericErrorInfo>("DirectGLES", operation,
|
|
MG_Util::ConvertGLEnumToString(err)));
|
|
return false;
|
|
}
|
|
|
|
static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); }
|
|
|
|
// 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:
|
|
explicit ScopedDetachedTextureFramebufferAttachments(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
|
if (texture == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(texture.get());
|
|
if (!backendTextureSlot || !*backendTextureSlot) {
|
|
return;
|
|
}
|
|
const GLuint backendTextureId = (*backendTextureSlot)->GetBackendTextureId();
|
|
|
|
for (auto it = FramebufferImpl::g_backendFramebufferObjects.begin();
|
|
it != FramebufferImpl::g_backendFramebufferObjects.end(); ++it) {
|
|
auto* stateFBO = it->first;
|
|
const auto& backendFBO = it->second.backend;
|
|
// An entry whose state object died is only waiting for the next collection;
|
|
// the key is a dangling address, so it must not be dereferenced here.
|
|
if (stateFBO == nullptr || !backendFBO || it->second.stateRef.expired() ||
|
|
stateFBO->IsDefaultFramebuffer()) {
|
|
continue;
|
|
}
|
|
|
|
const auto& attachments = stateFBO->GetAllAttachmentObjects();
|
|
for (SizeT i = 0; i < attachments.size(); ++i) {
|
|
const auto& attachmentObject = attachments[i];
|
|
if (!attachmentObject.IsTexture() || attachmentObject.GetTexture().get() != texture.get()) {
|
|
continue;
|
|
}
|
|
|
|
const auto frontendType = static_cast<FramebufferAttachmentType>(i);
|
|
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) {
|
|
continue;
|
|
}
|
|
|
|
GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
|
|
attachmentObject.GetTextureUploadTarget());
|
|
if (textureTarget == GL_UNKNOWN_MGL) {
|
|
textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(texture->GetTarget());
|
|
}
|
|
|
|
const GLuint backendFBOId = backendFBO->GetBackendFramebufferId();
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId);
|
|
if (attachmentObject.IsLayered()) {
|
|
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>(attachmentObject.GetTextureLevel()), attachmentObject.IsLayered()});
|
|
}
|
|
}
|
|
}
|
|
|
|
~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");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static void GenerateDepthTexture2DMipmap(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
|
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
|
MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateDepthTexture2DMipmap needs texture.");
|
|
MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D,
|
|
"DirectGLES depth mipmap generation only supports GL_TEXTURE_2D.");
|
|
MOBILEGL_ASSERT(IsDepthOnlyFormat(texture->GetFormat()),
|
|
"DirectGLES depth mipmap generation requires a depth-only texture.");
|
|
|
|
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
|
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage.");
|
|
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
|
|
MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage.");
|
|
|
|
const GLuint textureId = backendTexture->GetBackendTextureId();
|
|
for (Uint level = 1; level < mipLevelCount; ++level) {
|
|
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1);
|
|
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 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 GenerateColorTexture2DMipmap(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
|
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
|
|
MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateColorTexture2DMipmap needs texture.");
|
|
MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D,
|
|
"DirectGLES color mipmap generation only supports GL_TEXTURE_2D.");
|
|
MOBILEGL_ASSERT(IsColorOnlyFormat(texture->GetFormat()),
|
|
"DirectGLES color mipmap generation requires a color-only texture.");
|
|
|
|
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
|
|
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage.");
|
|
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
|
|
MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage.");
|
|
|
|
const GLenum filter = IsIntegerColorFormat(texture->GetFormat()) ? GL_NEAREST : GL_LINEAR;
|
|
const GLuint textureId = backendTexture->GetBackendTextureId();
|
|
for (Uint level = 1; level < mipLevelCount; ++level) {
|
|
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1);
|
|
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 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_State::pGLContext->GetActiveTextureUnit();
|
|
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit)
|
|
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
|
.GetBoundObject();
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
|
if (!backendTextureSlot || !*backendTextureSlot) {
|
|
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
|
textureObject ? textureObject->GetExternalIndex() : 0);
|
|
return;
|
|
}
|
|
(*backendTextureSlot)->Bind(target, activeTextureUnit);
|
|
|
|
auto mgInternalFormat = textureObject->GetFormat();
|
|
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)(*backendTextureSlot)->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_State::pGLContext->GetActiveTextureUnit();
|
|
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
|
|
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
|
.GetBoundObject();
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
|
if (!backendTextureSlot || !*backendTextureSlot) {
|
|
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
|
textureObject ? textureObject->GetExternalIndex() : 0);
|
|
return;
|
|
}
|
|
(*backendTextureSlot)->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 = (*backendTextureSlot)->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());
|
|
});
|
|
}
|
|
}
|
|
|
|
// ES has no colour-renderable three-channel float format, and its glGenerateMipmap
|
|
// requires one, so it rejects GL_RGB16F and GL_RGB32F outright where every desktop
|
|
// driver accepts them. Both store a plain array of floats, and a format the driver
|
|
// cannot render into is a format nothing can have rendered into - so the frontend's own
|
|
// copy of the texels is the authority, and the chain can be filtered there and carried
|
|
// down by the ordinary upload path. Returns false for anything else, leaving the
|
|
// driver's answer (including its error) in place.
|
|
static Bool GenerateThreeChannelFloatMipmapOnCpu(
|
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
|
if (!texture) return false;
|
|
const TextureInternalFormat format = texture->GetFormat();
|
|
const Bool isHalf = format == TextureInternalFormat::RGB16F;
|
|
if (!isHalf && format != TextureInternalFormat::RGB32F) return false;
|
|
|
|
auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get());
|
|
if (mipmapTexture == nullptr) return false;
|
|
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
|
constexpr Int kChannels = 3;
|
|
|
|
for (const auto uploadTarget : texture->GetUploadTargets()) {
|
|
for (Uint level = 1; level < levelCount; ++level) {
|
|
const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level - 1);
|
|
const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
|
|
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return false;
|
|
auto* src = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level - 1));
|
|
auto* dst = static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, level));
|
|
if (src == nullptr || dst == nullptr) return false;
|
|
|
|
const SizeT componentBytes = isHalf ? sizeof(Uint16) : sizeof(Float);
|
|
const SizeT texelBytes = componentBytes * kChannels;
|
|
const auto load = [&](const Uint8* base, Int x, Int y, Int channel) {
|
|
const Uint8* texel = base + (static_cast<SizeT>(y) * srcSize.x() + x) * texelBytes +
|
|
channel * componentBytes;
|
|
if (isHalf) {
|
|
Uint16 bits = 0;
|
|
Memcpy(&bits, texel, sizeof(bits));
|
|
return MG_Util::DecodeHalfBitsToFloat(bits);
|
|
}
|
|
Float value = 0.0f;
|
|
Memcpy(&value, texel, sizeof(value));
|
|
return value;
|
|
};
|
|
|
|
// Box filter over the 2x2 source footprint, clamped where a dimension is
|
|
// already 1 (GL 4.6 core 8.14.4 leaves the exact filter to the implementation
|
|
// and this is the one it describes for power-of-two levels).
|
|
for (Int y = 0; y < dstSize.y(); ++y) {
|
|
for (Int x = 0; x < dstSize.x(); ++x) {
|
|
const Int x0 = std::min(x * 2, srcSize.x() - 1);
|
|
const Int x1 = std::min(x * 2 + 1, srcSize.x() - 1);
|
|
const Int y0 = std::min(y * 2, srcSize.y() - 1);
|
|
const Int y1 = std::min(y * 2 + 1, srcSize.y() - 1);
|
|
for (Int channel = 0; channel < kChannels; ++channel) {
|
|
const Float average = 0.25f * (load(src, x0, y0, channel) + load(src, x1, y0, channel) +
|
|
load(src, x0, y1, channel) + load(src, x1, y1, channel));
|
|
Uint8* texel = dst + (static_cast<SizeT>(y) * dstSize.x() + x) * texelBytes +
|
|
channel * componentBytes;
|
|
if (isHalf) {
|
|
const Uint16 bits = MG_Util::EncodeFloatToHalfBits(average);
|
|
Memcpy(texel, &bits, sizeof(bits));
|
|
} else {
|
|
Memcpy(texel, &average, sizeof(average));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mipmapTexture->MarkStorageDirty(uploadTarget, level, true);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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
|
|
auto unitIndex = MG_State::pGLContext->GetActiveTextureUnit();
|
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(unitIndex);
|
|
auto& slot = unit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
auto& texture = slot.GetBoundObject();
|
|
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
|
|
if (texture->GetFormat() == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(texture->GetFormat()) ||
|
|
texture->GetFormat() == TextureInternalFormat::RGB16F ||
|
|
texture->GetFormat() == TextureInternalFormat::RGB32F) {
|
|
EnsureGenerateMipmapStorageAllocated(texture);
|
|
}
|
|
// Filtered on the CPU before the backend sync, so the dirty levels ride down with it.
|
|
if (GenerateThreeChannelFloatMipmapOnCpu(texture)) {
|
|
TextureImpl::SyncTextureObjectToBackend(texture);
|
|
return;
|
|
}
|
|
auto& backendTexture = TextureImpl::SyncTextureObjectToBackend(texture);
|
|
|
|
if (IsDepthOnlyFormat(texture->GetFormat())) {
|
|
GenerateDepthTexture2DMipmap(texture, backendTexture);
|
|
return;
|
|
}
|
|
if (texture->GetFormat() == TextureInternalFormat::R11FG11FB10F &&
|
|
texture->GetTarget() == TextureTarget::Texture2D) {
|
|
GenerateColorTexture2DMipmap(texture, backendTexture);
|
|
return;
|
|
}
|
|
|
|
const GLenum backendTarget =
|
|
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
|
|
backendTexture->Bind(backendTarget, unitIndex);
|
|
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps.
|
|
// Also detach the source texture from synced FBO objects for ANGLE's validation.
|
|
ScopedDetachedTextureFramebufferAttachments detachedAttachments(texture);
|
|
// Bind a complete internal FBO that does not reference the source texture.
|
|
ScopedCompleteFramebufferBinding completeFramebuffer;
|
|
// ErrorLopper is compiled out at the default log level; RecordGLError below
|
|
// forwards the next queued error to the APP, so stale flags from earlier
|
|
// best-effort calls must be drained by the always-live helper.
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glGenerateMipmap(backendTarget);
|
|
RecordGLError("glGenerateMipmap", backendTarget, texture->GetFormat());
|
|
}
|
|
|
|
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) {
|
|
g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(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;
|
|
}
|
|
};
|
|
|
|
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
|
|
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
|
|
// reachable from a path that has a renderbuffer but no framebuffer.
|
|
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
|
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
|
|
if (!renderbufferObject) return nullptr;
|
|
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
|
|
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
|
|
backendRenderbufferObject = *slot;
|
|
} else {
|
|
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
|
|
if (!newSlot) {
|
|
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
}
|
|
backendRenderbufferObject = newSlot;
|
|
}
|
|
backendRenderbufferObject->SyncToBackend(renderbufferObject);
|
|
return backendRenderbufferObject;
|
|
}
|
|
|
|
static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y,
|
|
GLint z, GLESCopyImageEndpoint& out) {
|
|
if (endpoint.IsRenderbuffer()) {
|
|
out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer);
|
|
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.
|
|
if (!endpoint.Texture) return false;
|
|
out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture);
|
|
if (!out.texture) return false;
|
|
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
|
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
|
// 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) {
|
|
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;
|
|
|
|
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
|
|
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
|
|
const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast<Uint>(srcLevel));
|
|
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
|
|
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
|
|
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
|
|
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
|
|
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
|
|
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
|
|
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
|
|
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(dstLevel));
|
|
// A shadow that is not exactly texels x texelSize bytes is one this cannot index (a
|
|
// compressed blob, or a level whose allocation disagrees with its recorded extent).
|
|
const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels;
|
|
if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 ||
|
|
dstBytes != dstTexels * texelBytes) {
|
|
return;
|
|
}
|
|
if (static_cast<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
|
|
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
|
|
static_cast<SizeT>(srcZ) + depth > srcSlices ||
|
|
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
|
|
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
|
|
static_cast<SizeT>(dstZ) + depth > dstSlices) {
|
|
return;
|
|
}
|
|
|
|
const auto* srcBase = static_cast<const Uint8*>(
|
|
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
|
|
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
|
|
if (!srcBase || !dstBase) return;
|
|
|
|
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
|
|
for (GLsizei slice = 0; slice < depth; ++slice) {
|
|
for (GLsizei row = 0; row < height; ++row) {
|
|
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
|
|
static_cast<SizeT>(srcY + row)) *
|
|
static_cast<SizeT>(srcSize.x()) +
|
|
static_cast<SizeT>(srcX)) *
|
|
texelBytes;
|
|
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
|
|
static_cast<SizeT>(dstY + row)) *
|
|
static_cast<SizeT>(dstSize.x()) +
|
|
static_cast<SizeT>(dstX)) *
|
|
texelBytes;
|
|
Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes);
|
|
}
|
|
}
|
|
MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height,
|
|
depth);
|
|
}
|
|
|
|
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.
|
|
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;
|
|
}
|
|
|
|
// 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.");
|
|
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)texture;
|
|
(void)level;
|
|
(void)layered;
|
|
(void)layer;
|
|
(void)access;
|
|
(void)format;
|
|
TextureImpl::SyncImageTextureBinding(unit);
|
|
}
|
|
|
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
|
|
if (!data) return;
|
|
|
|
switch (target) {
|
|
case GL_SHADER_STORAGE_BUFFER_BINDING: {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
|
auto& obj = point.GetBoundObject();
|
|
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
return;
|
|
}
|
|
case GL_SHADER_STORAGE_BUFFER_START: {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
|
*data = static_cast<GLint>(point.GetRange().start);
|
|
return;
|
|
}
|
|
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
|
auto& obj = point.GetBoundObject();
|
|
if (!obj) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
const auto& range = point.GetRange();
|
|
const auto start = std::min(range.start, obj->GetSize());
|
|
const auto end = std::min(range.end, obj->GetSize());
|
|
*data = static_cast<GLint>(end - start);
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_NAME: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_LEVEL: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = imageBinding.Level;
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_LAYERED: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = imageBinding.Layered;
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_LAYER: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = imageBinding.Layer;
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_ACCESS: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = static_cast<GLint>(imageBinding.Access);
|
|
return;
|
|
}
|
|
case GL_IMAGE_BINDING_FORMAT: {
|
|
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
|
*data = static_cast<GLint>(imageBinding.Format);
|
|
return;
|
|
}
|
|
default:
|
|
if (g_GLESFuncs.glGetIntegeri_v) {
|
|
g_GLESFuncs.glGetIntegeri_v(target, index, data);
|
|
} else {
|
|
*data = 0;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
|
if (!data) return;
|
|
|
|
switch (target) {
|
|
case GL_SHADER_STORAGE_BUFFER_START: {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
|
*data = static_cast<GLint64>(point.GetRange().start);
|
|
return;
|
|
}
|
|
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
|
auto& obj = point.GetBoundObject();
|
|
if (!obj) {
|
|
*data = 0;
|
|
return;
|
|
}
|
|
const auto& range = point.GetRange();
|
|
const auto start = std::min(range.start, obj->GetSize());
|
|
const auto end = std::min(range.end, obj->GetSize());
|
|
*data = static_cast<GLint64>(end - start);
|
|
return;
|
|
}
|
|
default:
|
|
if (g_GLESFuncs.glGetInteger64i_v) {
|
|
g_GLESFuncs.glGetInteger64i_v(target, index, data);
|
|
} else {
|
|
*data = 0;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
|
|
if (!params) return;
|
|
GLuint backendProgramId = GetBackendProgramId(program);
|
|
if (!backendProgramId) {
|
|
params[0] = 0;
|
|
return;
|
|
}
|
|
g_GLESFuncs.glGetProgramiv(backendProgramId, pname, params);
|
|
}
|
|
|
|
// 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 optimisation: 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;
|
|
if (!MG_State::pGLContext->ValidateProgramName(program)) return;
|
|
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
|
if (!programObject) return;
|
|
|
|
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
|
|
if (!backendProgramSlot || !*backendProgramSlot) return;
|
|
auto& backendObj = *backendProgramSlot;
|
|
// Not merely "a program id exists": a backend object whose synced link version has
|
|
// fallen behind is about to be rebuilt anyway, and its current driver interface is
|
|
// the PREVIOUS link's - applying to it could land the binding on an unrelated block.
|
|
if (!backendObj->GetBackendProgramId() ||
|
|
backendObj->GetSyncedLinkVersion() != programObject->GetLinkVersion()) {
|
|
return; // SyncToBackend's reseed will carry it
|
|
}
|
|
PrgramImpl::ApplyShaderStorageBlockBinding(backendObj->GetBackendProgramId(), storageBlockName,
|
|
storageBlockBinding);
|
|
}
|
|
|
|
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 {
|
|
using FramebufferImpl::SubstituteWidenedClearAlpha;
|
|
|
|
// A colour 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;
|
|
}
|
|
|
|
// The same question for an explicitly named framebuffer (the DSA clears), which is NOT
|
|
// the one g_alphaWidenedDrawBufferMask describes at the point these run.
|
|
Bool IsWidenedNamedDrawBuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
GLenum buffer, GLint drawbuffer) {
|
|
using FBO = MG_State::GLState::FramebufferObject;
|
|
if (buffer != GL_COLOR || !framebuffer || drawbuffer < 0 ||
|
|
drawbuffer >= static_cast<GLint>(FBO::MAX_DRAW_BUFFERS)) {
|
|
return false;
|
|
}
|
|
const auto frontendBuf = framebuffer->GetDrawBuffers()[static_cast<SizeT>(drawbuffer)];
|
|
if (frontendBuf < FramebufferAttachmentType::Color0 ||
|
|
frontendBuf > FramebufferAttachmentType::Color31) {
|
|
return false;
|
|
}
|
|
return FramebufferImpl::IsAlphaWidenedColorAttachment(framebuffer->GetAttachment(frontendBuf));
|
|
}
|
|
} // 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));
|
|
}
|
|
|
|
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
|
GLfloat widenedValue[4] = {};
|
|
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer), 1.0f,
|
|
widenedValue);
|
|
g_GLESFuncs.glClearBufferfv(buffer, drawbuffer, value);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
ForceBindCurrentFBO(FramebufferTarget::Draw);
|
|
}
|
|
|
|
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
RenderStateImpl::SyncRenderState();
|
|
|
|
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
|
g_GLESFuncs.glClearBufferfi(buffer, drawbuffer, depth, stencil);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
ForceBindCurrentFBO(FramebufferTarget::Draw);
|
|
}
|
|
|
|
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
|
GLint widenedValue[4] = {};
|
|
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
|
|
GLint(1), widenedValue);
|
|
g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
ForceBindCurrentFBO(FramebufferTarget::Draw);
|
|
}
|
|
|
|
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
|
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
|
|
DebugImpl::OpenGLScopeMarker marker(__func__);
|
|
#endif
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
RenderStateImpl::SyncRenderState(/*forColorClear=*/buffer == GL_COLOR);
|
|
|
|
SyncAndBindFramebufferObject(framebuffer, FramebufferTarget::Draw, true);
|
|
GLuint widenedValue[4] = {};
|
|
value = SubstituteWidenedClearAlpha(value, IsWidenedNamedDrawBuffer(framebuffer, buffer, drawbuffer),
|
|
GLuint(1), widenedValue);
|
|
g_GLESFuncs.glClearBufferuiv(buffer, drawbuffer, value);
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
ForceBindCurrentFBO(FramebufferTarget::Draw);
|
|
}
|
|
|
|
static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) {
|
|
const SizeT resolvedAlignment = static_cast<SizeT>(std::max(alignment, 1));
|
|
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
|
|
}
|
|
|
|
// Destination walk shared by the depth, stencil and packed depth-stencil readbacks.
|
|
// Each of those produces its rows tightly packed and has to land them in the caller's
|
|
// destination - client memory or the bound pixel-pack buffer - under the PACK
|
|
// pixel-store parameters. `fillRow` is handed the row index and a buffer of exactly one
|
|
// packed row to populate. Only real pixel rows are written, so PACK skip/row-length gap
|
|
// regions stay untouched.
|
|
template <typename FillRow>
|
|
static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels,
|
|
const char* what, FillRow&& fillRow) {
|
|
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
|
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
|
const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
|
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
|
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
|
const SizeT rowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
|
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride + rowBytes;
|
|
const auto& pixelPackBufferObject =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
|
|
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
|
|
MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what);
|
|
return false;
|
|
}
|
|
Vector<Uint8> rowBuf(rowBytes);
|
|
for (GLsizei row = 0; row < height; ++row) {
|
|
fillRow(row, rowBuf.data());
|
|
const SizeT rowOffset = dstOffset + static_cast<SizeT>(row) * dstRowStride;
|
|
if (pixelPackBufferObject) {
|
|
pixelPackBufferObject->WritebackFromBackend({rowBuf.data(), rowBytes}, pboOffset + rowOffset);
|
|
} else if (pixels != nullptr) {
|
|
Memcpy(static_cast<Uint8*>(pixels) + rowOffset, rowBuf.data(), rowBytes);
|
|
}
|
|
}
|
|
if (pixelPackBufferObject) {
|
|
// WritebackFromBackend bumps change serials with no backend op; re-open the
|
|
// buffer draw-clean memos (once for the whole row loop).
|
|
BufferImpl::BumpBufferMutationEpoch();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A normalized depth scaled into the full range of an unsigned integer of `maxValue`
|
|
// (GL 4.6 core table 18.2), without ever rounding past the top of that range.
|
|
static Uint32 NormalizedDepthToUnsigned(Float depth, Double maxValue) {
|
|
const Double clamped = std::min(std::max(static_cast<Double>(depth), 0.0), 1.0);
|
|
const Double scaled = clamped * maxValue + 0.5;
|
|
return static_cast<Uint32>(scaled >= maxValue ? maxValue : scaled);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Depth / stencil readback by shader sampling
|
|
//
|
|
// Desktop GL reads depth and stencil back through glReadPixels; ES has no such call at
|
|
// all. GL_DEPTH_COMPONENT, GL_STENCIL_INDEX and GL_DEPTH_STENCIL are simply not
|
|
// accepted formats there, and the optional extensions that add them (GL_NV_read_depth,
|
|
// GL_NV_read_stencil, GL_NV_read_depth_stencil) are absent on both the Adreno device
|
|
// and Mesa's ES. Every native attempt therefore failed with a GL error and wrote
|
|
// NOTHING, so the caller kept whatever its buffer already held - which is how the CTS
|
|
// reports "expected DEPTH[0.25] but got DEPTH[0.2]": 0.2 is the poison value the test
|
|
// itself put there.
|
|
//
|
|
// What ES *can* do is sample a depth texture, so the emulation goes the long way round:
|
|
//
|
|
// 1. Stage. glBlitFramebuffer the requested rectangle out of the bound READ
|
|
// framebuffer into a scratch depth(-stencil) TEXTURE of the very same sized
|
|
// internal format. One staging copy serves every source kind uniformly - a
|
|
// texture attachment of any target/level/layer, a renderbuffer (not samplable at
|
|
// all), the default framebuffer, and a multisample attachment (the blit resolves
|
|
// it on the way). ES rejects a depth/stencil blit between differing formats, so
|
|
// the scratch has to match the source exactly; see DescribeReadDepthStencilSource.
|
|
// 2. Convert. Draw a full-screen triangle that samples the staged texture into a
|
|
// scratch R32UI colour target: depth as floatBitsToUint (bit-exact for every depth
|
|
// format, and an integer colour target needs no float-renderable extension),
|
|
// stencil through GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX.
|
|
// 3. Read. glReadPixels the colour target with GL_RGBA_INTEGER / GL_UNSIGNED_INT -
|
|
// the pair ES guarantees for an unsigned-integer attachment - and hand the values
|
|
// to the existing re-encoders, which already own the client-side (format, type)
|
|
// layout and the PACK pixel-store parameters.
|
|
//
|
|
// Both scratch images hold the rectangle at their own origin and the pass runs with a
|
|
// matching viewport, so GL's bottom-up row order survives untouched: scratch row 0 is
|
|
// source row `y`, which is exactly the first row glReadPixels(x, y, ...) owes the
|
|
// caller. No flip anywhere.
|
|
namespace DepthStencilSamplingReadImpl {
|
|
static Uint s_contextGeneration = ~0u;
|
|
static GLuint s_colorFramebuffer = 0;
|
|
static GLuint s_colorTexture = 0;
|
|
static GLsizei s_colorWidth = 0;
|
|
static GLsizei s_colorHeight = 0;
|
|
static GLuint s_vertexArray = 0;
|
|
static GLuint s_depthProgram = 0;
|
|
static GLuint s_stencilProgram = 0;
|
|
static GLint s_depthUvTransform = -1;
|
|
static GLint s_stencilUvTransform = -1;
|
|
static Bool s_programsFailed = false;
|
|
|
|
// One staging slot per aspect. A framebuffer is allowed to carry its depth and its
|
|
// stencil in two DIFFERENT objects with two different formats - the framebuffer_blit
|
|
// cases pair a DEPTH_COMPONENT* attachment with a separate STENCIL_INDEX8 one - and
|
|
// the two aspects are staged independently for exactly that reason. A single shared
|
|
// slot would also throw its immutable storage away and re-create it on every
|
|
// alternation between the two.
|
|
struct StageSlot {
|
|
// A framebuffer of its own, not a shared one. GL_DEPTH_STENCIL_ATTACHMENT sets
|
|
// the depth AND the stencil point, so a packed scratch staged for one aspect
|
|
// would leave the other aspect's point pointing at it; the next stage of that
|
|
// other aspect attaches its own (differently formatted) texture to its own point
|
|
// and the framebuffer is then incomplete - which reads as "no candidate format
|
|
// worked" and writes nothing at all.
|
|
GLuint framebuffer = 0;
|
|
GLuint texture = 0;
|
|
GLenum format = 0;
|
|
GLsizei width = 0;
|
|
GLsizei height = 0;
|
|
// The default framebuffer has no queryable internal format, so the one that turns
|
|
// out to be blit-compatible is remembered: it cannot change for the life of the
|
|
// context, and re-probing it on every read would cost a failed blit each time.
|
|
GLenum defaultFramebufferFormat = 0;
|
|
};
|
|
static StageSlot s_slots[2]; // [0] depth, [1] stencil
|
|
|
|
// `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in
|
|
// the fragment language, which is allowed to be 16 bits - it would saw the top half
|
|
// off every depth bit pattern and every stencil fetch.
|
|
static const char* const kDepthFetchFragmentSource =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"precision highp sampler2D;\n"
|
|
"uniform sampler2D uSource;\n"
|
|
"in vec2 vUv;\n"
|
|
"layout(location = 0) out uvec4 oBits;\n"
|
|
"void main() {\n"
|
|
" oBits = uvec4(floatBitsToUint(texture(uSource, vUv).r), 0u, 0u, 0u);\n"
|
|
"}\n";
|
|
|
|
static const char* const kStencilFetchFragmentSource =
|
|
"#version 300 es\n"
|
|
"precision highp float;\n"
|
|
"precision highp int;\n"
|
|
"precision highp usampler2D;\n"
|
|
"uniform usampler2D uSource;\n"
|
|
"in vec2 vUv;\n"
|
|
"layout(location = 0) out uvec4 oBits;\n"
|
|
"void main() {\n"
|
|
" oBits = uvec4(texture(uSource, vUv).r, 0u, 0u, 0u);\n"
|
|
"}\n";
|
|
|
|
static Bool EnsureResources() {
|
|
if (s_contextGeneration != g_backendContextGeneration) {
|
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
|
s_colorFramebuffer = 0;
|
|
s_colorTexture = 0;
|
|
s_colorWidth = 0;
|
|
s_colorHeight = 0;
|
|
s_vertexArray = 0;
|
|
s_depthProgram = 0;
|
|
s_stencilProgram = 0;
|
|
s_programsFailed = false;
|
|
s_slots[0] = StageSlot{};
|
|
s_slots[1] = StageSlot{};
|
|
s_contextGeneration = g_backendContextGeneration;
|
|
}
|
|
if (s_programsFailed) {
|
|
return false;
|
|
}
|
|
if (s_depthProgram == 0) {
|
|
// Same full-screen vertex shader (and its uUvTransform) as the replicate
|
|
// blit: a staging slot is sized to the largest rectangle seen so far, so the
|
|
// quad's [0,1] coordinates have to be scaled down to the part it occupies.
|
|
s_depthProgram = ReplicateBlitImpl::BuildProgram(kDepthFetchFragmentSource);
|
|
s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource);
|
|
if (s_depthProgram == 0 || s_stencilProgram == 0) {
|
|
s_programsFailed = true;
|
|
MGLOG_E_ONCE("ReadPixels: could not build the depth/stencil readback programs");
|
|
return false;
|
|
}
|
|
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
|
|
s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform");
|
|
}
|
|
for (StageSlot& slot : s_slots) {
|
|
if (slot.framebuffer == 0) {
|
|
g_GLESFuncs.glGenFramebuffers(1, &slot.framebuffer);
|
|
if (slot.framebuffer == 0) return false;
|
|
}
|
|
}
|
|
if (s_colorFramebuffer == 0) {
|
|
g_GLESFuncs.glGenFramebuffers(1, &s_colorFramebuffer);
|
|
if (s_colorFramebuffer == 0) return false;
|
|
}
|
|
if (s_vertexArray == 0) {
|
|
g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray);
|
|
if (s_vertexArray == 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Ordered guesses at the sized internal format backing one aspect. More than one
|
|
// entry only when the source's own format cannot be queried (the default
|
|
// framebuffer), in which case the reported channel sizes narrow it down and the
|
|
// staging blit picks the winner by being the only one that raises no GL error.
|
|
struct AspectCandidates {
|
|
GLenum formats[4] = {0, 0, 0, 0};
|
|
Uint count = 0;
|
|
void Push(GLenum format) {
|
|
for (Uint i = 0; i < count; ++i) {
|
|
if (formats[i] == format) return;
|
|
}
|
|
if (count < 4) formats[count++] = format;
|
|
}
|
|
};
|
|
|
|
static GLenum AttachmentPointFor(Bool isDefault, Bool stencilAspect) {
|
|
// The default framebuffer names its buffers GL_DEPTH / GL_STENCIL; a user
|
|
// framebuffer names them GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT, and asking
|
|
// one for the other's spelling is GL_INVALID_ENUM.
|
|
if (isDefault) return stencilAspect ? GL_STENCIL : GL_DEPTH;
|
|
return stencilAspect ? GL_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT;
|
|
}
|
|
|
|
// Returns false when the bound READ framebuffer has no such aspect at all.
|
|
static Bool DescribeAspect(Bool stencilAspect, Bool* outIsDefault, AspectCandidates* out) {
|
|
GLint readFramebuffer = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer);
|
|
const Bool isDefault = readFramebuffer == 0;
|
|
if (outIsDefault) *outIsDefault = isDefault;
|
|
const GLenum point = AttachmentPointFor(isDefault, stencilAspect);
|
|
|
|
ClearGLErrors();
|
|
GLint objectType = GL_NONE;
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, point,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
|
|
ClearGLErrors();
|
|
|
|
// The channel sizes are read before the "is there anything here" decision, because
|
|
// they are the more trustworthy witness. Adreno answers GL_NONE for OBJECT_TYPE on
|
|
// an attachment made by glFramebufferTexture (a layered cube/array attachment) while
|
|
// still reporting its depth and stencil bits correctly, and taking OBJECT_TYPE at
|
|
// its word there makes the whole readback report "no such aspect" for a framebuffer
|
|
// that plainly has one. The OTHER aspect's size matters as much as this one's - a
|
|
// depth buffer that also carries stencil has to be staged into a packed scratch,
|
|
// because ES only blits depth between identical formats.
|
|
GLint depthBits = 0;
|
|
GLint stencilBits = 0;
|
|
GLint componentType = GL_UNSIGNED_NORMALIZED;
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER,
|
|
AttachmentPointFor(isDefault, false),
|
|
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER,
|
|
AttachmentPointFor(isDefault, true),
|
|
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
|
|
GL_READ_FRAMEBUFFER, AttachmentPointFor(isDefault, false),
|
|
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
|
ClearGLErrors();
|
|
|
|
const GLint aspectBits = stencilAspect ? stencilBits : depthBits;
|
|
if (objectType == GL_NONE && aspectBits <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!isDefault) {
|
|
// A real object: ask it directly and try that first. It is only a preference,
|
|
// not a verdict - the probe binds the attachment as GL_TEXTURE_2D, and a name
|
|
// whose target is not GL_TEXTURE_2D can leave it describing the wrong texture
|
|
// (see QueryAttachmentSizedFormat). The size-derived guesses below therefore
|
|
// stay in the list behind it, so a wrong first answer costs one rejected blit
|
|
// instead of the whole readback. The probe's own refusal must not be left on
|
|
// the error queue for the caller's next glGetError to pick up as its own.
|
|
//
|
|
// The cost of keeping the fallbacks: a source whose OWN format cannot back a
|
|
// staging texture (GL_STENCIL_INDEX8 without EXT/OES_texture_stencil8, say) no
|
|
// longer fails cleanly - it retries with a packed format, and a driver lax
|
|
// enough to accept the resulting mismatched depth/stencil blit would hand back
|
|
// data indistinguishable from a correct read. ES conformance forbids that blit,
|
|
// so this trades a spec-guaranteed rejection for a driver-bug-only wrong answer;
|
|
// the shapes it rescues (array and cube attachments) are otherwise unreadable.
|
|
const GLenum exact = ReplicateBlitImpl::QueryAttachmentSizedFormat(point);
|
|
ClearGLErrors();
|
|
if (exact != 0) {
|
|
out->Push(exact);
|
|
}
|
|
} else if (s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat != 0) {
|
|
out->Push(s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat);
|
|
}
|
|
|
|
const Bool floatDepth = componentType == GL_FLOAT;
|
|
const Bool packed = depthBits > 0 && stencilBits > 0;
|
|
if (stencilAspect) {
|
|
if (packed) {
|
|
out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8);
|
|
out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8);
|
|
}
|
|
out->Push(GL_STENCIL_INDEX8);
|
|
if (!packed) {
|
|
out->Push(GL_DEPTH24_STENCIL8);
|
|
}
|
|
} else if (packed) {
|
|
out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8);
|
|
out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8);
|
|
} else if (floatDepth) {
|
|
out->Push(GL_DEPTH_COMPONENT32F);
|
|
out->Push(GL_DEPTH32F_STENCIL8);
|
|
} else if (depthBits > 0 && depthBits <= 16) {
|
|
out->Push(GL_DEPTH_COMPONENT16);
|
|
out->Push(GL_DEPTH_COMPONENT24);
|
|
} else {
|
|
out->Push(GL_DEPTH_COMPONENT24);
|
|
out->Push(GL_DEPTH24_STENCIL8);
|
|
out->Push(GL_DEPTH_COMPONENT32F);
|
|
}
|
|
return out->count != 0;
|
|
}
|
|
|
|
// Point a staging slot at `format`, growing it if the rectangle needs it, and leave
|
|
// it bound on the borrowed texture unit.
|
|
static Bool EnsureStageTexture(StageSlot& slot, GLenum format, GLsizei width, GLsizei height) {
|
|
if (slot.texture != 0 && slot.format == format && slot.width >= width && slot.height >= height) {
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture);
|
|
return true;
|
|
}
|
|
if (slot.texture != 0) {
|
|
g_GLESFuncs.glDeleteTextures(1, &slot.texture); // immutable storage cannot be resized
|
|
ScratchFBOImpl::NoteTextureIdDeleted(slot.texture);
|
|
slot.texture = 0;
|
|
}
|
|
slot.format = 0;
|
|
slot.width = std::max(slot.width, width);
|
|
slot.height = std::max(slot.height, height);
|
|
g_GLESFuncs.glGenTextures(1, &slot.texture);
|
|
if (slot.texture == 0) return false;
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, format, slot.width, slot.height);
|
|
const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
// A depth texture left in compare mode samples to 0/1 instead of the stored value.
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
|
|
ClearGLErrors();
|
|
slot.format = ok ? format : 0;
|
|
return ok;
|
|
}
|
|
|
|
static Bool EnsureColorTexture(GLsizei width, GLsizei height) {
|
|
if (s_colorTexture != 0 && s_colorWidth >= width && s_colorHeight >= height) {
|
|
return true;
|
|
}
|
|
if (s_colorTexture != 0) {
|
|
g_GLESFuncs.glDeleteTextures(1, &s_colorTexture);
|
|
ScratchFBOImpl::NoteTextureIdDeleted(s_colorTexture);
|
|
s_colorTexture = 0;
|
|
}
|
|
s_colorWidth = std::max(s_colorWidth, width);
|
|
s_colorHeight = std::max(s_colorHeight, height);
|
|
g_GLESFuncs.glGenTextures(1, &s_colorTexture);
|
|
if (s_colorTexture == 0) return false;
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_colorTexture);
|
|
ClearGLErrors();
|
|
// R32UI is colour-renderable in ES 3.0 core - no float-renderability extension
|
|
// needed - and carries a depth bit pattern or a stencil index without loss.
|
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, s_colorWidth, s_colorHeight);
|
|
const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
ClearGLErrors();
|
|
if (!ok) {
|
|
s_colorWidth = 0;
|
|
s_colorHeight = 0;
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
// Copy one aspect of the requested rectangle out of the bound READ framebuffer into
|
|
// its staging slot, trying each candidate format until one is blit-compatible.
|
|
// Requires the slot's own framebuffer bound as DRAW.
|
|
static Bool StageAspect(StageSlot& slot, const AspectCandidates& candidates, Bool stencilAspect, Bool isDefault,
|
|
GLint x, GLint y, GLsizei width, GLsizei height) {
|
|
const GLbitfield aspectBit = stencilAspect ? GL_STENCIL_BUFFER_BIT : GL_DEPTH_BUFFER_BIT;
|
|
for (Uint candidate = 0; candidate < candidates.count; ++candidate) {
|
|
const GLenum format = candidates.formats[candidate];
|
|
const Bool formatHasAspect = stencilAspect ? ReplicateBlitImpl::FormatHasStencil(format)
|
|
: ReplicateBlitImpl::FormatHasDepth(format);
|
|
if (!formatHasAspect) {
|
|
continue;
|
|
}
|
|
if (!EnsureStageTexture(slot, format, width, height)) {
|
|
continue;
|
|
}
|
|
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
|
|
ReplicateBlitImpl::ScratchAttachmentFor(format), GL_TEXTURE_2D,
|
|
slot.texture, 0);
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
continue;
|
|
}
|
|
ClearGLErrors();
|
|
// Only this aspect: a packed scratch standing in for a separate attachment
|
|
// has a second half with nothing to copy into it.
|
|
g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, aspectBit, GL_NEAREST);
|
|
const GLenum blitErr = g_GLESFuncs.glGetError();
|
|
if (blitErr != GL_NO_ERROR) {
|
|
continue;
|
|
}
|
|
if (isDefault) {
|
|
slot.defaultFramebufferFormat = format;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Runs one conversion pass over a staged slot and reads its colour target back.
|
|
// Requires the slot's texture bound on the borrowed unit and s_colorFramebuffer
|
|
// bound as DRAW.
|
|
static Bool ConvertAndRead(const StageSlot& slot, Bool stencilAspect, GLsizei width, GLsizei height,
|
|
Vector<Uint32>& outWords) {
|
|
if (ReplicateBlitImpl::FormatHasDepth(slot.format) && ReplicateBlitImpl::FormatHasStencil(slot.format)) {
|
|
g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE,
|
|
stencilAspect ? GL_STENCIL_INDEX : GL_DEPTH_COMPONENT);
|
|
}
|
|
const Float uvScaleX = static_cast<Float>(width) / static_cast<Float>(slot.width);
|
|
const Float uvScaleY = static_cast<Float>(height) / static_cast<Float>(slot.height);
|
|
g_GLESFuncs.glUseProgram(stencilAspect ? s_stencilProgram : s_depthProgram);
|
|
g_GLESFuncs.glUniform4f(stencilAspect ? s_stencilUvTransform : s_depthUvTransform, uvScaleX, uvScaleY,
|
|
0.0f, 0.0f);
|
|
g_GLESFuncs.glViewport(0, 0, width, height);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
MGLOG_E_ONCE("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth");
|
|
return false;
|
|
}
|
|
|
|
// GL_RGBA_INTEGER / GL_UNSIGNED_INT is the pair ES guarantees for an unsigned
|
|
// integer attachment whatever its channel count, so four words come back per
|
|
// pixel and only the first carries anything.
|
|
outWords.assign(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4u, 0u);
|
|
ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false);
|
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, s_colorFramebuffer);
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data());
|
|
const GLenum readError = g_GLESFuncs.glGetError();
|
|
if (readError != GL_NO_ERROR) {
|
|
MGLOG_E_ONCE("ReadPixels: could not read the %s conversion target back: %s",
|
|
stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Stage, convert and read one aspect. The caller owns the state guard and the DRAW
|
|
// framebuffer scope.
|
|
static Bool ReadAspect(Bool stencilAspect, GLint x, GLint y, GLsizei width, GLsizei height,
|
|
Vector<Uint32>& outWords) {
|
|
Bool isDefault = false;
|
|
AspectCandidates candidates;
|
|
if (!DescribeAspect(stencilAspect, &isDefault, &candidates)) {
|
|
return false;
|
|
}
|
|
StageSlot& slot = s_slots[stencilAspect ? 1 : 0];
|
|
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer);
|
|
if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) {
|
|
MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source",
|
|
stencilAspect ? "stencil" : "depth");
|
|
return false;
|
|
}
|
|
|
|
if (!EnsureColorTexture(width, height)) {
|
|
MGLOG_E_ONCE("ReadPixels: could not allocate the depth/stencil conversion target");
|
|
return false;
|
|
}
|
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer);
|
|
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
|
s_colorTexture, 0);
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
MGLOG_E_ONCE("ReadPixels: the depth/stencil conversion target is not renderable");
|
|
return false;
|
|
}
|
|
|
|
// EnsureColorTexture may have taken the borrowed unit for its own storage call.
|
|
VertexArrayImpl::BindBackendVAOId(s_vertexArray);
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture);
|
|
return ConvertAndRead(slot, stencilAspect, width, height, outWords);
|
|
}
|
|
|
|
// Fills whichever of the two outputs the caller asked for from the bound READ
|
|
// framebuffer. Returns false when the emulation could not service the request at
|
|
// all, leaving the caller to report the failure the way it always has.
|
|
static Bool Read(GLint x, GLint y, GLsizei width, GLsizei height, Vector<Float>* outDepth,
|
|
Vector<Uint8>* outStencil) {
|
|
if (width <= 0 || height <= 0 || (outDepth == nullptr && outStencil == nullptr)) {
|
|
return false;
|
|
}
|
|
// Sampling the stencil half of a packed texture goes through
|
|
// GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the
|
|
// pname would just raise GL_INVALID_ENUM and the shader would read depth bits as
|
|
// stencil.
|
|
const Bool supportsStencilTextureMode =
|
|
g_GLESCapabilities.GLESVersion.Major > 3 ||
|
|
(g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1);
|
|
if (outStencil != nullptr && !supportsStencilTextureMode) {
|
|
return false;
|
|
}
|
|
if (!EnsureResources()) {
|
|
return false;
|
|
}
|
|
|
|
// Taken before the first scratch texture bind: the guard owns the borrowed
|
|
// texture unit as well as the pipeline, and it is what puts the scissor test out
|
|
// of the way of the staging blit.
|
|
ScopedEmulationDrawState emulationState;
|
|
ScopedFramebufferBinding drawBinding(/*saveRead=*/false, /*saveDraw=*/true);
|
|
|
|
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
|
Vector<Uint32> words;
|
|
if (outDepth != nullptr) {
|
|
if (!ReadAspect(/*stencilAspect=*/false, x, y, width, height, words)) {
|
|
return false;
|
|
}
|
|
outDepth->assign(pixelCount, 0.0f);
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
const Uint32 bits = words[i * 4u];
|
|
Float value = 0.0f;
|
|
Memcpy(&value, &bits, sizeof(value));
|
|
(*outDepth)[i] = value;
|
|
}
|
|
}
|
|
if (outStencil != nullptr) {
|
|
if (!ReadAspect(/*stencilAspect=*/true, x, y, width, height, words)) {
|
|
return false;
|
|
}
|
|
outStencil->assign(pixelCount, 0);
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
(*outStencil)[i] = static_cast<Uint8>(words[i * 4u] & 0xFFu);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace DepthStencilSamplingReadImpl
|
|
|
|
// One normalized depth value per pixel, tightly packed. Which native read a driver
|
|
// accepts depends on the attached format: a fixed-point depth buffer takes
|
|
// GL_UNSIGNED_INT, while a floating-point one (DEPTH_COMPONENT32F,
|
|
// DEPTH32F_STENCIL8 - what dEQP's own fbo-surface-type wrapper framebuffer uses)
|
|
// rejects it with GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both.
|
|
static Bool ReadDepthValuesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector<Float>& outDepth) {
|
|
outDepth.assign(static_cast<SizeT>(width) * static_cast<SizeT>(height), 0.0f);
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
|
GLenum floatError = GL_INVALID_OPERATION;
|
|
if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) {
|
|
Vector<Uint32> raw(outDepth.size());
|
|
// Drain first: a stale flag some earlier best-effort call left queued
|
|
// must not be misattributed to this read (it would silently drop the
|
|
// whole readback in production builds where ErrorLopper is compiled out).
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, raw.data());
|
|
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
|
|
for (SizeT i = 0; i < outDepth.size(); ++i) {
|
|
outDepth[i] = static_cast<Float>(static_cast<Double>(raw[i]) / 4294967295.0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, outDepth.data());
|
|
floatError = g_GLESFuncs.glGetError();
|
|
if (floatError == GL_NO_ERROR) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Neither native spelling exists on this driver (which is the ordinary case: ES has
|
|
// no depth readback in core and GL_NV_read_depth is rare), so sample the attachment
|
|
// instead. The scoped pack state above is irrelevant to that path - it reads its own
|
|
// scratch colour target - but harmless, and leaving it in place keeps the restore in
|
|
// one place.
|
|
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) {
|
|
return true;
|
|
}
|
|
MGLOG_E_ONCE("ReadPixels: no depth readback path is available: native reads failed with %s and the "
|
|
"sampling emulation could not service the source",
|
|
MG_Util::ConvertGLEnumToString(floatError).c_str());
|
|
return false;
|
|
}
|
|
|
|
// GL_DEPTH_COMPONENT readback into the client's layout, honouring the PACK pixel-store
|
|
// parameters. GL 4.6 core 18.2.8: the normalized depth is written as-is for GL_FLOAT and
|
|
// scaled into the full range of whichever integer width the client asked for otherwise.
|
|
static Bool ReadPixelsDepthComponent(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type,
|
|
void* pixels) {
|
|
SizeT dstPixelBytes = 0;
|
|
switch (type) {
|
|
case GL_UNSIGNED_BYTE: dstPixelBytes = sizeof(Uint8); break;
|
|
case GL_UNSIGNED_SHORT: dstPixelBytes = sizeof(Uint16); break;
|
|
case GL_UNSIGNED_INT: dstPixelBytes = sizeof(Uint32); break;
|
|
case GL_FLOAT: dstPixelBytes = sizeof(GLfloat); break;
|
|
default: return false;
|
|
}
|
|
if (width <= 0 || height <= 0) {
|
|
return true;
|
|
}
|
|
|
|
Vector<Float> depth;
|
|
if (!ReadDepthValuesNative(x, y, width, height, depth)) {
|
|
return true; // already reported; nothing was written, as before
|
|
}
|
|
|
|
StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "depth",
|
|
[&](GLsizei row, Uint8* dst) {
|
|
const Float* srcRow =
|
|
depth.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
|
|
for (GLsizei col = 0; col < width; ++col) {
|
|
switch (type) {
|
|
case GL_UNSIGNED_BYTE:
|
|
dst[col] = static_cast<Uint8>(
|
|
NormalizedDepthToUnsigned(srcRow[col], 255.0));
|
|
break;
|
|
case GL_UNSIGNED_SHORT:
|
|
reinterpret_cast<Uint16*>(dst)[col] = static_cast<Uint16>(
|
|
NormalizedDepthToUnsigned(srcRow[col], 65535.0));
|
|
break;
|
|
case GL_UNSIGNED_INT:
|
|
reinterpret_cast<Uint32*>(dst)[col] =
|
|
NormalizedDepthToUnsigned(srcRow[col], 4294967295.0);
|
|
break;
|
|
default:
|
|
reinterpret_cast<GLfloat*>(dst)[col] = srcRow[col];
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// One stencil byte per pixel, tightly packed. ES has no guaranteed stencil readback
|
|
// at all: GL_STENCIL_INDEX needs GL_NV_read_stencil, and a driver without it rejects
|
|
// the read outright. Where the attachment is a combined depth-stencil buffer the
|
|
// packed GL_DEPTH_STENCIL read (which the packed_depth_stencil.verify_* cases already
|
|
// rely on) carries the same bytes in its low octet, so use that as the fallback.
|
|
static Bool ReadStencilBytesNative(GLint x, GLint y, GLsizei width, GLsizei height, Vector<Uint8>& outStencil) {
|
|
outStencil.assign(static_cast<SizeT>(width) * static_cast<SizeT>(height), 0);
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
|
GLenum packedError = GL_INVALID_OPERATION;
|
|
if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) {
|
|
// Drain first: see ReadDepthValuesNative.
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, outStencil.data());
|
|
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
|
|
return true;
|
|
}
|
|
|
|
Vector<Uint32> packed(outStencil.size(), 0);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data());
|
|
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
|
|
for (SizeT i = 0; i < outStencil.size(); ++i) {
|
|
outStencil[i] = static_cast<Uint8>(packed[i] & 0xFFu);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A DEPTH32F_STENCIL8 attachment rejects the 24_8 type: its packed layout is a
|
|
// float depth followed by a padded stencil byte, eight bytes per pixel with the
|
|
// index at offset 4.
|
|
Vector<Uint8> packed32f(outStencil.size() * 8u, 0);
|
|
ClearGLErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV,
|
|
packed32f.data());
|
|
packedError = g_GLESFuncs.glGetError();
|
|
if (packedError == GL_NO_ERROR) {
|
|
for (SizeT i = 0; i < outStencil.size(); ++i) {
|
|
outStencil[i] = packed32f[i * 8u + 4u];
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// No native spelling worked, which is the ordinary case on ES: sample the stencil
|
|
// half of the attachment instead.
|
|
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) {
|
|
return true;
|
|
}
|
|
MGLOG_E_ONCE("ReadPixels: no stencil readback path is available: native reads failed with %s and the "
|
|
"sampling emulation could not service the source",
|
|
MG_Util::ConvertGLEnumToString(packedError).c_str());
|
|
return false;
|
|
}
|
|
|
|
// GL_STENCIL_INDEX readback into the client's integer layout, honouring the PACK
|
|
// pixel-store parameters. Handles the widths desktop clients ask for; the stencil
|
|
// values themselves are always 8 bits.
|
|
static Bool ReadPixelsStencilViaNative(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type,
|
|
void* pixels) {
|
|
// GL 4.6 core 18.2.8: a stencil index is written unconverted into whichever integer width
|
|
// the client asked for, and converted to a float value for GL_FLOAT. The signed widths are
|
|
// as legal as the unsigned ones - the CTS reads stencil with GL_INT - and rejecting them
|
|
// here used to let the call fall through to a native ES read the driver refuses, after
|
|
// which nothing was written at all and the caller kept its zeros.
|
|
SizeT dstPixelBytes = 0;
|
|
switch (type) {
|
|
case GL_UNSIGNED_BYTE:
|
|
case GL_BYTE: dstPixelBytes = sizeof(Uint8); break;
|
|
case GL_UNSIGNED_SHORT:
|
|
case GL_SHORT: dstPixelBytes = sizeof(Uint16); break;
|
|
case GL_UNSIGNED_INT:
|
|
case GL_INT: dstPixelBytes = sizeof(Uint32); break;
|
|
case GL_FLOAT: dstPixelBytes = sizeof(GLfloat); break;
|
|
default: return false;
|
|
}
|
|
if (width <= 0 || height <= 0) {
|
|
return true;
|
|
}
|
|
|
|
Vector<Uint8> raw;
|
|
if (!ReadStencilBytesNative(x, y, width, height, raw)) {
|
|
return true;
|
|
}
|
|
|
|
StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "stencil",
|
|
[&](GLsizei row, Uint8* dst) {
|
|
const Uint8* srcRow =
|
|
raw.data() + static_cast<SizeT>(row) * static_cast<SizeT>(width);
|
|
for (GLsizei col = 0; col < width; ++col) {
|
|
switch (type) {
|
|
case GL_UNSIGNED_BYTE:
|
|
case GL_BYTE:
|
|
dst[static_cast<SizeT>(col)] = srcRow[col];
|
|
break;
|
|
case GL_UNSIGNED_SHORT:
|
|
case GL_SHORT:
|
|
reinterpret_cast<Uint16*>(dst)[col] = srcRow[col];
|
|
break;
|
|
case GL_FLOAT:
|
|
reinterpret_cast<GLfloat*>(dst)[col] = static_cast<GLfloat>(srcRow[col]);
|
|
break;
|
|
default:
|
|
reinterpret_cast<Uint32*>(dst)[col] = srcRow[col];
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// GL_DEPTH_STENCIL readback: the two aspects are fetched separately and woven into the
|
|
// packed layout the client asked for (GL 4.6 core table 8.6). This is what
|
|
// KHR-GL3x.packed_depth_stencil.verify_read_pixels / verify_get_tex_image /
|
|
// verify_copy_tex_image read their gradients with.
|
|
static Bool ReadPixelsDepthStencilPacked(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type,
|
|
void* pixels) {
|
|
SizeT dstPixelBytes = 0;
|
|
switch (type) {
|
|
case GL_UNSIGNED_INT_24_8: dstPixelBytes = sizeof(Uint32); break;
|
|
case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: dstPixelBytes = sizeof(Float) + sizeof(Uint32); break;
|
|
default: return false;
|
|
}
|
|
if (width <= 0 || height <= 0) {
|
|
return true;
|
|
}
|
|
|
|
Vector<Float> depth;
|
|
Vector<Uint8> stencil;
|
|
if (!ReadDepthValuesNative(x, y, width, height, depth)) {
|
|
return true;
|
|
}
|
|
if (!ReadStencilBytesNative(x, y, width, height, stencil)) {
|
|
return true;
|
|
}
|
|
|
|
StoreReadbackRowsToClient(
|
|
width, height, dstPixelBytes, pixels, "packed depth/stencil", [&](GLsizei row, Uint8* dst) {
|
|
const SizeT base = static_cast<SizeT>(row) * static_cast<SizeT>(width);
|
|
for (GLsizei col = 0; col < width; ++col) {
|
|
const Uint32 stencilIndex = stencil[base + static_cast<SizeT>(col)];
|
|
const Float depthValue = depth[base + static_cast<SizeT>(col)];
|
|
if (type == GL_UNSIGNED_INT_24_8) {
|
|
reinterpret_cast<Uint32*>(dst)[col] =
|
|
(NormalizedDepthToUnsigned(depthValue, 16777215.0) << 8) | stencilIndex;
|
|
} else {
|
|
// Depth float first, then a word whose low octet is the index and
|
|
// whose top 24 bits are unused.
|
|
Uint8* pixel = dst + static_cast<SizeT>(col) * (sizeof(Float) + sizeof(Uint32));
|
|
Memcpy(pixel, &depthValue, sizeof(depthValue));
|
|
Memcpy(pixel + sizeof(Float), &stencilIndex, sizeof(stencilIndex));
|
|
}
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// ---- Client-format readback conversion ---------------------------------------------------------------------
|
|
// ES 3.x glReadPixels only guarantees GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/GL_(UNSIGNED_)INT,
|
|
// GL_RGBA/GL_FLOAT for float buffers plus one implementation-defined pair, while desktop GL clients read
|
|
// back narrower layouts (RED, RG, RGB, BGR, byte-order packed types, ...). For those we read a guaranteed
|
|
// wide RGBA format into scratch memory and repack into the caller's (format, type) layout on the CPU,
|
|
// honoring the client-side PACK pixel-store parameters. The pure repacking helpers live in
|
|
// ReadbackImpl (Utils.cpp) so unit tests can assert the exact packed words.
|
|
|
|
using ReadbackImpl::GetReadbackChannelMapping;
|
|
using ReadbackImpl::GetReadbackComponentSize;
|
|
using ReadbackImpl::GetReadbackDstPixelSize;
|
|
using ReadbackImpl::ReadbackChannelMapping;
|
|
|
|
static Bool CanDecodeWideSourceType(GLenum type) {
|
|
switch (type) {
|
|
case GL_UNSIGNED_BYTE:
|
|
case GL_BYTE:
|
|
case GL_UNSIGNED_SHORT:
|
|
case GL_SHORT:
|
|
case GL_HALF_FLOAT:
|
|
case GL_FLOAT:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Component-array read formats usable as (possibly narrow) wide-read sources.
|
|
static Int GetWideReadChannelCount(GLenum format) {
|
|
switch (format) {
|
|
case GL_RED:
|
|
case GL_RED_INTEGER:
|
|
return 1;
|
|
case GL_RG:
|
|
case GL_RG_INTEGER:
|
|
return 2;
|
|
case GL_RGB:
|
|
case GL_RGB_INTEGER:
|
|
return 3;
|
|
case GL_RGBA:
|
|
case GL_RGBA_INTEGER:
|
|
return 4;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static Bool IsIntegerReadFormat(GLint format) {
|
|
return format == GL_RED_INTEGER || format == GL_RG_INTEGER || format == GL_RGB_INTEGER ||
|
|
format == GL_RGBA_INTEGER;
|
|
}
|
|
|
|
// The bit pattern of 1.0 in a wide-read component type: what GL reports for a channel the
|
|
// attachment's format does not have.
|
|
static void FillWideReadOneBits(GLenum componentType, Uint8* oneBits) {
|
|
switch (componentType) {
|
|
case GL_UNSIGNED_BYTE:
|
|
oneBits[0] = 0xFF;
|
|
break;
|
|
case GL_BYTE:
|
|
oneBits[0] = 0x7F;
|
|
break;
|
|
case GL_UNSIGNED_SHORT: {
|
|
const Uint16 one = 0xFFFF;
|
|
Memcpy(oneBits, &one, sizeof(one));
|
|
break;
|
|
}
|
|
case GL_SHORT: {
|
|
const Int16 one = 0x7FFF;
|
|
Memcpy(oneBits, &one, sizeof(one));
|
|
break;
|
|
}
|
|
case GL_HALF_FLOAT: {
|
|
const Uint16 one = 0x3C00;
|
|
Memcpy(oneBits, &one, sizeof(one));
|
|
break;
|
|
}
|
|
case GL_FLOAT: {
|
|
const Float one = 1.0f;
|
|
Memcpy(oneBits, &one, sizeof(one));
|
|
break;
|
|
}
|
|
case GL_UNSIGNED_INT:
|
|
case GL_INT: {
|
|
const Uint32 one = 1;
|
|
Memcpy(oneBits, &one, sizeof(one));
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Overwrites the alpha of a 4-channel wide read with the format's implied 1.0. Used for an
|
|
// attachment the backend widened from three channels to keep it colour-renderable: the storage
|
|
// has a real alpha channel holding whatever the draw wrote, but the format the application
|
|
// asked for has none, and GL reads a missing channel back as one.
|
|
static void ForceWideReadAlphaToOne(Vector<Uint8>& data, SizeT pixelCount, GLenum componentType) {
|
|
const SizeT componentSize = GetReadbackComponentSize(componentType);
|
|
if (componentSize == 0 || data.size() < pixelCount * 4 * componentSize) {
|
|
return;
|
|
}
|
|
Uint8 oneBits[4] = {0, 0, 0, 0};
|
|
FillWideReadOneBits(componentType, oneBits);
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
Memcpy(data.data() + (i * 4 + 3) * componentSize, oneBits, componentSize);
|
|
}
|
|
}
|
|
|
|
// Expands a tightly-packed narrow read (1-3 channels per texel) into the 4-channel wide RGBA
|
|
// layout ConvertWideReadbackRow expects. Missing G/B read zero; missing A reads one, encoded in
|
|
// the source component type.
|
|
static void ExpandNarrowWideRead(Vector<Uint8>& data, SizeT pixelCount, Int srcChannels, GLenum componentType) {
|
|
const SizeT componentSize = GetReadbackComponentSize(componentType);
|
|
if (componentSize == 0 || srcChannels <= 0 || srcChannels >= 4) {
|
|
return;
|
|
}
|
|
Uint8 zeroBits[4] = {0, 0, 0, 0};
|
|
Uint8 oneBits[4] = {0, 0, 0, 0};
|
|
FillWideReadOneBits(componentType, oneBits);
|
|
|
|
Vector<Uint8> expanded(pixelCount * 4 * componentSize);
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
const Uint8* src = data.data() + i * static_cast<SizeT>(srcChannels) * componentSize;
|
|
Uint8* dst = expanded.data() + i * 4 * componentSize;
|
|
for (Int ch = 0; ch < 4; ++ch) {
|
|
if (ch < srcChannels) {
|
|
Memcpy(dst + static_cast<SizeT>(ch) * componentSize, src + static_cast<SizeT>(ch) * componentSize,
|
|
componentSize);
|
|
} else {
|
|
Memcpy(dst + static_cast<SizeT>(ch) * componentSize, ch == 3 ? oneBits : zeroBits, componentSize);
|
|
}
|
|
}
|
|
}
|
|
data = std::move(expanded);
|
|
}
|
|
|
|
static void DrainESErrors() { DrainDriverErrors("ReadPixels"); }
|
|
|
|
static GLenum QueryReadAttachmentComponentType() {
|
|
GLint framebufferId = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &framebufferId);
|
|
if (framebufferId == 0) {
|
|
return GL_UNSIGNED_NORMALIZED; // default framebuffers are normalized fixed-point
|
|
}
|
|
GLint readBuffer = GL_COLOR_ATTACHMENT0;
|
|
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
|
|
if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) {
|
|
readBuffer = GL_COLOR_ATTACHMENT0;
|
|
}
|
|
GLint componentType = 0;
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
|
|
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
|
DrainESErrors();
|
|
return componentType != 0 ? static_cast<GLenum>(componentType) : GL_UNSIGNED_NORMALIZED;
|
|
}
|
|
|
|
|
|
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
|
|
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
|
|
// "not implemented" skip); returns true when the request was handled, even if it degraded to a logged no-op.
|
|
// `forceOpaqueAlpha`: the source image is a three-channel format the backend widened to four to
|
|
// keep it colour-renderable, so its alpha channel holds whatever the draw wrote and has to be
|
|
// answered with the 1.0 the application's format implies. Passed in rather than derived here:
|
|
// glReadPixels reads the bound READ framebuffer, but glGetTexImage reads a texture through a
|
|
// scratch framebuffer, so the frontend's READ binding describes a different image entirely -
|
|
// consulting it there would both miss real widenings and corrupt readbacks of ordinary
|
|
// textures taken while some unrelated widened attachment happened to be bound.
|
|
// The image-format widening's READ half, for the seven normalized formats whose carrier holds
|
|
// their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see
|
|
// TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those
|
|
// right: the attachment is an integer one while the application's format is normalized, so the
|
|
// class check below would refuse the read outright, and a repack that got past it would hand
|
|
// back 65535.0 where GL owes 1.0.
|
|
//
|
|
// Inactive (ChannelMax all zero) for every other read, which is all but a handful.
|
|
struct NormalizedImageCarrierRead {
|
|
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
|
Bool SignedNormalized = false;
|
|
|
|
Bool Active() const { return ChannelMax[0] != 0u; }
|
|
};
|
|
|
|
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
|
|
GLenum type, void* pixels, Bool honorPackImageParams,
|
|
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha,
|
|
const NormalizedImageCarrierRead& normalizedCarrier = {}) {
|
|
ReadbackChannelMapping mapping{};
|
|
if (!GetReadbackChannelMapping(format, mapping)) {
|
|
return false;
|
|
}
|
|
// Covers unknown types, packed field-count/format mismatches and float types on integer formats.
|
|
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
|
if (dstPixelBytes == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return true;
|
|
}
|
|
const auto& pixelPackBufferObject =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
if (!pixelPackBufferObject && pixels == nullptr) {
|
|
return true;
|
|
}
|
|
|
|
const GLenum attachmentComponentType = QueryReadAttachmentComponentType();
|
|
const Bool integerAttachment =
|
|
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
|
|
// A normalized image carrier is EXACTLY the case where the two disagree on purpose, and
|
|
// it is the caller - which knows the TEXTURE being read, not just the attachment - that
|
|
// says so. An integer client format through such a carrier is not a shape GL can ask for
|
|
// (the frontend format is normalized), so it is refused here rather than converted.
|
|
if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) {
|
|
MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a "
|
|
"normalized client format; skipping",
|
|
MG_Util::ConvertGLEnumToString(format).c_str());
|
|
return true;
|
|
}
|
|
if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) {
|
|
MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
|
|
MG_Util::ConvertGLEnumToString(format).c_str());
|
|
return true;
|
|
}
|
|
|
|
// Prefer the implementation-defined pair (full precision on e.g. norm16 buffers, and possibly
|
|
// a narrow format like GL_RED/GL_UNSIGNED_SHORT), then the spec/extension-guaranteed pair for
|
|
// the attachment class. Narrow reads are expanded to RGBA on the CPU afterwards.
|
|
GLint implFormat = 0;
|
|
GLint implType = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
|
|
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
|
|
|
|
struct WideReadCandidate {
|
|
GLenum format;
|
|
GLenum type;
|
|
};
|
|
WideReadCandidate candidates[4];
|
|
Int candidateCount = 0;
|
|
if (normalizedCarrier.Active()) {
|
|
// The storage IS an integer texture, whatever the application's format says, so the
|
|
// only read that can answer is the integer one. The codes it hands back are turned
|
|
// into the floats the client asked for below.
|
|
candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT};
|
|
} else if (mapping.isInteger) {
|
|
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && IsIntegerReadFormat(implFormat) &&
|
|
(implType == GL_INT || implType == GL_UNSIGNED_INT)) {
|
|
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
|
|
}
|
|
candidates[candidateCount++] = {
|
|
GL_RGBA_INTEGER,
|
|
attachmentComponentType == GL_INT ? static_cast<GLenum>(GL_INT) : static_cast<GLenum>(GL_UNSIGNED_INT)};
|
|
} else {
|
|
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && !IsIntegerReadFormat(implFormat) &&
|
|
(CanDecodeWideSourceType(static_cast<GLenum>(implType)) ||
|
|
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) {
|
|
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
|
|
}
|
|
if (attachmentComponentType == GL_FLOAT) {
|
|
candidates[candidateCount++] = {GL_RGBA, GL_FLOAT};
|
|
}
|
|
if (attachmentComponentType == GL_SIGNED_NORMALIZED) {
|
|
// EXT_render_snorm attachments read back as RGBA/BYTE (8-bit) or RGBA/SHORT (16-bit).
|
|
candidates[candidateCount++] = {GL_RGBA, GL_SHORT};
|
|
candidates[candidateCount++] = {GL_RGBA, GL_BYTE};
|
|
} else {
|
|
candidates[candidateCount++] = {GL_RGBA, GL_UNSIGNED_BYTE};
|
|
}
|
|
}
|
|
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
|
|
|
Vector<Uint8> wide;
|
|
GLenum wideType = GL_NONE;
|
|
GLenum readFormat = GL_NONE;
|
|
Int readChannels = 0;
|
|
DrainESErrors();
|
|
for (Int i = 0; i < candidateCount; ++i) {
|
|
const WideReadCandidate candidate = candidates[i];
|
|
Bool alreadyTried = false;
|
|
for (Int j = 0; j < i; ++j) {
|
|
alreadyTried =
|
|
alreadyTried || (candidates[j].format == candidate.format && candidates[j].type == candidate.type);
|
|
}
|
|
if (alreadyTried) {
|
|
continue;
|
|
}
|
|
const Int channels = GetWideReadChannelCount(candidate.format);
|
|
const SizeT candidateComponentSize = GetReadbackComponentSize(candidate.type);
|
|
wide.resize(static_cast<SizeT>(width) * static_cast<SizeT>(height) *
|
|
static_cast<SizeT>(channels) * candidateComponentSize);
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, candidate.format, candidate.type, wide.data());
|
|
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) {
|
|
wideType = candidate.type;
|
|
readFormat = candidate.format;
|
|
readChannels = channels;
|
|
break;
|
|
}
|
|
}
|
|
if (wideType == GL_NONE) {
|
|
MGLOG_E_ONCE("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return true;
|
|
}
|
|
|
|
if (normalizedCarrier.Active()) {
|
|
// GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same
|
|
// denominators, so a texel an imageStore wrote and a texel the upload seeded read back
|
|
// identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with
|
|
// the signed code recovered from the low sixteen bits of the unsigned channel.
|
|
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
|
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
|
|
auto* dst = reinterpret_cast<Float*>(floatWide.data());
|
|
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
for (SizeT channel = 0; channel < 4; ++channel) {
|
|
const Uint32 code = src[i * 4 + channel];
|
|
const auto denominator = static_cast<Float>(normalizedCarrier.ChannelMax[channel]);
|
|
if (normalizedCarrier.SignedNormalized) {
|
|
const auto signedCode = static_cast<Int16>(static_cast<Uint16>(code));
|
|
dst[i * 4 + channel] =
|
|
std::max(static_cast<Float>(signedCode) / denominator, -1.0f);
|
|
} else {
|
|
dst[i * 4 + channel] = static_cast<Float>(code) / denominator;
|
|
}
|
|
}
|
|
}
|
|
wide = Move(floatWide);
|
|
wideType = GL_FLOAT;
|
|
readChannels = 4;
|
|
}
|
|
if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
|
|
// Unpack the packed words into a float wide buffer (full 10-bit precision on e.g.
|
|
// GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV).
|
|
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
|
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
|
|
auto* dst = reinterpret_cast<Float*>(floatWide.data());
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
Uint32 word;
|
|
Memcpy(&word, wide.data() + i * 4, sizeof(word));
|
|
dst[i * 4 + 0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
|
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
|
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
|
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
|
}
|
|
wide = std::move(floatWide);
|
|
wideType = GL_FLOAT;
|
|
readChannels = 4;
|
|
}
|
|
if (readChannels < 4) {
|
|
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
|
|
}
|
|
|
|
// Undo the three-channel widening (see the parameter's comment). Deliberately not gated on
|
|
// applyFixedPointReadClamp: that flag implements GL_CLAMP_READ_COLOR, which glGetTexImage
|
|
// is exempt from, whereas "a format without alpha reads as 1.0" is the format's own
|
|
// semantics and applies to every read.
|
|
if (forceOpaqueAlpha) {
|
|
ForceWideReadAlphaToOne(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), wideType);
|
|
}
|
|
|
|
// GL clamps a read from a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR
|
|
// defaults to GL_FIXED_ONLY). Formats the backend substitutes with a floating-point
|
|
// one keep the out-of-range value the app stored, so apply the clamp here - a
|
|
// GL_R16_SNORM target holding -0.125 must still read back as 0.
|
|
// glReadPixels only: GL_CLAMP_READ_COLOR does not apply to glGetTexImage, which
|
|
// reaches this helper through the same scratch-framebuffer path.
|
|
if (applyFixedPointReadClamp && FramebufferImpl::IsFixedPointFallbackReadAttachment()) {
|
|
const SizeT componentSize = GetReadbackComponentSize(wideType);
|
|
const SizeT valueCount = componentSize != 0 ? wide.size() / componentSize : 0;
|
|
switch (wideType) {
|
|
case GL_FLOAT: {
|
|
auto* values = reinterpret_cast<Float*>(wide.data());
|
|
for (SizeT i = 0; i < valueCount; ++i) values[i] = std::clamp(values[i], 0.0f, 1.0f);
|
|
break;
|
|
}
|
|
case GL_HALF_FLOAT: {
|
|
auto* values = reinterpret_cast<Uint16*>(wide.data());
|
|
for (SizeT i = 0; i < valueCount; ++i) {
|
|
values[i] = MG_Util::EncodeFloatToHalfBits(
|
|
std::clamp(MG_Util::DecodeHalfBitsToFloat(values[i]), 0.0f, 1.0f));
|
|
}
|
|
break;
|
|
}
|
|
case GL_SHORT: {
|
|
// Signed normalized: the negative half is exactly what the clamp removes.
|
|
auto* values = reinterpret_cast<Int16*>(wide.data());
|
|
for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max<Int16>(values[i], 0);
|
|
break;
|
|
}
|
|
case GL_BYTE: {
|
|
auto* values = reinterpret_cast<Int8*>(wide.data());
|
|
for (SizeT i = 0; i < valueCount; ++i) values[i] = std::max<Int8>(values[i], 0);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels,
|
|
honorPackImageParams)) {
|
|
return false;
|
|
}
|
|
|
|
MGLOG_D("Readback conversion: converted %s/%s from wide %s/%s", MG_Util::ConvertGLEnumToString(format).c_str(),
|
|
MG_Util::ConvertGLEnumToString(type).c_str(), MG_Util::ConvertGLEnumToString(readFormat).c_str(),
|
|
MG_Util::ConvertGLEnumToString(wideType).c_str());
|
|
return true;
|
|
}
|
|
|
|
// GetTexImage fallback for internal formats the ES driver cannot attach to a framebuffer
|
|
// (SNORM, RGB16, RGB9_E5, ...): decodes the canonical CPU shadow-mip storage into wide RGBA
|
|
// rows and repacks them into the client layout. Only valid while the shadow copy is
|
|
// authoritative, which holds for non-renderable formats (they can never be GPU-written).
|
|
static Bool GetTexImageViaShadowConversion(MG_State::GLState::TextureObjectMipmap* textureMipmapObject,
|
|
TextureUploadTarget uploadTarget, GLint level, GLsizei width,
|
|
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
|
|
void* pixels, Bool applyPackImageParams) {
|
|
ReadbackChannelMapping mapping{};
|
|
if (!GetReadbackChannelMapping(format, mapping)) {
|
|
return false;
|
|
}
|
|
if (GetReadbackDstPixelSize(mapping, type) == 0) {
|
|
return false;
|
|
}
|
|
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
|
|
return true;
|
|
}
|
|
const auto& pixelPackBufferObject =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
if (!pixelPackBufferObject && pixels == nullptr) {
|
|
return true;
|
|
}
|
|
|
|
const void* shadow = textureMipmapObject->MapMipmapData(uploadTarget, level);
|
|
if (!shadow) {
|
|
return false;
|
|
}
|
|
|
|
// glGetTexImage returns the stored texels, and for a packed internal format read with the
|
|
// matching client type the shadow word already IS the client word. Decoding it to float and
|
|
// re-encoding would canonicalize an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000: the
|
|
// same value, different bits), so those pairs copy the words straight through.
|
|
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
|
textureMipmapObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
|
|
if (!ReadbackImpl::StorePackedWordsToClient(static_cast<const Uint8*>(shadow), width, sliceHeight,
|
|
sliceCount, type, pixels, applyPackImageParams)) {
|
|
return false;
|
|
}
|
|
MGLOG_D("GetTexImage: copied %s/%s verbatim from the CPU shadow copy",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return true;
|
|
}
|
|
|
|
Vector<Uint8> wide;
|
|
Bool isInteger = false;
|
|
Bool isSigned = false;
|
|
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(
|
|
textureMipmapObject->GetFormat(), shadow,
|
|
static_cast<SizeT>(width) * static_cast<SizeT>(sliceHeight) * static_cast<SizeT>(sliceCount),
|
|
wide, isInteger, isSigned)) {
|
|
return false;
|
|
}
|
|
if (mapping.isInteger != isInteger) {
|
|
// Spec-invalid combinations are rejected with GL errors at the state layer already.
|
|
return false;
|
|
}
|
|
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
|
|
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels,
|
|
applyPackImageParams)) {
|
|
return false;
|
|
}
|
|
MGLOG_D("GetTexImage: converted %s/%s from the CPU shadow copy",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return true;
|
|
}
|
|
|
|
// ---- Bit-exact readback of a 32-bit packed colour level ---------------------------------------
|
|
//
|
|
// glGetTexImage of a packed format read with its OWN client type owes the application the words
|
|
// the image HOLDS, and neither of the two routes above can promise that once anything other than
|
|
// a glTexImage has written the level:
|
|
//
|
|
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
|
// RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and
|
|
// collapses an R11F_G11F_B10F NaN to the canonical payload 1
|
|
// (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly
|
|
// such a NaN in the 10-bit blue field every time, because the source's shared-exponent
|
|
// field is all ones;
|
|
// * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote
|
|
// it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that
|
|
// up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer
|
|
// source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a
|
|
// self-copy - and the decline is silent, so the stale words are served as truth.
|
|
//
|
|
// glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour
|
|
// format in one compatibility class, so copying the level into a scratch GL_R32UI image and
|
|
// reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote
|
|
// them. This is what lets the shadow stop being the authority for these formats: it is tried
|
|
// first, and every step reports rather than guesses, so a driver that turns any of it down
|
|
// simply leaves the old shadow/attachment fallbacks to run.
|
|
static GLuint g_packedWordScratchTextureId = 0;
|
|
static GLsizei g_packedWordScratchWidth = 0;
|
|
static GLsizei g_packedWordScratchHeight = 0;
|
|
|
|
// Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused
|
|
// the storage, which is a decline and not an error.
|
|
static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) {
|
|
if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width &&
|
|
g_packedWordScratchHeight >= height) {
|
|
return g_packedWordScratchTextureId;
|
|
}
|
|
const GLsizei newWidth = std::max(width, g_packedWordScratchWidth);
|
|
const GLsizei newHeight = std::max(height, g_packedWordScratchHeight);
|
|
if (g_packedWordScratchTextureId != 0) {
|
|
// A scratch FBO may still name the old id, and the driver is free to hand the same
|
|
// number back for the replacement - which would false-skip the re-attach.
|
|
ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId);
|
|
g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId);
|
|
g_packedWordScratchTextureId = 0;
|
|
g_packedWordScratchWidth = 0;
|
|
g_packedWordScratchHeight = 0;
|
|
}
|
|
GLuint texture = 0;
|
|
g_GLESFuncs.glGenTextures(1, &texture);
|
|
if (texture == 0) return 0;
|
|
|
|
ClearGLErrors();
|
|
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture);
|
|
// Immutable single-level storage: glCopyImageSubData wants a complete image, and
|
|
// glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete
|
|
// under the default mipmapping filter.
|
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight);
|
|
const GLenum storageError = g_GLESFuncs.glGetError();
|
|
// Re-bind whatever the binding cache says lives on the temp unit, so the cache stays
|
|
// truthful without a driver query (same discipline as CopyR32FTexture2D).
|
|
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
|
[static_cast<SizeT>(TextureTarget::Texture2D)];
|
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
|
if (storageError != GL_NO_ERROR) {
|
|
g_GLESFuncs.glDeleteTextures(1, &texture);
|
|
MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable",
|
|
newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str());
|
|
return 0;
|
|
}
|
|
g_packedWordScratchTextureId = texture;
|
|
g_packedWordScratchWidth = newWidth;
|
|
g_packedWordScratchHeight = newHeight;
|
|
return texture;
|
|
}
|
|
|
|
static void ReleasePackedWordScratchTexture() {
|
|
// The ES context (and the name with it) is gone; deleting here would target a recycled
|
|
// name in the successor context.
|
|
g_packedWordScratchTextureId = 0;
|
|
g_packedWordScratchWidth = 0;
|
|
g_packedWordScratchHeight = 0;
|
|
}
|
|
|
|
// One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`.
|
|
static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice,
|
|
GLsizei width, GLsizei height, Uint32* outWords) {
|
|
if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false;
|
|
if (!g_GLESFuncs.glCopyImageSubData) return false;
|
|
|
|
// Horizontal bands, so neither the scratch image nor the staging buffer scales with the
|
|
// level. The scratch is grow-only on purpose - a sweep down a mip chain must not
|
|
// reallocate per level - which without a band cap would leave a 4096x4096 readback's
|
|
// 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with
|
|
// 4 MiB of staging behind it because the read lands four words per texel.
|
|
constexpr SizeT kMaxScratchTexels = SizeT{1} << 18;
|
|
const GLsizei bandRows = std::max<GLsizei>(
|
|
1, static_cast<GLsizei>(std::min<SizeT>(kMaxScratchTexels / static_cast<SizeT>(width),
|
|
static_cast<SizeT>(height))));
|
|
const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows);
|
|
if (scratch == 0) return false;
|
|
|
|
ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false);
|
|
auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer();
|
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB));
|
|
ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0);
|
|
ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0);
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back");
|
|
return false;
|
|
}
|
|
|
|
// GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer
|
|
// colour buffer, so the read lands four words per texel and the red one is compacted out
|
|
// here. The PACK scope is the tight default rather than the application's, so a row comes
|
|
// back packed at exactly `width * 4` words. One glGetError covers the whole loop: it
|
|
// accumulates, and a failure anywhere means the caller falls back rather than trusting a
|
|
// partial result.
|
|
const SizeT wordsPerRow = static_cast<SizeT>(width) * 4;
|
|
Vector<Uint32> staging(static_cast<SizeT>(bandRows) * wordsPerRow);
|
|
ScopedPixelPackBuffer packBuffer(0);
|
|
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
|
ClearGLErrors();
|
|
for (GLsizei y = 0; y < height; y += bandRows) {
|
|
const GLsizei rows = std::min(bandRows, height - y);
|
|
g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0,
|
|
0, 0, width, rows, 1);
|
|
g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data());
|
|
for (GLsizei row = 0; row < rows; ++row) {
|
|
const Uint32* srcRow = staging.data() + static_cast<SizeT>(row) * wordsPerRow;
|
|
Uint32* dstRow = outWords + static_cast<SizeT>(y + row) * static_cast<SizeT>(width);
|
|
for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast<SizeT>(x) * 4];
|
|
}
|
|
}
|
|
const GLenum error = g_GLESFuncs.glGetError();
|
|
if (error != GL_NO_ERROR) {
|
|
MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back",
|
|
MG_Util::ConvertGLEnumToString(backendTarget).c_str(),
|
|
MG_Util::ConvertGLEnumToString(error).c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
|
|
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
|
|
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
|
}
|
|
|
|
static Bool IsLegacyNativeReadPixelsType(GLenum type) {
|
|
// GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV are only ever valid
|
|
// paired with GL_DEPTH_STENCIL (packed_depth_stencil.verify_read_pixels); the real
|
|
// driver already implements this readback natively.
|
|
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV ||
|
|
type == GL_INT || type == GL_FLOAT || type == GL_UNSIGNED_INT_24_8 ||
|
|
type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
|
|
}
|
|
|
|
// The (format, type) pairs the depth/stencil helpers below can service. They are not
|
|
// covered by the colour tables above - GetReadbackChannelMapping has no entry for any
|
|
// depth or stencil format, so without this gate a read the helpers CAN serve (a
|
|
// GL_UNSIGNED_SHORT depth, a GL_SHORT stencil) is turned away before it reaches them.
|
|
static Bool IsSupportedDepthStencilReadPixelsPair(GLenum format, GLenum type) {
|
|
switch (format) {
|
|
case GL_DEPTH_COMPONENT:
|
|
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_SHORT || type == GL_UNSIGNED_INT ||
|
|
type == GL_FLOAT;
|
|
case GL_STENCIL_INDEX:
|
|
return type == GL_UNSIGNED_BYTE || type == GL_BYTE || type == GL_UNSIGNED_SHORT || type == GL_SHORT ||
|
|
type == GL_UNSIGNED_INT || type == GL_INT || type == GL_FLOAT;
|
|
case GL_DEPTH_STENCIL:
|
|
return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
|
MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height,
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels);
|
|
|
|
// Combinations the ES driver has always handled directly keep the native path; other color layouts go
|
|
// through the wide-format conversion path. Anything still uncovered degrades to a logged no-op instead
|
|
// of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer.
|
|
const Bool useNativeReadback = (IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type)) ||
|
|
IsSupportedDepthStencilReadPixelsPair(format, type);
|
|
ReadbackChannelMapping conversionMapping{};
|
|
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
|
|
GetReadbackDstPixelSize(conversionMapping, type) != 0;
|
|
if (!useNativeReadback && !convertible) {
|
|
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return;
|
|
}
|
|
|
|
MGLOG_D("ReadPixels: SyncNeccessaryTextures()");
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
|
|
MGLOG_D("ReadPixels: SyncCurrentFBO()");
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
|
|
MGLOG_D("ReadPixels: BindCurrentFBO(Read)");
|
|
BindCurrentFBO(FramebufferTarget::Read);
|
|
|
|
MGLOG_D("ReadPixels: Applying the PACK pixel-store scope");
|
|
ScopedPackState packParamsScope(PackStateFromContext());
|
|
|
|
GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
|
|
MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
|
|
|
|
if (fbStatus != GL_FRAMEBUFFER_COMPLETE) {
|
|
MGLOG_E_ONCE("ReadPixels: bound READ FBO is not complete");
|
|
return;
|
|
}
|
|
// ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the
|
|
// matching attachment class; every other convertible color layout (including GL_RGBA/GL_FLOAT
|
|
// and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type
|
|
// the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so
|
|
// it always takes the conversion path (which swaps on the CPU).
|
|
const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;
|
|
// The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly
|
|
// the right thing to ask here.
|
|
const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment();
|
|
// An attachment widened from three channels to stay colour-renderable also has to leave
|
|
// the fast pair: only the conversion path knows to answer its alpha with the 1.0 the
|
|
// application's format implies instead of whatever the draw wrote into the added channel.
|
|
const Bool nativeFastPair = !packSwapBytes && !forceOpaqueAlpha &&
|
|
((format == GL_RGBA && type == GL_UNSIGNED_BYTE) ||
|
|
(format == GL_RGBA_INTEGER && (type == GL_UNSIGNED_INT || type == GL_INT)));
|
|
if (convertible && !nativeFastPair) {
|
|
if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels,
|
|
/*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true,
|
|
forceOpaqueAlpha)) {
|
|
MGLOG_D("ReadPixels: finished via client-format conversion");
|
|
return;
|
|
}
|
|
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return;
|
|
}
|
|
// Every depth and stencil read goes through the helpers, not just the widening ones:
|
|
// ES has no guaranteed readback for either aspect, so even a byte-for-byte case
|
|
// needs the fallback chain (the other native spelling, then the sampling emulation)
|
|
// on a driver without GL_NV_read_depth / GL_NV_read_stencil.
|
|
if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(x, y, width, height, type, pixels)) {
|
|
MGLOG_D("ReadPixels: finished via depth readback helper");
|
|
return;
|
|
}
|
|
if (format == GL_STENCIL_INDEX && ReadPixelsStencilViaNative(x, y, width, height, type, pixels)) {
|
|
MGLOG_D("ReadPixels: finished via stencil readback helper");
|
|
return;
|
|
}
|
|
if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(x, y, width, height, type, pixels)) {
|
|
MGLOG_D("ReadPixels: finished via packed depth/stencil readback helper");
|
|
return;
|
|
}
|
|
|
|
// Handle PBO. The pack binding is scoped: it returns to the resting 0 state
|
|
// on every exit path, so a later readback can never land in a stale PBO
|
|
// (the driver-level binding used to stay on the user PBO after this call,
|
|
// capturing subsequent client-memory readbacks into it).
|
|
auto& pixelPackBufferObject =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
Bool usePBO = false;
|
|
GLuint packBufferId = 0;
|
|
if (pixelPackBufferObject) {
|
|
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
|
|
MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("ReadPixels: No backend buffer found for PBO %u.",
|
|
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
|
|
return;
|
|
}
|
|
usePBO = true;
|
|
packBufferId = backendResource->id;
|
|
} else {
|
|
MGLOG_D("ReadPixels: Not using PBO");
|
|
}
|
|
ScopedPixelPackBuffer packBufferBinding(packBufferId);
|
|
|
|
MGLOG_D("ReadPixels: glReadPixels()");
|
|
DrainESErrors();
|
|
g_GLESFuncs.glReadPixels(x, y, width, height, format, type, pixels);
|
|
const GLenum nativeReadError = g_GLESFuncs.glGetError();
|
|
if (nativeReadError != GL_NO_ERROR) {
|
|
// ES drivers only guarantee GL_RGBA/GL_UNSIGNED_BYTE, GL_RGBA_INTEGER/(U)INT, float RGBA and one
|
|
// implementation-defined pair; legacy combos like GL_RED/GL_UNSIGNED_INT are rejected by e.g.
|
|
// Adreno with a GL error and an untouched destination (GL CTS packed_pixels r8_format_red). The
|
|
// failed read wrote nothing (client memory and PBO alike), so re-service the request through the
|
|
// wide-format conversion path before any PBO writeback can capture stale contents. The conversion
|
|
// helper saves/restores the ES pixel-pack binding and handles the state-layer PBO itself.
|
|
DrainESErrors();
|
|
MGLOG_D("ReadPixels: native read of %s/%s failed (%s), retrying via client-format conversion",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(),
|
|
MG_Util::ConvertGLEnumToString(nativeReadError).c_str());
|
|
if (ReadPixelsViaFormatConversion(x, y, width, height, format, type, pixels,
|
|
/*honorPackImageParams=*/false, /*applyFixedPointReadClamp=*/true,
|
|
forceOpaqueAlpha)) {
|
|
MGLOG_D("ReadPixels: finished via client-format conversion after native failure");
|
|
return;
|
|
}
|
|
MGLOG_E_ONCE("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, "
|
|
"skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(),
|
|
MG_Util::ConvertGLEnumToString(nativeReadError).c_str());
|
|
return;
|
|
}
|
|
if (usePBO) {
|
|
// pull back to client memory if PBO is used
|
|
MGLOG_D("ReadPixels: PBO used, mapping buffer to client memory");
|
|
GLvoid* pboMappedPtr = g_GLESFuncs.glMapBufferRange(
|
|
GL_PIXEL_PACK_BUFFER, 0, (GLsizeiptr)pixelPackBufferObject->GetSize(), GL_MAP_READ_BIT);
|
|
if (pboMappedPtr) {
|
|
MGLOG_D("ReadPixels: Copying data from PBO to client memory");
|
|
SizeT size = pixelPackBufferObject->GetSize();
|
|
pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0);
|
|
// Serial bumped with no backend op; re-open the buffer draw-clean memos.
|
|
BufferImpl::BumpBufferMutationEpoch();
|
|
MGLOG_D("ReadPixels: Unmapping PBO");
|
|
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
|
} else {
|
|
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr");
|
|
}
|
|
}
|
|
MGLOG_D("ReadPixels: finished");
|
|
}
|
|
|
|
// Combinations the ES driver has always handled directly for GetTexImage; everything else that maps
|
|
// to a color channel layout is repacked via ReadPixelsViaFormatConversion.
|
|
static Bool IsNativeGetTexImagePair(GLenum format, GLenum type) {
|
|
if (format == GL_RGBA) {
|
|
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV ||
|
|
type == GL_INT || type == GL_FLOAT || type == GL_HALF_FLOAT ||
|
|
type == GL_UNSIGNED_INT_8_8_8_8_REV;
|
|
}
|
|
if (format == GL_RGBA_INTEGER) {
|
|
return type == GL_INT || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV;
|
|
}
|
|
if (format == GL_DEPTH_STENCIL || format == GL_DEPTH_COMPONENT) {
|
|
return IsSupportedDepthStencilReadPixelsPair(format, type);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, void* pixels) {
|
|
DebugImpl::ErrorLopper errorLopper;
|
|
MGLOG_D("GetTexImage: target=%s level=%d format=%s type=%s pixels=%p",
|
|
MG_Util::ConvertGLEnumToString(target).c_str(), level, MG_Util::ConvertGLEnumToString(format).c_str(),
|
|
MG_Util::ConvertGLEnumToString(type).c_str(), pixels);
|
|
|
|
// Unimplemented readback formats degrade to a logged no-op instead of killing the process;
|
|
// spec-invalid combinations are already rejected with GL errors at the state layer.
|
|
const Bool useNativeReadback = IsNativeGetTexImagePair(format, type);
|
|
ReadbackChannelMapping conversionMapping{};
|
|
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
|
|
GetReadbackDstPixelSize(conversionMapping, type) != 0;
|
|
if (!useNativeReadback && !convertible) {
|
|
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return;
|
|
}
|
|
|
|
GLenum esFormat = format, esType = type;
|
|
// On little-endian hosts UNSIGNED_INT_8_8_8_8_REV has the same memory layout as UNSIGNED_BYTE.
|
|
if (esType == GL_UNSIGNED_INT_8_8_8_8_REV) esType = GL_UNSIGNED_BYTE;
|
|
|
|
MGLOG_D("GetTexImage: SyncNeccessaryTextures()");
|
|
TextureImpl::SyncNeccessaryTextures();
|
|
|
|
MGLOG_D("GetTexImage: SyncCurrentFBO()");
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
|
|
auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
MGLOG_D("GetTexImage: active texture unit = %u", activeTextureUnit);
|
|
|
|
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
|
|
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
|
|
.GetBoundObject();
|
|
|
|
MGLOG_D("GetTexImage: bound texture object = %p (name=%u)", textureObject.get(),
|
|
textureObject ? textureObject->GetExternalIndex() : 0);
|
|
|
|
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
|
|
|
if (!backendTextureSlot || !*backendTextureSlot) {
|
|
MGLOG_E_ONCE("GetTexImage: No backend texture found for texture %u.",
|
|
textureObject ? textureObject->GetExternalIndex() : 0);
|
|
return;
|
|
}
|
|
|
|
GLuint backendTexId = (*backendTextureSlot)->GetBackendTextureId();
|
|
MGLOG_D("GetTexImage: backend texture id = %u", backendTexId);
|
|
|
|
// Force pending rendering to complete before reading the texture back through the temp READ FBO.
|
|
// Tile-based GPUs (Mali) do not guarantee that a render into this texture through its own FBO has
|
|
// been resolved to memory when it is subsequently sampled through a *different* (temp) FBO: the
|
|
// cross-FBO glReadPixels below races the deferred tile resolve and returns the pre-render (clear)
|
|
// contents, so distinct render targets read back byte-identical (e.g. KHR-GLxx.glsl_noperspective
|
|
// fails on Mali-G715, all four programs reading as the clear colour). glGetTexImage is already a
|
|
// CPU/GPU sync point, so the extra drain is negligible; Adreno resolves eagerly and is unaffected.
|
|
g_GLESFuncs.glFinish();
|
|
|
|
MGLOG_D("GetTexImage: Binding temporary FBO");
|
|
TempFBOBinder tempFBOBinder(true);
|
|
auto& tempFB = tempFBOBinder.Framebuffer();
|
|
|
|
MGLOG_D("GetTexImage: attaching level %d to the scratch FBO", level);
|
|
const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
|
|
MG_Util::ConvertGLEnumToTextureUploadTarget(target));
|
|
// GL_DEPTH_STENCIL can't be attached as a color attachment (glCheckFramebufferStatus
|
|
// would report it incomplete); it has its own combined depth+stencil attachment point.
|
|
// glReadBuffer only selects among color attachments, so it does not apply here.
|
|
if (format == GL_DEPTH_STENCIL || format == GL_DEPTH_COMPONENT) {
|
|
ScratchFBOImpl::EnsureDepthAttachment2D(
|
|
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
|
|
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
|
|
/*withStencil=*/format == GL_DEPTH_STENCIL);
|
|
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) {
|
|
// ES cannot attach 3D/array textures through glFramebufferTexture2D; layer 0 here, and
|
|
// the deeper slices one at a time in the per-layer loop below. A CUBE MAP ARRAY is in
|
|
// this list for the same reason its layer-faces are addressed as array layers:
|
|
// glFramebufferTexture2D has no target token for it, so the 2D attach it used to take
|
|
// left the scratch FBO incomplete and every read fell through to the (stale) CPU
|
|
// shadow - which is exactly the all-zero result the conformance suite saw.
|
|
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
|
|
} else {
|
|
ScratchFBOImpl::EnsureColorAttachment2D(
|
|
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
|
|
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level);
|
|
}
|
|
if (format != GL_DEPTH_STENCIL && format != GL_DEPTH_COMPONENT) {
|
|
MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)");
|
|
ScratchFBOImpl::EnsureReadBuffer(tempFB, GL_COLOR_ATTACHMENT0);
|
|
}
|
|
|
|
GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
|
|
MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
|
|
|
|
// Non-renderable internal formats (SNORM, RGB16, RGB9_E5, ...) leave the temp FBO incomplete;
|
|
// those readbacks are served from the CPU shadow copy below instead of bailing out.
|
|
const Bool tempFBOComplete = fbStatus == GL_FRAMEBUFFER_COMPLETE;
|
|
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
MGLOG_D("GetTexImage: Applying the PACK pixel-store scope");
|
|
ScopedPackState packParamsScope(PackStateFromContext());
|
|
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
|
|
const auto& storageType = textureObject->GetStorageType();
|
|
MGLOG_D("GetTexImage: texture storage type = %d", (int)storageType);
|
|
|
|
if (storageType == TextureStorageType::Buffer) {
|
|
MGLOG_E_ONCE("GetTexImage: Texture storage type Buffer is not supported.");
|
|
return;
|
|
}
|
|
|
|
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
|
|
|
auto& levelRange = textureMipmapObject->GetLevelRange();
|
|
MGLOG_D("GetTexImage: mipmap level range = [%d, %d]", levelRange.x(), levelRange.y());
|
|
|
|
// levelRange.y() is GL_TEXTURE_MAX_LEVEL, an inclusive level index — a single-level
|
|
// texture has range [0, 0] and level 0 must be readable.
|
|
if (static_cast<Uint>(level) < levelRange.x() || static_cast<Uint>(level) > levelRange.y()) {
|
|
MGLOG_E_ONCE("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback",
|
|
level, levelRange.x(), levelRange.y());
|
|
return;
|
|
}
|
|
|
|
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level);
|
|
|
|
// GL_TEXTURE_1D_ARRAY keeps its LAYERS in the state-side height (that is what
|
|
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means), while the ES texture behind it is a
|
|
// 2D array of height 1 with the layers in depth - GetBackendUploadSize performs exactly
|
|
// that swap on the way in. Everything below addresses the ES image, so the same swap has
|
|
// to happen here: without it the readback asked layer 0 for a `layers`-row rectangle it
|
|
// does not have, and every layer but the first came back undefined (all zeroes on Adreno,
|
|
// KHR-GL4x.shader_image_load_store.basic-allTargets-*).
|
|
const Bool oneDimensionalArray = textureObject->GetTarget() == TextureTarget::Texture1DArray;
|
|
if (oneDimensionalArray) {
|
|
size = TextureImpl::GetBackendUploadSize(TextureTarget::Texture1DArray, size);
|
|
}
|
|
|
|
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
|
|
|
|
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs
|
|
// are only guaranteed for matching attachment classes (e.g. GL_RGBA/GL_UNSIGNED_INT is invalid
|
|
// for normalized attachments), while the conversion path reads a wide format that is always
|
|
// accepted and repacks on the CPU.
|
|
if (convertible) {
|
|
// The image being read is this texture, not whatever the application left bound to
|
|
// GL_READ_FRAMEBUFFER, so the widening question has to be asked of the texture.
|
|
const Bool forceOpaqueAlpha =
|
|
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
|
|
// An image-bindable texture in one of the seven normalized formats has its ES storage
|
|
// in a GL_RGBA16UI, holding the format's own channel CODES. glGetTexImage still owes
|
|
// the application the NORMALIZED value, so the conversion has to be undone here - and
|
|
// it can only be asked of the TEXTURE, which is why it is not derived from the
|
|
// attachment the scratch framebuffer happens to hold.
|
|
NormalizedImageCarrierRead normalizedCarrier;
|
|
if (const auto imageWidening =
|
|
TextureImpl::GetImageBindableStorageWidening(textureObject->GetFormat());
|
|
imageWidening && imageWidening.CarriesNormalizedCodes() &&
|
|
(*backendTextureSlot)->RequiresImageBindableStorage()) {
|
|
for (SizeT channel = 0; channel < 4; ++channel) {
|
|
normalizedCarrier.ChannelMax[channel] = imageWidening.ChannelMax[channel];
|
|
}
|
|
normalizedCarrier.SignedNormalized = imageWidening.SignedNormalized;
|
|
}
|
|
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
|
|
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
|
|
// them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
|
|
// as a single two-dimensional image whose ROWS are the layers, so the layer stride is
|
|
// one packed row and the image parameters do not enter into it - even though the ES
|
|
// texture underneath is an array and is read one layer at a time.
|
|
const Bool applyPackImageParams = !oneDimensionalArray &&
|
|
(backendAttachTarget == GL_TEXTURE_3D ||
|
|
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY);
|
|
const GLsizei sliceCount = std::max(size.z(), 1);
|
|
const Bool multiSlice = size.z() > 1;
|
|
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
|
|
// is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes,
|
|
// which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same
|
|
// value 8064, different words), and the conformance suite compares the words
|
|
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
|
|
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
|
|
// shadow branch further down was unreachable. Every other format still prefers the
|
|
// GPU, so a rendered-into texture is unaffected.
|
|
const Bool rawPackedWordRead = MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
|
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
|
// ...and the GPU CAN answer with the stored words after all, for any 32-bit packed
|
|
// format and whoever wrote the level, by going through a scratch GL_R32UI image (see
|
|
// ReadPackedLevelWordsViaScratch). Preferred over both routes below because it is the
|
|
// only one that is right for a level glCopyImageSubData wrote: the shadow may never
|
|
// have seen that write, and re-encoding the attachment cannot reproduce an RGB9_E5
|
|
// shared exponent or an R11F_G11F_B10F NaN payload. A multisample image is excluded
|
|
// because copy-image requires matching sample counts.
|
|
if (rawPackedWordRead && textureObject->GetSamples() == 0) {
|
|
// Copy-image addresses a cube map as ONE image with the face on z, where
|
|
// glGetTexImage names the face in its target.
|
|
const auto readUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
|
const GLint copyBaseSlice =
|
|
(readUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
|
readUploadTarget <= TextureUploadTarget::CubeMapNegativeZ)
|
|
? static_cast<GLint>(readUploadTarget) -
|
|
static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX)
|
|
: 0;
|
|
const GLenum copyTarget =
|
|
TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
|
const SizeT sliceWords = static_cast<SizeT>(size.x()) * static_cast<SizeT>(size.y());
|
|
Vector<Uint32> words(sliceWords * static_cast<SizeT>(sliceCount));
|
|
Bool allSlicesRead = true;
|
|
for (GLsizei slice = 0; slice < sliceCount && allSlicesRead; ++slice) {
|
|
allSlicesRead = ReadPackedLevelWordsViaScratch(backendTexId, copyTarget, level,
|
|
copyBaseSlice + slice, size.x(), size.y(),
|
|
words.data() + sliceWords * static_cast<SizeT>(slice));
|
|
}
|
|
if (allSlicesRead &&
|
|
ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(words.data()), size.x(),
|
|
size.y(), sliceCount, type, pixels,
|
|
applyPackImageParams)) {
|
|
MGLOG_D("GetTexImage: finished %d slice(s) via the bit-exact GL_R32UI word readback", sliceCount);
|
|
return;
|
|
}
|
|
}
|
|
// The last resort for the one format the attachment route can never answer for: the
|
|
// shadow is only right while nothing but a glTexImage has written the level, which is
|
|
// why CopyImageSubData mirrors itself into it where it can.
|
|
const Bool verbatimPackedShadowRead =
|
|
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
|
|
rawPackedWordRead;
|
|
if (verbatimPackedShadowRead &&
|
|
GetTexImageViaShadowConversion(textureMipmapObject,
|
|
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
|
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
|
|
MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)");
|
|
return;
|
|
}
|
|
// A multi-slice read used to go to the CPU shadow outright, on the grounds that the
|
|
// scratch FBO can only expose one layer at a time. But the shadow only holds what was
|
|
// uploaded, so every slice that was rendered to came back stale - which is exactly what
|
|
// a layered framebuffer produces, and what textures_storage_multisample_3d_* checks.
|
|
// Attach the layers one at a time instead and read each off the GPU, keeping the shadow
|
|
// for the formats the FBO cannot represent at all.
|
|
if (multiSlice && tempFBOComplete &&
|
|
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY)) {
|
|
// Each slice is packed as its own 2D image, so the per-slice call must not apply
|
|
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
|
|
// over them, using the same layout StoreWideRowsToClient computes.
|
|
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
|
const SizeT dstPixelBytes = GetReadbackDstPixelSize(conversionMapping, type);
|
|
const SizeT rowPixels =
|
|
static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : size.x());
|
|
const SizeT alignment =
|
|
packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : SizeT{1};
|
|
const SizeT dstRowStride = (rowPixels * dstPixelBytes + alignment - 1) / alignment * alignment;
|
|
const SizeT imageRows = applyPackImageParams && packParams.ImageHeight > 0
|
|
? static_cast<SizeT>(packParams.ImageHeight)
|
|
: static_cast<SizeT>(size.y());
|
|
const SizeT dstImageStride = imageRows * dstRowStride;
|
|
const SizeT skipImages =
|
|
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
|
|
|
Bool allSlicesRead = true;
|
|
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
|
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level,
|
|
slice);
|
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
allSlicesRead = false;
|
|
break;
|
|
}
|
|
const SizeT sliceOffset = (skipImages + static_cast<SizeT>(slice)) * dstImageStride;
|
|
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
|
|
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
|
|
/*honorPackImageParams=*/false,
|
|
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha,
|
|
normalizedCarrier)) {
|
|
allSlicesRead = false;
|
|
break;
|
|
}
|
|
}
|
|
// Leave the scratch FBO on layer 0 so the single-slice paths below see what they
|
|
// set up.
|
|
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
|
|
if (allSlicesRead) {
|
|
MGLOG_D("GetTexImage: finished %d slices via per-layer readback", sliceCount);
|
|
return;
|
|
}
|
|
}
|
|
if (multiSlice &&
|
|
GetTexImageViaShadowConversion(textureMipmapObject,
|
|
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
|
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
|
|
MGLOG_D("GetTexImage: finished via shadow conversion");
|
|
return;
|
|
}
|
|
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
|
|
applyPackImageParams,
|
|
/*applyFixedPointReadClamp=*/false,
|
|
forceOpaqueAlpha, normalizedCarrier)) {
|
|
MGLOG_D("GetTexImage: finished via client-format conversion");
|
|
return;
|
|
}
|
|
if (GetTexImageViaShadowConversion(textureMipmapObject,
|
|
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
|
size.y(), sliceCount, format, type, pixels, applyPackImageParams)) {
|
|
MGLOG_D("GetTexImage: finished via shadow conversion");
|
|
return;
|
|
}
|
|
if (!tempFBOComplete) {
|
|
MGLOG_E_ONCE("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
|
|
return;
|
|
}
|
|
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
|
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
|
return;
|
|
}
|
|
if (!tempFBOComplete) {
|
|
MGLOG_E_ONCE("GetTexImage: bound READ FBO is not complete");
|
|
return;
|
|
}
|
|
|
|
// The level is attached to the scratch READ framebuffer above, so the depth and
|
|
// stencil aspects are read with exactly the same helpers glReadPixels uses - native
|
|
// where the driver has it, shader sampling where it does not. ES accepts neither
|
|
// spelling natively, which is why glGetTexImage(GL_DEPTH_STENCIL) used to leave
|
|
// packed_depth_stencil.verify_get_tex_image reading its own zero-filled buffer.
|
|
if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(0, 0, size.x(), size.y(), type, pixels)) {
|
|
MGLOG_D("GetTexImage: finished via depth readback helper");
|
|
return;
|
|
}
|
|
if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(0, 0, size.x(), size.y(), type, pixels)) {
|
|
MGLOG_D("GetTexImage: finished via packed depth/stencil readback helper");
|
|
return;
|
|
}
|
|
|
|
// Handle PBO. The pack binding is scoped: it returns to the resting 0 state
|
|
// on every exit path, so a later readback can never land in a stale PBO.
|
|
auto& pixelPackBufferObject =
|
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
Bool usePBO = false;
|
|
GLuint packBufferId = 0;
|
|
if (pixelPackBufferObject) {
|
|
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
|
|
MGLOG_D("GetTexImage: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("GetTexImage: No backend buffer found for PBO %u.",
|
|
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
|
|
return;
|
|
}
|
|
usePBO = true;
|
|
packBufferId = backendResource->id;
|
|
} else {
|
|
MGLOG_D("GetTexImage: Not using PBO");
|
|
}
|
|
ScopedPixelPackBuffer packBufferBinding(packBufferId);
|
|
|
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
|
|
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
|
});
|
|
MGLOG_D("GetTexImage: glReadPixels(0, 0, %d, %d, %s, %s, %p)", size.x(), size.y(),
|
|
MG_Util::ConvertGLEnumToString(esFormat).c_str(), MG_Util::ConvertGLEnumToString(esType).c_str(),
|
|
pixels);
|
|
g_GLESFuncs.glReadPixels(0, 0, size.x(), size.y(), esFormat, esType, pixels);
|
|
|
|
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 (usePBO) {
|
|
// pull back to client memory if PBO is used
|
|
MGLOG_D("ReadPixels: PBO used, mapping buffer to client memory");
|
|
GLvoid* pboMappedPtr = g_GLESFuncs.glMapBufferRange(
|
|
GL_PIXEL_PACK_BUFFER, 0, (GLsizeiptr)pixelPackBufferObject->GetSize(), GL_MAP_READ_BIT);
|
|
if (pboMappedPtr) {
|
|
MGLOG_D("ReadPixels: Copying data from PBO to client memory");
|
|
SizeT size = pixelPackBufferObject->GetSize();
|
|
pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0);
|
|
// Serial bumped with no backend op; re-open the buffer draw-clean memos.
|
|
BufferImpl::BumpBufferMutationEpoch();
|
|
MGLOG_D("ReadPixels: Unmapping PBO");
|
|
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
|
} else {
|
|
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned 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());
|
|
});
|
|
MGLOG_D("GetTexImage: finished");
|
|
}
|
|
|
|
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs) {
|
|
g_EGLFuncs = eglFuncs;
|
|
}
|
|
|
|
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs) {
|
|
g_GLESFuncs = glesFuncs;
|
|
}
|
|
|
|
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities) {
|
|
g_GLESCapabilities = capabilities;
|
|
}
|
|
|
|
static EGLDisplay g_Display = EGL_NO_DISPLAY;
|
|
static EGLContext g_Context = EGL_NO_CONTEXT;
|
|
static EGLSurface g_Surface = EGL_NO_SURFACE;
|
|
static EGLConfig g_Config = nullptr;
|
|
|
|
static Bool QueryCurrentSurfaceSize(Int& outWidth, Int& outHeight) {
|
|
outWidth = 0;
|
|
outHeight = 0;
|
|
if (!g_EGLFuncs.eglQuerySurface || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) {
|
|
return false;
|
|
}
|
|
|
|
EGLint width = 0;
|
|
EGLint height = 0;
|
|
if (!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_WIDTH, &width) ||
|
|
!g_EGLFuncs.eglQuerySurface(g_Display, g_Surface, EGL_HEIGHT, &height) ||
|
|
width <= 0 || height <= 0) {
|
|
return false;
|
|
}
|
|
|
|
outWidth = static_cast<Int>(width);
|
|
outHeight = static_cast<Int>(height);
|
|
return true;
|
|
}
|
|
|
|
// The frontend's default framebuffer is a placeholder FramebufferObject whose attachments
|
|
// carry a format and nothing else (MG_Impl/Init.cpp), and it is built before any surface
|
|
// exists - so it starts on a guess, GL_DEPTH32F_STENCIL8. Every attachment query about the
|
|
// default framebuffer is answered out of that guess, and being wrong is not cosmetic: GL
|
|
// blits depth/stencil only between IDENTICAL formats, so a caller that reads
|
|
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was told about and blits
|
|
// gets GL_INVALID_OPERATION and a silently dropped blit - colour bits included, because a
|
|
// rejected glBlitFramebuffer transfers nothing at all. DirectVulkan already publishes its
|
|
// real format when it creates the swapchain (SwapchainObject::Create); this is the
|
|
// DirectGLES half, and here the answer can simply be asked of the ES default framebuffer.
|
|
//
|
|
// Only the format is published. The placeholder's 512x512 extent is left alone: all three
|
|
// attachments share it, and FramebufferObject::CheckCompleteness requires them to agree, so
|
|
// resizing depth/stencil without colour would report the default framebuffer incomplete.
|
|
static void PublishDefaultFramebufferDepthStencilFormat() {
|
|
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
|
|
if (!defaultFBOInfo || !g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
|
|
|
|
// GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer
|
|
// would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM
|
|
// for these. Nothing else can be bound this early, but bind explicitly so the answer
|
|
// describes the default framebuffer even if this is ever called later.
|
|
GLint previousDrawFramebuffer = 0;
|
|
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFramebuffer);
|
|
if (previousDrawFramebuffer != 0) {
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
ClearGLErrors();
|
|
GLint depthBits = 0;
|
|
GLint stencilBits = 0;
|
|
GLint depthComponentType = GL_UNSIGNED_NORMALIZED;
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
|
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
|
|
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
|
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
|
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE,
|
|
&depthComponentType);
|
|
ClearGLErrors();
|
|
|
|
if (previousDrawFramebuffer != 0) {
|
|
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDrawFramebuffer));
|
|
}
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
|
|
// A driver that refuses the query leaves both at 0. Fall back to what the EGL config
|
|
// was chosen with, which is what the surface actually has.
|
|
if (depthBits == 0 && stencilBits == 0 && g_EGLFuncs.eglGetConfigAttrib && g_Display != EGL_NO_DISPLAY &&
|
|
g_Config != nullptr) {
|
|
EGLint eglDepth = 0;
|
|
EGLint eglStencil = 0;
|
|
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_DEPTH_SIZE, &eglDepth)) {
|
|
depthBits = static_cast<GLint>(eglDepth);
|
|
}
|
|
if (g_EGLFuncs.eglGetConfigAttrib(g_Display, g_Config, EGL_STENCIL_SIZE, &eglStencil)) {
|
|
stencilBits = static_cast<GLint>(eglStencil);
|
|
}
|
|
}
|
|
|
|
if (depthBits <= 0 && stencilBits <= 0) {
|
|
// Nothing usable came back from either witness. Returning here leaves whatever a
|
|
// PREVIOUS surface published in place, which would be stale - this function runs
|
|
// once per surface activation, not once per process. It is written this way anyway
|
|
// because the branch is unreachable for a surface MobileGL chose itself:
|
|
// InitDisplayAndContext asks eglChooseConfig for EGL_DEPTH_SIZE 24 and
|
|
// EGL_STENCIL_SIZE 8, and so does its alpha-free retry, so g_Config always has both
|
|
// and the EGL fallback above always answers. A caller that supplies its own
|
|
// depth-less config would keep the previous surface's description; publishing a
|
|
// guess instead would be a different lie, and the placeholder attachment model has
|
|
// no way to say "this buffer does not exist" short of detaching it.
|
|
MGLOG_D("DirectGLES: default framebuffer reports no depth or stencil; leaving the "
|
|
"placeholder attachment formats untouched");
|
|
return;
|
|
}
|
|
|
|
const Bool floatDepth = depthComponentType == GL_FLOAT;
|
|
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
|
|
TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8;
|
|
if (depthBits > 0 && stencilBits > 0) {
|
|
// Packed: both frontend attachments name the same combined format, as DirectVulkan does.
|
|
depthFormat = (floatDepth || depthBits > 24) ? TextureInternalFormat::Depth32FStencil8
|
|
: TextureInternalFormat::Depth24Stencil8;
|
|
stencilFormat = depthFormat;
|
|
} else if (depthBits > 0) {
|
|
depthFormat = floatDepth ? TextureInternalFormat::DepthComponent32F
|
|
: (depthBits <= 16) ? TextureInternalFormat::DepthComponent16
|
|
: TextureInternalFormat::DepthComponent24;
|
|
stencilFormat = depthFormat;
|
|
} else {
|
|
depthFormat = TextureInternalFormat::StencilIndex8;
|
|
stencilFormat = TextureInternalFormat::StencilIndex8;
|
|
}
|
|
|
|
auto* depthTexture = defaultFBOInfo->depthAttachment.get();
|
|
auto* stencilTexture = defaultFBOInfo->stencilAttachment.get();
|
|
if (depthTexture) depthTexture->SetInternalFormat(depthFormat);
|
|
if (stencilTexture) stencilTexture->SetInternalFormat(stencilFormat);
|
|
MGLOG_D("DirectGLES: default framebuffer depth=%d stencil=%d float=%d; published attachment "
|
|
"formats depth=%d stencil=%d",
|
|
depthBits, stencilBits, floatDepth ? 1 : 0, static_cast<int>(depthFormat),
|
|
static_cast<int>(stencilFormat));
|
|
}
|
|
|
|
#if defined(__linux__) && !defined(__ANDROID__)
|
|
static void* OpenX11Lib() {
|
|
void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW);
|
|
if (!x11Lib) {
|
|
x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW);
|
|
}
|
|
return x11Lib;
|
|
}
|
|
#endif
|
|
|
|
static EGLint QueryDefaultX11VisualId() {
|
|
#if defined(__linux__) && !defined(__ANDROID__)
|
|
const char* displayName = std::getenv("DISPLAY");
|
|
if (!displayName) {
|
|
return 0;
|
|
}
|
|
|
|
void* x11Lib = OpenX11Lib();
|
|
if (!x11Lib) {
|
|
return 0;
|
|
}
|
|
|
|
using XOpenDisplayFn = void* (*)(const char*);
|
|
using XDefaultScreenFn = int (*)(void*);
|
|
using XDefaultVisualFn = void* (*)(void*, int);
|
|
using XVisualIDFromVisualFn = unsigned long (*)(void*);
|
|
using XCloseDisplayFn = int (*)(void*);
|
|
|
|
auto* xOpenDisplay = reinterpret_cast<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
|
auto* xDefaultScreen = reinterpret_cast<XDefaultScreenFn>(dlsym(x11Lib, "XDefaultScreen"));
|
|
auto* xDefaultVisual = reinterpret_cast<XDefaultVisualFn>(dlsym(x11Lib, "XDefaultVisual"));
|
|
auto* xVisualIDFromVisual = reinterpret_cast<XVisualIDFromVisualFn>(dlsym(x11Lib, "XVisualIDFromVisual"));
|
|
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(dlsym(x11Lib, "XCloseDisplay"));
|
|
if (!xOpenDisplay || !xDefaultScreen || !xDefaultVisual || !xVisualIDFromVisual || !xCloseDisplay) {
|
|
dlclose(x11Lib);
|
|
return 0;
|
|
}
|
|
|
|
void* display = xOpenDisplay(displayName);
|
|
if (!display) {
|
|
dlclose(x11Lib);
|
|
return 0;
|
|
}
|
|
const int screen = xDefaultScreen(display);
|
|
void* visual = xDefaultVisual(display, screen);
|
|
const auto visualId = visual ? static_cast<EGLint>(xVisualIDFromVisual(visual)) : 0;
|
|
xCloseDisplay(display);
|
|
dlclose(x11Lib);
|
|
return visualId;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static EGLint QueryX11WindowVisualId(NativeWindowType window) {
|
|
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<X11/Xlib.h>)
|
|
if (!window) {
|
|
return 0;
|
|
}
|
|
const char* displayName = std::getenv("DISPLAY");
|
|
if (!displayName) {
|
|
return 0;
|
|
}
|
|
|
|
void* x11Lib = OpenX11Lib();
|
|
if (!x11Lib) {
|
|
return 0;
|
|
}
|
|
|
|
using XOpenDisplayFn = Display* (*)(const char*);
|
|
using XGetWindowAttributesFn = int (*)(Display*, Window, XWindowAttributes*);
|
|
using XVisualIDFromVisualFn = unsigned long (*)(Visual*);
|
|
using XCloseDisplayFn = int (*)(Display*);
|
|
|
|
auto* xOpenDisplay = reinterpret_cast<XOpenDisplayFn>(dlsym(x11Lib, "XOpenDisplay"));
|
|
auto* xGetWindowAttributes =
|
|
reinterpret_cast<XGetWindowAttributesFn>(dlsym(x11Lib, "XGetWindowAttributes"));
|
|
auto* xVisualIDFromVisual = reinterpret_cast<XVisualIDFromVisualFn>(dlsym(x11Lib, "XVisualIDFromVisual"));
|
|
auto* xCloseDisplay = reinterpret_cast<XCloseDisplayFn>(dlsym(x11Lib, "XCloseDisplay"));
|
|
if (!xOpenDisplay || !xGetWindowAttributes || !xVisualIDFromVisual || !xCloseDisplay) {
|
|
dlclose(x11Lib);
|
|
return 0;
|
|
}
|
|
|
|
Display* display = xOpenDisplay(displayName);
|
|
if (!display) {
|
|
dlclose(x11Lib);
|
|
return 0;
|
|
}
|
|
|
|
XWindowAttributes attrs{};
|
|
EGLint visualId = 0;
|
|
if (xGetWindowAttributes(display, static_cast<Window>(window), &attrs) && attrs.visual) {
|
|
visualId = static_cast<EGLint>(xVisualIDFromVisual(attrs.visual));
|
|
}
|
|
xCloseDisplay(display);
|
|
dlclose(x11Lib);
|
|
return visualId;
|
|
#else
|
|
(void)window;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static Bool GetConfigAttrib(EGLConfig config, EGLint attr, EGLint& value) {
|
|
return g_EGLFuncs.eglGetConfigAttrib && g_EGLFuncs.eglGetConfigAttrib(g_Display, config, attr, &value);
|
|
}
|
|
|
|
static Bool ConfigSupports(EGLConfig config, EGLint surfaceBit) {
|
|
EGLint surfaceType = 0;
|
|
EGLint renderableType = 0;
|
|
if (!GetConfigAttrib(config, EGL_SURFACE_TYPE, surfaceType)) {
|
|
return false;
|
|
}
|
|
if (!GetConfigAttrib(config, EGL_RENDERABLE_TYPE, renderableType)) {
|
|
return false;
|
|
}
|
|
return (surfaceType & surfaceBit) && (renderableType & EGL_OPENGL_ES3_BIT);
|
|
}
|
|
|
|
static Bool ChooseConfigForSurface(EGLint surfaceBit, EGLConfig& outConfig,
|
|
NativeWindowType window = static_cast<NativeWindowType>(0)) {
|
|
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,
|
|
EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
|
EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8,
|
|
EGL_NONE};
|
|
|
|
EGLint numConfigs = 0;
|
|
if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, nullptr, 0, &numConfigs) || numConfigs == 0) {
|
|
return false;
|
|
}
|
|
|
|
Vector<EGLConfig> configs(static_cast<SizeT>(numConfigs));
|
|
if (!g_EGLFuncs.eglChooseConfig(g_Display, configAttribs, configs.data(), numConfigs, &numConfigs) ||
|
|
numConfigs == 0) {
|
|
return false;
|
|
}
|
|
configs.resize(static_cast<SizeT>(numConfigs));
|
|
|
|
if (surfaceBit == EGL_WINDOW_BIT) {
|
|
// X11 drivers can reserve every alpha-8 config for 32-bit ARGB
|
|
// visuals (NVIDIA), so a default-visual (depth 24) window only
|
|
// matches an alpha-0 config; keep those as a second candidate tier
|
|
// for the visual match or eglCreateWindowSurface hits BAD_CONFIG.
|
|
Vector<EGLConfig> alphaFreeConfigs;
|
|
const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE,
|
|
EGL_OPENGL_ES3_BIT,
|
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE,
|
|
8,
|
|
EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE,
|
|
24,
|
|
EGL_STENCIL_SIZE, 8, EGL_NONE};
|
|
EGLint numAlphaFree = 0;
|
|
if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) &&
|
|
numAlphaFree > 0) {
|
|
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
|
|
if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(),
|
|
numAlphaFree, &numAlphaFree)) {
|
|
numAlphaFree = 0;
|
|
}
|
|
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
|
|
}
|
|
|
|
const EGLint windowVisualId = QueryX11WindowVisualId(window);
|
|
const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()};
|
|
for (const auto visualId : visualIds) {
|
|
if (visualId == 0) {
|
|
continue;
|
|
}
|
|
for (const auto* candidates : {&configs, &alphaFreeConfigs}) {
|
|
for (const auto config : *candidates) {
|
|
EGLint nativeVisualId = 0;
|
|
if (ConfigSupports(config, surfaceBit) &&
|
|
GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) &&
|
|
nativeVisualId == visualId) {
|
|
outConfig = config;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto config : configs) {
|
|
if (ConfigSupports(config, surfaceBit)) {
|
|
outConfig = config;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
outConfig = configs.front();
|
|
return true;
|
|
}
|
|
|
|
static Bool InitDisplayAndContext(EGLint surfaceBit, NativeWindowType window = static_cast<NativeWindowType>(0)) {
|
|
DestroyEGLContext();
|
|
|
|
g_Display = g_EGLFuncs.eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
|
if (g_Display == EGL_NO_DISPLAY) return false;
|
|
|
|
if (!g_EGLFuncs.eglInitialize(g_Display, nullptr, nullptr)) return false;
|
|
g_EGLFuncs.eglBindAPI(EGL_OPENGL_ES_API);
|
|
|
|
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
|
|
|
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
|
|
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
|
|
// request yields a 3.0 context that lacks the 3.1/3.2 texture targets the
|
|
// capability probes exercise; mobile drivers ignore the minor and hand out
|
|
// their maximum either way.
|
|
for (const EGLint minorVersion : {2, 1, 0}) {
|
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
|
|
EGL_CONTEXT_MINOR_VERSION, minorVersion,
|
|
EGL_NONE};
|
|
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
|
|
if (g_Context != EGL_NO_CONTEXT) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
|
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs);
|
|
return g_Context != EGL_NO_CONTEXT;
|
|
}
|
|
|
|
namespace {
|
|
// Last swap interval the app requested through eglSwapInterval; -1 = never
|
|
// requested (keep the EGL default of 1). Re-applied when the window surface
|
|
// is (re)created since interval is per-surface state.
|
|
Int g_requestedSwapInterval = -1;
|
|
|
|
void ApplyRequestedSwapInterval() {
|
|
if (g_requestedSwapInterval < 0) return;
|
|
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return;
|
|
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
|
|
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
|
|
ok ? "ok" : "failed");
|
|
}
|
|
} // namespace
|
|
|
|
void SetSwapInterval(Int interval) {
|
|
g_requestedSwapInterval = interval;
|
|
ApplyRequestedSwapInterval();
|
|
}
|
|
|
|
Bool InitWindowSurface(NativeWindowType window) {
|
|
if (!window) return false;
|
|
|
|
if (!InitDisplayAndContext(EGL_WINDOW_BIT, window)) return false;
|
|
|
|
g_Surface = g_EGLFuncs.eglCreateWindowSurface(g_Display, g_Config, window, nullptr);
|
|
if (g_Surface == EGL_NO_SURFACE) return false;
|
|
|
|
if (!MakeCurrent()) return false;
|
|
|
|
ApplyRequestedSwapInterval();
|
|
PublishDefaultFramebufferDepthStencilFormat();
|
|
|
|
MGLOG_D("EGL context created successfully: display=%p, surface=%p, context=%p. window=%p", g_Display, g_Surface,
|
|
g_Context, window);
|
|
return true;
|
|
}
|
|
|
|
Bool InitPbufferSurface(EGLint width, EGLint height) {
|
|
if (width <= 0 || height <= 0) return false;
|
|
if (!InitDisplayAndContext(EGL_PBUFFER_BIT)) return false;
|
|
|
|
const EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
|
|
g_Surface = g_EGLFuncs.eglCreatePbufferSurface(g_Display, g_Config, surfaceAttribs);
|
|
if (g_Surface == EGL_NO_SURFACE) return false;
|
|
|
|
if (!MakeCurrent()) return false;
|
|
|
|
PublishDefaultFramebufferDepthStencilFormat();
|
|
|
|
MGLOG_D("EGL pbuffer context created successfully: display=%p, surface=%p, context=%p. size=%dx%d", g_Display,
|
|
g_Surface, g_Context, width, height);
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
// The single backend ES context migrates between app threads (FCL/pojav-style
|
|
// LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must
|
|
// live in ONE global slot: a per-thread flag can never be cleared on the
|
|
// LOSING thread when another thread takes (or destroys/releases) the context,
|
|
// leaving a stale "current" claim behind. A stale claim makes buffer ops issue
|
|
// GL calls that silently no-op (no context is current on that thread) while
|
|
// still updating shadow bookkeeping (bind cache, synced serials), permanently
|
|
// desynchronizing backend buffer state.
|
|
std::atomic<std::thread::id> g_backendContextOwnerThread{};
|
|
|
|
// Bumped whenever the backend ES context is destroyed; fence and
|
|
// timer-query handles created under an older generation belong to a
|
|
// dead context and must never be passed back to GL (mirrors
|
|
// BufferImpl's context tracking).
|
|
Uint g_syncContextGeneration = 1;
|
|
|
|
// One-fence-per-frame ring driving the buffer-storage pool's recycle gate.
|
|
// A buffer retired during frame N is safe to reuse once frame N's fence has
|
|
// signaled. Touched only in Present()/DestroyEGLContext() on the owning
|
|
// thread -> no lock. Each fence carries the sync generation so a dead-context
|
|
// GLsync is never polled/deleted. Depth 4 >> the 2-3 frames Adreno keeps in
|
|
// flight; wrap-before-signal only happens during a stall and just degrades to
|
|
// the allocate path.
|
|
std::atomic<Uint64> g_currentFrameSerial{0};
|
|
std::atomic<Uint64> g_completedFrameSerial{0};
|
|
constexpr int kFrameFenceRingDepth = 4;
|
|
struct FrameFence {
|
|
GLsync sync = nullptr;
|
|
Uint contextGeneration = 0;
|
|
Uint64 serial = 0;
|
|
};
|
|
FrameFence g_frameFenceRing[kFrameFenceRingDepth];
|
|
|
|
// Backend fence handle: a native ES sync plus the ES context
|
|
// generation it was created under.
|
|
struct GLESSyncObject {
|
|
GLsync esSync = nullptr;
|
|
Uint contextGeneration = 0;
|
|
};
|
|
|
|
// Backend timer-query handle: a native GL query object name plus the
|
|
// ES context generation it was created under. Stale-generation
|
|
// handles read as available with a zero result, and deleting them
|
|
// only frees the wrapper (the dead ES context already reclaimed the
|
|
// query object).
|
|
struct GLESQueryObject {
|
|
GLuint queryId = 0;
|
|
Uint contextGeneration = 0;
|
|
// Non-zero for the core (non-timer) query targets - occlusion and transform
|
|
// feedback primitives - and then holds the glBeginQuery target, which glEndQuery
|
|
// needs back. Their results are counts reachable only through the core
|
|
// glGetQueryObjectuiv getter: GL_EXT_disjoint_timer_query's 64-bit
|
|
// glGetQueryObjectui64vEXT is timer-specific and may be entirely absent on
|
|
// drivers that otherwise fully support these core ES queries.
|
|
GLenum coreTarget = 0;
|
|
};
|
|
}
|
|
|
|
// Defined next to the stamp globals below; every owner-id writer must reset the
|
|
// EGL verification stamp BEFORE publishing the new owner.
|
|
void InvalidateEglVerifiedStamp();
|
|
|
|
Bool MakeCurrent() {
|
|
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE ||
|
|
g_Context == EGL_NO_CONTEXT) {
|
|
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
|
|
return false;
|
|
}
|
|
if (!g_EGLFuncs.eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context)) {
|
|
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
|
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
|
return false;
|
|
}
|
|
InvalidateEglVerifiedStamp();
|
|
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
|
|
// The ops table may have been unregistered when a previous ES context was
|
|
// destroyed (e.g. a probe context); re-register now that GL is usable.
|
|
BufferImpl::RegisterBufferBackendOps();
|
|
// Conservatively drop the redundant-glUseProgram guard: re-issuing one bind
|
|
// after a MakeCurrent is cheaper than trusting a possibly-reset context.
|
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
|
// The GLContext becoming current may be a fresh one whose slot versions
|
|
// restarted at zero; the broadcast memo's key is only monotonic within one.
|
|
PrgramImpl::InvalidateBroadcastMemo();
|
|
BufferImpl::InvalidateIndexedBufferBindingCache();
|
|
BufferImpl::InvalidatePixelBufferBindingCaches();
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
PixelStoreImpl::InvalidatePackStateCache();
|
|
// The render-state shadow belongs in this list for the same reason as the ones above:
|
|
// it describes the real ES context, which outlives the MobileGL context that is
|
|
// becoming current. See InvalidateSyncedRenderState.
|
|
RenderStateImpl::InvalidateSyncedRenderState();
|
|
// eglSwapInterval requires a current context; a request made while none was
|
|
// current (and dropped by the driver) is retried here.
|
|
ApplyRequestedSwapInterval();
|
|
return true;
|
|
}
|
|
|
|
Bool ReleaseCurrent() {
|
|
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) {
|
|
InvalidateEglVerifiedStamp();
|
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
|
return true;
|
|
}
|
|
if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
|
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
|
MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
|
return false;
|
|
}
|
|
// Clearing the global owner works from ANY thread (a release request can
|
|
// legally arrive on a thread other than the current owner); erring towards
|
|
// "not current" only defers buffer ops, which is always safe.
|
|
InvalidateEglVerifiedStamp();
|
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
// EGL ground-truth verification stamp. glvnd's eglGetCurrentContext performs
|
|
// fork detection with a real getpid() syscall on every call, and this
|
|
// predicate sits 2-3 deep in every draw - measured at 16% of the render
|
|
// thread on a live workload. NOT thread_local although the stamp is
|
|
// per-owner-thread state: a shared-library thread_local costs a
|
|
// __tls_get_addr call per access, which itself showed at 1.5% of the draw
|
|
// loop. Plain globals are equivalent because only the thread that passes the
|
|
// owner-identity check below can ever stamp or trust them, only one thread
|
|
// can be the owner at a time, and the ONLY writers of the owner id -
|
|
// MakeCurrent/ReleaseCurrent - reset the stamps before publishing a new
|
|
// owner, so a stamp can never leak across an ownership change. Atomics
|
|
// (relaxed) because the resets may come from a non-owner thread; the
|
|
// owner-id release/acquire pairing orders them.
|
|
std::atomic<Uint64> g_eglVerifiedFrameSerial{~0ull};
|
|
std::atomic<Uint> g_eglVerifiedContextGeneration{0};
|
|
} // namespace
|
|
|
|
void InvalidateEglVerifiedStamp() {
|
|
// ~0 frame serial matches no real frame, so the next owner-thread call
|
|
// re-verifies against EGL itself.
|
|
g_eglVerifiedFrameSerial.store(~0ull, std::memory_order_relaxed);
|
|
g_eglVerifiedContextGeneration.store(0, std::memory_order_relaxed);
|
|
}
|
|
|
|
Bool IsBackendContextCurrentOnThisThread() {
|
|
if (g_Context == EGL_NO_CONTEXT) {
|
|
return false;
|
|
}
|
|
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
|
|
return false;
|
|
}
|
|
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
|
|
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
|
|
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
|
|
// cache, synced serials) still advances. Re-verify once per (frame, context
|
|
// generation) rather than per call: an external migration is caught at the
|
|
// next frame boundary instead of the next call, which recovers the
|
|
// bookkeeping just the same, without paying a syscall on every draw.
|
|
const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed);
|
|
if (g_eglVerifiedFrameSerial.load(std::memory_order_relaxed) == frameSerial &&
|
|
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
|
|
return true;
|
|
}
|
|
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
|
|
return false;
|
|
}
|
|
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
|
|
g_eglVerifiedContextGeneration.store(g_syncContextGeneration, std::memory_order_relaxed);
|
|
return true;
|
|
}
|
|
|
|
BackendSyncHandle FenceSync() {
|
|
// ES fences can only be created on the thread that owns the ES context
|
|
// (Flywheel and friends fence on the render thread, which does).
|
|
// Returning null makes the frontend fall back to an always-signaled
|
|
// sync object.
|
|
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) {
|
|
return nullptr;
|
|
}
|
|
GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
if (esSync == nullptr) {
|
|
return nullptr;
|
|
}
|
|
return new GLESSyncObject{esSync, g_syncContextGeneration};
|
|
}
|
|
|
|
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
|
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
|
if (sync == nullptr) {
|
|
return GL_ALREADY_SIGNALED;
|
|
}
|
|
// The creating ES context is gone: its GPU work either completed or
|
|
// died with the context; waiting is meaningless either way.
|
|
if (sync->contextGeneration != g_syncContextGeneration) {
|
|
return GL_ALREADY_SIGNALED;
|
|
}
|
|
// Degraded path: a thread that does not own the ES context cannot
|
|
// issue GL calls, so report signaled instead of blocking on state we
|
|
// cannot observe. Fence waits normally arrive on the render thread,
|
|
// which owns the context.
|
|
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) {
|
|
return GL_ALREADY_SIGNALED;
|
|
}
|
|
return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout);
|
|
}
|
|
|
|
void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
|
(void)flags;
|
|
(void)timeout;
|
|
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
|
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
|
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) {
|
|
return;
|
|
}
|
|
// ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED.
|
|
g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED);
|
|
}
|
|
|
|
void DeleteSync(BackendSyncHandle handle) {
|
|
auto* sync = static_cast<GLESSyncObject*>(handle);
|
|
if (sync == nullptr) {
|
|
return;
|
|
}
|
|
if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
|
|
g_GLESFuncs.glDeleteSync) {
|
|
g_GLESFuncs.glDeleteSync(sync->esSync);
|
|
}
|
|
// Otherwise the ES sync is abandoned; the ES context reclaims all of
|
|
// its sync objects when it is destroyed.
|
|
delete sync;
|
|
}
|
|
|
|
Bool GetSyncStatus(BackendSyncHandle handle) {
|
|
const auto* sync = static_cast<GLESSyncObject*>(handle);
|
|
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
|
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) {
|
|
return true;
|
|
}
|
|
GLint status = GL_SIGNALED;
|
|
GLsizei length = 0;
|
|
g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status);
|
|
return status == GL_SIGNALED;
|
|
}
|
|
|
|
// GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop
|
|
// tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF),
|
|
// GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and
|
|
// GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their
|
|
// _EXT counterparts.
|
|
|
|
Bool AreTimerQueriesSupported() {
|
|
return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries &&
|
|
g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery &&
|
|
g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT &&
|
|
g_GLESFuncs.glGetQueryObjectui64vEXT;
|
|
}
|
|
|
|
namespace {
|
|
// The entry point the resolved tier's support ships, or null when there is none.
|
|
MG_External::GLES::glTexBuffer_PTR ResolveTexBufferEntryPoint() {
|
|
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
|
|
switch (g_GLESCapabilities.TextureBufferSupport) {
|
|
case Tier::ExtensionEXT:
|
|
return g_GLESFuncs.glTexBufferEXT ? g_GLESFuncs.glTexBufferEXT : g_GLESFuncs.glTexBuffer;
|
|
case Tier::ExtensionOES:
|
|
return g_GLESFuncs.glTexBufferOES ? g_GLESFuncs.glTexBufferOES : g_GLESFuncs.glTexBuffer;
|
|
case Tier::CoreEs32:
|
|
return g_GLESFuncs.glTexBuffer;
|
|
case Tier::None:
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
MG_External::GLES::glTexBufferRange_PTR ResolveTexBufferRangeEntryPoint() {
|
|
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
|
|
switch (g_GLESCapabilities.TextureBufferSupport) {
|
|
case Tier::ExtensionEXT:
|
|
return g_GLESFuncs.glTexBufferRangeEXT ? g_GLESFuncs.glTexBufferRangeEXT
|
|
: g_GLESFuncs.glTexBufferRange;
|
|
case Tier::ExtensionOES:
|
|
return g_GLESFuncs.glTexBufferRangeOES ? g_GLESFuncs.glTexBufferRangeOES
|
|
: g_GLESFuncs.glTexBufferRange;
|
|
case Tier::CoreEs32:
|
|
return g_GLESFuncs.glTexBufferRange;
|
|
case Tier::None:
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
Bool AreBufferTexturesSupported() {
|
|
// Both halves matter. The tier is what the driver ADVERTISES, and it is only meaningful
|
|
// once the capabilities have been filled in; the resolved pointer is what MobileGL can
|
|
// actually call, through the spelling that tier's support ships. Gating on the
|
|
// unsuffixed name alone would call an entry point an EXT/OES driver never exported.
|
|
return g_GLESCapabilities.TextureBufferSupport !=
|
|
MG_External::GLESCapabilities::TextureBufferTier::None &&
|
|
ResolveTexBufferEntryPoint() != nullptr;
|
|
}
|
|
|
|
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer) {
|
|
MG_External::GLES::glTexBuffer_PTR entryPoint = ResolveTexBufferEntryPoint();
|
|
if (entryPoint == nullptr) {
|
|
return;
|
|
}
|
|
entryPoint(target, internalFormat, buffer);
|
|
}
|
|
|
|
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
|
MG_External::GLES::glTexBufferRange_PTR entryPoint = ResolveTexBufferRangeEntryPoint();
|
|
if (entryPoint == nullptr) {
|
|
return false;
|
|
}
|
|
entryPoint(target, internalFormat, buffer, offset, size);
|
|
return true;
|
|
}
|
|
|
|
const char* GetBufferTextureTierName() {
|
|
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
|
|
switch (g_GLESCapabilities.TextureBufferSupport) {
|
|
case Tier::CoreEs32:
|
|
return "core (ES 3.2)";
|
|
case Tier::ExtensionEXT:
|
|
return "GL_EXT_texture_buffer";
|
|
case Tier::ExtensionOES:
|
|
return "GL_OES_texture_buffer";
|
|
case Tier::None:
|
|
default:
|
|
return "unsupported";
|
|
}
|
|
}
|
|
|
|
BackendQueryHandle BeginTimeElapsedQuery() {
|
|
// Query objects can only be created on the thread that owns the ES
|
|
// context (MC's F3 profiler queries on the render thread, which
|
|
// does). Returning null makes the frontend fall back to an
|
|
// immediately available zero result.
|
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
|
|
return nullptr;
|
|
}
|
|
GLuint queryId = 0;
|
|
g_GLESFuncs.glGenQueries(1, &queryId);
|
|
if (queryId == 0) {
|
|
return nullptr;
|
|
}
|
|
g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId);
|
|
return new GLESQueryObject{queryId, g_syncContextGeneration};
|
|
}
|
|
|
|
void EndTimeElapsedQuery(BackendQueryHandle handle) {
|
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
|
|
return;
|
|
}
|
|
// ES tracks the active query per target, not per object, so the
|
|
// handle only guards the degraded paths above.
|
|
g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED);
|
|
}
|
|
|
|
BackendQueryHandle QueryCounterTimestamp() {
|
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
|
|
return nullptr;
|
|
}
|
|
GLuint queryId = 0;
|
|
g_GLESFuncs.glGenQueries(1, &queryId);
|
|
if (queryId == 0) {
|
|
return nullptr;
|
|
}
|
|
g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP);
|
|
return new GLESQueryObject{queryId, g_syncContextGeneration};
|
|
}
|
|
|
|
// The core (non-timer) query targets - occlusion and transform feedback primitives.
|
|
// Unlike the timer queries above these are core ES (no GL_EXT_disjoint_timer_query
|
|
// needed), so they share one begin/end pair keyed on the glBeginQuery target.
|
|
static Bool AreCoreQueriesSupported() {
|
|
return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery &&
|
|
g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv;
|
|
}
|
|
|
|
static BackendQueryHandle BeginCoreQuery(GLenum target) {
|
|
if (!IsBackendContextCurrentOnThisThread() || !AreCoreQueriesSupported()) {
|
|
return nullptr;
|
|
}
|
|
GLuint queryId = 0;
|
|
g_GLESFuncs.glGenQueries(1, &queryId);
|
|
if (queryId == 0) {
|
|
return nullptr;
|
|
}
|
|
g_GLESFuncs.glBeginQuery(target, queryId);
|
|
return new GLESQueryObject{queryId, g_syncContextGeneration, target};
|
|
}
|
|
|
|
static void EndCoreQuery(BackendQueryHandle handle) {
|
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
|
if (query == nullptr || query->coreTarget == 0 ||
|
|
query->contextGeneration != g_syncContextGeneration || !IsBackendContextCurrentOnThisThread() ||
|
|
!g_GLESFuncs.glEndQuery) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glEndQuery(query->coreTarget);
|
|
}
|
|
|
|
Bool AreOcclusionQueriesSupported() { return AreCoreQueriesSupported(); }
|
|
|
|
BackendQueryHandle BeginOcclusionQuery() {
|
|
// ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact
|
|
// GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED*
|
|
// targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation.
|
|
return BeginCoreQuery(GL_ANY_SAMPLES_PASSED);
|
|
}
|
|
|
|
void EndOcclusionQuery(BackendQueryHandle handle) { EndCoreQuery(handle); }
|
|
|
|
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED. The frontend
|
|
// otherwise counts primitives on the CPU from the draw calls, which cannot see a
|
|
// geometry shader's amplification; the real driver's counters are exact. Returning
|
|
// null keeps that CPU accounting as the fallback.
|
|
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
|
|
// GL_PRIMITIVES_GENERATED is only a legal query target from ES 3.2 on (it comes
|
|
// with geometry shaders); issuing it earlier just leaves a stray GL_INVALID_ENUM
|
|
// that some later unrelated glGetError would report as its own failure.
|
|
if (generated && g_GLESCapabilities.GLESVersion.Major * 10 + g_GLESCapabilities.GLESVersion.Minor < 32) {
|
|
return nullptr;
|
|
}
|
|
return BeginCoreQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
}
|
|
|
|
void EndXfbPrimitivesQuery(BackendQueryHandle handle) { EndCoreQuery(handle); }
|
|
|
|
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
|
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
|
// Null/stale handles report available so the frontend proceeds to
|
|
// GetQueryResult64, which finalizes them as zero. A thread that does
|
|
// not own the ES context also reports available: GetQueryResult64
|
|
// then returns false and the frontend keeps the handle for a later
|
|
// read from the owning thread.
|
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) {
|
|
return true;
|
|
}
|
|
GLuint available = GL_FALSE;
|
|
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
|
|
return available != GL_FALSE;
|
|
}
|
|
|
|
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
|
|
*outNanoseconds = 0;
|
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
|
// Null handles never had a GL query object, handles from a
|
|
// since-destroyed ES context lost theirs, and missing entry points
|
|
// can never produce a reading (belt and braces: the creators already
|
|
// require them): zero is the FINAL result in all three cases, so
|
|
// report it as produced and let the frontend cache it and release
|
|
// the handle.
|
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
|
!g_GLESFuncs.glGetQueryObjectuiv ||
|
|
(query->coreTarget == 0 && !g_GLESFuncs.glGetQueryObjectui64vEXT)) {
|
|
return true;
|
|
}
|
|
// A thread that does not own the ES context cannot issue GL calls,
|
|
// but the result still lands on the owning context eventually: report
|
|
// "not obtainable yet" so the frontend keeps the handle and a later
|
|
// availability poll / result read from the owning thread can still
|
|
// produce the real value.
|
|
if (!IsBackendContextCurrentOnThisThread()) {
|
|
return false;
|
|
}
|
|
if (wait) {
|
|
// Reading GL_QUERY_RESULT blocks in the driver until the result
|
|
// lands, but only after the commands were flushed; flush once,
|
|
// then poll availability for a bounded ~100ms before dropping to
|
|
// a glFinish as the last resort (ClientWaitSync has no polling
|
|
// loop to mirror - it delegates its timeout to the driver, which
|
|
// a query-object read cannot do).
|
|
if (g_GLESFuncs.glFlush) {
|
|
g_GLESFuncs.glFlush();
|
|
}
|
|
constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll
|
|
GLuint available = GL_FALSE;
|
|
for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) {
|
|
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
|
|
if (available == GL_FALSE) {
|
|
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
|
}
|
|
}
|
|
if (available == GL_FALSE && g_GLESFuncs.glFinish) {
|
|
g_GLESFuncs.glFinish();
|
|
}
|
|
}
|
|
// GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is
|
|
// deliberately ignored: after a disjoint event (power state change,
|
|
// context switch) the result may be garbage, which is tolerable for
|
|
// an F3 GPU% readout, and consuming the latched flag here could hide
|
|
// the event from another observer.
|
|
if (query->coreTarget != 0) {
|
|
GLuint result32 = 0;
|
|
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32);
|
|
*outNanoseconds = static_cast<Uint64>(result32);
|
|
return true;
|
|
}
|
|
GLuint64 result = 0;
|
|
g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result);
|
|
*outNanoseconds = static_cast<Uint64>(result);
|
|
return true;
|
|
}
|
|
|
|
void DeleteBackendQuery(BackendQueryHandle handle) {
|
|
auto* query = static_cast<GLESQueryObject*>(handle);
|
|
if (query == nullptr) {
|
|
return;
|
|
}
|
|
if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
|
|
g_GLESFuncs.glDeleteQueries) {
|
|
g_GLESFuncs.glDeleteQueries(1, &query->queryId);
|
|
}
|
|
// Otherwise the GL query object is abandoned; the ES context reclaims
|
|
// all of its query objects when it is destroyed.
|
|
delete query;
|
|
}
|
|
|
|
Int64 GetGpuTimestampNs() {
|
|
// Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP
|
|
// getter to fall back.
|
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() ||
|
|
!g_GLESFuncs.glGetInteger64v) {
|
|
return 0;
|
|
}
|
|
GLint64 timestamp = 0;
|
|
g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, ×tamp);
|
|
return static_cast<Int64>(timestamp);
|
|
}
|
|
|
|
Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); }
|
|
Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); }
|
|
|
|
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) {
|
|
if (CompletedFrameSerial() >= serial) return true;
|
|
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false;
|
|
// Fences signal in submission order, so the live fence with the SMALLEST
|
|
// serial at or past the target is the earliest event that proves the
|
|
// target frame retired. A recycled slot (GPU more than ring-depth frames
|
|
// behind) leaves no usable fence; report failure and let the caller pick
|
|
// its own fallback rather than draining the whole queue here.
|
|
FrameFence* best = nullptr;
|
|
for (FrameFence& slot : g_frameFenceRing) {
|
|
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
|
|
if (slot.serial < serial) continue;
|
|
if (!best || slot.serial < best->serial) best = &slot;
|
|
}
|
|
if (!best) return false;
|
|
const GLenum status =
|
|
g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
|
|
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
|
|
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
|
|
if (best->serial > completed) {
|
|
g_completedFrameSerial.store(best->serial, std::memory_order_relaxed);
|
|
}
|
|
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync);
|
|
best->sync = nullptr;
|
|
return true;
|
|
}
|
|
|
|
void Present() {
|
|
// Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush
|
|
// makes it reachable), then non-blocking-poll prior frames' fences AFTER to
|
|
// advance the completed-frame watermark that gates buffer-pool recycling.
|
|
const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync;
|
|
if (canFence) {
|
|
const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth];
|
|
if (slot.sync && slot.contextGeneration == g_syncContextGeneration && g_GLESFuncs.glDeleteSync) {
|
|
g_GLESFuncs.glDeleteSync(slot.sync);
|
|
}
|
|
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
|
|
}
|
|
|
|
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface);
|
|
|
|
if (canFence && g_GLESFuncs.glGetSynciv) {
|
|
// Fences signal in submission order within one context, so the highest
|
|
// signaled serial is a valid contiguous completion watermark.
|
|
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
|
|
for (FrameFence& slot : g_frameFenceRing) {
|
|
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
|
|
GLint status = GL_SIGNALED;
|
|
GLsizei length = 0;
|
|
g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status);
|
|
if (status == GL_SIGNALED) {
|
|
if (slot.serial > completed) completed = slot.serial;
|
|
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
|
|
slot.sync = nullptr;
|
|
}
|
|
}
|
|
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
|
|
}
|
|
|
|
// After the watermark advanced: retire grown-away ring stores and record the
|
|
// frame's ring high-water marks for slot reclamation.
|
|
BufferImpl::UboRingOnPresent();
|
|
BufferImpl::UnpackRingOnPresent();
|
|
BufferImpl::UploadRingOnPresent();
|
|
BufferImpl::TrimBufferPool();
|
|
}
|
|
|
|
void DestroyEGLContext() {
|
|
BufferImpl::OnBackendContextDestroyed();
|
|
XfbImpl::OnBackendContextDestroyed();
|
|
MultiDrawImpl::OnBackendContextDestroyed();
|
|
OnRestartSubstitutionContextDestroyed();
|
|
ScratchFBOImpl::OnBackendContextDestroyed();
|
|
ReleasePackedWordScratchTexture();
|
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
VertexArrayImpl::InvalidateVAOBindingCache();
|
|
PixelStoreImpl::InvalidatePackStateCache();
|
|
PrgramImpl::InvalidateBroadcastMemo();
|
|
// Texture ids belong to the dying context; wrappers destroyed later must
|
|
// not glDeleteTextures a recycled name in a successor context.
|
|
++g_backendContextGeneration;
|
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
|
// Outstanding fence handles now refer to a dead context; treat them as
|
|
// signaled from here on.
|
|
++g_syncContextGeneration;
|
|
// The frame-fence ring's syncs belong to the dead context too; abandon them
|
|
// (the context reclaims its syncs) and floor the completed watermark to the
|
|
// current serial so buffers retired under the old context read as GPU-idle.
|
|
for (FrameFence& slot : g_frameFenceRing) slot = {};
|
|
g_completedFrameSerial.store(g_currentFrameSerial.load(std::memory_order_relaxed),
|
|
std::memory_order_relaxed);
|
|
if (g_Display != EGL_NO_DISPLAY) {
|
|
g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
if (g_Context != EGL_NO_CONTEXT) {
|
|
g_EGLFuncs.eglDestroyContext(g_Display, g_Context);
|
|
g_Context = EGL_NO_CONTEXT;
|
|
}
|
|
if (g_Surface != EGL_NO_SURFACE) {
|
|
g_EGLFuncs.eglDestroySurface(g_Display, g_Surface);
|
|
g_Surface = EGL_NO_SURFACE;
|
|
}
|
|
g_EGLFuncs.eglTerminate(g_Display);
|
|
g_Display = EGL_NO_DISPLAY;
|
|
}
|
|
}
|
|
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|