Compare commits

..
8 Commits
Author SHA1 Message Date
BZLZHH 10d0441040 DirectGLES: gate the replicate blit's stencil pass on ES 3.1
Reading the stencil half of a packed depth/stencil texture goes through
GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state. On an older driver
the pname would raise GL_INVALID_ENUM and the shader would go on sampling
depth bits as if they were stencil, so decline the emulation instead.
2026-08-02 04:55:05 -04:00
BZLZHH ad03e059e0 Ask whether a colour format is renderable per target, not in general
The framebuffer-completeness check scanned every row of the backend's
format-capability cache and called the format renderable if any target
said so. That was already loose, and it broke outright once DirectGLES
started widening three-channel formats so they stay renderable as
multisample storage: the caveat capability recorded for the multisample
target made GL_RGB8_SNORM look renderable everywhere, so an ordinary 2D
GL_RGB8_SNORM texture attachment reported GL_FRAMEBUFFER_COMPLETE while
the driver's own framebuffer was INCOMPLETE_ATTACHMENT.

KHR-GL3x.packed_pixels stopped skipping those formats and read a
framebuffer that could not be read, so all 18 of its rgb8_snorm cases got
back an untouched buffer.

Pass the row the attachment actually lives in - the texture's target, or
the renderbuffer row - and consult only that one; a format is still asked
about in general when the caller has no target.
2026-08-02 04:54:48 -04:00
BZLZHH 440e569c98 DirectGLES: emulate a depth/stencil blit into a multisample framebuffer
Desktop GL replicates the source sample into every destination sample
when the read framebuffer is single-sampled and the draw framebuffer is
not. ES forbids the call outright - "an INVALID_OPERATION error is
generated if SAMPLE_BUFFERS for the draw framebuffer is greater than
zero" - so the blit did nothing at all, and every one of
KHR-GL3x.packed_depth_stencil.blit's replicate iterations verified a
destination that still held its clear values.

Emulate it by drawing a full-screen triangle into the multisample
framebuffer: every pixel is fully covered, so every sample of it receives
the same value, which is precisely the replicate rule. The source
rectangle is first copied into a scratch texture of its own format (both
sides single-sampled, which ES does allow), then depth is written through
gl_FragDepth and stencil - which has no shader output on ES - one bit
plane at a time with REPLACE and a discard for the pixels whose source
bit is clear.

The draw runs inside the caller's framebuffer, so every piece of pipeline
state it touches is read back and restored, including the per-draw-buffer
colour masks the non-indexed glColorMask does not cover: the sync layer's
shadow of the driver state has to stay true across this.

Colour replicate is not emulated (it would need a sampler variant per
component type); it now says so instead of failing silently.
2026-08-02 04:34:25 -04:00
BZLZHH cb62431299 DirectGLES: report the alpha added by the multisample widening as ONE
A three-channel format widened to four for a multisample target gains an
alpha channel the application never asked for, and it holds whatever the
draw that filled the texture happened to write there. GL says a format
without alpha reads back as 1.0, so KHR-GL33.texture_swizzle - which
fills such a texture by rendering vec4(r, g, b, 0.0) and then swizzles
red from alpha - read 0 where it expected the maximum.

Fold ONE into the texture's swizzle for exactly those textures, composed
with the swizzle the application set, so the promotion stays invisible.
2026-08-02 04:25:08 -04:00
BZLZHH 06ce55dac3 DirectGLES: keep 16-bit SNORM precision through the multisample widening
GL_RGB16_SNORM widened to GL_RGBA16F to stay renderable as multisample
storage, and a half float's 11-bit mantissa cannot hold a 16-bit
signed-normalized channel: KHR-GL33.texture_swizzle's blue channel came
back several units of 32767 away from the value the reference computes,
well outside its one-unit tolerance.

