blob: 25d97d765129f09dc0fec9df65966b577a854590 [file]
/*
* Copyright 2026 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
// Regression test for b/532941869.
//
// Imagination GL drivers can keep an internal reference to the previously bound (complete)
// framebuffer and touch it after it has been deleted, which turns into a use-after-free inside the
// driver. Chromium works around the same bug (gpu_driver_bug_list.json entry 471: "IMG drivers can
// sometimes reference previously bound complete framebuffers"). Skia's
// ensure_previous_framebuffer_not_deleted workaround does what Chromium's GLES2DecoderImpl does:
// - GrGLGpu::bindFramebuffer() binds a never-deleted 1x1 complete FBO before every framebuffer it
// binds, so a Skia FBO is never left behind as the previous binding of either target.
// - GrGLGpu::deleteFramebuffer() always rebinds FBO 0 first, so the FBO being deleted is not the
// current binding of the draw or the read target either.
//
// The invariant checked here: an FBO is never deleted while it is the current *or* the previous
// binding of GL_DRAW_FRAMEBUFFER or GL_READ_FRAMEBUFFER.
//
// GLFramebufferDeletion runs Skia-level scenarios on a GrDirectContext with
// ensure_previous_framebuffer_not_deleted forced on (on every GPU) and hooks glBindFramebuffer and
// glDeleteFramebuffers to verify the invariant on every deletion.
// Deleting a client-owned FBO while Skia still has it bound is up to the client to avoid.
#include "tests/Test.h"
#ifdef SK_GL
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPaint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSize.h"
#include "include/core/SkSurface.h"
#include "include/gpu/GpuTypes.h"
#include "include/gpu/ganesh/GrBackendSurface.h"
#include "include/gpu/ganesh/GrContextOptions.h"
#include "include/gpu/ganesh/GrDirectContext.h"
#include "include/gpu/ganesh/GrTypes.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "include/gpu/ganesh/gl/GrGLBackendSurface.h"
#include "include/gpu/ganesh/gl/GrGLDirectContext.h"
#include "include/gpu/ganesh/gl/GrGLFunctions.h"
#include "include/gpu/ganesh/gl/GrGLInterface.h"
#include "include/gpu/ganesh/gl/GrGLTypes.h"
#include "include/private/gpu/ganesh/GrTypesPriv.h"
#include "src/gpu/ganesh/GrDirectContextPriv.h"
#include "src/gpu/ganesh/GrGpu.h"
#include "src/gpu/ganesh/GrSamplerState.h"
#include "src/gpu/ganesh/GrTexture.h"
#include "src/gpu/ganesh/gl/GrGLCaps.h"
#include "src/gpu/ganesh/gl/GrGLDefines.h"
#include "src/gpu/ganesh/gl/GrGLGpu.h"
#include "src/gpu/ganesh/gl/GrGLUtil.h"
#include "tests/CtsEnforcement.h"
#include "tools/ganesh/gl/GLTestContext.h"
#include <algorithm>
#include <vector>
namespace {
// GL_READ_FRAMEBUFFER_BINDING (GL/GLES 3.0 and the framebuffer_blit extensions).
// GR_GL_FRAMEBUFFER_BINDING has the same value as GL_DRAW_FRAMEBUFFER_BINDING.
constexpr GrGLenum kReadFramebufferBinding = 0x8CAA;
constexpr int kSkiaIterations = 32;
// GrGLGpu doesn't call glCheckFramebufferStatus under TSAN (see skbug.com/40043725); neither do we.
#if defined(__has_feature)
#if __has_feature(thread_sanitizer)
constexpr bool kCanCheckFramebufferStatus = false;
#else
constexpr bool kCanCheckFramebufferStatus = true;
#endif
#else
constexpr bool kCanCheckFramebufferStatus = true;
#endif
bool has_separate_read_draw_targets(const GrGLInterface* gl) {
// GL/GLES 3.0, or one of the GLES 2.0 extensions that add GL_READ_FRAMEBUFFER and
// GL_DRAW_FRAMEBUFFER (EXT/ANGLE/NV_framebuffer_blit, APPLE_framebuffer_multisample).
return GrGLGetVersion(gl) >= GR_GL_VER(3, 0) ||
static_cast<bool>(gl->fFunctions.fBlitFramebuffer) ||
static_cast<bool>(gl->fFunctions.fResolveMultisampleFramebuffer);
}
// Makes sure TexImage2D/ReadPixels don't read from or write into a pixel buffer object.
void unbind_pixel_buffers(const GrGLInterface* gl) {
const bool hasPBOs = gl->fStandard == kGL_GrGLStandard
? GrGLGetVersion(gl) >= GR_GL_VER(2, 1)
: GrGLGetVersion(gl) >= GR_GL_VER(3, 0);
if (hasPBOs) {
GR_GL_CALL(gl, BindBuffer(GR_GL_PIXEL_PACK_BUFFER, 0));
GR_GL_CALL(gl, BindBuffer(GR_GL_PIXEL_UNPACK_BUFFER, 0));
}
}
// Creates an uninitialized size x size RGBA texture.
GrGLuint make_texture(const GrGLInterface* gl, int size) {
GrGLuint tex = 0;
GR_GL_CALL(gl, GenTextures(1, &tex));
GR_GL_CALL(gl, BindTexture(GR_GL_TEXTURE_2D, tex));
GR_GL_CALL(gl, TexParameteri(GR_GL_TEXTURE_2D, GR_GL_TEXTURE_MIN_FILTER, GR_GL_NEAREST));
GR_GL_CALL(gl, TexParameteri(GR_GL_TEXTURE_2D, GR_GL_TEXTURE_MAG_FILTER, GR_GL_NEAREST));
GR_GL_CALL(gl, TexImage2D(GR_GL_TEXTURE_2D, 0, GR_GL_RGBA, size, size, 0, GR_GL_RGBA,
GR_GL_UNSIGNED_BYTE, nullptr));
GR_GL_CALL(gl, BindTexture(GR_GL_TEXTURE_2D, 0));
return tex;
}
// Creates an FBO with 'tex' as its color attachment and leaves it bound to GL_FRAMEBUFFER.
// Returns 0 if the FBO is incomplete.
GrGLuint make_fbo(const GrGLInterface* gl, GrGLuint tex) {
GrGLuint fbo = 0;
GR_GL_CALL(gl, GenFramebuffers(1, &fbo));
GR_GL_CALL(gl, BindFramebuffer(GR_GL_FRAMEBUFFER, fbo));
GR_GL_CALL(gl, FramebufferTexture2D(GR_GL_FRAMEBUFFER, GR_GL_COLOR_ATTACHMENT0,
GR_GL_TEXTURE_2D, tex, 0));
if (kCanCheckFramebufferStatus) {
GrGLenum status;
GR_GL_CALL_RET(gl, status, CheckFramebufferStatus(GR_GL_FRAMEBUFFER));
if (status != GR_GL_FRAMEBUFFER_COMPLETE) {
GR_GL_CALL(gl, DeleteFramebuffers(1, &fbo));
return 0;
}
}
return fbo;
}
// Tracks the current and previous binding of GL_DRAW_FRAMEBUFFER and GL_READ_FRAMEBUFFER through a
// hooked GrGLInterface, and records both at every glDeleteFramebuffers call.
class FBOBindingWatcher {
public:
// A binding we know nothing about. Never matches a real FBO.
static constexpr GrGLuint kUnknownFBO = ~0u;
struct Binding {
GrGLuint fCurrent = kUnknownFBO;
GrGLuint fPrevious = kUnknownFBO;
};
struct Deletion {
GrGLuint fFBO;
Binding fDraw;
Binding fRead;
bool currentDraw() const { return fFBO == fDraw.fCurrent; }
bool currentRead() const { return fFBO == fRead.fCurrent; }
bool previousDraw() const { return fFBO == fDraw.fPrevious; }
bool previousRead() const { return fFBO == fRead.fPrevious; }
bool unsafe() const {
return this->currentDraw() || this->currentRead() || this->previousDraw() ||
this->previousRead();
}
};
// Returns a copy of 'base' that routes glBindFramebuffer and glDeleteFramebuffers through this
// watcher. The watcher must outlive any GrDirectContext created with the returned interface,
// and every framebuffer bind or delete must go through the returned interface.
sk_sp<const GrGLInterface> wrap(sk_sp<const GrGLInterface> base) {
fBase = std::move(base);
fSeparateTargets = has_separate_read_draw_targets(fBase.get());
sk_sp<GrGLInterface> hooked(new GrGLInterface(*fBase));
FBOBindingWatcher* self = this;
hooked->fFunctions.fBindFramebuffer = [self](GrGLenum target, GrGLuint fbo) {
self->onBindFramebuffer(target, fbo);
};
hooked->fFunctions.fDeleteFramebuffers = [self](GrGLsizei n, const GrGLuint* fbos) {
self->onDeleteFramebuffers(n, fbos);
};
GrGLuint draw, read;
this->queryBindings(&draw, &read);
fDraw = {draw, kUnknownFBO};
fRead = {read, kUnknownFBO};
return hooked;
}
const std::vector<Deletion>& deletions() const { return fDeletions; }
// How often the tracked bindings didn't match GL, i.e. something bound a framebuffer without
// going through the hooked interface. The results aren't reliable if this is non-zero.
int desyncCount() const { return fDesyncCount; }
private:
void queryBindings(GrGLuint* draw, GrGLuint* read) const {
GrGLint d = 0;
GrGLint r = 0;
fBase->fFunctions.fGetIntegerv(GR_GL_FRAMEBUFFER_BINDING, &d);
if (fSeparateTargets) {
fBase->fFunctions.fGetIntegerv(kReadFramebufferBinding, &r);
} else {
r = d; // Without separate targets GL_FRAMEBUFFER is both.
}
*draw = static_cast<GrGLuint>(d);
*read = static_cast<GrGLuint>(r);
}
static void Bind(Binding* binding, GrGLuint fbo) {
// Conservatively assume that a redundant bind doesn't replace the driver's previous FBO.
if (binding->fCurrent != fbo) {
binding->fPrevious = binding->fCurrent;
binding->fCurrent = fbo;
}
}
// Once 'fbo' is deleted GL rebinds 0 if it was bound, and its name can be reused.
static void Forget(Binding* binding, GrGLuint fbo) {
if (binding->fCurrent == fbo) {
binding->fCurrent = 0;
binding->fPrevious = kUnknownFBO;
} else if (binding->fPrevious == fbo) {
binding->fPrevious = kUnknownFBO;
}
}
void onBindFramebuffer(GrGLenum target, GrGLuint fbo) {
fBase->fFunctions.fBindFramebuffer(target, fbo);
if (target == GR_GL_FRAMEBUFFER || !fSeparateTargets) {
Bind(&fDraw, fbo);
Bind(&fRead, fbo);
} else if (target == GR_GL_DRAW_FRAMEBUFFER) {
Bind(&fDraw, fbo);
} else if (target == GR_GL_READ_FRAMEBUFFER) {
Bind(&fRead, fbo);
}
}
void onDeleteFramebuffers(GrGLsizei n, const GrGLuint* fbos) {
GrGLuint draw, read;
this->queryBindings(&draw, &read);
if (draw != fDraw.fCurrent) {
++fDesyncCount;
fDraw = {draw, kUnknownFBO};
}
if (read != fRead.fCurrent) {
++fDesyncCount;
fRead = {read, kUnknownFBO};
}
for (GrGLsizei i = 0; i < n; ++i) {
if (fbos[i]) { // glDeleteFramebuffers silently ignores 0.
fDeletions.push_back({fbos[i], fDraw, fRead});
}
}
fBase->fFunctions.fDeleteFramebuffers(n, fbos);
for (GrGLsizei i = 0; i < n; ++i) {
if (fbos[i]) {
Forget(&fDraw, fbos[i]);
Forget(&fRead, fbos[i]);
}
}
}
sk_sp<const GrGLInterface> fBase;
bool fSeparateTargets = false;
Binding fDraw;
Binding fRead;
std::vector<Deletion> fDeletions;
int fDesyncCount = 0;
};
SkImageInfo surface_info() {
return SkImageInfo::Make(128, 128, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
}
void draw_content(SkCanvas* canvas, int i) {
canvas->clear(SK_ColorWHITE);
SkPaint paint;
paint.setColor((i & 1) ? SK_ColorRED : SK_ColorBLUE);
canvas->drawRect(SkRect::MakeXYWH(8 + (i % 16), 8, 64, 64), paint);
}
// Scenarios get the GrDirectContext and the hooked interface for the GL calls the "client" makes.
// The client calls resetContext() after making GL calls behind Skia's back. A scenario returns
// false if it isn't supported.
using ScenarioFn = bool (*)(GrDirectContext*, const GrGLInterface* clientGL);
enum class BeforeDelete { kNothing, kResetContext, kClientBindsZero };
bool draw_flush_and_delete(GrDirectContext* dContext,
const GrGLInterface* clientGL,
BeforeDelete beforeDelete) {
for (int i = 0; i < kSkiaIterations; ++i) {
sk_sp<SkSurface> surface =
SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, surface_info());
if (!surface) {
return false;
}
draw_content(surface->getCanvas(), i);
dContext->flushAndSubmit(surface.get(), GrSyncCpu::kNo);
switch (beforeDelete) {
case BeforeDelete::kNothing:
break;
case BeforeDelete::kResetContext:
dContext->resetContext();
break;
case BeforeDelete::kClientBindsZero:
GR_GL_CALL(clientGL, BindFramebuffer(GR_GL_FRAMEBUFFER, 0));
dContext->resetContext(kRenderTarget_GrGLBackendState);
break;
}
surface.reset();
// Purges the render target: GrGLRenderTarget::onRelease() -> GrGLGpu::deleteFramebuffer().
dContext->freeGpuResources();
}
return true;
}
// The originally reported pattern: Skia deletes the FBO it last drew to. The landed workaround
// bound FBO 0 first, which still left the deleted FBO as the previous binding.
bool scenario_delete_last_drawn(GrDirectContext* dContext, const GrGLInterface* clientGL) {
return draw_flush_and_delete(dContext, clientGL, BeforeDelete::kNothing);
}
// The client calls resetContext() between the flush and the delete without touching framebuffer
// bindings (e.g. Chrome's PessimisticallyResetGrContext(), or a client that made unrelated GL
// calls). GrGLGpu::onResetContext() assumes FBO 0 is bound without binding it, so the landed
// workaround deleted the FBO while it was still bound.
bool scenario_delete_after_reset_context(GrDirectContext* dContext,
const GrGLInterface* clientGL) {
return draw_flush_and_delete(dContext, clientGL, BeforeDelete::kResetContext);
}
// The client binds FBO 0 itself (e.g. to draw to its window) and calls resetContext() before Skia
// deletes its FBO. The FBO is no longer bound, but it is the previous binding.
bool scenario_delete_after_client_unbind(GrDirectContext* dContext,
const GrGLInterface* clientGL) {
return draw_flush_and_delete(dContext, clientGL, BeforeDelete::kClientBindsZero);
}
// Scaling copies into a non-renderable texture. A renderable dst would be copied with a draw, so
// the only way GrGLGpu::onCopySurface() can do these is glBlitFramebuffer, which leaves the source
// FBO bound to GL_READ_FRAMEBUFFER (and GrGLGpu's temporary dst FBO bound to GL_DRAW_FRAMEBUFFER).
// A renderable source uses its own FBO; a non-renderable one uses GrGLGpu's temporary src FBO.
bool blit_into_non_renderable(GrDirectContext* dContext,
GrRenderable srcRenderable,
int iterations) {
// createTexture() and copySurface() have to be called through GrGpu: GrGLGpu declares a
// private createTexture() that hides the GrGpu overloads.
GrGpu* gpu = dContext->priv().getGpu();
auto glGpu = static_cast<GrGLGpu*>(gpu);
const GrBackendFormat format = GrBackendFormats::MakeGL(GR_GL_RGBA8, GR_GL_TEXTURE_2D);
const SkIRect srcRect = SkIRect::MakeWH(64, 64);
const SkIRect dstRect = SkIRect::MakeWH(32, 32);
const GrTextureType texType = GrTextureType::k2D;
const int srcSampleCnt = srcRenderable == GrRenderable::kYes ? 1 : 0;
if (!glGpu->glCaps().canCopyAsBlit(GrGLFormat::kRGBA8, /*dstSampleCnt=*/0, &texType,
GrGLFormat::kRGBA8, srcSampleCnt, &texType,
SkRect::Make(srcRect), /*srcBoundsExact=*/true,
srcRect, dstRect)) {
return false;
}
sk_sp<GrTexture> dst = gpu->createTexture(dstRect.size(), format, texType, GrRenderable::kNo,
1, skgpu::Mipmapped::kNo, skgpu::Budgeted::kNo,
GrProtected::kNo, "GLFBODeletion_BlitDst");
if (!dst) {
return false;
}
for (int i = 0; i < iterations; ++i) {
sk_sp<GrTexture> src = gpu->createTexture(srcRect.size(), format, texType, srcRenderable,
1, skgpu::Mipmapped::kNo, skgpu::Budgeted::kNo,
GrProtected::kNo, "GLFBODeletion_BlitSrc");
if (!src) {
return false;
}
if (!gpu->copySurface(dst.get(), dstRect, src.get(), srcRect,
GrSamplerState::Filter::kLinear)) {
return false;
}
src.reset();
dContext->freeGpuResources();
}
return true;
}
// A renderable source's FBO is deleted while it is still bound to GL_READ_FRAMEBUFFER, which the
// landed workaround didn't track. MSAA (non-MSRTT) snapshot copies end up in the same state.
bool scenario_delete_blit_source(GrDirectContext* dContext, const GrGLInterface*) {
return blit_into_non_renderable(dContext, GrRenderable::kYes, kSkiaIterations);
}
// GrGLGpu's temporary src FBO stays bound to GL_READ_FRAMEBUFFER until it is deleted along with the
// context (and the temporary dst FBO stays bound to GL_DRAW_FRAMEBUFFER).
bool scenario_delete_temp_fbos_on_teardown(GrDirectContext* dContext, const GrGLInterface*) {
return blit_into_non_renderable(dContext, GrRenderable::kNo, 1);
}
// Resolving an MSAA surface (without EXT_multisampled_render_to_texture) blits from its multisample
// FBO, bound to GL_READ_FRAMEBUFFER, into its resolve FBO, bound to GL_DRAW_FRAMEBUFFER. Releasing
// the surface then deletes both while they are still bound.
bool scenario_delete_msaa_resolve_fbos(GrDirectContext* dContext, const GrGLInterface*) {
const int sampleCount =
std::min(4, dContext->maxSurfaceSampleCountForColorType(kRGBA_8888_SkColorType));
if (sampleCount < 2) {
return false;
}
for (int i = 0; i < kSkiaIterations; ++i) {
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(
dContext, skgpu::Budgeted::kNo, surface_info(), sampleCount, nullptr);
if (!surface) {
return false;
}
draw_content(surface->getCanvas(), i);
// Flushing the surface resolves it.
dContext->flushAndSubmit(surface.get(), GrSyncCpu::kNo);
surface.reset();
dContext->freeGpuResources();
}
return true;
}
// The client wraps its own FBO and deletes it once Skia has moved on to another surface, so the FBO
// is no longer bound. That is only safe if Skia didn't leave it behind as the previous binding.
bool scenario_client_deletes_wrapped_fbo(GrDirectContext* dContext,
const GrGLInterface* clientGL) {
const SkImageInfo info = surface_info();
sk_sp<SkSurface> other = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, info);
if (!other) {
return false;
}
for (int i = 0; i < kSkiaIterations; ++i) {
unbind_pixel_buffers(clientGL);
GrGLuint tex = make_texture(clientGL, info.width());
GrGLuint fbo = make_fbo(clientGL, tex);
dContext->resetContext();
if (!fbo) {
GR_GL_CALL(clientGL, DeleteTextures(1, &tex));
dContext->resetContext();
return false;
}
GrGLFramebufferInfo fboInfo;
fboInfo.fFBOID = fbo;
fboInfo.fFormat = GR_GL_RGBA8;
GrBackendRenderTarget backendRT = GrBackendRenderTargets::MakeGL(
info.width(), info.height(), /*sampleCnt=*/1, /*stencilBits=*/0, fboInfo);
sk_sp<SkSurface> wrapped = SkSurfaces::WrapBackendRenderTarget(
dContext, backendRT, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType,
/*colorSpace=*/nullptr, /*surfaceProps=*/nullptr);
const bool ok = static_cast<bool>(wrapped);
if (ok) {
draw_content(wrapped->getCanvas(), i);
dContext->flushAndSubmit(wrapped.get(), GrSyncCpu::kNo);
wrapped.reset(); // Borrowed, so Skia doesn't delete the FBO.
// Skia moves on to another surface.
draw_content(other->getCanvas(), i);
dContext->flushAndSubmit(other.get(), GrSyncCpu::kNo);
}
GR_GL_CALL(clientGL, DeleteFramebuffers(1, &fbo));
GR_GL_CALL(clientGL, DeleteTextures(1, &tex));
dContext->resetContext();
if (!ok) {
return false;
}
}
return true;
}
// GrGpu's testing-only backend render target. Creating the first one probes for a compatible
// stencil format (GrGLGpu::getCompatibleStencilIndex()), which creates and deletes FBOs as well.
// The client wraps it and draws to it, then Skia deletes the FBO while it is still bound.
bool scenario_delete_testing_only_backend_rt(GrDirectContext* dContext, const GrGLInterface*) {
GrGpu* gpu = dContext->priv().getGpu();
const SkImageInfo info = surface_info();
for (int i = 0; i < kSkiaIterations; ++i) {
GrBackendRenderTarget backendRT = gpu->createTestingOnlyBackendRenderTarget(
info.dimensions(), GrColorType::kRGBA_8888);
if (!backendRT.isValid()) {
return false;
}
sk_sp<SkSurface> surface = SkSurfaces::WrapBackendRenderTarget(
dContext, backendRT, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType,
/*colorSpace=*/nullptr, /*surfaceProps=*/nullptr);
const bool ok = static_cast<bool>(surface);
if (ok) {
draw_content(surface->getCanvas(), i);
dContext->flushAndSubmit(surface.get(), GrSyncCpu::kYes);
surface.reset();
}
gpu->deleteTestingOnlyBackendRenderTarget(backendRT);
if (!ok) {
return false;
}
}
return true;
}
struct SkiaScenario {
const char* fName;
ScenarioFn fRun;
};
constexpr SkiaScenario kSkiaScenarios[] = {
{"DeleteLastDrawn", scenario_delete_last_drawn},
{"DeleteAfterResetContext", scenario_delete_after_reset_context},
{"DeleteAfterClientUnbind", scenario_delete_after_client_unbind},
{"DeleteBlitSource", scenario_delete_blit_source},
{"DeleteTempFBOsOnTeardown", scenario_delete_temp_fbos_on_teardown},
{"DeleteMSAAResolveFBOs", scenario_delete_msaa_resolve_fbos},
{"ClientDeletesWrappedFBO", scenario_client_deletes_wrapped_fbo},
{"DeleteTestingOnlyBackendRT", scenario_delete_testing_only_backend_rt},
};
// With the workaround, every deletion should see GrGLGpu's complete FBO as the previous binding of
// both targets. Checks that it is one and the same FBO, and that it really is complete: the driver
// bug is about previously bound *complete* framebuffers.
void check_previous_fbo_is_complete(skiatest::Reporter* reporter,
const char* name,
GrDirectContext* dContext,
const GrGLInterface* gl,
const FBOBindingWatcher& watcher) {
constexpr GrGLuint kUnknown = FBOBindingWatcher::kUnknownFBO;
GrGLuint previous = kUnknown;
for (const FBOBindingWatcher::Deletion& d : watcher.deletions()) {
const GrGLuint previousBindings[] = {d.fDraw.fPrevious, d.fRead.fPrevious};
for (GrGLuint p : previousBindings) {
if (p == kUnknown) {
continue;
}
if (previous != kUnknown && p != previous) {
ERRORF(reporter, "%s: expected FBO %u as the previous binding, found FBO %u", name,
previous, p);
return;
}
previous = p;
}
}
if (previous == kUnknown) {
return; // Nothing has been deleted yet; it all happens when the context is destroyed.
}
if (previous == 0) {
ERRORF(reporter, "%s: FBO 0 was the previous binding when an FBO was deleted", name);
return;
}
if (!kCanCheckFramebufferStatus) {
return;
}
// Binding 'previous' and then 0 leaves the same state GrGLGpu leaves behind after a deletion.
GR_GL_CALL(gl, BindFramebuffer(GR_GL_FRAMEBUFFER, previous));
GrGLenum status;
GR_GL_CALL_RET(gl, status, CheckFramebufferStatus(GR_GL_FRAMEBUFFER));
GR_GL_CALL(gl, BindFramebuffer(GR_GL_FRAMEBUFFER, 0));
dContext->resetContext(kRenderTarget_GrGLBackendState);
if (status != GR_GL_FRAMEBUFFER_COMPLETE) {
ERRORF(reporter, "%s: the FBO bound before every other FBO (%u) is incomplete (0x%x)", name,
previous, status);
}
}
} // anonymous namespace
DEF_GANESH_TEST_FOR_GL_CONTEXT(GLFramebufferDeletion, reporter, ctxInfo, CtsEnforcement::kNever) {
sk_gpu_test::GLTestContext* glCtx = ctxInfo.glContext();
// We share the GL context with the factory's GrDirectContext. Get it out of the way first.
ctxInfo.directContext()->flushAndSubmit(GrSyncCpu::kYes);
GrContextOptions options = ctxInfo.options();
// Enable the Imagination workaround on every GPU so the test is meaningful everywhere.
options.fDriverBugWorkarounds.ensure_previous_framebuffer_not_deleted = true;
for (const SkiaScenario& scenario : kSkiaScenarios) {
// Observes everything from context creation to destruction (which deletes GrGLGpu's
// temporary FBOs), so it must outlive the context.
FBOBindingWatcher watcher;
bool ran = false;
{
sk_sp<const GrGLInterface> gl = watcher.wrap(sk_ref_sp(glCtx->gl()));
sk_sp<GrDirectContext> dContext = GrDirectContexts::MakeGL(gl, options);
if (!dContext) {
ERRORF(reporter, "Could not create a GrDirectContext with the hooked interface");
return;
}
auto gpu = static_cast<GrGLGpu*>(dContext->priv().getGpu());
if (!gpu->glCaps().workarounds().ensure_previous_framebuffer_not_deleted) {
ERRORF(reporter, "ensure_previous_framebuffer_not_deleted did not get enabled");
return;
}
ran = scenario.fRun(dContext.get(), gl.get());
if (ran) {
check_previous_fbo_is_complete(reporter, scenario.fName, dContext.get(), gl.get(),
watcher);
}
}
if (!ran) {
INFOF(reporter, "%s: skipped (unsupported)\n", scenario.fName);
continue;
}
if (watcher.desyncCount()) {
ERRORF(reporter,
"%s: %d framebuffer binding changes bypassed the hooked interface, so the "
"results are unreliable",
scenario.fName, watcher.desyncCount());
}
int unsafe = 0;
int currentDraw = 0;
int currentRead = 0;
int previousDraw = 0;
int previousRead = 0;
const FBOBindingWatcher::Deletion* firstUnsafe = nullptr;
for (const FBOBindingWatcher::Deletion& d : watcher.deletions()) {
currentDraw += d.currentDraw() ? 1 : 0;
currentRead += d.currentRead() ? 1 : 0;
previousDraw += d.previousDraw() ? 1 : 0;
previousRead += d.previousRead() ? 1 : 0;
if (d.unsafe()) {
++unsafe;
if (!firstUnsafe) {
firstUnsafe = &d;
}
}
}
const int total = static_cast<int>(watcher.deletions().size());
INFOF(reporter,
"%s: %d FBO deletions, %d unsafe (current binding: %d draw, %d read; previous "
"binding: %d draw, %d read)\n",
scenario.fName, total, unsafe, currentDraw, currentRead, previousDraw, previousRead);
if (firstUnsafe) {
ERRORF(reporter,
"%s: ensure_previous_framebuffer_not_deleted is enabled, but %d of %d FBO "
"deletions deleted the current or previous binding. First: FBO %u, draw "
"binding %u (previous %u), read binding %u (previous %u)",
scenario.fName, unsafe, total, firstUnsafe->fFBO, firstUnsafe->fDraw.fCurrent,
firstUnsafe->fDraw.fPrevious, firstUnsafe->fRead.fCurrent,
firstUnsafe->fRead.fPrevious);
}
}
// We made GL calls behind the factory context's back.
glCtx->makeCurrent();
ctxInfo.directContext()->resetContext();
}
#endif // SK_GL