| /* | |
| * 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 |