mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Perf] (MG_Backend): give DirectGLES unit bindings an epoch the sampler churn cannot fake
The texture-binding memos added earlier keyed on the frontend texture-bind generation, and 26.2-style unit switching defeats them: glBindSampler bumps the generation even when it re-binds the sampler the unit already carries, so a frame that cycles active units re-ran the full two-pass, eleven-slot alias resolution and the unbind walks on every draw. mc_sampler_churn sat at 1674 ns/op against the native driver's 239 - the worst multiplier left on this backend - with about half the time in two virtual calls per binding slot. The units now carry an epoch: a snapshot of each touched unit's slot objects and sampler object, compared by weak_ptr OWNERSHIP rather than raw pointer - a held weak_ptr pins its control block, so a freed-and-recycled object can never owner-equal its predecessor, which is the ABA hole a pointer key would have and the reason version keying was rejected (WithTemporarilyBoundNamedTexture bumps slot versions without touching the bind generation). The (context id, bind generation, high-water mark) triple gates the snapshot walk to at most once per draw; the epoch moves only when a binding really changed. Both per-draw memos key on the epoch plus the sampling-resolution generation, which carries what the epoch cannot see: a default texture's image appearing, and every completeness input. Two smaller memos ride along: the per-unit sampler-registry lookup (owner-keyed, misses never cached - the backend object may be created later in the same draw), and the pending-vertex-attribute mask, whose first version scanned all 32 slots and put +10% on the VAO-cycling case before being restricted to the program's active locations. ns per op, DriverBench on a GTX 1660 SUPER, isolated A/B, all nine cases on both backends: mc_sampler_churn 1673 -> 732, mc_use_program 4513 -> 4279, mc_state_toggle 2365 -> 2247, everything else within noise and nothing worse. 7.0x native to 3.1x on the churn case. Unit tests 421/421.
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||
#include <Config.h>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -51,6 +52,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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);
|
||||
}
|
||||
|
||||
static Bool IsDualSourceBlendFactor(BlendFactor v) {
|
||||
switch (v) {
|
||||
case BlendFactor::Src1Color:
|
||||
@@ -780,11 +789,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
||||
if (activeAttribMask == 0) return;
|
||||
|
||||
constexpr Uint32 maxVertexAttribs =
|
||||
static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
for (Uint32 location = 0; location < maxVertexAttribs; ++location) {
|
||||
if ((activeAttribMask & (1u << location)) == 0) continue;
|
||||
if (vao->GetAttribute(location).Enabled) continue;
|
||||
// Which of the program's attributes lack an enabled array cannot change without
|
||||
// the VAO's config version moving (Enable/DisableAttribute bump it), the VAO
|
||||
// changing, or the program's active mask changing, so the per-draw Enabled probes
|
||||
// reduce to three compares. The rebuild visits only the ACTIVE locations - a VAO
|
||||
// switch per draw (vanilla cycles section VAOs) pays the same probes the unmemoed
|
||||
// walk did, not a full 32-slot scan. Owner-equality, not the raw pointer: a freed
|
||||
// VAO's heap slot can be reused, and the held weak_ptr pins the control block so
|
||||
// a successor can never alias it.
|
||||
static WeakPtr<MG_State::GLState::VertexArrayObject> s_pendingMaskVao;
|
||||
static Uint32 s_pendingMaskConfigVersion = 0;
|
||||
static Uint32 s_pendingMaskActiveMask = 0;
|
||||
static Uint32 s_pendingMask = 0;
|
||||
const Uint32 configVersion = vao->GetConfigVersion();
|
||||
if (configVersion != s_pendingMaskConfigVersion || activeAttribMask != s_pendingMaskActiveMask ||
|
||||
!OwnerEquals(s_pendingMaskVao, vao)) {
|
||||
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);
|
||||
}
|
||||
s_pendingMaskVao = vao;
|
||||
s_pendingMaskConfigVersion = configVersion;
|
||||
s_pendingMaskActiveMask = activeAttribMask;
|
||||
s_pendingMask = pending;
|
||||
}
|
||||
if (s_pendingMask == 0) return;
|
||||
|
||||
for (Uint32 remaining = s_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));
|
||||
@@ -831,10 +864,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return backendObj;
|
||||
}
|
||||
|
||||
// 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
|
||||
// texture bind generation and the touched-unit high-water mark are a complete key -
|
||||
// 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.
|
||||
@@ -854,9 +969,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Vector<UnitTextureSyncEntry> g_unitTextureSyncList;
|
||||
static Bool g_unitTextureSyncListValid = false;
|
||||
static Uint64 g_unitTextureSyncListContextId = 0;
|
||||
static Uint64 g_unitTextureSyncListBindGeneration = 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
|
||||
@@ -881,12 +997,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Units past the frontend's high-water mark have provably-empty slots.
|
||||
const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
const Uint64 unitBindingsEpoch = CurrentUnitBindingsEpoch(maxTouchedUnit);
|
||||
// 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 == MG_State::pGLContext->GetTextureContextId() &&
|
||||
g_unitTextureSyncListBindGeneration == bindGeneration &&
|
||||
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
||||
g_unitTextureSyncListContextGeneration == g_textureContextGeneration) {
|
||||
g_unitTextureSyncListContextGeneration == g_textureContextGeneration &&
|
||||
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
||||
g_unitTextureSyncListSamplingGeneration == samplingGeneration) {
|
||||
for (const auto& entry : g_unitTextureSyncList) {
|
||||
entry.backend->SyncTextureParamsToBackend(*entry.slot);
|
||||
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
|
||||
@@ -908,9 +1031,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
g_unitTextureSyncListContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
g_unitTextureSyncListBindGeneration = bindGeneration;
|
||||
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
||||
g_unitTextureSyncListContextGeneration = g_textureContextGeneration;
|
||||
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
||||
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
||||
g_unitTextureSyncListValid = true;
|
||||
}
|
||||
|
||||
@@ -1831,6 +1955,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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. Deliberately
|
||||
// NOT part of the memo below: BindCurrentProgramWithResources rewrites the sampler of
|
||||
// every unit its program samples right after this runs (raw-depth-fetch substitution,
|
||||
@@ -1841,9 +1997,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
if (samplerObject) {
|
||||
auto* backendSamplerSlot = SamplerImpl::g_backendSamplerObjects.Find(samplerObject.get());
|
||||
if (backendSamplerSlot && *backendSamplerSlot) {
|
||||
(*backendSamplerSlot)->Bind(unit);
|
||||
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
|
||||
@@ -1862,10 +2017,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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;
|
||||
// * its texture bind generation - glBindTexture, glBindTextureUnit, glBindTextures,
|
||||
// glBindSampler, the delete-unbind in MarkTextureObjectForDeletion, the sampler swap in
|
||||
// TextureUnit::SetSamplerObject (which is how glDeleteSamplers unbinds), and a default
|
||||
// texture gaining or losing an image all bump it;
|
||||
// * 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
|
||||
@@ -1890,7 +2049,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool valid = false;
|
||||
Uint64 glContextId = 0;
|
||||
Int maxTouchedUnit = -1;
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint64 unitBindingsEpoch = 0;
|
||||
Uint64 samplingResolutionGeneration = 0;
|
||||
const void* program = nullptr;
|
||||
Uint64 programLifetimeId = 0;
|
||||
@@ -1919,9 +2078,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto& memo = g_resolvedTextureBindingMemo;
|
||||
const SizeT shadowBytes =
|
||||
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
|
||||
const Uint64 unitBindingsEpoch = TextureImpl::CurrentUnitBindingsEpoch(maxTouchedUnit);
|
||||
const Bool keysMatch = memo.valid && memo.glContextId == MG_State::pGLContext->GetTextureContextId() &&
|
||||
memo.maxTouchedUnit == maxTouchedUnit &&
|
||||
memo.bindGeneration == MG_State::pGLContext->GetTextureBindGeneration() &&
|
||||
memo.unitBindingsEpoch == unitBindingsEpoch &&
|
||||
memo.samplingResolutionGeneration ==
|
||||
MG_State::pGLContext->GetSamplingResolutionGeneration() &&
|
||||
memo.program == static_cast<const void*>(currentProgram.get()) &&
|
||||
@@ -1938,7 +2098,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) {
|
||||
memo.glContextId = MG_State::pGLContext->GetTextureContextId();
|
||||
memo.maxTouchedUnit = maxTouchedUnit;
|
||||
memo.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
memo.unitBindingsEpoch = unitBindingsEpoch;
|
||||
memo.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
memo.program = currentProgram.get();
|
||||
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
||||
@@ -2119,19 +2279,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GetRawDepthFetchSampler()->Bind(unit);
|
||||
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
|
||||
} else if (samplerObject) {
|
||||
auto* backendSamplerSlot =
|
||||
SamplerImpl::g_backendSamplerObjects.Find(samplerObject.get());
|
||||
auto& backendObj = backendSamplerSlot
|
||||
? *backendSamplerSlot
|
||||
: SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
|
||||
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
|
||||
if (!backendSampler) {
|
||||
auto& backendObj = SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
|
||||
if (!backendObj) {
|
||||
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
|
||||
}
|
||||
backendSampler = backendObj.get();
|
||||
}
|
||||
backendObj->SyncToBackend(samplerObject);
|
||||
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.
|
||||
backendObj->Bind(unit);
|
||||
backendSampler->Bind(unit);
|
||||
} else {
|
||||
SamplerImpl::UnbindSampler(unit);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user