GL_EXT_render_snorm makes the signed-normalized formats colour-renderable
on ES, so widen to GL_RGBA16_SNORM instead wherever it and
EXT_texture_norm16 are both present, and only fall back to the half float
otherwise. Threaded through as its own normalize option so the capability
probe and the runtime pick the same format, the way every other
driver-dependent substitution here is decided.
2026-08-02 04:24:23 -04:00
BZLZHH 7400f46955 DirectGLES: probe format capabilities on the target ES stores them on
1D, 1D-array and rectangle textures are emulated on ES 2D and 2D-array
targets, but the capability probe kept asking the driver about the
desktop-only target itself. glTexImage2D(GL_TEXTURE_1D, ...) is not
something an ES driver has ever accepted, so those rows of the cache
stayed empty - and an empty row reads as "nothing is known", not as "the
format needs help", so no fallback format was ever selected for them.

GL_DEPTH_COMPONENT32 on a 1D texture therefore went to the driver
unchanged instead of as GL_DEPTH_COMPONENT24, and the texture ended up
with no storage (KHR-GL33.texture_swizzle format_idx_65 on both 1D
targets read the wrong value for every pixel).

Probe the ES target the texture will actually live on, while still
recording the capabilities against the target the frontend asked for.
2026-08-02 04:18:20 -04:00
BZLZHH 1679bf9a1e DirectGLES: turn off the driver's sRGB framebuffer encoding
GLES core always encodes a fragment written into an sRGB colour
attachment, and offers no switch to stop it. Desktop GL has one,
GL_FRAMEBUFFER_SRGB, and it starts out disabled - so a GL application
that never touches it expects its writes to land raw. The frontend models
exactly that (the capability reads as disabled and DirectVulkan attaches
the UNORM twin to honour it), but DirectGLES was passing the draw
straight to a driver that encodes anyway.

The value therefore came back one conversion short of the reference
wherever it was written and then read again: rendering into an sRGB
texture and fetching it in a shader decodes once but had encoded twice,
which is how KHR-GL32.texture_size_promotion read 0.0142 for
GL_SRGB8_ALPHA8 where 0.00111 was expected.

Detect GL_EXT_sRGB_write_control and sync GL_FRAMEBUFFER_SRGB from the
frontend capability alongside the other enables, starting from the
driver's enabled state so the first sync always pushes the disable down.
2026-08-02 04:10:13 -04:00
BZLZHH 8673e89b13 DirectGLES: widen three-channel formats for multisample textures
GLES has no colour-renderable three-channel format beyond RGB8, so
glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with
GL_INVALID_ENUM and the texture is left with no storage at all - every
draw into it then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read
came back zero.

The existing fallback machinery could not help: it picks one replacement
format per requested format, from the driver's capabilities, and never
re-checks that replacement against the target it is going to be used
with. GL_RGB16's fallback is GL_RGB32F, which is a perfectly legal ES
texture format and a perfectly illegal multisample storage format, and
with EXT_texture_norm16 present no fallback was selected at all.

Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied
only to multisample targets, mapping GL_RGB16 to GL_RGBA32F and the
three-channel SNORM formats to GL_RGBA16F. Widening the channel count is
safe precisely there and nowhere else: a multisample texture can never be
uploaded to, only rendered into, so no transfer path has to expand
three-channel client data, and the alpha a draw writes for a
three-channel source is already the 1.0 the frontend format implies.

