CVE-2026-102318
Overview
Background
- Validating GLES2 decoder
- the GPU-process command decoder that interprets renderer-issued GL commands and forwards them to the real driver, maintaining a service-side mirror of GL state.
- `UNPACK_ROW_LENGTH` / `UNPACK_IMAGE_HEIGHT`
- pixel-store unpack parameters that describe source row/image stride when uploading pixel data; per OpenGL ES 3.2 sec. 8.7 they are ignored when decoding a compressed texture image.
- `PIXEL_UNPACK_BUFFER`
- a GL buffer object bound as the source of texture-upload data; when one is bound the decoder forwards the client’s unpack parameters to the real driver (see
HandlePixelStorei). - `ScopedCompressedUnpackStateScrub`
- the new RAII guard that resets the two unpack values to their ES defaults (0) around a compressed dispatch and restores the tracked values on scope exit.
Root Cause Analysis
Compressed texture uploads (DoCompressedTexImage, DoCompressedTexSubImage, and the ClearCompressedTextureLevel zero-fill paths) dispatched to the real driver while renderer-supplied UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT values were still resident in the driver, even though the spec says pixel storage modes must be ignored for compressed images. With a PIXEL_UNPACK_BUFFER bound, those client-controlled stride values were live in the driver, and some drivers erroneously consumed them during the compressed upload, computing source strides/offsets that read out of bounds relative to the bound buffer.
The fix introduces ScopedCompressedUnpackStateScrub, which — whenever a pixel unpack buffer is bound — calls ContextState::SetUnpackParametersForCompressedTexImage() to force both values to 0 before the compressed dispatch and ContextState::UpdateUnpackParameters() to restore the tracked client values afterward. It notably does not unbind the unpack buffer (unlike ScopedPixelUnpackState) because compressed uploads legitimately source from it. Conformant drivers see no behavioral change and client-visible state is preserved, while non-conformant drivers can no longer be steered by attacker-chosen stride values.
UNPACK_ROW_LENGTH/UNPACK_IMAGE_HEIGHT to 0 around each compressed dispatch and restores them after.Attack Path
- Bind a pixel unpack buffer
A renderer using a WebGL2/ES3 context binds a
GL_PIXEL_UNPACK_BUFFERso unpack parameters are forwarded to the real driver. - Set oversized unpack geometry
The renderer issues
PixelStorei(GL_UNPACK_ROW_LENGTH, ...)/UNPACK_IMAGE_HEIGHTwith large values (the test useskRowLength = 524292,kImageHeight = 16384, large enough that stride products wrap 32 bits) that remain resident in the driver. - Dispatch a compressed upload
The renderer calls a compressed texture entry point (
CompressedTexImage*/CompressedTexSubImage*); pre-patch the decoder forwards the dispatch with the client’s stride values still active. - Driver reads out of bounds A driver that (against spec) honors those unpack values for a compressed image computes a read offset/stride beyond the bound buffer, an out-of-bounds read in the GPU process. (inferred, driver-dependent)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchgpu/command_buffer/service/context_state.cc |
modified | |
ScopedCompressedUnpackStateScrubgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
BackTexturegpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
ifgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
GLES3DecoderCompressedUnpackScrubTestgpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc |
modified |
Files Changed
gpu/command_buffer/service/context_state.ccgpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.ccgpu/command_buffer/tests/compressed_texture_test.ccgpu/command_buffer/service/context_state.h
Audit Directions
- Spec-mandated state ignored in theory but resident in driverLook for GL state the spec says an operation ignores but which the service still forwards to the driver, where a nonconformant driver could act on it.
- Compressed vs. uncompressed upload asymmetryAudit compressed entry points for missing the state normalization applied to their uncompressed counterparts (
ScopedPixelUnpackState). - Client-controlled stride/geometry reaching the driverTrace renderer-supplied
PixelStoreivalues (row length, image height, skip) through buffer-bound upload paths to confirm they cannot drive OOB source reads.
Patch
From d3af0ad932cae4ac03762a381ccd02d4f50c27de Mon Sep 17 00:00:00 2001 From: Andrew Williams <[email protected]> Date: Mon, 21 Sep 2026 17:25:42 -0700 Subject: [PATCH] gpu: Ignore client unpack geometry on compressed texture uploads Per OpenGL ES 3.2, "All pixel storage modes are ignored when decoding a compressed texture image" (sec. 8.7; sec. 8.4.1 scopes unpack state to TexImage*, TexSubImage*, and ReadPixels only). With a PIXEL_UNPACK_BUFFER bound, however, the validating decoder leaves renderer-supplied UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT values resident in the real driver across compressed uploads, and some drivers consume them there, causing bad reads relative to the bound buffer. Add ScopedCompressedUnpackStateScrub, which resets the two values to the ES defaults around every compressed dispatch in the validating decoder (DoCompressedTexImage, DoCompressedTexSubImage, and the ClearCompressedTextureLevel paths, which upload CPU-sourced data while the client's values are still resident) and restores the tracked values afterwards via ContextState::UpdateUnpackParameters(). Conformant drivers cannot observe the change, and client-visible unpack state is unchanged. This matches what ANGLE has done at its compressed entry points since crrev.com/c/3296622, so the passthrough decoder already behaves this way. Bug: 562279351 Change-Id: I86cc40508da104089166de631f07c4674b586825 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8419890 Commit-Queue: Andrew Williams <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702241} --- diff --git a/gpu/command_buffer/service/context_state.cc b/gpu/command_buffer/service/context_state.cc index fa73e3ba..506783e6 100644 --- a/gpu/command_buffer/service/context_state.cc +++ b/gpu/command_buffer/service/context_state.cc @@ -713,6 +713,14 @@ } } +void ContextState::SetUnpackParametersForCompressedTexImage() const { + if (!feature_info_->IsES3Capable()) { + return; + } + api()->glPixelStoreiFn(GL_UNPACK_ROW_LENGTH, 0); + api()->glPixelStoreiFn(GL_UNPACK_IMAGE_HEIGHT, 0); +} + void ContextState::SetBoundBuffer(GLenum target, Buffer* buffer) { bool do_refcounting = feature_info_->IsWebGL2OrES3Context(); switch (target) { diff --git a/gpu/command_buffer/service/context_state.h b/gpu/command_buffer/service/context_state.h index e405ec4..9cdc54874 100644 --- a/gpu/command_buffer/service/context_state.h +++ b/gpu/command_buffer/service/context_state.h @@ -321,6 +321,13 @@ // parameters user values; otherwise, set them to 0. void UpdateUnpackParameters() const; + // Resets UNPACK_ROW_LENGTH and UNPACK_IMAGE_HEIGHT in the real driver to + // their initial values (0) for a compressed texture upload; pixel storage + // modes are ignored when decoding a compressed texture image (ES 3.2 + // sec. 8.7). No-op when the driver is not ES3-capable, like + // UpdateUnpackParameters(), which restores the tracked values afterwards. + void SetUnpackParametersForCompressedTexImage() const; + void SetMaxWindowRectangles(size_t max); size_t GetMaxWindowRectangles() const; void SetWindowRectangles(GLenum mode, diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc index 5a6cb4e..fc213cb 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc @@ -343,6 +343,28 @@ raw_ptr<ContextState> state_; }; +// Temporarily resets UNPACK_ROW_LENGTH and UNPACK_IMAGE_HEIGHT in the real +// driver to their initial values (0) around a compressed texture upload, +// restoring the tracked values when it goes out of scope. Unlike +// ScopedPixelUnpackState it does not unbind the pixel unpack buffer: +// compressed uploads legitimately source from it. See the comment in +// DoCompressedTexImage for why compressed dispatches need this. +class ScopedCompressedUnpackStateScrub { + public: + explicit ScopedCompressedUnpackStateScrub(ContextState* state); + + ScopedCompressedUnpackStateScrub(const ScopedCompressedUnpackStateScrub&) = + delete; + ScopedCompressedUnpackStateScrub& operator=( + const ScopedCompressedUnpackStateScrub&) = delete; + + ~ScopedCompressedUnpackStateScrub(); + + private: + raw_ptr<ContextState> state_; + bool active_ = false; +}; + // Encapsulates an OpenGL texture. class BackTexture { public: @@ -2851,6 +2873,33 @@ state_->RestoreUnpackState(); } +ScopedCompressedUnpackStateScrub::ScopedCompressedUnpackStateScrub( + ContextState* state) + : state_(state) { + DCHECK(state_); + // These are ES3 pixel store parameters, forwarded to the real driver only + // while a pixel unpack buffer is bound; without one the driver already + // holds their initial values. Deliberately wider than the minimum: like + // ContextState::PushTextureUnpackState() (and ANGLE's compressed entry + // points, which always use an empty PixelUnpackState), the scrub runs + // whenever a pixel unpack buffer is bound, even if the tracked values are + // already the defaults, so the guarantee does not depend on the tracked + // state mirroring the driver state. + active_ = state_->bound_pixel_unpack_buffer.get() != nullptr; + if (active_) { + state_->SetUnpackParametersForCompressedTexImage(); + } +} + +ScopedCompressedUnpackStateScrub::~ScopedCompressedUnpackStateScrub() { + // Re-applies the tracked unpack geometry (the client's values while a pixel + // unpack buffer is bound) — the same logic used everywhere else the service + // restores this state. + if (active_) { + state_->UpdateUnpackParameters(); + } +} + BackTexture::BackTexture(GLES2DecoderImpl* decoder) : memory_tracker_(decoder->memory_tracker()), bytes_allocated_(0), @@ -12646,6 +12695,10 @@ } LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER("ClearCompressedTextureLevel"); + // This zero-fill sources from CPU memory, but the client's unpack + // geometry is still resident in the real driver and must not influence a + // compressed upload; see the comment in DoCompressedTexImage. + ScopedCompressedUnpackStateScrub scrub(&state_); api()->glCompressedTexSubImage2DFn(target, level, 0, 0, width, height, format, zero.size(), zero.data()); // Some compressed formats are not supported by CompressedTexSubImage* @@ -12712,6 +12765,10 @@ } LOCAL_COPY_REAL_GL_ERRORS_TO_WRAPPER("ClearCompressedTextureLevel3D"); + // This zero-fill sources from CPU memory, but the client's unpack + // geometry is still resident in the real driver and must not influence a + // compressed upload; see the comment in DoCompressedTexImage. + ScopedCompressedUnpackStateScrub scrub(&state_); api()->glCompressedTexSubImage3DFn(target, level, 0, 0, 0, width, height, depth, format, zero.size(), zero.data()); // Some compressed formats are not supported by CompressedTexSubImage* @@ -13462,6 +13519,21 @@ format_info->decompressed_type, decompressed_data.data()); } } else { + // Per OpenGL ES 3.2, "All pixel storage modes are ignored when decoding + // a compressed texture image" (sec. 8.7; SubImage data is "interpreted + // as though ... provided to CompressedTexImage*", and sec. 8.4.1 defines + // unpack state as pertaining only to TexImage*, TexSubImage*, and + // ReadPixels), so UNPACK_ROW_LENGTH and UNPACK_IMAGE_HEIGHT must never + // influence a compressed upload. With a PIXEL_UNPACK buffer bound, + // however, the renderer-supplied values are resident in the real driver + // (see HandlePixelStorei), so they are scrubbed to their initial values + // (0, table 8.1) around the dispatch. ANGLE's frontend has applied this + // same neutralization to its compressed entry points since 2021 + // (crbug.com/1267496), so the passthrough decoder is already protected. + // The other compressed dispatch sites (DoCompressedTexSubImage and the + // ClearCompressedTextureLevel paths) reference this comment. + // https://crbug.com/562279351 + ScopedCompressedUnpackStateScrub scrub(&state_); bool reset_base_level = workarounds().reset_base_level_for_astc_image && IsASTCFormat(internal_format) && texture->base_level() != 0; @@ -13928,6 +14000,9 @@ decompressed_data.data()); } } else { + // Compressed uploads must ignore client unpack state; see the comment in + // DoCompressedTexImage. + ScopedCompressedUnpackStateScrub scrub(&state_); bool reset_base_level = workarounds().reset_base_level_for_astc_image && IsASTCFormat(format) && texture->base_level() != 0; if (reset_base_level) { diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc index 1abfc15..7d7964ee 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc @@ -1007,6 +1007,325 @@ EXPECT_EQ(GL_NO_ERROR, GetGLError()); } +// Tests for ScopedCompressedUnpackStateScrub. Per OpenGL ES 3.2, "All pixel +// storage modes are ignored when decoding a compressed texture image" +// (sec. 8.7), so the decoder resets UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT +// in the real driver around every compressed dispatch that runs while a +// pixel unpack buffer is bound, restoring the tracked values afterwards +// (crbug.com/562279351). These tests assert that bracketing at each +// compressed dispatch site, and its absence when no pixel unpack buffer is +// bound. +class GLES3DecoderCompressedUnpackScrubTest : public GLES3DecoderTest { + protected: + static constexpr GLint kLevel = 0; + static constexpr GLsizei kWidth = 4; + static constexpr GLsizei kHeight = 4; + static constexpr GLsizei kDepth = 4; + static constexpr GLint kBorder = 0; + static constexpr GLsizei kBufferSize = 16384; + // The reported tuple: large enough that a driver consuming these against + // the spec computes strides whose products wrap 32 bits. + static constexpr GLint kRowLength = 524292; + static constexpr GLint kImageHeight = 16384; + // Nonzero because the GL mock dispatches calls whose data pointer is null + // to separate *NoData mock methods, which the expectations in these tests + // would not match. + static constexpr uint32_t kPixelUnpackBufferOffset = 16; + + // Expects UNPACK_ROW_LENGTH and then UNPACK_IMAGE_HEIGHT to be applied to + // the real driver. + void ExpectUnpackGeometry(GLint row_length, GLint image_height) { + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, row_length)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, image_height)) + .Times(1) + .RetiresOnSaturation(); + } + + // Binds and sizes a pixel unpack buffer; binding one re-applies the + // tracked unpack parameters to the real driver. + void SetupPixelUnpackBuffer() { + ExpectUnpackGeometry(0, 0); + DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId); + DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize); + } + + // Sets kRowLength / kImageHeight through the command stream; with a pixel + // unpack buffer bound they are forwarded to the real driver. + void SetLargeUnpackGeometry() { + ExpectUnpackGeometry(kRowLength, kImageHeight); + cmds::PixelStorei cmd; + cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + cmd.Init(GL_UNPACK_IMAGE_HEIGHT, kImageHeight); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + } +}; + +INSTANTIATE_TEST_SUITE_P(Service, + GLES3DecoderCompressedUnpackScrubTest, + ::testing::Bool()); + +TEST_P(GLES3DecoderCompressedUnpackScrubTest, + CompressedTexImage3DWithPixelUnpackBufferScrubsUnpackState) { + const GLenum kTarget = GL_TEXTURE_2D_ARRAY; + const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC; + const GLsizei kImageSize = 32; + + DoBindTexture(kTarget, client_texture_id_, kServiceTextureId); + SetupPixelUnpackBuffer(); + SetLargeUnpackGeometry(); + + // "All pixel storage modes are ignored when decoding a compressed texture + // image" (ES 3.2 sec. 8.7): the dispatch must be bracketed by a scrub of + // both values to their initial values and a restore of the tracked values + // afterwards. + { + InSequence scrub_then_upload_then_restore; + ExpectUnpackGeometry(0, 0); + EXPECT_CALL(*gl_, + CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth, + kHeight, kDepth, kBorder, kImageSize, _)) + .Times(1) + .RetiresOnSaturation(); + ExpectUnpackGeometry(kRowLength, kImageHeight); + } + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); + + cmds::CompressedTexImage3D cmd; + cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth, + kImageSize, /*data_shm_id=*/0, kPixelUnpackBufferOffset); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); +} + +TEST_P(GLES3DecoderCompressedUnpackScrubTest, + CompressedTexImage3DWithPixelUnpackBufferScrubsDefaultUnpackState) { + const GLenum kTarget = GL_TEXTURE_2D_ARRAY; + const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC; + const GLsizei kImageSize = 32; + + DoBindTexture(kTarget, client_texture_id_, kServiceTextureId); + SetupPixelUnpackBuffer(); + + // The scrub is deliberately wider than the demonstrated need: it brackets + // the dispatch even though the tracked values are already the defaults, so + // the guarantee does not depend on the tracked state mirroring the driver + // state. + { + InSequence scrub_then_upload_then_restore; + ExpectUnpackGeometry(0, 0); + EXPECT_CALL(*gl_, + CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth, + kHeight, kDepth, kBorder, kImageSize, _)) + .Times(1) + .RetiresOnSaturation(); + ExpectUnpackGeometry(0, 0); + } + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); + + cmds::CompressedTexImage3D cmd; + cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth, + kImageSize, /*data_shm_id=*/0, kPixelUnpackBufferOffset); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); +} + +TEST_P(GLES3DecoderCompressedUnpackScrubTest, + CompressedTexImage3DWithoutPixelUnpackBufferDoesNotScrub) { + const uint32_t kBucketId = 123; + const GLenum kTarget = GL_TEXTURE_2D_ARRAY; + const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC; + const GLsizei kImageSize = 32; + CommonDecoder::Bucket* bucket = decoder_->CreateBucket(kBucketId); + ASSERT_TRUE(bucket != nullptr); + bucket->SetSize(kImageSize); + + DoBindTexture(kTarget, client_texture_id_, kServiceTextureId); + + // Without a pixel unpack buffer these values are tracked service-side but + // never forwarded to the real driver (see HandlePixelStorei), so the + // compressed dispatch below needs no scrub. StrictMock: any PixelStorei + // call in this test is a failure. + { + cmds::PixelStorei cmd; + cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + } + { + cmds::PixelStorei cmd; + cmd.Init(GL_UNPACK_IMAGE_HEIGHT, kImageHeight); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + } + + EXPECT_CALL(*gl_, + CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth, + kHeight, kDepth, kBorder, kImageSize, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); + cmds::CompressedTexImage3DBucket cmd; + cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth, + kBucketId); + EXPECT_EQ(error::kNoError, ExecuteCmd(cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); +} + +TEST_P(GLES3DecoderCompressedUnpackScrubTest, + CompressedTexImage2DWithPixelUnpackBufferScrubsUnpackState) { + const GLenum kTarget = GL_TEXTURE_2D; + const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC; + const GLsizei kImageSize = 8; + + DoBindTexture(kTarget, client_texture_id_, kServiceTextureId); + SetupPixelUnpackBuffer(); + SetLargeUnpackGeometry(); + + // The 2D compressed dispatch must be bracketed by the same scrub/restore + // as the 3D one. + { + InSequence scrub_then_upload_then_restore; + ExpectUnpackGeometry(0, 0); + EXPECT_CALL(*gl_, + CompressedTexImage2D(kTarget, kLevel, kInternalFormat, kWidth, + kHeight, kBorder, kImageSize, _)) + .Times(1) + .RetiresOnSaturation(); + ExpectUnpackGeometry(kRowLength, kImageHeight); + } + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)) + .RetiresOnSaturation(); + + cmds::CompressedTexImage2D cmd; + cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kImageSize, ... (file diff truncated)
Regression Test / PoC
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 1abfc15..7d7964ee 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -1007,6 +1007,325 @@
EXPECT_EQ(GL_NO_ERROR, GetGLError());
}
+// Tests for ScopedCompressedUnpackStateScrub. Per OpenGL ES 3.2, "All pixel
+// storage modes are ignored when decoding a compressed texture image"
+// (sec. 8.7), so the decoder resets UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT
+// in the real driver around every compressed dispatch that runs while a
+// pixel unpack buffer is bound, restoring the tracked values afterwards
+// (crbug.com/562279351). These tests assert that bracketing at each
+// compressed dispatch site, and its absence when no pixel unpack buffer is
+// bound.
+class GLES3DecoderCompressedUnpackScrubTest : public GLES3DecoderTest {
+ protected:
+ static constexpr GLint kLevel = 0;
+ static constexpr GLsizei kWidth = 4;
+ static constexpr GLsizei kHeight = 4;
+ static constexpr GLsizei kDepth = 4;
+ static constexpr GLint kBorder = 0;
+ static constexpr GLsizei kBufferSize = 16384;
+ // The reported tuple: large enough that a driver consuming these against
+ // the spec computes strides whose products wrap 32 bits.
+ static constexpr GLint kRowLength = 524292;
+ static constexpr GLint kImageHeight = 16384;
+ // Nonzero because the GL mock dispatches calls whose data pointer is null
+ // to separate *NoData mock methods, which the expectations in these tests
+ // would not match.
+ static constexpr uint32_t kPixelUnpackBufferOffset = 16;
+
+ // Expects UNPACK_ROW_LENGTH and then UNPACK_IMAGE_HEIGHT to be applied to
+ // the real driver.
+ void ExpectUnpackGeometry(GLint row_length, GLint image_height) {
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_ROW_LENGTH, row_length))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, PixelStorei(GL_UNPACK_IMAGE_HEIGHT, image_height))
+ .Times(1)
+ .RetiresOnSaturation();
+ }
+
+ // Binds and sizes a pixel unpack buffer; binding one re-applies the
+ // tracked unpack parameters to the real driver.
+ void SetupPixelUnpackBuffer() {
+ ExpectUnpackGeometry(0, 0);
+ DoBindBuffer(GL_PIXEL_UNPACK_BUFFER, client_buffer_id_, kServiceBufferId);
+ DoBufferData(GL_PIXEL_UNPACK_BUFFER, kBufferSize);
+ }
+
+ // Sets kRowLength / kImageHeight through the command stream; with a pixel
+ // unpack buffer bound they are forwarded to the real driver.
+ void SetLargeUnpackGeometry() {
+ ExpectUnpackGeometry(kRowLength, kImageHeight);
+ cmds::PixelStorei cmd;
+ cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ cmd.Init(GL_UNPACK_IMAGE_HEIGHT, kImageHeight);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ }
+};
+
+INSTANTIATE_TEST_SUITE_P(Service,
+ GLES3DecoderCompressedUnpackScrubTest,
+ ::testing::Bool());
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexImage3DWithPixelUnpackBufferScrubsUnpackState) {
+ const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+ const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC;
+ const GLsizei kImageSize = 32;
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+ SetupPixelUnpackBuffer();
+ SetLargeUnpackGeometry();
+
+ // "All pixel storage modes are ignored when decoding a compressed texture
+ // image" (ES 3.2 sec. 8.7): the dispatch must be bracketed by a scrub of
+ // both values to their initial values and a restore of the tracked values
+ // afterwards.
+ {
+ InSequence scrub_then_upload_then_restore;
+ ExpectUnpackGeometry(0, 0);
+ EXPECT_CALL(*gl_,
+ CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth,
+ kHeight, kDepth, kBorder, kImageSize, _))
+ .Times(1)
+ .RetiresOnSaturation();
+ ExpectUnpackGeometry(kRowLength, kImageHeight);
+ }
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
+
+ cmds::CompressedTexImage3D cmd;
+ cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth,
+ kImageSize, /*data_shm_id=*/0, kPixelUnpackBufferOffset);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexImage3DWithPixelUnpackBufferScrubsDefaultUnpackState) {
+ const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+ const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC;
+ const GLsizei kImageSize = 32;
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+ SetupPixelUnpackBuffer();
+
+ // The scrub is deliberately wider than the demonstrated need: it brackets
+ // the dispatch even though the tracked values are already the defaults, so
+ // the guarantee does not depend on the tracked state mirroring the driver
+ // state.
+ {
+ InSequence scrub_then_upload_then_restore;
+ ExpectUnpackGeometry(0, 0);
+ EXPECT_CALL(*gl_,
+ CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth,
+ kHeight, kDepth, kBorder, kImageSize, _))
+ .Times(1)
+ .RetiresOnSaturation();
+ ExpectUnpackGeometry(0, 0);
+ }
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
+
+ cmds::CompressedTexImage3D cmd;
+ cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth,
+ kImageSize, /*data_shm_id=*/0, kPixelUnpackBufferOffset);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexImage3DWithoutPixelUnpackBufferDoesNotScrub) {
+ const uint32_t kBucketId = 123;
+ const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+ const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC;
+ const GLsizei kImageSize = 32;
+ CommonDecoder::Bucket* bucket = decoder_->CreateBucket(kBucketId);
+ ASSERT_TRUE(bucket != nullptr);
+ bucket->SetSize(kImageSize);
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+
+ // Without a pixel unpack buffer these values are tracked service-side but
+ // never forwarded to the real driver (see HandlePixelStorei), so the
+ // compressed dispatch below needs no scrub. StrictMock: any PixelStorei
+ // call in this test is a failure.
+ {
+ cmds::PixelStorei cmd;
+ cmd.Init(GL_UNPACK_ROW_LENGTH, kRowLength);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ }
+ {
+ cmds::PixelStorei cmd;
+ cmd.Init(GL_UNPACK_IMAGE_HEIGHT, kImageHeight);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ }
+
+ EXPECT_CALL(*gl_,
+ CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth,
+ kHeight, kDepth, kBorder, kImageSize, _))
+ .Times(1)
+ .RetiresOnSaturation();
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
+ cmds::CompressedTexImage3DBucket cmd;
+ cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth,
+ kBucketId);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexImage2DWithPixelUnpackBufferScrubsUnpackState) {
+ const GLenum kTarget = GL_TEXTURE_2D;
+ const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC;
+ const GLsizei kImageSize = 8;
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+ SetupPixelUnpackBuffer();
+ SetLargeUnpackGeometry();
+
+ // The 2D compressed dispatch must be bracketed by the same scrub/restore
+ // as the 3D one.
+ {
+ InSequence scrub_then_upload_then_restore;
+ ExpectUnpackGeometry(0, 0);
+ EXPECT_CALL(*gl_,
+ CompressedTexImage2D(kTarget, kLevel, kInternalFormat, kWidth,
+ kHeight, kBorder, kImageSize, _))
+ .Times(1)
+ .RetiresOnSaturation();
+ ExpectUnpackGeometry(kRowLength, kImageHeight);
+ }
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR))
+ .RetiresOnSaturation();
+
+ cmds::CompressedTexImage2D cmd;
+ cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kImageSize,
+ /*data_shm_id=*/0, kPixelUnpackBufferOffset);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexSubImage3DWithPixelUnpackBufferScrubsUnpackState) {
+ const uint32_t kBucketId = 123;
+ const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+ const GLenum kInternalFormat = GL_COMPRESSED_R11_EAC;
+ const GLsizei kImageSize = 32;
+
+ EXPECT_CALL(*gl_, GetError()).WillRepeatedly(Return(GL_NO_ERROR));
+
+ CommonDecoder::Bucket* bucket = decoder_->CreateBucket(kBucketId);
+ ASSERT_TRUE(bucket != nullptr);
+ bucket->SetSize(kImageSize);
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+
+ // Define (and clear) the level with a bucket upload before any PBO exists.
+ EXPECT_CALL(*gl_,
+ CompressedTexImage3D(kTarget, kLevel, kInternalFormat, kWidth,
+ kHeight, kDepth, kBorder, kImageSize, _))
+ .Times(1)
+ .RetiresOnSaturation();
+ {
+ cmds::CompressedTexImage3DBucket tex_cmd;
+ tex_cmd.Init(kTarget, kLevel, kInternalFormat, kWidth, kHeight, kDepth,
+ kBucketId);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(tex_cmd));
+ }
+
+ SetupPixelUnpackBuffer();
+ SetLargeUnpackGeometry();
+
+ // Full-level sub-upload from the pixel unpack buffer (skips the clear
+ // path): the dispatch must be bracketed by scrub and restore.
+ {
+ InSequence scrub_then_upload_then_restore;
+ ExpectUnpackGeometry(0, 0);
+ EXPECT_CALL(*gl_, CompressedTexSubImage3DWithData(
+ kTarget, kLevel, 0, 0, 0, kWidth, kHeight, kDepth,
+ kInternalFormat, kImageSize))
+ .Times(1)
+ .RetiresOnSaturation();
+ ExpectUnpackGeometry(kRowLength, kImageHeight);
+ }
+
+ cmds::CompressedTexSubImage3D cmd;
+ cmd.Init(kTarget, kLevel, /*xoffset=*/0, /*yoffset=*/0, /*zoffset=*/0, kWidth,
+ kHeight, kDepth, kInternalFormat, kImageSize,
+ /*data_shm_id=*/0, kPixelUnpackBufferOffset);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
+TEST_P(GLES3DecoderCompressedUnpackScrubTest,
+ CompressedTexSubImage3DClearPathScrubsUnpackState) {
+ const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+ const GLenum kInternalFormat = GL_COMPRESSED_RGB8_ETC2;
+ const GLsizei kLevelImageSize = 32; // whole level, zero-filled by the clear
+ const GLsizei kSubImageSize = 8; // one 4x4 slice
+
+ EXPECT_CALL(*gl_, GetError()).WillRepeatedly(Return(GL_NO_ERROR));
+
+ DoBindTexture(kTarget, client_texture_id_, kServiceTextureId);
+ SetupPixelUnpackBuffer();
+ SetLargeUnpackGeometry();
+
+ // Immutable compressed storage: the level starts uncleared.
+ EXPECT_CALL(
+ *gl_, TexStorage3D(kTarget, 1, kInternalFormat, kWidth, kHeight, kDepth))
+ .Times(1)
+ .RetiresOnSaturation();
+ {
+ cmds::TexStorage3D cmd;
+ cmd.Init(kTarget, /*levels=*/1, kInternalFormat, kWidth, kHeight, kDepth);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(cmd));
+ }
+
+ // The partial sub-upload first zero-fills the uncleared level
+ // (ClearCompressedTextureLevel3D, which unbinds/rebinds the pixel unpack
+ // buffer around a CPU-sourced upload) and then performs the caller's
+ // upload. Both compressed dispatches must be bracketed by the scrub — the
+ // client's values are resident in the driver either way.
+ EXPECT_CALL(*gl_, BindBuffer(GL_PIXEL_UNPACK_BUFFER, _)).Times(AnyNumber());
+ EXPECT_CALL(*gl_, BindTexture(_, _)).Times(AnyNumber());
+ {
... (truncated)