mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
345 lines
22 KiB
C++
345 lines
22 KiB
C++
// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.h
|
|
// Copyright (c) 2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
|
|
|
namespace MobileGL::MG_Util::SelfTest {
|
|
// ===================== KNOWN DRIVER BUGS =====================
|
|
//
|
|
// THIS IS THE DESIGNATED HOME FOR DRIVER-CAPABILITY LIES.
|
|
//
|
|
// The rest of the POST suite answers a different question: does the extension exist, and
|
|
// does a simple probe show it working. The entries here are not extension questions at
|
|
// all - they are CORE functionality that a driver advertises, accepts without error, and
|
|
// then does not perform. Nothing in an extension string or a limit query says so, which
|
|
// is exactly why each one needs its own executable probe.
|
|
//
|
|
// The inventory comes from CAMPAIGN FINDINGS, not from anything the driver reports.
|
|
//
|
|
// EVERY PROBE MUST CARRY A CONTROL. The geometry entry below is why the rule is written
|
|
// down: the same defect was first characterised as "this driver drops all geometry-stage
|
|
// storage-buffer writes", which would have justified withdrawing
|
|
// GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS entirely. A control showed geometry-stage writes
|
|
// land perfectly well when they precede EmitVertex(), so the limit is not a lie and
|
|
// withdrawing it would have broken shaders that work today. A probe without a control
|
|
// measures a symptom and invites exactly that over-correction.
|
|
//
|
|
// ADDING A SIBLING IS ONE FUNCTION: write an `Optional<DriverBugFinding> ProbeXxx(gl)`
|
|
// that returns nullopt when the driver is not affected, and add it to the table in
|
|
// CollectGlesKnownDriverBugs().
|
|
|
|
// What MobileGL can do about a bug this device HAS. There is deliberately no "not
|
|
// affected" member: a driver that passes the probe produces no finding at all, so the
|
|
// report only ever lists bugs actually present on this device.
|
|
enum class DriverBugVerdict : Uint8 {
|
|
// A MobileGL quirk repairs or substitutes for the defect and the application sees
|
|
// correct behaviour.
|
|
Fixed,
|
|
// There is no substitute. `detail` says what MobileGL does defensively instead, and
|
|
// what an application can still rely on.
|
|
Unfixable,
|
|
};
|
|
|
|
struct DriverBugFinding {
|
|
// Short name of the bug, not of the feature.
|
|
String name;
|
|
DriverBugVerdict verdict = DriverBugVerdict::Unfixable;
|
|
// One line: what the driver does wrong, and what MobileGL does about it.
|
|
String detail;
|
|
};
|
|
|
|
// What the located-interface-block probe measured.
|
|
struct LocatedIoBlockMeasurement {
|
|
// The driver delivers nothing through an inter-stage interface block that carries an
|
|
// explicit layout(location=) once a geometry stage is in the pipeline. The only field
|
|
// any caller's behaviour depends on.
|
|
Bool detected = false;
|
|
// ...and it does the same WITHOUT a geometry stage, i.e. between a vertex and a
|
|
// fragment stage. False on the device this was characterised on, and reported because
|
|
// DirectGLES's repair is scoped to tessellation/geometry programs: a driver that
|
|
// answered true here would be losing block payloads the repair does not reach.
|
|
Bool alsoAffectsVertexToFragment = false;
|
|
};
|
|
|
|
// Draws one full-viewport triangle through VS+GS+FS whose two interface blocks carry an
|
|
// explicit layout(location = 0), and reports whether the payload the vertex stage wrote
|
|
// reached the fragment stage.
|
|
//
|
|
// The Mali-G1-Ultra ES driver (r54p1) delivers ZEROES: the stages compile, the program
|
|
// links with an empty info log, the draw runs without error, and the block is empty. It is
|
|
// the whole of the KHR-GLxx.shading_language_420pack interface-block group's failures on
|
|
// that device, and of a further 21 tessellation and geometry bodies beside it.
|
|
//
|
|
// TWO CONTROLS, and the first is why this is a LOCATION finding rather than a block one:
|
|
// (1) the identical three-stage program with the qualifier removed from both blocks must
|
|
// deliver its payload - without that, "this driver cannot carry an interface block through
|
|
// a geometry stage" would be the claim, which is false and would justify flattening every
|
|
// block on the device; and (2) a two-stage vertex-to-fragment program with a LOCATED block
|
|
// is measured separately, because that one works on the affected driver and is what scopes
|
|
// the repair to programs with a tessellation or geometry stage.
|
|
//
|
|
// Returns `detected` false when an entry point is missing, when the driver has no geometry
|
|
// stage, or when the unlocated control fails - an inconclusive probe must never be reported
|
|
// as a bug, and must never arm the repair. Restores every piece of GL state it touches.
|
|
LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeLocatedIoBlocksLosePayload(), evaluated at most once per process.
|
|
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the
|
|
// copy landed where it was asked to. Returns true only when the destination layer is ignored
|
|
// while the control lands correctly.
|
|
//
|
|
// Adreno 830 writes to layer 0 whatever layer the DRAW framebuffer's
|
|
// glFramebufferTextureLayer attachment names, for colour and depth alike, and raises no
|
|
// error. Everything else about the layer works on the same driver, which is what makes this
|
|
// a blit defect rather than a layered-attachment one.
|
|
//
|
|
// THE CONTROL is the same blit onto destination layer 0. It passes on every implementation
|
|
// that can blit between array layers at all, and because the value it looks for exists only
|
|
// on the SOURCE's layer 1 it also proves the source layer is honoured - so a driver with no
|
|
// working glFramebufferTextureLayer reaches no verdict instead of being reported as this.
|
|
//
|
|
// Returns false when an entry point is missing, when the probe's own framebuffers come back
|
|
// incomplete, or when the control fails. Restores every piece of GL state it touches.
|
|
Bool ProbeBlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeBlitIgnoresDestinationArrayLayer(), evaluated at most once per process.
|
|
Bool BlitIgnoresDestinationArrayLayer(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// What the vertex-input location probe measured. The ceiling is reported rather than
|
|
// hard-coded: it is a driver property, and a clamp derived from a number measured on some
|
|
// other device is exactly the hard-coded vendor quirk this file exists to avoid.
|
|
struct VertexInputLocationCeilingMeasurement {
|
|
Bool detected = false;
|
|
// GL_MAX_VERTEX_ATTRIBS as the driver answers it.
|
|
Int advertisedMaxVertexAttribs = 0;
|
|
// How many locations `layout(location = N)` on a vertex input actually accepts, i.e. the
|
|
// highest N that compiles plus one. Equal to advertisedMaxVertexAttribs when the driver
|
|
// is not affected, and when the probe reached no verdict - so a caller can clamp to it
|
|
// unconditionally and an inconclusive probe changes nothing.
|
|
Int usableLocations = 0;
|
|
// Whether glBindAttribLocation(advertisedMaxVertexAttribs - 1) still links and resolves.
|
|
// Only measured when `detected`; see the second control in the .cpp for why it decides
|
|
// what the finding is allowed to claim.
|
|
Bool bindAttribLocationReachesAdvertisedMax = false;
|
|
// The first line of the driver's compile log for a refused declaration, so the report
|
|
// quotes the driver rather than paraphrasing it.
|
|
String driverMessage;
|
|
};
|
|
|
|
// Compiles `layout(location = N) in vec4` on its own at a series of N and finds the highest
|
|
// one the driver's ESSL compiler accepts.
|
|
//
|
|
// Adreno 830 advertises GL_MAX_VERTEX_ATTRIBS = 32 and then refuses the qualifier for every
|
|
// N >= 16 ("the location is not within attribute range [0, MAX_ATTRIBUTES-1]"), for float and
|
|
// integer inputs alike - so half the attributes it advertises cannot be declared. MobileGL
|
|
// emits vertex inputs as layout qualifiers, which makes the advertised count a promise it
|
|
// cannot keep; the measured ceiling is what it advertises instead.
|
|
//
|
|
// TWO CONTROLS. Location 0 must compile, or the probe has measured its own failure rather
|
|
// than the driver's. And glBindAttribLocation at the advertised maximum is tried separately,
|
|
// because that is what separates "only the layout qualifier is capped" (which is what this
|
|
// driver does) from "the attributes are not there at all" - two findings that justify the
|
|
// same clamp but very different report text.
|
|
//
|
|
// Compile-only, and bisected: one shader compile on a conforming driver, about seven on an
|
|
// affected one. Returns a measurement with `detected` false and `usableLocations` equal to
|
|
// the advertised count when an entry point is missing or a control fails, so an
|
|
// inconclusive probe never withdraws anything.
|
|
VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling(
|
|
const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeExplicitVertexInputLocationCeiling(), evaluated at most once per process.
|
|
const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling(
|
|
const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one
|
|
// BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the
|
|
// before-emit write lands and the after-emit write does not.
|
|
//
|
|
// The before-emit write is the control, and it is the whole point of the probe. Adreno 830
|
|
// discards geometry-stage storage writes issued after the last emit while performing the
|
|
// identical write issued before it (measured both ways, and for both point and triangle
|
|
// geometry shaders, so the primitive shape is not the variable). Reading only the
|
|
// after-emit half would say "geometry storage writes do not work on this driver", which is
|
|
// false and would justify withdrawing a limit applications legitimately use.
|
|
//
|
|
// Deterministic by construction - the write either reaches memory or the driver
|
|
// structurally discards it - so the answer is latched, not sampled. Returns false when the
|
|
// driver advertises no geometry storage blocks, when an entry point is missing, or when
|
|
// anything about the probe fails to set up: an inconclusive probe must never be reported
|
|
// as a bug. Restores every piece of GL state it touches.
|
|
Bool ProbeGeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process.
|
|
Bool GeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice,
|
|
// with a separately linked program each time. Returns true only when the swizzled read goes
|
|
// wrong while every control read stays right.
|
|
//
|
|
// Adreno 830 returns uninitialised memory - a different value every run - for
|
|
// texelFetch(sampler2DMS, ..., sampleIndex != 0).w on an R32F multisample texture whose
|
|
// GL_TEXTURE_SWIZZLE_A is not the default, from the SECOND such program in the context
|
|
// onward. The first program reads correctly, which is why the probe links two.
|
|
//
|
|
// THREE CONTROLS, each identical to the subject but for one variable, and all three must
|
|
// read correctly for a wrong subject to count: (1) the same fetch with
|
|
// GL_TEXTURE_SWIZZLE_A left at its default, (2) the same fetch at sample index 0, and
|
|
// (3) the same swizzled texture read through .x instead of .w. Without them a driver that
|
|
// simply cannot render R32F, or cannot sample multisample textures at all, would be
|
|
// reported as having this very specific corruption.
|
|
//
|
|
// Returns false when the driver cannot host the shape (no multisample R32F colour target,
|
|
// fewer than two samples, a missing entry point, an incomplete framebuffer): an
|
|
// inconclusive probe must never be reported as a bug. Restores every piece of GL state it
|
|
// touches.
|
|
Bool ProbeR32FMultisampleSwizzleCorruption(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeR32FMultisampleSwizzleCorruption(), evaluated at most once per process.
|
|
Bool R32FMultisampleSwizzleCorrupted(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// What the image-location budget probe measured. `detected` is the only field the verdict
|
|
// depends on; the rest exist so the report can say what the shape was instead of asserting
|
|
// a number that was true on one device in one campaign.
|
|
struct ImageLocationBudgetMeasurement {
|
|
Bool detected = false;
|
|
// Image uniforms declared per stage in both the subject and the control - one more than
|
|
// GL_MAX_GEOMETRY_IMAGE_UNIFORMS, which is the smallest of the three stages' budgets.
|
|
Int perStageImageUniforms = 0;
|
|
// Distinct uniform NAMES in the subject (per-stage-unique) and in the control (shared).
|
|
Int subjectDistinctNames = 0;
|
|
Int controlDistinctNames = 0;
|
|
// The first line of the driver's info log for the failing link, so the report quotes the
|
|
// driver rather than paraphrasing it.
|
|
String driverMessage;
|
|
};
|
|
|
|
// Links the same three-stage (vertex, geometry, fragment) program twice: once with every
|
|
// stage naming its image uniforms uniquely, once with all three stages sharing one set of
|
|
// names. Both declare the same number of image uniforms per stage, on the same bindings,
|
|
// with the same qualifier and the same stores - the names are the only difference.
|
|
//
|
|
// Adreno 830 charges its image-location budget per distinct NAME, so the shared-name program
|
|
// links while the per-stage-named one is rejected with "Image location or component exceeds
|
|
// max allowed", even though nothing about the image USAGE changed. That is what makes the
|
|
// shared-name link the control: it proves the driver can host this exact amount of image
|
|
// work and that only the naming moved the answer.
|
|
//
|
|
// `detected` is false unless the subject fails AND the control links. Both failing means the
|
|
// shape is simply too large for the driver (an honest refusal); both linking means the
|
|
// driver does not have this bug.
|
|
ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeImageLocationPerNameBudget(), evaluated at most once per process.
|
|
const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Draws one quad whose vertex stage stores to a `coherent writeonly` image and whose
|
|
// fragment stage reads the same image declared `coherent readonly` under the SAME name, then
|
|
// checks every fragment saw the store. Returns true only when the same-name program loses
|
|
// the store while the different-name control keeps it.
|
|
//
|
|
// Adreno 830 merges the two declarations into one uniform and silently discards the writing
|
|
// stage's stores. The control is the identical pair of shaders with the two halves renamed -
|
|
// exactly what MobileGL's image-uniform repair emits - which keeps every store. Without it
|
|
// the probe would be indistinguishable from "this driver cannot store to images from the
|
|
// vertex stage", which is a different and much larger claim.
|
|
//
|
|
// Returns false when the driver advertises no vertex-stage image uniforms, when an entry
|
|
// point is missing, or when the setup fails.
|
|
Bool ProbeCrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeCrossStageImageQualifierMergeDropsWrites(), evaluated at most once per process.
|
|
Bool CrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// What the image coherency probe measured. The residual is reported rather than hard-coded:
|
|
// it is a rate, it differs between devices, and a report that quotes a number measured
|
|
// somewhere else is worse than no number at all.
|
|
struct ImageCoherencyResidualMeasurement {
|
|
Bool detected = false;
|
|
// Texels the STRONGEST in-shader shape missed - that is what makes the defect unfixable.
|
|
Int mismatchedTexels = 0;
|
|
// Texels the shape MobileGL emits today missed, on the same driver in the same run. It
|
|
// is what applications actually get, and it is not always the same number.
|
|
Int emittedShapeMismatchedTexels = 0;
|
|
Int totalTexels = 0;
|
|
};
|
|
|
|
// Counts the texels whose dependent imageLoad() did not observe the imageStore() that
|
|
// precedes it in the same fragment invocation.
|
|
//
|
|
// THE SUBJECT IS THE STRONGEST SHAPE THE LANGUAGE OFFERS - a `coherent volatile`
|
|
// readonly/writeonly pair on one binding with BOTH memoryBarrierImage() and memoryBarrier()
|
|
// between the store and the read - and that choice is the whole reason the row can say
|
|
// "unfixable". Probing only the shape MobileGL emits today (`coherent` plus
|
|
// memoryBarrierImage()) reports a bug on drivers where simply adding `volatile` makes the
|
|
// read correct, which is a defect MobileGL could fix rather than one it cannot: measured on
|
|
// Mesa llvmpipe, the emitted shape misses every texel while the `volatile` shape misses
|
|
// none. Only a driver that fails even the strongest shape has no in-shader substitute left.
|
|
//
|
|
// The control is the same dependency split across TWO draws with a glMemoryBarrier and a
|
|
// glFinish between them. It separates "this driver cannot make image writes visible at all"
|
|
// (control also dirty - a far worse defect, and the probe declines to call it this one) from
|
|
// the finding, which is about ordering inside one invocation.
|
|
//
|
|
// `detected` is false unless the strongest shape is dirty AND the control is clean. The
|
|
// shape MobileGL emits is measured either way, so the report can say what applications get.
|
|
ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual(
|
|
const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeImageWriteReadCoherencyResidual(), evaluated at most once per process.
|
|
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
|
const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Copies one known GL_UNSIGNED_SHORT_5_5_5_1 word out of both mip levels of a GL_RGB5_A1
|
|
// 2D array into plain 2D images with glCopyImageSubData and reads the landed texels back
|
|
// - for SEVERAL ALLOCATION RECIPES of the same array. Returns true only when a copy from
|
|
// any level of any recipe delivers the word's 5_5_5_1 <-> 1_5_5_5_REV field-order mirror
|
|
// while the controls below hold.
|
|
//
|
|
// The affected Mali stores SOME 16-bit packed allocations (RGB565 / RGB5_A1 / RGBA4) with
|
|
// their fields packed from the other end of the word. The mirrored layout is an
|
|
// ALLOCATION property, not a mip-level one - the failing device delivers the mirror from
|
|
// level 0 and level 1 alike, which is what vetoed the first deployment's "level 0 is the
|
|
// clean control" design - and WHICH allocations get it is a heuristic keying on texture
|
|
// state during the uploads and on context history, measured to INVERT between a raw
|
|
// standalone context and MobileGL's live one. No single allocation recipe is therefore
|
|
// entitled to speak for the layout: the probe builds the CTS's failing shape (three-level
|
|
// chains both endpoints: 30/15/7 x12 array, 7/3/1 plain, FUNCTIONAL_TEST_N_LEVELS = 3)
|
|
// with three recipes - uploads-first (the minted-backend-texture order), params-first,
|
|
// and the CTS copy-test shape (MAX_LEVEL bounded, MIN_FILTER left mipmapped-default) -
|
|
// in situ, in the very context the application's copies run in. Uploads and readbacks
|
|
// decode each image's own layout consistently, so nothing but a raw texel-block move can
|
|
// see the divergence - which is exactly what glCopyImageSubData is defined to be, and why
|
|
// the whole KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* matrix fails there while
|
|
// every other suite touching these formats passes.
|
|
//
|
|
// CONTROLS. The machinery: an identical copy between two SAME-shape plain-2D images,
|
|
// which share a layout whatever it is, so it must deliver the word on any driver that can
|
|
// run copy_image on these formats - a driver that cannot reaches no verdict instead of
|
|
// being reported as this. And per recipe, the array's own round trip: a direct FBO
|
|
// readback of its level 1 must answer the word, or the UPLOAD is what corrupts - a
|
|
// different defect. The subjects must also match the mirror PREDICTION, not merely differ
|
|
// from the word - a copy that delivered anything else is a different defect and reaches
|
|
// no verdict either. Restores every piece of GL state it touches.
|
|
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// ProbeCopyImageMirrorsPacked16FieldOrder(), evaluated at most once per process. The
|
|
// DirectGLES format normalization consults this to decide whether the three 16-bit packed
|
|
// normalized formats must be stored as 8-bit-per-channel ES storage (see
|
|
// PixelFormatNormalizeOptionBit::WidenPacked16Norm).
|
|
Bool CopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
|
|
|
// Every known driver bug this GLES driver actually has. Bugs it does not have are absent,
|
|
// so an unaffected device renders an empty section rather than a wall of "not affected".
|
|
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
|
} // namespace MobileGL::MG_Util::SelfTest
|