The capability probe recomputes its fallback per target for the same
reason, so the probed format and the format the texture is actually
created with stay in agreement.
2026-08-02 04:10:01 -04:00
8 changed files with 30 additions and 117 deletions
+2 -10
View File
@@ -1196,16 +1196,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
// No fast path on the binding slot's version. It is a 16-bit counter that only
// ForceBindCurrentFBO ever stamps here, so the comparison was against an arbitrarily old
// snapshot and any later slot version that happened to land on it - one wrap of the
// counter, or simply enough rebinds - read as "already bound" and left the driver on a
// completely different framebuffer. KHR-GL32.packed_pixels then read its gradient back
// out of the previous subtest's framebuffer.
//
// Skipping the work is BindFramebufferId's job anyway: it shadows the driver's own
// draw/read bindings and drops the glBindFramebuffer when the target already holds the id,
// which is where the cost actually is. What is left here is one registry lookup.
if (slot.GetVersion() == FramebufferImpl::g_fboBindVersions[(SizeT)target]) return;
const auto& currentFBO = slot.GetBoundObject();
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
const auto& backendFBOIt = FramebufferImpl::g_backendFramebufferObjects.find(currentFBO.get());
@@ -1246,20 +1246,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
!it->second.storageUsageResolved;
}
Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
const auto it = m_textureResources.find(identity);
// No image yet: the first sync sizes the chain from the levels the texture already
// defines, so nothing is recreated and there is nothing to order against.
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
const TextureResource& resource = it->second;
const IntVec3 extent = {static_cast<Int>(resource.extent.width), static_cast<Int>(resource.extent.height),
static_cast<Int>(resource.depth)};
return resource.mipLevels < ComputeFullMipLevelCount(extent);
}
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
const auto it = m_textureResources.find(identity);
@@ -350,10 +350,6 @@ public:
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// The same ordering question for the other recreate-and-preserve trigger: true when this
// texture's live image carries a shorter mip chain than a full one, so defining the missing
// levels recreates it and copies the old contents forward.
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
@@ -7793,20 +7793,6 @@ void main() {
const Uint32 baseMipLevel = std::min(static_cast<Uint32>(texture->GetLevelRange().x()), currentMipLevelCount - 1);
// A texture that has only ever defined level 0 carries a single-level backing, so defining
// the rest of the chain below recreates the image and carries the old contents over with a
// copy that is submitted and waited on out of band. Anything this frame has already
// recorded into the old image is not submitted yet, so that copy would read pre-flush
// content and every generated level would descend from a stale level 0 - the same hazard
// the storage-usage upgrade flushes for before its own preserve-copy.
if (m_textureManager->NeedsMipChainGrowth(*texture) && HasPendingRecordedWork()) {
if (FlushPendingCommands()) {
// Fresh command buffer: the sampled-descriptor-set memo describes bindings that
// only existed in the retired one.
m_lastSampledSetValid = false;
}
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
-26
View File
@@ -241,32 +241,6 @@ namespace MobileGL::MG_State {
}
void GLContext::MarkTextureObjectForDeletion(Uint index) {
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
// that is currently bound acts as if FramebufferTexture* had been called with texture
// zero for every attachment point it occupied there. Framebuffers that are NOT bound
// keep the orphaned attachment, so only the bound ones are touched.
//
// Without this the framebuffer object goes on holding the deleted texture alive as its
// attachment, and a later read through that framebuffer returns the dead texture's
// contents rather than those of whatever the application put in its place - the name
// it deleted usually comes straight back from the next glGenTextures, so the two are
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
if (const auto& textureObject = m_textureState.GetTextureObject(index)) {
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
++targetIndex) {
const auto& framebuffer =
GetFramebufferBindingSlot(static_cast<FramebufferTarget>(targetIndex)).GetBoundObject();
if (!framebuffer || framebuffer->IsDefaultFramebuffer()) {
continue;
}
const auto& attachments = framebuffer->GetAllAttachmentObjects();
for (SizeT i = 0; i < attachments.size(); ++i) {
if (attachments[i].IsTexture() && attachments[i].GetTexture() == textureObject) {
framebuffer->Detach(static_cast<FramebufferAttachmentType>(i));
}
}
}
}
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
}
+19 -35
View File
@@ -223,7 +223,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("in vec3 position;"), String::npos);
EXPECT_NE(source.find("out vec2 uv;"), String::npos);
EXPECT_EQ(source.find("attribute"), String::npos);
@@ -323,7 +323,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos);
EXPECT_NE(source.find("in vec2 uv;"), String::npos);
EXPECT_NE(source.find("texture(texture0, uv)"), String::npos);
@@ -379,7 +379,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos);
EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos);
EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos);
@@ -405,7 +405,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Vertex, vertexSource);
EXPECT_EQ(vertexSource.find("#version 330 core "), 0);
EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0);
EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos);
ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
@@ -425,7 +425,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, fragmentSource);
EXPECT_EQ(fragmentSource.find("#version 330 core "), 0);
EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0);
EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos);
EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos);
@@ -483,9 +483,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
// An explicitly declared modern core version keeps its number (see "keep declared modern
// GLSL versions strict"); only the BOM goes.
EXPECT_EQ(source.find(String(inputVersion) + "\n"), 0);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
@@ -570,12 +568,10 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
const SizeT versionPos = source.find("#version 330 core ");
const SizeT versionPos = source.find("#version 330 core\n");
const SizeT outputPos = source.find("out vec4 mg_FragColor;\n");
EXPECT_NE(versionPos, String::npos);
// The normalized directive carries a marker recording that this 330 came from a legacy
// declaration, so measure the line rather than assuming its length.
EXPECT_EQ(outputPos, source.find('\n', versionPos) + 1);
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
EXPECT_NE(source.find("// #version 460 core"), String::npos);
// This #line sits ahead of the version directive, where GLSL would never have honoured it, so
// it is still dropped. Directives that follow the version line are kept - see
@@ -688,7 +684,7 @@ void main() {
}
}
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtItsDeclaredVersion) {
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 400 core
@@ -701,7 +697,7 @@ void main() {
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 400 core\n"), 0);
EXPECT_EQ(source.find("#version 460 core\n"), 0);
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
@@ -750,7 +746,7 @@ void main() {
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_EQ(source.find("#version 330 core "), 0);
EXPECT_EQ(source.find("#version 330 core\n"), 0);
EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos);
EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos);
EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos);
@@ -906,17 +902,16 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
}
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
// used to be forced to. A legacy shader using 420-era syntax without the matching #extension line
// is accepted by real drivers, so CompileShader retries the normalized source at 460 rather than
// failing. Only MobileGL's own normalization is rescued this way - an application-declared
// "#version 330" keeps strict 3.30 semantics, which is what the CTS negative-compile cases need.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenNormalizedLegacyVersionRejects420Syntax) {
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 130
String source = R"(#version 330
layout(binding = 0) uniform sampler2D InSampler;
varying vec2 texCoord;
in vec2 texCoord;
out vec4 fragColor;
void main() {
gl_FragColor = texture2D(InSampler, texCoord);
fragColor = texture(InSampler, texCoord);
})";
PreprocessShaderSource(ShaderStage::Fragment, source);
// The normal path still emits 330 - the retry must not become the default.
@@ -957,21 +952,10 @@ void main() {
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
using namespace MG_Util::ShaderTranspiler;
// Only MobileGL's own normalization is retargetable, and it is recognised by the marker the
// preprocessor leaves on the directive line - so normalize a legacy source rather than
// hand-writing the directive the marker belongs to.
String normalized = "#version 130\nvoid main() {}\n";
PreprocessShaderSource(ShaderStage::Vertex, normalized);
ASSERT_EQ(normalized.find("#version 330 core "), 0u);
String normalized = "#version 330 core\nvoid main() {}\n";
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
// An application that declared 330 itself keeps strict 3.30 semantics: raising it would
// re-legalize the CTS negative-compile cases.
String declared330 = "#version 330 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(declared330));
EXPECT_EQ(declared330.find("#version 330 core"), 0u);
// Already modern: nothing to retarget.
String modern = "#version 460 core\nvoid main() {}\n";
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
+6 -9
View File
@@ -197,15 +197,13 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
}
// The advertised target went back to 3.3 (see "restore target GL version to 3.3"); Voxy only ever
// needed the extensions, which stay advertised, so assert what the backend really reports.
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
@@ -399,14 +397,13 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
MobileGL::IntVec2(512, 512));
}
// See the DirectGLES twin above: the target version is 3.3 again, the extensions are what matter.
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensions) {
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
+3 -5
View File
@@ -1867,13 +1867,11 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D));
// Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and
// 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and
// rectangle as a plain 2D - it is single-level and already clamps, so only the
// non-normalized coordinates differ and LowerRectImagesForEssl handles those.
// 1D and 1D-array are emulated as 2D / 2D-array (MapToBackendTextureTarget), matching
// SPIRV-Cross's ES 1D-as-2D shader emission; only rectangle textures stay unsupported.
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
}
// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9);