CVE-2026-95329
Overview
Background
- NPOT (non-power-of-two) texture
- a texture whose
widthorheightis not a power of two, detected here via!has_single_bit(width) || !has_single_bit(height). - Host-twiddled layout
- an internal, driver-specific memory reordering (“twiddling”) of client pixel data that some GPU drivers apply during a single-call
glTexImage2Dupload. - `use_tex_sub_image_for_client_data_npot_uploads`
- the renamed GPU driver-bug workaround (formerly
use_tex_sub_image_for_host_twiddled_npot_uploads) that splits an NPOT client-data upload into aTexImage2Dallocation followed by aTexSubImage2Ddata copy. - Client-data upload
- a texture upload sourced from caller-supplied pixel data (
args.pixels), as opposed to a null-data allocation or a GPU-to-GPU copy.
Root Cause Analysis
On affected GPUs, certain drivers mishandle a single-call glTexImage2D upload of NPOT client pixel data: while twiddling the data into their internal layout they compute an incorrect destination extent and write out of bounds. Chrome shipped the use_tex_sub_image_for_host_twiddled_npot_uploads workaround to avoid this by allocating with TexImage2D and then copying data with TexSubImage2D, but it gated the workaround behind IsHostTwiddledFormat(internal_format, args.format, args.type), restricting it to two texture formats. That predicate was too narrow: the driver’s faulty twiddling path is triggered by any NPOT client-data upload, not only the two enumerated formats.
As a result, NPOT uploads in every other format still went through the single glTexImage2D call and continued to trigger the driver OOB write.
The fix removes the IsHostTwiddledFormat check so the workaround now applies whenever width/height is NPOT, regardless of internal_format, format, or type. The workaround is correspondingly renamed to use_tex_sub_image_for_client_data_npot_uploads to reflect that it covers all client-data uploads.
TexSubImage2D workaround existed but was gated by a format predicate (IsHostTwiddledFormat) that failed to cover every format the buggy driver twiddling path affects.Attack Path
- Target an affected GPU
the victim runs Chrome on a GPU/driver combination that
gpu_driver_bug_list.jsonflags for this workaround. - Choose an unguarded format
pick a WebGL texture
internal_format/format/typethat is NOT one of the two formatsIsHostTwiddledFormatmatched, so the old workaround stays off. - Issue an NPOT client-data upload
call
texImage2Dwith caller-supplied pixels and NPOTwidth/height(!has_single_bit(width) || !has_single_bit(height)), on a non-immutable texture, driving the single-callglTexImage2Dpath. - Trigger the driver OOB write the driver’s host-twiddling logic miscomputes the destination extent and writes client-controlled bytes past the allocation.
Impact Assessment
gpu_driver_bug_list.json.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/texture_manager.cc |
modified | |
GLNPOTClientDataTextureTestgpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc |
modified | |
ifgpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc |
modified | |
TEST_Pgpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc |
modified | |
forgpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc |
modified |
Files Changed
gpu/BUILD.gngpu/command_buffer/service/texture_manager.hgpu/command_buffer/tests/gl_npot_client_data_texture_unittest.ccgpu/config/gpu_driver_bug_list.jsongpu/command_buffer/service/texture_manager.ccgpu/config/gpu_workaround_list.txt
Audit Directions
- Format-gated driver workaroundsaudit other GPU driver-bug mitigations that condition on a format/type predicate (like
IsHostTwiddledFormat); if the underlying driver defect is triggered by geometry (e.g. NPOT) rather than format, the predicate under-scopes the fix and leaves formats exposed. - Single-call vs. split uploadsreview remaining single-
glTexImage2Dclient-data upload paths intexture_manager.ccfor cases where driver-side layout conversion of caller-controlled dimensions can write out of bounds. - Workaround coverage in configcross-check
gpu_driver_bug_list.jsonandgpu_workaround_list.txtentries against the code guards they enable to confirm the code applies each workaround for the full set of conditions the driver bug requires.
Patch
From 594bffac95f78e6e73914b494359a3bbb7561826 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Mon, 14 Sep 2026 12:27:22 -0700 Subject: [PATCH] GPU: apply host-twiddled TexSubImage2D workaround to all formats. The previous use_tex_sub_image_for_host_twiddled_npot_uploads workaround is needed for more texture formats, so simply apply it to all formats on affected GPUs, and rename the workaround to use_tex_sub_image_for_client_data_npot_uploads . Bug: 559320837 Change-Id: I179c3479bad4bac06cbd1b6465b6f9229178c13d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8391507 Reviewed-by: Geoff Lang <[email protected]> Reviewed-by: Zhenyao Mo <[email protected]> Auto-Submit: Kenneth Russell <[email protected]> Commit-Queue: Kenneth Russell <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697432} --- diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc index 7032ad4..e8c8adac 100644 --- a/gpu/command_buffer/service/texture_manager.cc +++ b/gpu/command_buffer/service/texture_manager.cc @@ -432,13 +432,6 @@ uint32_t previous_size_; }; -bool IsHostTwiddledFormat(GLenum internal_format, GLenum format, GLenum type) { - return (internal_format == GL_RGB10_A2 && format == GL_RGBA && - type == GL_UNSIGNED_INT_2_10_10_10_REV) || - (internal_format == GL_SRGB8_ALPHA8 && format == GL_RGBA && - type == GL_UNSIGNED_BYTE); -} - } // namespace anonymous DecoderTextureState::DecoderTextureState( @@ -457,8 +450,8 @@ workarounds.split_level_0_pbo_full_sub_image_2d), upload_oversized_mip_levels_via_unpack_buffer( workarounds.upload_oversized_mip_levels_via_unpack_buffer), - use_tex_sub_image_for_host_twiddled_npot_uploads( - workarounds.use_tex_sub_image_for_host_twiddled_npot_uploads) {} + use_tex_sub_image_for_client_data_npot_uploads( + workarounds.use_tex_sub_image_for_client_data_npot_uploads) {} TextureManager::DestructionObserver::DestructionObserver() = default; @@ -3000,21 +2993,18 @@ uploaded = true; } - GLenum internal_format = 0; - GLenum tex_type = 0; - texture->GetLevelType(args.target, args.level, &tex_type, &internal_format); - if (uploaded) { // Upload was performed by one of the workarounds above. } else if (full_image && !texture->IsImmutable() && - !(texture_state - ->use_tex_sub_image_for_host_twiddled_npot_uploads && + !(texture_state->use_tex_sub_image_for_client_data_npot_uploads && args.command_type == DoTexSubImageArguments::CommandType::kTexSubImage2D && (!std::has_single_bit(static_cast<uint32_t>(args.width)) || - !std::has_single_bit(static_cast<uint32_t>(args.height))) && - IsHostTwiddledFormat(internal_format, args.format, args.type))) { + !std::has_single_bit(static_cast<uint32_t>(args.height))))) { TRACE_EVENT0("gpu", "FullImage"); + GLenum internal_format = 0; + GLenum tex_type = 0; + texture->GetLevelType(args.target, args.level, &tex_type, &internal_format); // NOTE: In OpenGL ES 2/3 border is always zero. If that changes we'll need // to look it up. if (args.command_type == @@ -3468,11 +3458,10 @@ AdjustTexFormat(feature_info_.get(), args.format), args.type, args.pixels); } else { - if (texture_state->use_tex_sub_image_for_host_twiddled_npot_uploads && + if (texture_state->use_tex_sub_image_for_client_data_npot_uploads && (args.pixels != nullptr || unpack_buffer_bound) && (!std::has_single_bit(static_cast<uint32_t>(args.width)) || - !std::has_single_bit(static_cast<uint32_t>(args.height))) && - IsHostTwiddledFormat(args.internal_format, args.format, args.type)) { + !std::has_single_bit(static_cast<uint32_t>(args.height)))) { glTexImage2D(args.target, args.level, AdjustTexInternalFormat(feature_info_.get(), args.internal_format, args.type), diff --git a/gpu/command_buffer/service/texture_manager.h b/gpu/command_buffer/service/texture_manager.h index dd336939..c4be683 100644 --- a/gpu/command_buffer/service/texture_manager.h +++ b/gpu/command_buffer/service/texture_manager.h @@ -712,7 +712,7 @@ bool unpack_overlapping_rows_separately_unpack_buffer; bool split_level_0_pbo_full_sub_image_2d; bool upload_oversized_mip_levels_via_unpack_buffer; - bool use_tex_sub_image_for_host_twiddled_npot_uploads; + bool use_tex_sub_image_for_client_data_npot_uploads; }; // This class keeps track of the textures and their sizes so we can do NPOT and diff --git a/gpu/config/gpu_driver_bug_list.json b/gpu/config/gpu_driver_bug_list.json index b142833..13d82259 100644 --- a/gpu/config/gpu_driver_bug_list.json +++ b/gpu/config/gpu_driver_bug_list.json @@ -3893,10 +3893,10 @@ { "id": 494, "cr_bugs": [553172761], - "description": "For texture formats which would be twiddled on the host CPU, upload via TexSubImage2D instead of TexImage2D.", + "description": "Upload non-power-of-two textures via TexSubImage2D instead of TexImage2D when uploading client-side data.", "gl_vendor": "Imagination.*", "features": [ - "use_tex_sub_image_for_host_twiddled_npot_uploads" + "use_tex_sub_image_for_client_data_npot_uploads" ] }, { diff --git a/gpu/config/gpu_workaround_list.txt b/gpu/config/gpu_workaround_list.txt index 68ffd77..e57a0c2 100644 --- a/gpu/config/gpu_workaround_list.txt +++ b/gpu/config/gpu_workaround_list.txt @@ -142,7 +142,7 @@ use_g22_topleft_color_space_for_yuv_vp use_gpu_driver_workaround_for_testing use_non_zero_size_for_client_side_stream_buffers -use_tex_sub_image_for_host_twiddled_npot_uploads +use_tex_sub_image_for_client_data_npot_uploads use_virtualized_gl_contexts validate_max_per_stage_uniform_blocks_at_compile_time wake_up_gpu_before_drawing diff --git a/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc b/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc new file mode 100644 index 0000000..7e22791 --- /dev/null +++ b/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc @@ -0,0 +1,263 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <GLES2/gl2.h> +#include <GLES2/gl2ext.h> +#include <GLES3/gl3.h> +#include <stdint.h> + +#include <vector> + +#include "gpu/command_buffer/tests/gl_manager.h" +#include "gpu/command_buffer/tests/gl_test_utils.h" +#include "gpu/config/gpu_driver_bug_workarounds.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gpu { + +namespace { + +void BlitTexture(GLuint program) { + glUseProgram(program); + const GLuint kVertexPositionAttrib = 0; + const GLfloat kQuadVertices[] = {-1.0f, -1.0f, 1.0f, -1.0f, + 1.0f, 1.0f, -1.0f, 1.0f}; + GLuint buffer_id = 0; + glGenBuffers(1, &buffer_id); + glBindBuffer(GL_ARRAY_BUFFER, buffer_id); + glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVertices), kQuadVertices, + GL_STATIC_DRAW); + + glEnableVertexAttribArray(kVertexPositionAttrib); + glVertexAttribPointer(kVertexPositionAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0); + + GLuint sampler_handle = glGetUniformLocation(program, "u_sampler"); + glUniform1i(sampler_handle, 0); + + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glDisableVertexAttribArray(kVertexPositionAttrib); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &buffer_id); +} + +} // namespace + +class GLNPOTClientDataTextureTest : public testing::TestWithParam<bool> { + protected: + void SetUp() override { + GLManager::Options options; + options.context_type = CONTEXT_TYPE_OPENGLES3; + options.size = gfx::Size(256, 256); + + GpuDriverBugWorkarounds workarounds; + workarounds.use_tex_sub_image_for_client_data_npot_uploads = GetParam(); + gl_.InitializeWithWorkarounds(options, workarounds); + + if (!IsApplicable()) { + return; + } + + gl_.MakeCurrent(); + + static const char* kVertexShader = + "attribute vec2 a_position;\n" + "varying mediump vec2 v_uv;\n" + "void main(void) {\n" + " gl_Position = vec4(a_position, 0, 1);\n" + " v_uv = 0.5 * (a_position + vec2(1, 1));\n" + "}\n"; + static const char* kFragmentShader = + "precision mediump float;\n" + "uniform sampler2D u_sampler;\n" + "varying mediump vec2 v_uv;\n" + "void main(void) {\n" + " gl_FragColor = texture2D(u_sampler, v_uv);\n" + "}\n"; + + program_ = GLTestHelper::LoadProgram(kVertexShader, kFragmentShader); + ASSERT_NE(0u, program_); + glBindAttribLocation(program_, 0, "a_position"); + glLinkProgram(program_); + + glGenFramebuffers(1, &fbo_); + } + + void TearDown() override { + if (IsApplicable()) { + if (program_) { + glDeleteProgram(program_); + } + if (fbo_) { + glDeleteFramebuffers(1, &fbo_); + } + } + gl_.Destroy(); + } + + bool IsApplicable() const { return gl_.IsInitialized(); } + + GLManager gl_; + GLuint program_ = 0; + GLuint fbo_ = 0; +}; + +INSTANTIATE_TEST_SUITE_P(GLNPOTClientDataTextureTests, + GLNPOTClientDataTextureTest, + ::testing::Bool()); + +// Test that non-power-of-two uploads of RGB10_A2 with +// UNSIGNED_INT_2_10_10_10_REV data succeed and verify texture contents. +TEST_P(GLNPOTClientDataTextureTest, RGB10A2) { + if (!IsApplicable()) { + return; + } + + constexpr GLsizei kWidth = 65; + constexpr GLsizei kHeight = 64; + + for (int iter = 0; iter < 50; ++iter) { + std::vector<uint32_t> data(kWidth * kHeight); + for (GLsizei y = 0; y < kHeight; ++y) { + for (GLsizei x = 0; x < kWidth; ++x) { + uint32_t r = ((x + iter) % 2 == 0) ? 0x3FF : 0; + uint32_t g = ((y + iter) % 2 == 0) ? 0x3FF : 0; + uint32_t b = ((x + y + iter) % 2 == 0) ? 0x3FF : 0; + uint32_t a = 3; // 1.0 in 2-bit alpha + data[y * kWidth + x] = (r & 0x3FF) | ((g & 0x3FF) << 10) | + ((b & 0x3FF) << 20) | ((a & 0x3) << 30); + } + } + + GLuint tex = 0; + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, kWidth, kHeight, 0, GL_RGBA, + GL_UNSIGNED_INT_2_10_10_10_REV, data.data()); + EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + // Verify texture contents via FBO readback + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + tex, 0); + EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE), + glCheckFramebufferStatus(GL_FRAMEBUFFER)); + + std::vector<uint8_t> readback(kWidth * kHeight * 4); + glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, + readback.data()); + EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + for (GLsizei y = 0; y < kHeight; ++y) { + for (GLsizei x = 0; x < kWidth; ++x) { + uint8_t expected_r = ((x + iter) % 2 == 0) ? 255 : 0; + uint8_t expected_g = ((y + iter) % 2 == 0) ? 255 : 0; + uint8_t expected_b = ((x + y + iter) % 2 == 0) ? 255 : 0; + uint8_t expected_a = 255; + size_t idx = (y * kWidth + x) * 4; + EXPECT_EQ(readback[idx], expected_r) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[idx + 1], expected_g) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[idx + 2], expected_b) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[idx + 3], expected_a) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + } + } + + // Also verify sampling via BlitTexture + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, 256, 256); + BlitTexture(program_); + + glDeleteTextures(1, &tex); + } +} + +// Test that non-power-of-two uploads of SRGB8_ALPHA8 with UNSIGNED_BYTE data +// succeed and verify texture contents, including non-default unpack alignment +// and skip pixels. +TEST_P(GLNPOTClientDataTextureTest, SRGB8Alpha8) { + if (!IsApplicable()) { + return; + } + + constexpr GLsizei kWidth = 255; + constexpr GLsizei kHeight = 256; + constexpr GLint kSkipPixels = 1; + constexpr GLsizei kRowLength = kWidth + kSkipPixels; + + for (int iter = 0; iter < 50; ++iter) { + std::vector<uint8_t> data(kHeight * kRowLength * 4); + for (size_t i = 0; i < data.size() / 4; ++i) { + data[i * 4] = static_cast<uint8_t>((i * 17 + iter * 3) % 256); + data[i * 4 + 1] = static_cast<uint8_t>((i * 31 + iter * 5 + 7) % 256); + data[i * 4 + 2] = static_cast<uint8_t>((i * 53 + iter * 11 + 13) % 256); + data[i * 4 + 3] = 255; + } + + GLuint tex = 0; + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 2); + glPixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, kSkipPixels); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kWidth, kHeight, 0, GL_RGBA, + GL_UNSIGNED_BYTE, data.data()); + EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + + // Verify texture contents via FBO readback + glBindFramebuffer(GL_FRAMEBUFFER, fbo_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + tex, 0); + EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE), + glCheckFramebufferStatus(GL_FRAMEBUFFER)); + + std::vector<uint8_t> readback(kWidth * kHeight * 4); + glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, + readback.data()); + EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + for (GLsizei y = 0; y < kHeight; ++y) { + for (GLsizei x = 0; x < kWidth; ++x) { + size_t src_idx = (y * kRowLength + x + kSkipPixels) * 4; + size_t dst_idx = (y * kWidth + x) * 4; + EXPECT_EQ(readback[dst_idx], data[src_idx]) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[dst_idx + 1], data[src_idx + 1]) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[dst_idx + 2], data[src_idx + 2]) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + EXPECT_EQ(readback[dst_idx + 3], data[src_idx + 3]) + << "Mismatch at (" << x << ", " << y << ") on iter " << iter; + } + } + + // Also verify sampling via BlitTexture + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, 256, 256); + BlitTexture(program_); + + glDeleteTextures(1, &tex); + } +} + ... (file diff truncated)
Regression Test / PoC
diff --git a/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc b/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc
new file mode 100644
index 0000000..7e22791
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_npot_client_data_texture_unittest.cc
@@ -0,0 +1,263 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "gpu/command_buffer/tests/gl_manager.h"
+#include "gpu/command_buffer/tests/gl_test_utils.h"
+#include "gpu/config/gpu_driver_bug_workarounds.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gpu {
+
+namespace {
+
+void BlitTexture(GLuint program) {
+ glUseProgram(program);
+ const GLuint kVertexPositionAttrib = 0;
+ const GLfloat kQuadVertices[] = {-1.0f, -1.0f, 1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f, 1.0f};
+ GLuint buffer_id = 0;
+ glGenBuffers(1, &buffer_id);
+ glBindBuffer(GL_ARRAY_BUFFER, buffer_id);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVertices), kQuadVertices,
+ GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(kVertexPositionAttrib);
+ glVertexAttribPointer(kVertexPositionAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
+
+ GLuint sampler_handle = glGetUniformLocation(program, "u_sampler");
+ glUniform1i(sampler_handle, 0);
+
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glDisableVertexAttribArray(kVertexPositionAttrib);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &buffer_id);
+}
+
+} // namespace
+
+class GLNPOTClientDataTextureTest : public testing::TestWithParam<bool> {
+ protected:
+ void SetUp() override {
+ GLManager::Options options;
+ options.context_type = CONTEXT_TYPE_OPENGLES3;
+ options.size = gfx::Size(256, 256);
+
+ GpuDriverBugWorkarounds workarounds;
+ workarounds.use_tex_sub_image_for_client_data_npot_uploads = GetParam();
+ gl_.InitializeWithWorkarounds(options, workarounds);
+
+ if (!IsApplicable()) {
+ return;
+ }
+
+ gl_.MakeCurrent();
+
+ static const char* kVertexShader =
+ "attribute vec2 a_position;\n"
+ "varying mediump vec2 v_uv;\n"
+ "void main(void) {\n"
+ " gl_Position = vec4(a_position, 0, 1);\n"
+ " v_uv = 0.5 * (a_position + vec2(1, 1));\n"
+ "}\n";
+ static const char* kFragmentShader =
+ "precision mediump float;\n"
+ "uniform sampler2D u_sampler;\n"
+ "varying mediump vec2 v_uv;\n"
+ "void main(void) {\n"
+ " gl_FragColor = texture2D(u_sampler, v_uv);\n"
+ "}\n";
+
+ program_ = GLTestHelper::LoadProgram(kVertexShader, kFragmentShader);
+ ASSERT_NE(0u, program_);
+ glBindAttribLocation(program_, 0, "a_position");
+ glLinkProgram(program_);
+
+ glGenFramebuffers(1, &fbo_);
+ }
+
+ void TearDown() override {
+ if (IsApplicable()) {
+ if (program_) {
+ glDeleteProgram(program_);
+ }
+ if (fbo_) {
+ glDeleteFramebuffers(1, &fbo_);
+ }
+ }
+ gl_.Destroy();
+ }
+
+ bool IsApplicable() const { return gl_.IsInitialized(); }
+
+ GLManager gl_;
+ GLuint program_ = 0;
+ GLuint fbo_ = 0;
+};
+
+INSTANTIATE_TEST_SUITE_P(GLNPOTClientDataTextureTests,
+ GLNPOTClientDataTextureTest,
+ ::testing::Bool());
+
+// Test that non-power-of-two uploads of RGB10_A2 with
+// UNSIGNED_INT_2_10_10_10_REV data succeed and verify texture contents.
+TEST_P(GLNPOTClientDataTextureTest, RGB10A2) {
+ if (!IsApplicable()) {
+ return;
+ }
+
+ constexpr GLsizei kWidth = 65;
+ constexpr GLsizei kHeight = 64;
+
+ for (int iter = 0; iter < 50; ++iter) {
+ std::vector<uint32_t> data(kWidth * kHeight);
+ for (GLsizei y = 0; y < kHeight; ++y) {
+ for (GLsizei x = 0; x < kWidth; ++x) {
+ uint32_t r = ((x + iter) % 2 == 0) ? 0x3FF : 0;
+ uint32_t g = ((y + iter) % 2 == 0) ? 0x3FF : 0;
+ uint32_t b = ((x + y + iter) % 2 == 0) ? 0x3FF : 0;
+ uint32_t a = 3; // 1.0 in 2-bit alpha
+ data[y * kWidth + x] = (r & 0x3FF) | ((g & 0x3FF) << 10) |
+ ((b & 0x3FF) << 20) | ((a & 0x3) << 30);
+ }
+ }
+
+ GLuint tex = 0;
+ glGenTextures(1, &tex);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, kWidth, kHeight, 0, GL_RGBA,
+ GL_UNSIGNED_INT_2_10_10_10_REV, data.data());
+ EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ // Verify texture contents via FBO readback
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ tex, 0);
+ EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+ glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+ std::vector<uint8_t> readback(kWidth * kHeight * 4);
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE,
+ readback.data());
+ EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ for (GLsizei y = 0; y < kHeight; ++y) {
+ for (GLsizei x = 0; x < kWidth; ++x) {
+ uint8_t expected_r = ((x + iter) % 2 == 0) ? 255 : 0;
+ uint8_t expected_g = ((y + iter) % 2 == 0) ? 255 : 0;
+ uint8_t expected_b = ((x + y + iter) % 2 == 0) ? 255 : 0;
+ uint8_t expected_a = 255;
+ size_t idx = (y * kWidth + x) * 4;
+ EXPECT_EQ(readback[idx], expected_r)
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[idx + 1], expected_g)
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[idx + 2], expected_b)
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[idx + 3], expected_a)
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ }
+ }
+
+ // Also verify sampling via BlitTexture
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glViewport(0, 0, 256, 256);
+ BlitTexture(program_);
+
+ glDeleteTextures(1, &tex);
+ }
+}
+
+// Test that non-power-of-two uploads of SRGB8_ALPHA8 with UNSIGNED_BYTE data
+// succeed and verify texture contents, including non-default unpack alignment
+// and skip pixels.
+TEST_P(GLNPOTClientDataTextureTest, SRGB8Alpha8) {
+ if (!IsApplicable()) {
+ return;
+ }
+
+ constexpr GLsizei kWidth = 255;
+ constexpr GLsizei kHeight = 256;
+ constexpr GLint kSkipPixels = 1;
+ constexpr GLsizei kRowLength = kWidth + kSkipPixels;
+
+ for (int iter = 0; iter < 50; ++iter) {
+ std::vector<uint8_t> data(kHeight * kRowLength * 4);
+ for (size_t i = 0; i < data.size() / 4; ++i) {
+ data[i * 4] = static_cast<uint8_t>((i * 17 + iter * 3) % 256);
+ data[i * 4 + 1] = static_cast<uint8_t>((i * 31 + iter * 5 + 7) % 256);
+ data[i * 4 + 2] = static_cast<uint8_t>((i * 53 + iter * 11 + 13) % 256);
+ data[i * 4 + 3] = 255;
+ }
+
+ GLuint tex = 0;
+ glGenTextures(1, &tex);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength);
+ glPixelStorei(GL_UNPACK_SKIP_PIXELS, kSkipPixels);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kWidth, kHeight, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, data.data());
+ EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
+
+ // Verify texture contents via FBO readback
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ tex, 0);
+ EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+ glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+ std::vector<uint8_t> readback(kWidth * kHeight * 4);
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE,
+ readback.data());
+ EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ for (GLsizei y = 0; y < kHeight; ++y) {
+ for (GLsizei x = 0; x < kWidth; ++x) {
+ size_t src_idx = (y * kRowLength + x + kSkipPixels) * 4;
+ size_t dst_idx = (y * kWidth + x) * 4;
+ EXPECT_EQ(readback[dst_idx], data[src_idx])
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[dst_idx + 1], data[src_idx + 1])
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[dst_idx + 2], data[src_idx + 2])
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ EXPECT_EQ(readback[dst_idx + 3], data[src_idx + 3])
+ << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+ }
+ }
+
+ // Also verify sampling via BlitTexture
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glViewport(0, 0, 256, 256);
+ BlitTexture(program_);
+
+ glDeleteTextures(1, &tex);
+ }
+}
+
+} // namespace gpu
diff --git a/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc b/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
deleted file mode 100644
index ea0e632..0000000
--- a/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
+++ /dev/null
@@ -1,263 +0,0 @@
-// Copyright 2026 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include <GLES2/gl2.h>
-#include <GLES2/gl2ext.h>
-#include <GLES3/gl3.h>
-#include <stdint.h>
-
-#include <vector>
-
-#include "gpu/command_buffer/tests/gl_manager.h"
-#include "gpu/command_buffer/tests/gl_test_utils.h"
-#include "gpu/config/gpu_driver_bug_workarounds.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace gpu {
-
-namespace {
-
-void BlitTexture(GLuint program) {
- glUseProgram(program);
- const GLuint kVertexPositionAttrib = 0;
- const GLfloat kQuadVertices[] = {-1.0f, -1.0f, 1.0f, -1.0f,
- 1.0f, 1.0f, -1.0f, 1.0f};
... (truncated)