mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d0441040 | ||
|
|
ad03e059e0 | ||
|
|
440e569c98 | ||
|
|
cb62431299 | ||
|
|
06ce55dac3 | ||
|
|
7400f46955 | ||
|
|
1679bf9a1e | ||
|
|
8673e89b13 | ||
|
|
6de38c666c | ||
|
|
9dff24f3e1 | ||
|
|
5a400e0297 | ||
|
|
b6a2bf08d4 | ||
|
|
6b2a2b5e00 | ||
|
|
512c857f18 | ||
|
|
9f3cac6691 | ||
|
|
4c7332d5e6 | ||
|
|
1c5f6c0986 | ||
|
|
8b75628dec | ||
|
|
8269a1786f | ||
|
|
3019c68945 | ||
|
|
800142c104 | ||
|
|
e9382f5329 | ||
|
|
df7d1edeca | ||
|
|
8025745fa3 | ||
|
|
4a533a215a | ||
|
|
0a5d7ceb6c | ||
|
|
ac81185968 | ||
|
|
9e861f3f7a | ||
|
|
da6f75dbd1 | ||
|
|
951da362f7 | ||
|
|
4c929b9b3f | ||
|
|
8d5072543a | ||
|
|
5de2b9e3e9 | ||
|
|
9192d156d1 | ||
|
|
27cdfbc0ca | ||
|
|
4567c3b468 | ||
|
|
d18c6a1bae | ||
|
|
dd745d7547 | ||
|
|
4407be89cd | ||
|
|
0c1a433af6 | ||
|
|
a2f3efe22c | ||
|
|
b9a15aed61 | ||
|
|
f748a06632 | ||
|
|
4532cae175 | ||
|
|
107b56d603 | ||
|
|
22b749dd37 | ||
|
|
f0c0211767 | ||
|
|
9ebbb76df1 | ||
|
|
95547ab9ce | ||
|
|
8eaf2d0069 | ||
|
|
54a8609c64 | ||
|
|
1fb0eb0737 | ||
|
|
282dd69230 | ||
|
|
e6ebe7078d | ||
|
|
30a91023f6 | ||
|
|
47dd8cdc05 | ||
|
|
8b36a15fb3 | ||
|
|
07fa84fb8d | ||
|
|
a03817b4ee | ||
|
|
641bc0cdd9 | ||
|
|
c069890ac7 | ||
|
|
48dd1c5956 | ||
|
|
a389477f78 | ||
|
|
94a8f1e3f3 | ||
|
|
45d506545e | ||
|
|
d9d63c9496 | ||
|
|
92140405c1 | ||
|
|
b9ecfef0b6 | ||
|
|
fba26ea169 | ||
|
|
93a3b55907 | ||
|
|
d1487bedf0 | ||
|
|
1bb736c57e | ||
|
|
eb76686c1e | ||
|
|
1a04fb8c0c | ||
|
|
306790ee7c | ||
|
|
170ccda3e7 | ||
|
|
e86a9bbec5 | ||
|
|
c5569e71b3 | ||
|
|
7e8c32a063 | ||
|
|
77bd03d962 | ||
|
|
37111ae992 | ||
|
|
6b0c2a15ab | ||
|
|
e9ffd99313 | ||
|
|
7c01ddea0c | ||
|
|
2d4d6e9cfb | ||
|
|
76b8957b99 | ||
|
|
ec685b9fa7 | ||
|
|
a12068df52 | ||
|
|
0b344792cc | ||
|
|
9fa32bdad0 | ||
|
|
a4980f2b56 | ||
|
|
8ca20e28ca | ||
|
|
c353a2055f | ||
|
|
421c20984e |
@@ -25,3 +25,5 @@ MobileGL/MG*/cmake-build*
|
||||
/android-plugin/app/src/trace/jniLibs
|
||||
/android-plugin/local.properties
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 26fe5ceb45...900b29d449
+1
-1
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
|
||||
@@ -220,6 +220,22 @@ namespace MobileGL {
|
||||
// and leave the query readable later.
|
||||
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
||||
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
|
||||
// the frontend then rejects the target). Results/deletion flow through
|
||||
// GetQueryResult64 / DeleteBackendQuery like timer queries.
|
||||
BackendQueryHandle (*BeginOcclusionQuery)();
|
||||
void (*EndOcclusionQuery)(BackendQueryHandle query);
|
||||
// Transform feedback primitive queries backed by real GPU query pools
|
||||
// (optional; null = frontend falls back to CPU accounting).
|
||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||
// called while the frontend capture state is still active, so the backend
|
||||
// can still see the capture program and buffer bindings.
|
||||
void (*BeginTransformFeedback)(GLenum primitiveMode);
|
||||
void (*EndTransformFeedback)();
|
||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||
};
|
||||
struct GlobalBackendFunctionsTable {
|
||||
@@ -300,7 +316,21 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
// GL 4.x fragment-interpolation offset limits. These defaults are the
|
||||
// core minimums and are replaced by live GLES/Vulkan device limits.
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
// For four fractional bits the greatest required legal offset is
|
||||
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// Whether a framebuffer whose depth and stencil attachments are distinct
|
||||
// images can be rendered to. GL only requires support when both refer to the
|
||||
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
|
||||
// otherwise, which is what DirectVulkan (one combined attachment) and the
|
||||
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
|
||||
// that never sets it keeps the permissive behaviour.
|
||||
Bool SupportsDistinctDepthStencilAttachments = true;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -209,6 +210,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
reasons.push_back("EXT_render_snorm not supported");
|
||||
}
|
||||
|
||||
String reason;
|
||||
for (SizeT i = 0; i < reasons.size(); ++i) {
|
||||
@@ -356,6 +363,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return complete;
|
||||
}
|
||||
|
||||
// Whether the driver renders to a framebuffer whose depth and stencil come from
|
||||
// two different renderbuffers. GL only requires support when both attachments are
|
||||
// the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
|
||||
// reporting COMPLETE from the frontend and then rendering into a framebuffer the
|
||||
// driver refuses leaves the results silently empty.
|
||||
Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
|
||||
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
|
||||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
|
||||
!gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
|
||||
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
|
||||
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
GLuint renderbuffers[2] = {0, 0};
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
gl.glGenRenderbuffers(2, renderbuffers);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
|
||||
const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
gl.glDeleteRenderbuffers(2, renderbuffers);
|
||||
return supported;
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
|
||||
GLuint renderbuffer,
|
||||
TextureInternalFormat format) {
|
||||
@@ -519,20 +562,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
||||
GLESProbeFormatInfo fallbackInfo;
|
||||
const Bool hasForcedFallback =
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
|
||||
if (!hasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
|
||||
GLESProbeFormatInfo outerFallbackInfo;
|
||||
const Bool outerHasForcedFallback =
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
|
||||
if (!outerHasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
|
||||
}
|
||||
|
||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||
// A multisample texture can only ever be rendered into, so its storage format
|
||||
// has to stay colour-renderable; the ordinary fallback for a three-channel
|
||||
// format is a three-channel one, which ES accepts as a texture but rejects as
|
||||
// multisample storage. Recompute the fallback per target so those formats get
|
||||
// widened here and nowhere else.
|
||||
Flags<PixelFormatNormalizeOptionBit> targetOptions;
|
||||
if (IsGLESProbeMultisampleTarget(target)) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
}
|
||||
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
||||
Bool hasForcedFallback = outerHasForcedFallback;
|
||||
if (targetOptions) {
|
||||
hasForcedFallback = BuildFallbackProbeFormatInfo(
|
||||
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
|
||||
if (!hasForcedFallback) {
|
||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions,
|
||||
false, fallbackInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 1D, 1D-array and rectangle textures live on an ES target (see
|
||||
// TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
|
||||
// probing the desktop-only target itself always failed, which left those slots
|
||||
// of the cache empty and stopped any fallback format from being selected for
|
||||
// them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
|
||||
const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||
|
||||
Bool shouldProbeFallback = hasForcedFallback;
|
||||
if (!hasForcedFallback) {
|
||||
Bool nativeRenderable = false;
|
||||
const Bool nativeCreated =
|
||||
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
||||
if (nativeCreated) {
|
||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||
@@ -547,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
||||
Bool fallbackRenderable = false;
|
||||
const Bool fallbackCreated =
|
||||
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
||||
if (fallbackCreated) {
|
||||
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
|
||||
@@ -563,8 +636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
|
||||
if (!hasForcedFallback) {
|
||||
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
||||
if (!outerHasForcedFallback) {
|
||||
const Bool nativeRenderbufferComplete =
|
||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||
if (nativeRenderbufferComplete) {
|
||||
@@ -578,16 +651,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
shouldProbeFallbackRenderbuffer = true;
|
||||
}
|
||||
}
|
||||
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,7 +676,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.ExtraVendor = Nullopt, // Extra vendor
|
||||
.RendererGLInfo =
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
||||
.TargetGLVersion = {3, 3, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
@@ -831,6 +904,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||
// Both are core from GL 3.2/3.3 on and implemented here for
|
||||
// every advertised version, but an app targeting 3.0/3.1
|
||||
// only reaches them through the extension string - the CTS
|
||||
// picks a whole different shader for draw_buffers without
|
||||
// explicit_attrib_location. DirectVulkan advertises both.
|
||||
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample,
|
||||
E_GL_ARB_shader_image_size};
|
||||
// Only advertised when the device driver actually has usable timer queries
|
||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||
@@ -934,11 +1013,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
|
||||
// and share the handle-based result/delete entries, which must exist even
|
||||
// when the timer-query group above is disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
// Real driver primitive counters: the frontend's CPU accounting cannot see a
|
||||
// geometry shader's amplification.
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
// Transform feedback is captured by the real ES driver rather than
|
||||
// reconstructed from the draw recording, so the frontend has to hand the
|
||||
// span boundaries over.
|
||||
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
|
||||
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -1025,6 +1118,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
|
||||
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
@@ -1034,6 +1129,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
|
||||
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_GLESCapabilities.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
|
||||
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset =
|
||||
m_GLESCapabilities.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_GLESCapabilities.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines =
|
||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
|
||||
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
|
||||
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
|
||||
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
|
||||
// which case the frontend falls back to counting primitives from the draw calls.
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||
// null or stale-generation handles IS final: the frontend may cache it
|
||||
@@ -154,6 +164,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
void DestroyEGLContext();
|
||||
|
||||
// Transform feedback capture spans, performed by the real ES driver. The
|
||||
// capture set is declared on the backend program at link time; the driver-side
|
||||
// begin is deferred to the first draw of the span (ES needs the capturing
|
||||
// program current and the capture buffers bound), and the end also mirrors the
|
||||
// captured bytes back into the frontend buffer shadows.
|
||||
namespace XfbImpl {
|
||||
Bool AreTransformFeedbacksSupported();
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback();
|
||||
void OnBackendContextDestroyed();
|
||||
} // namespace XfbImpl
|
||||
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
@@ -2405,7 +2405,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
|
||||
const auto& swizzleParams = stateTextureObject->GetAllSwizzleParams();
|
||||
// A three-channel format widened to four for a multisample target (see
|
||||
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
|
||||
// whatever the draw that filled it wrote there is not what GL would report: a format
|
||||
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
|
||||
// promotion stays invisible, composed with the swizzle the application asked for.
|
||||
Vec4<TextureSwizzleParam> swizzleParams = stateTextureObject->GetAllSwizzleParams();
|
||||
if (TextureImpl::BackendTextureFormatAddsAlpha(stateTextureObject->GetFormat(), targetInternal)) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
|
||||
swizzleParams[channel] = TextureSwizzleParam::One;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (swizzleParams != m_cacheSwizzleParams) {
|
||||
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
||||
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
||||
@@ -2679,6 +2691,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool IsFixedPointFallbackReadAttachment() {
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (!readFBO) {
|
||||
return false;
|
||||
}
|
||||
const auto readBuffer = readFBO->GetReadBuffer();
|
||||
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
|
||||
return false;
|
||||
}
|
||||
// Any signed-normalized attachment, not just the ones currently substituted:
|
||||
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
|
||||
// hands back the negative half that desktop GL clamps away.
|
||||
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
|
||||
if (attachmentObject.IsTexture()) {
|
||||
const auto& textureObject = attachmentObject.GetTexture();
|
||||
return textureObject && IsSnormFormat(textureObject->GetFormat());
|
||||
}
|
||||
if (attachmentObject.IsRenderbuffer()) {
|
||||
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
|
||||
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BackendFramebufferObject::SyncReadBufferToBackend(
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
||||
if (!stateFBOObject) {
|
||||
@@ -3181,6 +3218,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace PrgramImpl {
|
||||
Uint32 g_snormFallbackClampOutputMask = 0;
|
||||
Uint g_fragColorBroadcastCount = 1;
|
||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||
Uint g_lastUsedBackendProgramId = 0;
|
||||
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
||||
@@ -3232,8 +3270,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
||||
m_backendProgramUsable = true;
|
||||
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -3316,6 +3356,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||
// than approximating one. Where every use takes integer texel coordinates a
|
||||
// rectangle image is indistinguishable from a 2D one, so rewrite the type and let
|
||||
// it through; the pass declines anything it cannot convert exactly.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
|
||||
rectLoweredSpirv) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
@@ -3338,6 +3389,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
r.log += spvcSession.GetLastErrorString();
|
||||
r.errc = -5;
|
||||
MGLOG_E("%s", r.log.c_str());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3347,6 +3399,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
source = RemoveLayoutBinding(source);
|
||||
source = ProcessOutColorLocations(source);
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
|
||||
source = EmulateTextureLodBias(source);
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
||||
source = ForceSupporterOutput(source);
|
||||
@@ -3377,6 +3431,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<GLchar> log(logLength);
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3386,12 +3441,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||
}
|
||||
|
||||
// Transform feedback capture runs on the real driver (see XfbImpl in
|
||||
// DirectGLES.cpp), so the capture set has to be declared on the backend
|
||||
// program before it links. SPIRV-Cross keeps user output names verbatim in
|
||||
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
|
||||
// frontend's requested names carry over unchanged.
|
||||
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
|
||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
|
||||
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
|
||||
Vector<const GLchar*> xfbNames;
|
||||
xfbNames.reserve(xfbVaryings.size());
|
||||
for (const auto& xfbVarying : xfbVaryings) {
|
||||
xfbNames.push_back(xfbVarying.name.c_str());
|
||||
}
|
||||
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
|
||||
m_backendProgramId);
|
||||
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
|
||||
xfbNames.data(),
|
||||
stateProgramObject->GetTransformFeedbackBufferMode());
|
||||
}
|
||||
|
||||
// Link program
|
||||
MGLOG_D("Linking program %u", m_backendProgramId);
|
||||
g_GLESFuncs.glLinkProgram(m_backendProgramId);
|
||||
|
||||
GLint linkStatus;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
|
||||
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
|
||||
if (linkStatus != GL_TRUE) {
|
||||
GLint logLength;
|
||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
@@ -3504,6 +3580,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
binding.backendLocation = backendLoc;
|
||||
binding.uniformType = uniformType;
|
||||
binding.lastAssignedUnit = -1;
|
||||
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
|
||||
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
|
||||
binding.lodBiasLocation =
|
||||
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
|
||||
binding.lastAssignedLodBias = 0.0f;
|
||||
m_samplerUniformBindings.push_back(binding);
|
||||
}
|
||||
}
|
||||
@@ -3512,12 +3593,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (g_lastUsedBackendProgramId == m_backendProgramId) {
|
||||
// glUseProgram on a program that did not link is an INVALID_OPERATION and
|
||||
// leaves the *previous* program current, so the draw would silently render
|
||||
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
|
||||
// test case's alpha that way once a sampler2DRect stage failed to
|
||||
// transpile). Bind nothing instead: the draw is then a visible no-op.
|
||||
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
|
||||
if (g_lastUsedBackendProgramId == programToBind) {
|
||||
return;
|
||||
}
|
||||
MGLOG_D("Using program %u", m_backendProgramId);
|
||||
g_GLESFuncs.glUseProgram(m_backendProgramId);
|
||||
g_lastUsedBackendProgramId = m_backendProgramId;
|
||||
MGLOG_D("Using program %u", programToBind);
|
||||
g_GLESFuncs.glUseProgram(programToBind);
|
||||
g_lastUsedBackendProgramId = programToBind;
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
|
||||
|
||||
@@ -270,18 +270,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace TextureImpl {
|
||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
||||
// coordinate padding for 1D/1D-array shaders.
|
||||
return target != TextureTarget::TextureRectangle;
|
||||
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
|
||||
(void)target;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
||||
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
|
||||
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
|
||||
// they are single-level and already clamp, so only the non-normalized coordinates differ.
|
||||
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
|
||||
// ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module
|
||||
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
|
||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
@@ -297,6 +300,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||
switch (uploadTarget) {
|
||||
case TextureUploadTarget::Texture1D:
|
||||
case TextureUploadTarget::TextureRectangle:
|
||||
return GL_TEXTURE_2D;
|
||||
case TextureUploadTarget::Texture1DArray:
|
||||
return GL_TEXTURE_2D_ARRAY;
|
||||
@@ -438,6 +442,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
g_backendFramebufferObjects;
|
||||
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
|
||||
// backend actually stores in a floating-point format. GL clamps a read from a
|
||||
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
|
||||
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
|
||||
// has to apply the clamp itself.
|
||||
Bool IsFixedPointFallbackReadAttachment();
|
||||
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
||||
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
||||
@@ -576,6 +587,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int backendLocation = -1;
|
||||
GLenum uniformType = 0;
|
||||
Int lastAssignedUnit = -1;
|
||||
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
|
||||
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
|
||||
// lastAssignedLodBias mirrors the value the program currently holds,
|
||||
// so an unbiased shader issues no per-draw glUniform1f at all.
|
||||
Int lodBiasLocation = -1;
|
||||
Float lastAssignedLodBias = 0.0f;
|
||||
};
|
||||
|
||||
BackendProgramObjectImpl();
|
||||
@@ -587,9 +604,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||
// False when the last SyncToBackend could not produce a usable program (a
|
||||
// shader failed to transpile or compile, or the link itself failed). Use()
|
||||
// must not leave the previously bound program current in that case.
|
||||
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
|
||||
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -616,7 +638,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int m_indirectParamsBinding = -1;
|
||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||
Uint m_fragColorBroadcastCount = 1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
Int m_globalUboBackendBlockIndex = -1;
|
||||
Int m_globalUboBackendBlockSize = 0;
|
||||
@@ -629,6 +655,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
extern Uint32 g_snormFallbackClampOutputMask;
|
||||
extern Uint32 g_unormFallbackClampOutputMask;
|
||||
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
|
||||
// is framebuffer state that the shader has to be compiled against, so a program
|
||||
// whose snapshot no longer matches is relinked.
|
||||
extern Uint g_fragColorBroadcastCount;
|
||||
// Backend id of the last glUseProgram issued through this backend; lets Use()
|
||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||
// ES context is recreated.
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
@@ -45,15 +48,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return options;
|
||||
}
|
||||
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
|
||||
Flags<PixelFormatNormalizeOptionBit>
|
||||
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
|
||||
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
|
||||
using namespace MG_Util::TextureFormatProcessor;
|
||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
|
||||
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
|
||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
|
||||
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
|
||||
if (forcedOptions) {
|
||||
return forcedOptions;
|
||||
}
|
||||
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
||||
GetDriverPixelFormatNormalizeOptions());
|
||||
return GetApplicablePixelFormatNormalizeOptions(
|
||||
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
||||
}
|
||||
|
||||
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
|
||||
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
|
||||
// ES texture format but not a legal multisample storage format. Widening to four channels
|
||||
// is safe here precisely because there is no transfer path that would have to expand
|
||||
// three-channel client data, and the alpha the draw writes for a three-channel source is
|
||||
// already the 1.0 the frontend format implies.
|
||||
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
||||
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
|
||||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
|
||||
}
|
||||
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return options;
|
||||
}
|
||||
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
|
||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||
@@ -113,7 +141,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
|
||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||
GetRenderTargetNormalizeOptions(targetIndex));
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
@@ -148,6 +177,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||
}
|
||||
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||
const SizeT targetIndex =
|
||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const Flags<PixelFormatNormalizeOptionBit> options =
|
||||
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||
GetRenderTargetNormalizeOptions(targetIndex));
|
||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode) {
|
||||
@@ -276,6 +321,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The name is the marker: ShaderSourceProcessor only emits it when the source
|
||||
// wrote gl_FragColor, and such a shader can have no other output.
|
||||
static const char* const kLoweredName = "mg_FragColor";
|
||||
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
|
||||
return glslCode;
|
||||
}
|
||||
static const std::regex declRegex(
|
||||
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
|
||||
std::smatch declMatch;
|
||||
if (!std::regex_search(glslCode, declMatch, declRegex)) {
|
||||
return glslCode;
|
||||
}
|
||||
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
|
||||
|
||||
String replicaDecls;
|
||||
String replicaCopies;
|
||||
for (Uint location = 1; location < drawBufferCount; ++location) {
|
||||
const String name = String(kLoweredName) + "_" + std::to_string(location);
|
||||
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
|
||||
name + ";";
|
||||
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
|
||||
}
|
||||
|
||||
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
|
||||
std::smatch mainMatch;
|
||||
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
|
||||
return glslCode;
|
||||
}
|
||||
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
|
||||
Int depth = 0;
|
||||
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
|
||||
if (glslCode[pos] == '{') {
|
||||
++depth;
|
||||
} else if (glslCode[pos] == '}') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
glslCode.insert(pos, replicaCopies + "\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -342,6 +436,166 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How a lookup carries its level of detail, and how many arguments it takes
|
||||
// before the optional bias.
|
||||
struct LodLookupForm {
|
||||
const char* name;
|
||||
Int requiredArgs; // arguments before the optional bias (implicit form)
|
||||
Int explicitLodArg; // index of the explicit LOD argument, -1 for implicit
|
||||
};
|
||||
|
||||
// texelFetch* is deliberately absent: an integer fetch names its level directly
|
||||
// and takes no LOD bias. textureGather has no bias either. textureGrad* derives
|
||||
// the LOD from gradients and offers no argument to fold a bias into, so it is
|
||||
// left alone rather than rewritten incorrectly.
|
||||
constexpr LodLookupForm LOD_LOOKUP_FORMS[] = {
|
||||
{"textureProjLodOffset", 0, 2}, {"textureProjOffset", 4, -1}, {"textureProjLod", 0, 2},
|
||||
{"textureLodOffset", 0, 2}, {"textureOffset", 3, -1}, {"textureProj", 2, -1},
|
||||
{"textureLod", 0, 2}, {"texture", 2, -1},
|
||||
};
|
||||
|
||||
// Sampler types with no mip chain, or whose GLSL lookups have no bias overload
|
||||
// at all (the array-shadow forms), so nothing can or should be folded in.
|
||||
Bool IsBiasableSamplerType(const String& samplerType) {
|
||||
if (samplerType.find("MS") != String::npos) return false; // multisample
|
||||
if (samplerType.find("Buffer") != String::npos) return false; // texture buffer
|
||||
if (samplerType.find("Rect") != String::npos) return false; // rectangle: no mips
|
||||
if (samplerType == "sampler2DArrayShadow") return false;
|
||||
if (samplerType == "samplerCubeArrayShadow") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool IsIdentifierChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
|
||||
// Byte offsets of the top-level argument separators and of the closing paren,
|
||||
// starting from the '(' at openParen. Empty when the parentheses do not balance.
|
||||
Vector<SizeT> SplitCallArguments(const String& code, SizeT openParen) {
|
||||
Vector<SizeT> marks;
|
||||
Int depth = 0;
|
||||
for (SizeT i = openParen; i < code.size(); ++i) {
|
||||
const char c = code[i];
|
||||
if (c == '(' || c == '[') {
|
||||
++depth;
|
||||
} else if (c == ']') {
|
||||
--depth;
|
||||
} else if (c == ')') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
marks.push_back(i);
|
||||
return marks;
|
||||
}
|
||||
} else if (c == ',' && depth == 1) {
|
||||
marks.push_back(i);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
String EmulateTextureLodBias(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (glslCode.find("sampler") == String::npos || glslCode.find("texture") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Collect the mip-capable sampler uniforms this shader declares.
|
||||
static const std::regex samplerDeclRegex(
|
||||
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?([iu]?sampler[A-Za-z0-9]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
|
||||
UnorderedMap<String, String> samplerNames; // name -> bias uniform name
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), samplerDeclRegex), end; it != end; ++it) {
|
||||
const String samplerType = (*it)[1].str();
|
||||
if (!IsBiasableSamplerType(samplerType)) continue;
|
||||
const String name = (*it)[2].str();
|
||||
samplerNames.emplace(name, String(LOD_BIAS_UNIFORM_PREFIX) + name);
|
||||
}
|
||||
if (samplerNames.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Rewrite the lookups. Right-to-left so earlier offsets stay valid, and only for
|
||||
// samplers named directly as the first argument (SPIRV-Cross never produces an
|
||||
// expression there for ES output, which has no separate sampler objects).
|
||||
String result = glslCode;
|
||||
Vector<String> usedSamplers;
|
||||
for (SizeT scan = result.size(); scan-- > 0;) {
|
||||
if (result[scan] != 't') continue;
|
||||
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||
|
||||
const LodLookupForm* form = nullptr;
|
||||
SizeT openParen = 0;
|
||||
for (const auto& candidate : LOD_LOOKUP_FORMS) {
|
||||
const SizeT nameLength = std::strlen(candidate.name);
|
||||
if (result.compare(scan, nameLength, candidate.name) != 0) continue;
|
||||
SizeT after = result.find_first_not_of(" \t", scan + nameLength);
|
||||
if (after == String::npos || result[after] != '(') continue;
|
||||
form = &candidate;
|
||||
openParen = after;
|
||||
break;
|
||||
}
|
||||
if (form == nullptr) continue;
|
||||
|
||||
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||
if (marks.empty()) continue;
|
||||
const SizeT argCount = marks.size();
|
||||
const SizeT closeParen = marks.back();
|
||||
|
||||
// First argument must be one of our samplers.
|
||||
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
|
||||
SizeT firstArgEnd = marks.front();
|
||||
while (firstArgEnd > firstArgStart && (result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
|
||||
--firstArgEnd;
|
||||
}
|
||||
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
|
||||
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
|
||||
const auto samplerIt = samplerNames.find(samplerName);
|
||||
if (samplerIt == samplerNames.end()) continue;
|
||||
|
||||
const String& biasName = samplerIt->second;
|
||||
if (form->explicitLodArg >= 0) {
|
||||
// Explicit LOD: the bias adds to it, as Vulkan does for
|
||||
// OpImageSampleExplicitLod and as the CTS reference expects.
|
||||
const SizeT lodIndex = static_cast<SizeT>(form->explicitLodArg);
|
||||
if (argCount <= lodIndex) continue;
|
||||
const SizeT lodStart = marks[lodIndex - 1] + 1;
|
||||
const SizeT lodEnd = marks[lodIndex];
|
||||
result.insert(lodEnd, String(") + ") + biasName + ")");
|
||||
result.insert(lodStart, "((");
|
||||
} else {
|
||||
const SizeT required = static_cast<SizeT>(form->requiredArgs);
|
||||
if (argCount == required) {
|
||||
result.insert(closeParen, String(", ") + biasName);
|
||||
} else if (argCount == required + 1) {
|
||||
const SizeT biasStart = marks[argCount - 2] + 1;
|
||||
result.insert(closeParen, String(") + ") + biasName + ")");
|
||||
result.insert(biasStart, "((");
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
usedSamplers.push_back(samplerName);
|
||||
}
|
||||
if (usedSamplers.empty()) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Declare the bias uniforms that were actually referenced, right after the
|
||||
// sampler declaration line they belong to.
|
||||
for (const auto& samplerName : usedSamplers) {
|
||||
const String& biasName = samplerNames[samplerName];
|
||||
if (result.find(String("float ") + biasName + ";") != String::npos) continue;
|
||||
const std::regex declRegex(
|
||||
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?[iu]?sampler[A-Za-z0-9]*\s+)" + samplerName + R"(\s*;)");
|
||||
std::smatch match;
|
||||
if (!std::regex_search(result, match, declRegex)) continue;
|
||||
const SizeT declEnd = static_cast<SizeT>(match.position(0)) + match[0].str().size();
|
||||
result.insert(declEnd, String("\nuniform highp float ") + biasName + ";");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType);
|
||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
|
||||
// True when the format the texture is actually created with has an alpha channel the
|
||||
// frontend format does not (the three-channel multisample widening). GL reads such a
|
||||
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
@@ -104,7 +109,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
|
||||
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
|
||||
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
|
||||
// outputs and copies the value into them at the end of main. A no-op for
|
||||
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
|
||||
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
|
||||
// shader as a uniform and be folded into every lookup's level of detail. Declares
|
||||
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
|
||||
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
|
||||
// the bound texture's (or sampler object's) value into it; a shader whose samplers
|
||||
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||
// there is nothing to rewrite.
|
||||
String EmulateTextureLodBias(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -525,10 +526,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location};
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -634,6 +636,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||
}
|
||||
// Occlusion queries share the handle-based result/delete entries, which must
|
||||
// exist even when timer queries are disabled.
|
||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
funcsTableInitialized = true;
|
||||
}
|
||||
return funcsTable;
|
||||
@@ -796,6 +807,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
||||
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
||||
m_dynamicParameters.MinFragmentInterpolationOffset =
|
||||
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
|
||||
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
|
||||
? m_vulkanCaps.MinFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
||||
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset =
|
||||
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
||||
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||
m_vulkanCaps.FragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
|
||||
@@ -1250,10 +1250,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
pVulkanRenderer->Clear(mask);
|
||||
}
|
||||
|
||||
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
|
||||
// whose synthesized index list revisits the first vertex at the end.
|
||||
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = GL_LINE_STRIP;
|
||||
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
|
||||
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
|
||||
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
|
||||
payload.indexBufferView.forceClientMemory = true;
|
||||
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
|
||||
payload.params.instanceCount = 1;
|
||||
payload.params.vertexOffset = basevertex;
|
||||
pVulkanRenderer->DrawElements(payload);
|
||||
}
|
||||
|
||||
// Resolve a DrawElements index list (bound element-array buffer or client
|
||||
// memory) into uint32 values with the loop-closing first index appended.
|
||||
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
|
||||
Vector<Uint32>& outIndices) {
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0 || count < 2) {
|
||||
return false;
|
||||
}
|
||||
const Uint8* indexBytes = nullptr;
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (indexBufferShared != nullptr) {
|
||||
const SizeT offset = reinterpret_cast<SizeT>(indices);
|
||||
const SizeT bufferSize = indexBufferShared->GetSize();
|
||||
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
|
||||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
|
||||
return false;
|
||||
}
|
||||
indexBufferShared->SyncPersistentMappedRange();
|
||||
indexBytes = indexBufferShared->MappedData() + offset;
|
||||
} else {
|
||||
indexBytes = static_cast<const Uint8*>(indices);
|
||||
if (indexBytes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
outIndices.resize(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
switch (indexSize) {
|
||||
case 1: outIndices[i] = indexBytes[i]; break;
|
||||
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
|
||||
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
|
||||
}
|
||||
}
|
||||
outIndices[count] = outIndices[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
closedIndices[i] = static_cast<Uint32>(first + i);
|
||||
}
|
||||
closedIndices[count] = static_cast<Uint32>(first);
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.params.firstVertex = first;
|
||||
@@ -1266,6 +1332,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1334,6 +1408,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
payload.indexBufferView.indexType = type;
|
||||
@@ -1483,8 +1564,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// records are shared (SharedPtr) with the owning pool's pending list,
|
||||
// so deleting the query while results are still in flight is safe.
|
||||
struct VulkanTimerQuery {
|
||||
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
|
||||
Kind kind = Kind::Timer;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1574,6 +1659,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ever be produced, so resolve with a final 0.
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
|
||||
Uint64 samples = 0;
|
||||
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
|
||||
return false;
|
||||
}
|
||||
query->occlusionSlots.clear(); // slots are recycled by the resolve
|
||||
*outNanoseconds = samples;
|
||||
return true;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
return true;
|
||||
}
|
||||
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
||||
// complete until Present submits the commands, so the wait refuses to
|
||||
// block on the current unsubmitted serial. Returning false keeps the
|
||||
@@ -1606,6 +1711,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
delete static_cast<VulkanTimerQuery*>(handle);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
|
||||
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* query = new VulkanTimerQuery{};
|
||||
query->kind = VulkanTimerQuery::Kind::Occlusion;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndOcclusionQuery(BackendQueryHandle handle) {
|
||||
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
|
||||
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
|
||||
}
|
||||
|
||||
Int64 GetGpuTimestampNs() {
|
||||
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
||||
// exist as vkCmdWriteTimestamp results read back later, and
|
||||
|
||||
@@ -123,6 +123,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only while a live renderer exists whose device can actually time.
|
||||
Bool IsTimerQuerySupported();
|
||||
BackendQueryHandle BeginTimeElapsedQuery();
|
||||
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle BeginOcclusionQuery();
|
||||
void EndOcclusionQuery(BackendQueryHandle query);
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||
BackendQueryHandle QueryCounterTimestamp();
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||
|
||||
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = device;
|
||||
m_commandPool = commandPool;
|
||||
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
allocInfo.commandPool = commandPool;
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = frameCount;
|
||||
allocInfo.commandBufferCount = frameCount * 2;
|
||||
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
m_frames[i].commandBuffer = commandBuffers[i];
|
||||
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
|
||||
}
|
||||
|
||||
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
||||
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
||||
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
commandBuffers[i] = m_frames[i].commandBuffer;
|
||||
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
|
||||
}
|
||||
|
||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& frame : m_frames) {
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
}
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
|
||||
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||
}
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
||||
GetCurrent().isCommandRecording = false;
|
||||
GetCurrent().hasCommandBufferRecorded = false;
|
||||
GetCurrent().isPreCommandRecording = false;
|
||||
GetCurrent().hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
||||
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
|
||||
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
return frame.preCommandBuffer;
|
||||
}
|
||||
|
||||
void FrameContext::EndPreCommandRecordingIfOpen() {
|
||||
auto& frame = GetCurrent();
|
||||
if (!frame.isPreCommandRecording) {
|
||||
return;
|
||||
}
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = true;
|
||||
}
|
||||
|
||||
void FrameContext::AbandonPreCommandRecording() {
|
||||
auto& frame = GetCurrent();
|
||||
if (frame.isPreCommandRecording) {
|
||||
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
|
||||
}
|
||||
frame.isPreCommandRecording = false;
|
||||
frame.hasPreCommandBufferRecorded = false;
|
||||
}
|
||||
|
||||
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
|
||||
DestroySwapchainSemaphores(device);
|
||||
if (swapchainImageCount == 0) {
|
||||
@@ -202,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 swapchainImageIndex) const {
|
||||
const auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"GetSubmitInfo called while the pre-pass stream is still recording");
|
||||
AssertValidSwapchainImageIndex(swapchainImageIndex);
|
||||
SubmitInfoPacket packet{};
|
||||
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
||||
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
|
||||
packet.commandBuffer = frame.commandBuffer;
|
||||
|
||||
Uint32 commandBufferCount = 0;
|
||||
// The pre-pass stream executes strictly before the frame's commands.
|
||||
if (frame.hasPreCommandBufferRecorded) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
|
||||
}
|
||||
if (shouldSubmitCommandBuffer) {
|
||||
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
|
||||
}
|
||||
|
||||
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
|
||||
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
||||
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
||||
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
|
||||
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
|
||||
packet.submitInfo.commandBufferCount = commandBufferCount;
|
||||
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
|
||||
packet.submitInfo.signalSemaphoreCount = 1;
|
||||
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
||||
return packet;
|
||||
@@ -276,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_recordingObserver = observer;
|
||||
}
|
||||
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer() {
|
||||
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
||||
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
||||
auto& frame = GetCurrent();
|
||||
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the command buffer is still recording");
|
||||
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
|
||||
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||
@@ -289,10 +340,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
frame.preCommandBuffer = preReplacement;
|
||||
}
|
||||
// lastSubmitIndex was just written by the renderer for the submission
|
||||
// that carried this command buffer.
|
||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
// command buffer; submitInfo.pCommandBuffers points here.
|
||||
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
};
|
||||
|
||||
@@ -52,10 +54,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
struct FrameData {
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
// Pre-pass work stream: out-of-pass commands (deferred clear
|
||||
// materialization, sampled-layout transitions) for resources the
|
||||
// frame's recording has not touched yet. Submitted immediately
|
||||
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
|
||||
// into it never has to split the frame's active render pass.
|
||||
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
|
||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
||||
Bool isCommandRecording = false;
|
||||
Bool hasCommandBufferRecorded = false;
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
@@ -77,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
||||
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
||||
void EndCommandRecording();
|
||||
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
|
||||
VkCommandBuffer BeginPreCommandRecording();
|
||||
// Closes the pre stream if open, marking it for submission ahead of the
|
||||
// frame command buffer. Safe to call when it never opened.
|
||||
void EndPreCommandRecordingIfOpen();
|
||||
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
|
||||
// frame recordings, swapchain recreation).
|
||||
void AbandonPreCommandRecording();
|
||||
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
|
||||
void DestroySwapchainSemaphores(VkDevice device);
|
||||
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
|
||||
@@ -91,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// can restart while the submitted buffer is still executing. Retired
|
||||
// buffers are freed after the slot's fence is next waited, or as soon
|
||||
// as their submission is observed complete.
|
||||
VkResult RetireCurrentCommandBuffer();
|
||||
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
|
||||
|
||||
// Frees every retired command buffer whose tagged submission index is
|
||||
// known complete. Driven by the renderer's submit tracker on completion
|
||||
|
||||
@@ -923,6 +923,163 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
||||
};
|
||||
|
||||
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
||||
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
||||
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
||||
// variable copied before every OpReturn, BEFORE the position fixup runs,
|
||||
// so the captured value is the shader's own (pre-remap) gl_Position.
|
||||
class XfbCaptureDecoratePass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
struct CapturedVarying {
|
||||
std::string name;
|
||||
Uint32 bufferIndex = 0;
|
||||
Uint32 offsetBytes = 0;
|
||||
};
|
||||
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
||||
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
||||
: m_varyings(Move(varyings)), m_strides(Move(strides)) {}
|
||||
|
||||
Status Process() override {
|
||||
using namespace spvtools::opt;
|
||||
if (m_varyings.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
auto entryPointIter = get_module()->entry_points().begin();
|
||||
if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange;
|
||||
spvtools::opt::Instruction* entryPoint = &*entryPointIter;
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
|
||||
// Name -> result id map from the debug section.
|
||||
std::unordered_map<std::string, Uint32> idsByName;
|
||||
for (auto& debugInst : get_module()->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||
idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
auto* decorationManager = context()->get_decoration_mgr();
|
||||
const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) {
|
||||
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbBuffer),
|
||||
bufferIndex);
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbStride),
|
||||
stride);
|
||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
||||
offsetBytes);
|
||||
};
|
||||
|
||||
Bool modified = false;
|
||||
Bool needsPositionMirror = false;
|
||||
Uint32 positionBufferIndex = 0;
|
||||
Uint32 positionOffset = 0;
|
||||
for (const auto& varying : m_varyings) {
|
||||
if (varying.name == "gl_Position") {
|
||||
needsPositionMirror = true;
|
||||
positionBufferIndex = varying.bufferIndex;
|
||||
positionOffset = varying.offsetBytes;
|
||||
continue;
|
||||
}
|
||||
const auto idIt = idsByName.find(varying.name);
|
||||
if (idIt == idsByName.end()) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
||||
continue;
|
||||
}
|
||||
decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (needsPositionMirror) {
|
||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||
positionOffset, decorateForXfb);
|
||||
}
|
||||
|
||||
if (!modified) return Status::SuccessWithoutChange;
|
||||
|
||||
context()->AddCapability(spv::Capability::TransformFeedback);
|
||||
{
|
||||
auto executionMode = MakeUnique<spvtools::opt::Instruction>(
|
||||
context(), spv::Op::OpExecutionMode, 0, 0,
|
||||
std::initializer_list<spvtools::opt::Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}},
|
||||
{SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast<Uint32>(spv::ExecutionMode::Xfb)}}});
|
||||
get_module()->AddExecutionMode(Move(executionMode));
|
||||
}
|
||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename DecorateFn>
|
||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
|
||||
using namespace spvtools::opt;
|
||||
PositionTargetInfo target{};
|
||||
if (!FindPositionTarget(context(), &target)) {
|
||||
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
|
||||
return false;
|
||||
}
|
||||
if (!target.isMember) {
|
||||
// Standalone gl_Position variable: decorate it directly.
|
||||
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* typeManager = context()->get_type_mgr();
|
||||
const Uint32 mirrorPointerTypeId =
|
||||
typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output);
|
||||
if (mirrorPointerTypeId == 0) return false;
|
||||
|
||||
const Uint32 mirrorVariableId = context()->TakeNextId();
|
||||
auto mirrorVariable = MakeUnique<Instruction>(
|
||||
context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Output)}}});
|
||||
get_module()->AddGlobalValue(Move(mirrorVariable));
|
||||
|
||||
// A free output location: past every explicitly decorated output.
|
||||
Uint32 mirrorLocation = 0;
|
||||
for (auto& annotation : get_module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
||||
annotation.GetSingleWordInOperand(1) != static_cast<Uint32>(spv::Decoration::Location)) {
|
||||
continue;
|
||||
}
|
||||
mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1);
|
||||
}
|
||||
auto* decorationManager = context()->get_decoration_mgr();
|
||||
decorationManager->AddDecorationVal(mirrorVariableId,
|
||||
static_cast<Uint32>(spv::Decoration::Location), mirrorLocation);
|
||||
decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes);
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}});
|
||||
|
||||
auto* function = context()->GetFunction(entryFunctionId);
|
||||
if (function == nullptr) return false;
|
||||
const auto model = static_cast<spv::ExecutionModel>(entryPointModel);
|
||||
Bool injected = false;
|
||||
for (auto& block : *function) {
|
||||
for (auto instIter = block.begin(); instIter != block.end(); ++instIter) {
|
||||
// Geometry stages capture per emitted vertex; other stages at return.
|
||||
const Bool isInjectionSite =
|
||||
model == spv::ExecutionModel::Geometry
|
||||
? instIter->opcode() == spv::Op::OpEmitVertex
|
||||
: instIter->opcode() == spv::Op::OpReturn;
|
||||
if (!isInjectionSite) continue;
|
||||
InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone);
|
||||
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
|
||||
auto* access =
|
||||
builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
|
||||
if (access == nullptr) return injected;
|
||||
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
|
||||
if (value == nullptr) return injected;
|
||||
builder.AddStore(mirrorVariableId, value->result_id());
|
||||
injected = true;
|
||||
}
|
||||
}
|
||||
return injected;
|
||||
}
|
||||
|
||||
Vector<CapturedVarying> m_varyings;
|
||||
Vector<Uint32> m_strides;
|
||||
};
|
||||
|
||||
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
|
||||
// colour render target: the texture unit's derivative path reads outside the image's
|
||||
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
|
||||
@@ -1128,6 +1285,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||
}
|
||||
|
||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
const MG_State::GLState::ProgramObject& program) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
||||
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
|
||||
}
|
||||
Vector<Uint32> strides;
|
||||
strides.reserve(program.GetTransformFeedbackBufferCount());
|
||||
for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) {
|
||||
strides.push_back(program.GetTransformFeedbackStride(static_cast<Uint32>(i)));
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(spvtools::Optimizer::PassToken(
|
||||
MakeUnique<XfbCaptureDecoratePass>(Move(varyings), Move(strides))));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
if (input.empty()) {
|
||||
@@ -2173,7 +2365,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Apply position fixup if needed
|
||||
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
||||
TransformSpirvForVulkanPositionFix(spv, moduleSpirvs[i], flags);
|
||||
const Vector<Uint>* fixupInput = &spv;
|
||||
Vector<Uint> xfbSpirv;
|
||||
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0) {
|
||||
// Decorate BEFORE the position fixup so a captured gl_Position
|
||||
// mirror copies the shader's own (pre-remap) value.
|
||||
if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) {
|
||||
fixupInput = &xfbSpirv;
|
||||
}
|
||||
}
|
||||
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
|
||||
} else {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
||||
ExplicitLod0Sampling = 1 << 5,
|
||||
// Decorates the last vertex-processing stage's captured varyings with
|
||||
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
|
||||
// recorded while GL transform feedback is active, so plain draws keep the
|
||||
// undecorated variant.
|
||||
XfbCapture = 1 << 6,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
@@ -262,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_images.resize(imageCount, VK_NULL_HANDLE);
|
||||
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
||||
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -433,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_images.clear();
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
return m_imageContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
|
||||
m_imageContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
return m_depthStencilContentDefined[index];
|
||||
}
|
||||
|
||||
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
|
||||
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
|
||||
"Swapchain depth/stencil content index out of range");
|
||||
m_depthStencilContentDefined[index] = defined;
|
||||
}
|
||||
|
||||
void SwapchainObject::SetAllDepthStencilContentUndefined() {
|
||||
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
|
||||
m_depthStencilContentDefined[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
VkImage SwapchainObject::GetImage(Uint32 index) const {
|
||||
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
||||
return m_images[index];
|
||||
|
||||
@@ -52,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
||||
SizeT GetImageCount() const { return m_images.size(); }
|
||||
|
||||
// EGL content-validity tracking for the default framebuffer. A color
|
||||
// buffer's content is undefined once its image has been presented
|
||||
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
|
||||
// and every ancillary (depth/stencil) buffer's content is undefined
|
||||
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
|
||||
// render-pass manager turns an undefined attachment's tile load into
|
||||
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
|
||||
// garbage) and a render pass storing into an attachment sets it back
|
||||
// to defined.
|
||||
Bool IsImageContentDefined(Uint32 index) const;
|
||||
void SetImageContentDefined(Uint32 index, Bool defined);
|
||||
Bool IsDepthStencilContentDefined(Uint32 index) const;
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -77,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
||||
Vector<VkImageView> m_depthStencilImageViews;
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -204,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
||||
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
m_hasLastDescriptor = false;
|
||||
m_lastBindValid = false;
|
||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||
for (auto& memo : m_samplerResolveMemo) {
|
||||
@@ -1189,16 +1191,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferInfo.range = ubo.range;
|
||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||
} else {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||
binding, element);
|
||||
return false;
|
||||
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
||||
// uniform bytes re-use the slice already uploaded this frame.
|
||||
const Bool isGlobalUbo =
|
||||
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
|
||||
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
||||
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 uboContentVersion = program.GetUBOContentVersion();
|
||||
Bool reusedSlice = false;
|
||||
if (isGlobalUbo) {
|
||||
for (const auto& memo : m_globalUboMemo) {
|
||||
if (memo.buffer != VK_NULL_HANDLE &&
|
||||
memo.programLifetimeId == uboProgramLifetimeId &&
|
||||
memo.frameSerial == uboFrameSerial &&
|
||||
memo.uboContentVersion == uboContentVersion &&
|
||||
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
|
||||
bufferInfo.buffer = memo.buffer;
|
||||
bufferInfo.range = memo.range;
|
||||
dynOffset = static_cast<Uint32>(memo.offset);
|
||||
reusedSlice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reusedSlice) {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
|
||||
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
||||
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
||||
}
|
||||
}
|
||||
bufferInfo.buffer = slice.buffer;
|
||||
bufferInfo.range = ubo.payloadSize;
|
||||
dynOffset = static_cast<Uint32>(slice.offset);
|
||||
}
|
||||
bufferInfos.push_back(bufferInfo);
|
||||
// Dynamic offsets are consumed in binding order, then array element order,
|
||||
@@ -1337,8 +1370,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_hasLastDescriptor = cacheable;
|
||||
}
|
||||
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
|
||||
// Skip the driver call when this exact binding is already live on the
|
||||
// command buffer (see the bind-dedup shadow in the header).
|
||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
||||
if (identicalBind) {
|
||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
||||
identicalBind = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!identicalBind) {
|
||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
||||
&descriptorSet, offsetCount, dynamicOffsets.data());
|
||||
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
||||
m_lastBindValid = true;
|
||||
m_lastBindSet = descriptorSet;
|
||||
m_lastBindLayout = programObj.pipelineLayout;
|
||||
m_lastBindPoint = bindPoint;
|
||||
m_lastBindOffsetCount = offsetCount;
|
||||
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
||||
} else {
|
||||
m_lastBindValid = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -39,6 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
@@ -185,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_lastDescriptorSignature = 0;
|
||||
Bool m_hasLastDescriptor = false;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
|
||||
// layout+bind point, so a pipeline-layout switch always rebinds.
|
||||
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
|
||||
Bool m_lastBindValid = false;
|
||||
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
|
||||
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
Uint32 m_lastBindOffsetCount = 0;
|
||||
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
|
||||
|
||||
// Global-UBO transient-slice reuse: MC leaves the default uniform block
|
||||
// untouched across long GUI/terrain runs, so the per-draw re-upload of
|
||||
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
|
||||
// serial guards arena recycling; the content version guards writes).
|
||||
struct GlobalUboSliceMemo {
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint64 frameSerial = 0;
|
||||
Uint32 uboContentVersion = 0;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
VkDeviceSize range = 0;
|
||||
};
|
||||
static constexpr Uint32 kGlobalUboMemoSize = 4;
|
||||
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
|
||||
Uint32 m_globalUboMemoNext = 0;
|
||||
|
||||
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
|
||||
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
|
||||
// sampler objects with identical state still resolve to one VkSampler. This memo only
|
||||
|
||||
@@ -58,15 +58,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
const void* memoState = nullptr;
|
||||
Uint64 memoEpoch = 0;
|
||||
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
|
||||
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
|
||||
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *entry;
|
||||
}
|
||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||
return entry;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return it->second;
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
@@ -172,11 +184,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& entry = m_cache[hash];
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
@@ -205,8 +243,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert.
|
||||
++m_evictionEpoch;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
||||
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
||||
// pipelines on that minted one VkPipeline per chunk section for an
|
||||
// identical layout, defeating pipeline reuse and the per-draw memo.
|
||||
// Pipelines depend only on the layout, so they key on this instead.
|
||||
HashType layoutHash = 0;
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
||||
// a const entry reference.
|
||||
mutable Uint64 lastUsedFrameBoundary = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
Vector<VkVertexInputAttributeDescription> attributes;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
@@ -41,6 +50,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
@@ -80,9 +92,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
||||
// so INSERT invalidates references to stored values. The draw path (and
|
||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
||||
// only the unique_ptr cell moves, never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
||||
// their heap-allocated entry (stable across map insert/rehash by
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
|
||||
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
|
||||
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
|
||||
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
|
||||
// The app writes into the persistent map with no explicit flush, so its memory must
|
||||
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
|
||||
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
|
||||
@@ -465,7 +469,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it from the current shadow - MappedData() is still the shadow here because the
|
||||
// frontend adopts (and drops) the shadow only after this returns.
|
||||
DeferRelease(std::move(resource->buffer));
|
||||
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
|
||||
const VkBufferUsageFlags persistentUsage =
|
||||
kPersistentBackedUsage |
|
||||
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
|
||||
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
|
||||
resource->persistentMapped = false;
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
||||
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
Bool transientPersistentMapping = false;
|
||||
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
|
||||
// carries the transform feedback usage so capture targets can bind directly.
|
||||
Bool transformFeedbackUsageEnabled = false;
|
||||
};
|
||||
|
||||
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -127,6 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_pendingClears.erase(key);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
@@ -221,6 +223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -238,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -245,6 +249,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -260,6 +268,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||
@@ -287,6 +298,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!LockTextureLocked(key, outTexture)) {
|
||||
@@ -325,6 +339,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -345,6 +362,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -361,6 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it != m_pendingClears.end()) {
|
||||
m_pendingClears.erase(it);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -120,7 +121,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
public:
|
||||
// Lock-free probe for the consecutive-draw fast path: any pending clear
|
||||
// forces the full SetupDraw path (which materializes/consumes it).
|
||||
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
|
||||
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
|
||||
// read it before taking the lock: during draw batches the pending set
|
||||
// is almost always empty, so this turns several locked map probes per
|
||||
// draw into one relaxed load.
|
||||
std::atomic<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
|
||||
@@ -16,31 +16,21 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
|
||||
case 1:
|
||||
// GL promises "at least the requested samples", so a non-power-of-two
|
||||
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||
if (requestedSamples <= 1) {
|
||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
return true;
|
||||
case 2:
|
||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||
return true;
|
||||
case 4:
|
||||
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
|
||||
return true;
|
||||
case 8:
|
||||
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
|
||||
return true;
|
||||
case 16:
|
||||
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
|
||||
return true;
|
||||
case 32:
|
||||
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
|
||||
return true;
|
||||
case 64:
|
||||
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
|
||||
return true;
|
||||
default:
|
||||
}
|
||||
if (requestedSamples > 64) {
|
||||
return false;
|
||||
}
|
||||
Uint32 bit = 1;
|
||||
while (bit < static_cast<Uint32>(requestedSamples)) {
|
||||
bit <<= 1;
|
||||
}
|
||||
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
|
||||
@@ -166,6 +156,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, view, nullptr);
|
||||
}
|
||||
if (unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(device, unormTwinView, nullptr);
|
||||
}
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(allocator, image, allocation);
|
||||
}
|
||||
@@ -173,6 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
image = VK_NULL_HANDLE;
|
||||
allocation = nullptr;
|
||||
view = VK_NULL_HANDLE;
|
||||
unormTwinView = VK_NULL_HANDLE;
|
||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
format = VK_FORMAT_UNDEFINED;
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -231,10 +225,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
|
||||
m_deferredRenderbufferReleases.push_back(
|
||||
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
resource.view = VK_NULL_HANDLE;
|
||||
resource.unormTwinView = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
||||
@@ -249,6 +245,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (release.view != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.view, nullptr);
|
||||
}
|
||||
if (release.unormTwinView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
|
||||
}
|
||||
if (release.image != VK_NULL_HANDLE) {
|
||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
||||
}
|
||||
@@ -304,7 +303,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -314,6 +353,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
|
||||
// GL allows the implementation to allocate more samples than requested
|
||||
// (glRenderbufferStorageMultisample only promises "at least"), and devices
|
||||
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
|
||||
// nearest supported count for this format.
|
||||
if (renderbuffer->GetSamples() > 0) {
|
||||
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
|
||||
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
|
||||
VkImageFormatProperties formatProperties{};
|
||||
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
|
||||
&formatProperties) == VK_SUCCESS) {
|
||||
supported = formatProperties.sampleCounts;
|
||||
}
|
||||
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
|
||||
}
|
||||
const VkSampleCountFlags supported = supportedIt->second;
|
||||
if ((supported & sampleCount) == 0) {
|
||||
// Smallest supported count above the request, else the largest below it.
|
||||
Uint32 rounded = 0;
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rounded == 0) {
|
||||
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rounded != 0) {
|
||||
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||
const Bool needsCreate =
|
||||
resource.image == VK_NULL_HANDLE ||
|
||||
@@ -351,6 +430,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.usage = imageUsage;
|
||||
imageInfo.samples = sampleCount;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
|
||||
// is disabled, which needs a format-reinterpreting second view.
|
||||
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
|
||||
if (hasUnormTwin) {
|
||||
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
@@ -385,6 +470,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
||||
"vkCreateImageView(renderbuffer)");
|
||||
if (hasUnormTwin) {
|
||||
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
|
||||
"vkCreateImageView(renderbuffer unorm twin)");
|
||||
}
|
||||
|
||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
resource.format = format;
|
||||
@@ -481,12 +571,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
|
||||
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
|
||||
Bool includeDefaultFboDepthStencil) {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
}
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
auto readBuffer = fbo.GetReadBuffer();
|
||||
@@ -560,9 +656,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachment <= FramebufferAttachmentType::BackRight);
|
||||
if (isDefaultColorAttachment) {
|
||||
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// Content validity feeds the attachment's loadOp (see the
|
||||
// creation path), so it must key the cache as well.
|
||||
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
} else if (attachment == FramebufferAttachmentType::Depth ||
|
||||
attachment == FramebufferAttachmentType::Stencil) {
|
||||
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
||||
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
@@ -617,14 +721,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
combineFramebufferAttachmentObjHash(drawbuf);
|
||||
}
|
||||
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||
// entirely, so it must hash differently from the depth-full flavor.
|
||||
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
|
||||
if (depthStencilIncluded) {
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex) {
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
// depth attachment when the caller needs it, when a depth/stencil clear is
|
||||
// pending, or when the active pass already carries it (escalate-only, so
|
||||
// alternating depth-less draws never split an established depth pass).
|
||||
Bool includeDefaultFboDepthStencil = true;
|
||||
if (fbo.IsDefaultFramebuffer()) {
|
||||
Bool activeDefaultHasDepthStencil = false;
|
||||
if (const auto* active = GetActiveRenderPass()) {
|
||||
Bool activeIsSwapchainPass = false;
|
||||
Bool activeHasSwapchainDepthStencil = false;
|
||||
for (const auto& tracked : active->trackedAttachmentLayouts) {
|
||||
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
|
||||
activeHasSwapchainDepthStencil |=
|
||||
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
|
||||
}
|
||||
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
|
||||
}
|
||||
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const Bool pendingDepthStencilClear =
|
||||
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultDepthAtt) ||
|
||||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
|
||||
HasPendingRenderbufferClear(defaultStencilAtt);
|
||||
includeDefaultFboDepthStencil =
|
||||
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
|
||||
}
|
||||
|
||||
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
|
||||
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
for (auto attachment : drawBuffers) {
|
||||
@@ -674,6 +813,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
|
||||
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
@@ -682,7 +822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
|
||||
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
|
||||
if (activeRenderPass != nullptr &&
|
||||
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
||||
!hasPendingClearOnFramebuffer()) {
|
||||
@@ -699,10 +839,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
@@ -789,8 +930,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
rbDesc.format = rbResource->format;
|
||||
rbDesc.format = rbAttachmentFormat;
|
||||
rbDesc.samples = rbResource->sampleCount;
|
||||
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
||||
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
||||
@@ -825,7 +970,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.finalLayout = rbDesc.finalLayout,
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbResource->view);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
|
||||
@@ -894,6 +1040,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
||||
"GetOrCreateRenderPass: swapchain image index out of range");
|
||||
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
||||
// EGL: a presented color buffer's content is undefined when its
|
||||
// image comes back around (EGL_BUFFER_DESTROYED, the default
|
||||
// swap behaviour) - skip the tile load instead of reloading
|
||||
// stale pixels nobody may rely on.
|
||||
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
|
||||
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::SwapchainColor,
|
||||
.swapchainImageIndex = swapchainImageIndex,
|
||||
@@ -907,12 +1060,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = textureResource->format;
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = att.GetTexture(),
|
||||
.textureRaw = att.GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = desc.finalLayout,
|
||||
});
|
||||
@@ -976,6 +1132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
||||
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
|
||||
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
|
||||
// and their content is undefined anyway (EGL swap), so drop the attachment
|
||||
// and its whole tile load + store.
|
||||
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
|
||||
selectedDepthStencilAttachment = nullptr;
|
||||
}
|
||||
const Bool hasDistinctDepthAndStencilAttachments =
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
@@ -994,6 +1156,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
||||
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
|
||||
// undefined after a swap, so the first default-FBO pass of a frame can
|
||||
// skip the depth/stencil tile load outright.
|
||||
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
depthAttachmentDescription.flags = 0;
|
||||
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
Int depthAttachmentId = 0;
|
||||
@@ -1077,6 +1245,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
.target = TrackedAttachmentTarget::Texture,
|
||||
.texture = selectedDepthStencilAttachment->GetTexture(),
|
||||
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
||||
.textureMipLevel = attachmentMipLevel,
|
||||
.finalLayout = depthAttachmentDescription.finalLayout,
|
||||
});
|
||||
@@ -1122,6 +1291,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
|
||||
|
||||
// Declare only the used colour-reference span. The GL draw-buffer array
|
||||
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
|
||||
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
|
||||
// per-pixel render-backend/export path from the DECLARED count, so every
|
||||
// fragment of every pass paid the 8-target export cost (measured on
|
||||
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
|
||||
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
|
||||
// Interior GL_NONE holes keep their slots so fragment-output locations
|
||||
// still line up; a fragment output at a location past the trimmed count
|
||||
// is discarded, which is exactly GL's semantic for writing to a draw
|
||||
// buffer set to GL_NONE.
|
||||
while (!colorAttachmentRefs.empty() &&
|
||||
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
|
||||
colorAttachmentRefs.pop_back();
|
||||
}
|
||||
|
||||
// Subpass
|
||||
VkSubpassDescription subpassDesc;
|
||||
subpassDesc.flags = 0;
|
||||
@@ -1330,6 +1515,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassBeginInfo.pClearValues = clearValues.data();
|
||||
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
// Pre-pass stream bookkeeping: this pass's attachment images are now
|
||||
// referenced by the open frame recording.
|
||||
if (s_textureManager != nullptr) {
|
||||
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
||||
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
||||
if (const auto texture = tracked.texture.lock()) {
|
||||
s_textureManager->StampTextureRecordingUse(texture.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||
if (pending.hasInlinePayload) {
|
||||
if (s_renderPassManager != nullptr) {
|
||||
@@ -1382,11 +1578,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case TrackedAttachmentTarget::SwapchainColor:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
// The pass stored into the attachment: its content is defined
|
||||
// until the image is next presented.
|
||||
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
trackedAttachment.finalLayout);
|
||||
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||
|
||||
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct TrackedAttachmentLayoutInfo {
|
||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||
// May dangle once the texture dies - compare only, never dereference.
|
||||
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
Uint32 textureMipLevel = 0;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
@@ -188,8 +193,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool includePendingClear = true);
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
|
||||
Bool includePendingClear = true,
|
||||
Bool includeDefaultFboDepthStencil = true);
|
||||
// drawUsesDepthStencil: whether the operation about to run inside the pass
|
||||
// reads or writes the depth/stencil buffer (depth test or stencil test
|
||||
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
|
||||
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
|
||||
// default-FBO pass whose draws provably never touch depth/stencil is
|
||||
// created WITHOUT the depth attachment - on a tiler that skips the whole
|
||||
// depth tile load AND store. The flavor only escalates: once a pass with
|
||||
// depth is active, later depth-less draws keep using it, and a depth-using
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -219,6 +238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// image recreation.
|
||||
Uint64 m_renderbufferImageEpoch = 1;
|
||||
|
||||
public:
|
||||
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
|
||||
// snapshots include it so an attachment respecify forces a re-resolve.
|
||||
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
|
||||
|
||||
private:
|
||||
|
||||
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
|
||||
// framebuffer state is provably unchanged since the last resolution, the active render pass
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
@@ -231,6 +257,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
// Whether the memoized entry carries a depth/stencil attachment; a
|
||||
// default-FBO resolution whose effective depth request differs must
|
||||
// miss the memo (the depth-less/depth-full flavors hash differently).
|
||||
Bool m_rpFastHadDepthStencil = false;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
@@ -244,6 +274,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
// UNORM reinterpretation of an sRGB image, used as the attachment view while
|
||||
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
@@ -277,12 +310,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
|
||||
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
|
||||
@@ -120,31 +120,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
switch (requestedSamples) {
|
||||
case 1:
|
||||
// GL promises "at least the requested samples", so a non-power-of-two
|
||||
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||
if (requestedSamples <= 1) {
|
||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
return true;
|
||||
case 2:
|
||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||
return true;
|
||||
case 4:
|
||||
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
|
||||
return true;
|
||||
case 8:
|
||||
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
|
||||
return true;
|
||||
case 16:
|
||||
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
|
||||
return true;
|
||||
case 32:
|
||||
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
|
||||
return true;
|
||||
case 64:
|
||||
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
|
||||
return true;
|
||||
default:
|
||||
}
|
||||
if (requestedSamples > 64) {
|
||||
return false;
|
||||
}
|
||||
Uint32 bit = 1;
|
||||
while (bit < static_cast<Uint32>(requestedSamples)) {
|
||||
bit <<= 1;
|
||||
}
|
||||
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||
@@ -607,7 +597,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkTextureManager::Shutdown() {
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
ReclaimCompletedUploads(/*waitAll=*/true);
|
||||
}
|
||||
DestroyDeferredReleases();
|
||||
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
|
||||
m_textureResources.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_storageImageTextures.clear();
|
||||
@@ -629,6 +623,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frameIndex, m_deferredViewReleases.size());
|
||||
m_currentFrameIndex = frameIndex;
|
||||
CollectDeferredReleases(frameIndex);
|
||||
ReclaimCompletedUploads();
|
||||
|
||||
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
|
||||
// latency for dead textures regardless of draw traffic — workloads that churn
|
||||
@@ -659,6 +654,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_storageImageTextures.erase(identity);
|
||||
// Invalidate every cross-draw sampled-texture memo: the erased
|
||||
// resource's address may be reused by a future emplace.
|
||||
++m_resourceEraseEpoch;
|
||||
}
|
||||
|
||||
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -714,45 +712,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
auto aliveIt = m_aliveObjects.find(identity);
|
||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||
EraseTrackedTexture(aliveIt->first);
|
||||
aliveIt = m_aliveObjects.end();
|
||||
}
|
||||
|
||||
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
|
||||
// aliases can only come into existence through an address reuse, which by
|
||||
// construction introduces a new identity. Doing this unconditionally made every
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
aliveTexture = liveTexture;
|
||||
} else {
|
||||
// The name lookup legally fails while the object is alive: the name was
|
||||
// deleted with the texture still attached to an FBO (the attachment's
|
||||
// SharedPtr keeps it alive), or the name was reused by a new texture, or
|
||||
// this is a default texture object (name 0 lives outside the name map).
|
||||
// Register through the object's own control block so the resource created
|
||||
// below still participates in weak-expiry GC instead of becoming an
|
||||
// orphan no reclamation path can reach until Shutdown.
|
||||
aliveTexture = texture.weak_from_this();
|
||||
}
|
||||
if (!aliveTexture.expired()) {
|
||||
m_aliveObjects[identity] = Move(aliveTexture);
|
||||
PruneStaleTextureAliases(&texture);
|
||||
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
|
||||
// lookups and the (re)registration path for repeat-bound textures.
|
||||
TextureResource* resourcePtr = nullptr;
|
||||
for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) {
|
||||
const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i];
|
||||
if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId &&
|
||||
memo.eraseEpoch == m_resourceEraseEpoch) {
|
||||
resourcePtr = memo.resource;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
TextureResource initial{};
|
||||
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
|
||||
it = insertIt;
|
||||
if (resourcePtr == nullptr) {
|
||||
auto aliveIt = m_aliveObjects.find(identity);
|
||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||
EraseTrackedTexture(aliveIt->first);
|
||||
aliveIt = m_aliveObjects.end();
|
||||
}
|
||||
|
||||
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
|
||||
// aliases can only come into existence through an address reuse, which by
|
||||
// construction introduces a new identity. Doing this unconditionally made every
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
aliveTexture = liveTexture;
|
||||
} else {
|
||||
// The name lookup legally fails while the object is alive: the name was
|
||||
// deleted with the texture still attached to an FBO (the attachment's
|
||||
// SharedPtr keeps it alive), or the name was reused by a new texture, or
|
||||
// this is a default texture object (name 0 lives outside the name map).
|
||||
// Register through the object's own control block so the resource created
|
||||
// below still participates in weak-expiry GC instead of becoming an
|
||||
// orphan no reclamation path can reach until Shutdown.
|
||||
aliveTexture = texture.weak_from_this();
|
||||
}
|
||||
if (!aliveTexture.expired()) {
|
||||
m_aliveObjects[identity] = Move(aliveTexture);
|
||||
PruneStaleTextureAliases(&texture);
|
||||
}
|
||||
}
|
||||
|
||||
auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
TextureResource initial{};
|
||||
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
|
||||
it = insertIt;
|
||||
}
|
||||
resourcePtr = &(it->second);
|
||||
m_syncedTextureMemo[m_syncedTextureMemoNext] =
|
||||
SyncedTextureMemoEntry{&texture, identity.lifetimeId, m_resourceEraseEpoch, resourcePtr};
|
||||
m_syncedTextureMemoNext = (m_syncedTextureMemoNext + 1) % kSyncedTextureMemoSize;
|
||||
}
|
||||
|
||||
if (!SyncTexture(texture, it->second)) {
|
||||
if (!SyncTexture(texture, *resourcePtr)) {
|
||||
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
@@ -766,11 +782,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (!recorded) {
|
||||
m_drawSyncedThisDraw.push_back({identity, &(it->second)});
|
||||
m_drawSyncedThisDraw.push_back({identity, resourcePtr});
|
||||
}
|
||||
}
|
||||
|
||||
return &(it->second);
|
||||
return resourcePtr;
|
||||
}
|
||||
|
||||
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||
@@ -814,7 +830,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
|
||||
|
||||
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||
viewType == resource->viewType) {
|
||||
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||
}
|
||||
|
||||
@@ -823,6 +844,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.baseArrayLayer = baseArrayLayer,
|
||||
.layerCount = layerCount,
|
||||
.viewType = viewType,
|
||||
.viewFormat = attachmentFormat,
|
||||
};
|
||||
auto it = resource->attachmentViews.find(key);
|
||||
if (it == resource->attachmentViews.end()) {
|
||||
@@ -833,7 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return attachmentView;
|
||||
}
|
||||
|
||||
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
|
||||
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
|
||||
mipLevel, 1, baseArrayLayer, layerCount);
|
||||
if (attachmentView == VK_NULL_HANDLE) {
|
||||
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
|
||||
@@ -1049,6 +1071,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return view;
|
||||
}
|
||||
|
||||
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
if (it != m_textureResources.end()) {
|
||||
it->second.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -1076,6 +1108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
||||
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
|
||||
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
|
||||
StampResourceRecordingUse(resource);
|
||||
|
||||
if (resource.layout != newLayout && resource.mipLevels > 1) {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -1160,6 +1194,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -1189,6 +1225,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -1406,11 +1444,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
||||
TextureResource &resource) {
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkFormat format = formatInfo.format;
|
||||
VkFormat format = formatInfo.format;
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
|
||||
return false;
|
||||
}
|
||||
// X8_D24 lacks optimal-tiling support on several drivers (lavapipe included);
|
||||
// D32_SFLOAT holds every 24-bit depth value exactly, and the upload path
|
||||
// converts the shadow words to float (see the pure-depth branch below).
|
||||
if (format == VK_FORMAT_X8_D24_UNORM_PACK32) {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
constexpr VkFormatFeatureFlags kDepthAttachmentAndSample =
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) {
|
||||
format = VK_FORMAT_D32_SFLOAT;
|
||||
}
|
||||
}
|
||||
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
|
||||
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
|
||||
return false;
|
||||
@@ -1420,8 +1470,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
|
||||
// A texture that has only ever defined level 0 gets a single-level backing
|
||||
// (ANGLE's model). Preallocating the full chain put every render target
|
||||
// onto Adreno's multi-mip image layout and grew each texture by a third
|
||||
// for levels most textures never define. Once a second level is defined
|
||||
// the backing is recreated ONE time with the full chain (the
|
||||
// preserve-copy path below carries the pixels over), so sequentially-
|
||||
// defined atlas mips do not recreate per level, and glGenerateMipmap -
|
||||
// which defines every level before syncing - works unchanged.
|
||||
const Uint32 backingMipLevels =
|
||||
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
|
||||
isMultisampleTexture ? 1u
|
||||
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
|
||||
TextureShapeInfo shapeInfo{};
|
||||
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
||||
MOBILEGL_ASSERT(supportedShape,
|
||||
@@ -1473,6 +1532,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
|
||||
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
|
||||
// views - multisample sRGB render targets included.
|
||||
if (ResolveSrgbAttachmentWriteFormat(format, false) != format &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageUsageFlags desiredUsage =
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
@@ -1485,6 +1552,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
|
||||
// Round a multisample request up to a count the device supports for this
|
||||
// format (GL only promises "at least"), mirroring the renderbuffer path.
|
||||
if (isMultisampleTexture && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) {
|
||||
auto supportedIt = m_multisampleCountsByFormat.find(format);
|
||||
if (supportedIt == m_multisampleCountsByFormat.end()) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType,
|
||||
VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags,
|
||||
&imageFormatProperties) == VK_SUCCESS) {
|
||||
supported = imageFormatProperties.sampleCounts;
|
||||
}
|
||||
supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first;
|
||||
}
|
||||
const VkSampleCountFlags supported = supportedIt->second;
|
||||
if ((supported & resolvedSampleCount) == 0) {
|
||||
Uint32 rounded = 0;
|
||||
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT;
|
||||
bit <<= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rounded == 0) {
|
||||
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1; bit != 0; bit >>= 1) {
|
||||
if ((supported & bit) != 0) {
|
||||
rounded = bit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rounded != 0) {
|
||||
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||
@@ -1614,8 +1719,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaAllocationCreateInfo allocationInfo{};
|
||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
|
||||
"vmaCreateImage(texture)");
|
||||
// Soft failure like the unsupported-sample-count path above: a driver can pass the
|
||||
// vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse the creation
|
||||
// (e.g. multisampled depth on lavapipe); the texture simply stays unbacked.
|
||||
const VkResult createImageResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||
if (createImageResult != VK_SUCCESS) {
|
||||
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
"mips=%u samples=%d format=%d",
|
||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||
resource.image = VK_NULL_HANDLE;
|
||||
resource.allocation = nullptr;
|
||||
return false;
|
||||
}
|
||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||
|
||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -1683,6 +1801,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_deferredViewReleases[frameIndex].clear();
|
||||
}
|
||||
|
||||
void VkTextureManager::ReclaimCompletedUploads(Bool waitAll) {
|
||||
if (m_pendingUploadReclaims.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SizeT completed = 0;
|
||||
for (; completed < m_pendingUploadReclaims.size(); ++completed) {
|
||||
PendingUploadReclaim& entry = m_pendingUploadReclaims[completed];
|
||||
if (waitAll) {
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &entry.fence, VK_TRUE, UINT64_MAX),
|
||||
"vkWaitForFences(texture upload reclaim)");
|
||||
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
vkDestroyFence(m_device, entry.fence, nullptr);
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &entry.commandBuffer);
|
||||
vmaDestroyBuffer(m_allocator, entry.stagingBuffer, entry.stagingAllocation);
|
||||
}
|
||||
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
|
||||
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
|
||||
}
|
||||
|
||||
void VkTextureManager::DestroyDeferredReleases() {
|
||||
for (auto& deferredReleases : m_deferredReleases) {
|
||||
deferredReleases.clear();
|
||||
@@ -1884,17 +2024,119 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy
|
||||
// aspectMask must have exactly one bit set). Until that is implemented, skip the upload
|
||||
// instead of recording an invalid command buffer that kills the process.
|
||||
// Combined depth-stencil images need per-aspect copies (VkBufferImageCopy aspectMask
|
||||
// must have exactly one bit set), so de-interleave the shadow's GL wire format into
|
||||
// a depth plane followed by a stencil plane per upload item.
|
||||
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
||||
MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d",
|
||||
mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
const Bool isCombinedDepthStencil =
|
||||
(uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (isCombinedDepthStencil) {
|
||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
||||
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
stagingSize = 0;
|
||||
for (auto& item : uploadItems) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
|
||||
MOBILEGL_ASSERT(shadowTexelSize == 4 || shadowTexelSize == 8,
|
||||
"UploadDirtyMipLevels: unexpected depth-stencil shadow texel size %zu for textureId=%d",
|
||||
shadowTexelSize, mipmapTexture.GetExternalIndex());
|
||||
// Depth plane as the aspect's buffer-copy format (32-bit word for
|
||||
// D24: low 24 bits; float for D32F), then one stencil byte per texel.
|
||||
Vector<Uint8> deinterleaved(texelCount * 4 + texelCount);
|
||||
Uint8* depthPlane = deinterleaved.data();
|
||||
Uint8* stencilPlane = deinterleaved.data() + texelCount * 4;
|
||||
const Uint8* shadow = static_cast<const Uint8*>(item.source);
|
||||
for (SizeT t = 0; t < texelCount; ++t) {
|
||||
if (shadowTexelSize == 8) {
|
||||
// GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word
|
||||
// with stencil in its low 8 bits.
|
||||
float depthValue;
|
||||
Uint32 stencilWord;
|
||||
std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue));
|
||||
std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord));
|
||||
if (srcIsD32FS8) {
|
||||
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
|
||||
} else {
|
||||
const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f);
|
||||
const Uint32 depthWord = static_cast<Uint32>(clamped * 16777215.0f + 0.5f);
|
||||
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
|
||||
}
|
||||
stencilPlane[t] = static_cast<Uint8>(stencilWord & 0xFFu);
|
||||
} else {
|
||||
// GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8.
|
||||
Uint32 packed;
|
||||
std::memcpy(&packed, shadow + t * 4, sizeof(packed));
|
||||
if (srcIsD24S8) {
|
||||
const Uint32 depthWord = packed >> 8;
|
||||
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
|
||||
} else {
|
||||
const float depthValue = static_cast<float>(packed >> 8) / 16777215.0f;
|
||||
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
|
||||
}
|
||||
stencilPlane[t] = static_cast<Uint8>(packed & 0xFFu);
|
||||
}
|
||||
}
|
||||
item.expandedData = Move(deinterleaved);
|
||||
item.source = item.expandedData.data();
|
||||
item.uploadByteSize = item.expandedData.size();
|
||||
item.offset = stagingSize;
|
||||
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Pure-depth images whose canonical shadow layout differs from the image texel
|
||||
// layout (the shadow keeps a full-scale 16/32-bit unorm word or a float; the
|
||||
// image may be X8_D24 or a D32_SFLOAT fallback) convert per texel here.
|
||||
if (uploadAspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
const TextureInternalFormat depthInternal = mipmapTexture.GetFormat();
|
||||
const Bool shadowIsFloat = depthInternal == TextureInternalFormat::DepthComponent32F;
|
||||
const Bool dstIsFloat = outResource.format == VK_FORMAT_D32_SFLOAT;
|
||||
const Bool dstIsD24Word = outResource.format == VK_FORMAT_X8_D24_UNORM_PACK32;
|
||||
stagingSize = 0;
|
||||
for (auto& item : uploadItems) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
|
||||
const Bool needsConversion =
|
||||
(dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat);
|
||||
if (needsConversion) {
|
||||
Vector<Uint8> converted(texelCount * 4);
|
||||
const Uint8* shadow = static_cast<const Uint8*>(item.source);
|
||||
for (SizeT t = 0; t < texelCount; ++t) {
|
||||
Uint32 wide = 0;
|
||||
if (shadowTexelSize == 2) {
|
||||
Uint16 raw = 0;
|
||||
std::memcpy(&raw, shadow + t * 2, sizeof(raw));
|
||||
wide = (static_cast<Uint32>(raw) << 16) | raw;
|
||||
} else {
|
||||
std::memcpy(&wide, shadow + t * 4, sizeof(wide));
|
||||
}
|
||||
if (dstIsFloat) {
|
||||
const float value = static_cast<float>(static_cast<double>(wide) / 4294967295.0);
|
||||
std::memcpy(converted.data() + t * 4, &value, sizeof(value));
|
||||
} else { // X8_D24: depth in the low 24 bits of a 32-bit word
|
||||
const Uint32 word = wide >> 8;
|
||||
std::memcpy(converted.data() + t * 4, &word, sizeof(word));
|
||||
}
|
||||
}
|
||||
item.expandedData = Move(converted);
|
||||
item.source = item.expandedData.data();
|
||||
item.uploadByteSize = item.expandedData.size();
|
||||
}
|
||||
item.offset = stagingSize;
|
||||
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||
@@ -1947,7 +2189,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
// copy must address layerCount, not imageExtent.depth (which is invalid
|
||||
// for 2D images and silently dropped every layer past the first).
|
||||
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
for (const auto& item : uploadItems) {
|
||||
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = item.offset;
|
||||
copy.bufferRowLength = 0;
|
||||
@@ -1955,10 +2204,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
copy.imageSubresource.aspectMask = aspectMask;
|
||||
copy.imageSubresource.mipLevel = item.level;
|
||||
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
||||
copy.imageSubresource.layerCount = 1;
|
||||
copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1;
|
||||
copy.imageOffset = {0, 0, 0};
|
||||
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
||||
item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u};
|
||||
depthSelectsArrayLayer ? 1u : depthOrLayers};
|
||||
if (isCombinedDepthStencil) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
|
||||
VkBufferImageCopy depthCopy = copy;
|
||||
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
VkBufferImageCopy stencilCopy = copy;
|
||||
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
stencilCopy.bufferOffset = item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
||||
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
||||
continue;
|
||||
}
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, ©);
|
||||
}
|
||||
@@ -1989,11 +2252,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
||||
|
||||
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
|
||||
vkDestroyFence(m_device, uploadFence, nullptr);
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
|
||||
|
||||
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
|
||||
// Do NOT wait the fence here: this submit sits behind the previous
|
||||
// frame's rendering on the queue, so a synchronous wait stalls the CPU
|
||||
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
|
||||
// with animated textures. Ordering against the current frame's draws is
|
||||
// already guaranteed (its command buffer is submitted later, at
|
||||
// present), so only the transient objects need to survive execution;
|
||||
// park them until the fence signals.
|
||||
m_pendingUploadReclaims.push_back({uploadFence, commandBuffer, stagingBuffer, stagingAllocation});
|
||||
ReclaimCompletedUploads();
|
||||
// Backstop for pathological upload storms: bound in-flight staging
|
||||
// memory by blocking on the oldest upload only once the list is deep.
|
||||
constexpr SizeT kMaxPendingTextureUploads = 16;
|
||||
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
|
||||
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
|
||||
"vkWaitForFences(texture upload backstop)");
|
||||
ReclaimCompletedUploads();
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
MGLOG_D("%s: texture upload cmd failed", __func__);
|
||||
|
||||
@@ -28,6 +28,9 @@ public:
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
@@ -64,12 +67,16 @@ public:
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
// May differ from the image format: sRGB images attach through their UNORM
|
||||
// twin while GL_FRAMEBUFFER_SRGB is disabled.
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
|
||||
Bool operator==(const AttachmentViewKey& other) const {
|
||||
return mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer &&
|
||||
layerCount == other.layerCount &&
|
||||
viewType == other.viewType;
|
||||
viewType == other.viewType &&
|
||||
viewFormat == other.viewFormat;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,6 +87,8 @@ public:
|
||||
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
@@ -172,6 +181,13 @@ public:
|
||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||
Bool storageUsageResolved = false;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
|
||||
// command referencing this image that was recorded into the CURRENT frame
|
||||
// command buffer. An image untouched by the open recording may have its
|
||||
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -207,6 +223,7 @@ public:
|
||||
std::swap(this->usageFlags, that.usageFlags);
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
}
|
||||
@@ -307,6 +324,21 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
// recording; a resource whose stamp does not match was not referenced by
|
||||
// any command in the open recording, so its out-of-pass work may safely
|
||||
// execute ahead of the whole recording (in the pre command buffer).
|
||||
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
|
||||
void StampResourceRecordingUse(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
// Map-lookup variant for callers that only hold the GL texture object.
|
||||
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
@@ -364,6 +396,9 @@ public:
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
TextureResource &outResource);
|
||||
@@ -394,6 +429,11 @@ private:
|
||||
void DeferViewRelease(VkImageView view);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyDeferredReleases();
|
||||
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
||||
// upload whose fence has signaled (submission order = completion order on
|
||||
// the single queue, so the scan stops at the first still-pending entry).
|
||||
// waitAll blocks on every entry - Shutdown's drain.
|
||||
void ReclaimCompletedUploads(Bool waitAll = false);
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
@@ -423,6 +463,23 @@ private:
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
||||
// are resolved on every draw, so cache their resource pointers and skip the
|
||||
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
||||
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
||||
// every memo entry must match. SyncTexture still runs on memo hits, so
|
||||
// content/param freshness is unaffected. A dead-then-reused texture address
|
||||
// cannot false-hit: the new object carries a new lifetime id.
|
||||
struct SyncedTextureMemoEntry {
|
||||
const MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
Uint64 eraseEpoch = 0;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
||||
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
||||
Uint32 m_syncedTextureMemoNext = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
@@ -430,7 +487,21 @@ private:
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||
// behind the queue serialized the CPU against the previous frame's GPU
|
||||
// work every time an animated atlas re-uploaded). Their transient objects
|
||||
// are parked here and reclaimed once the upload fence signals.
|
||||
struct PendingUploadReclaim {
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||
VmaAllocation stagingAllocation = nullptr;
|
||||
};
|
||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum indexType = GL_UNSIGNED_SHORT;
|
||||
SizeT indexByteOffset = 0;
|
||||
SizeT indexByteSize = 0;
|
||||
// Interpret indexByteOffset as a raw client pointer even when an element
|
||||
// array buffer is bound (backend-synthesized index lists, e.g. the
|
||||
// GL_LINE_LOOP -> LINE_STRIP rewrite).
|
||||
Bool forceClientMemory = false;
|
||||
};
|
||||
|
||||
struct DrawIndexedCmd {
|
||||
@@ -151,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView = nullptr);
|
||||
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
|
||||
// resolved draw configuration; the next draw whose cheap version/identity
|
||||
// checks all match skips the resolution half (LOD probe, sampled-set
|
||||
// walk, render-pass and pipeline resolution) and jumps straight to the
|
||||
// per-draw tail. Returns false (leaving no side effects that the full
|
||||
// path cannot redo idempotently) whenever anything might have changed.
|
||||
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
|
||||
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||
const RenderPassEntry& compatibleRenderPassEntry);
|
||||
|
||||
@@ -188,6 +200,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
|
||||
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
|
||||
// CPU repacking into the requested client layout).
|
||||
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||
// expects command recording to be active and any render pass already ended.
|
||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels);
|
||||
// Same-extent depth blit between images of different depth formats: host
|
||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
|
||||
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
|
||||
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
@@ -273,6 +304,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkTimerQueryManager::TimestampRecord& end) const;
|
||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||
|
||||
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
|
||||
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
|
||||
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
|
||||
// unsupported) when the device lacks it.
|
||||
Bool StartOcclusionQueryCapture();
|
||||
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
|
||||
// Flushes pending commands, waits, sums the slots, and recycles them.
|
||||
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
||||
@@ -391,6 +431,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* m_platformDisplay = nullptr;
|
||||
void* m_platformLibrary = nullptr;
|
||||
void* m_platformCloseDisplay = nullptr;
|
||||
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
VulkanRendererConfig m_config;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
@@ -443,6 +492,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 stride);
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
||||
// draws within one glBeginTransformFeedback append GL-style.
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
// Non-zero while inside a GL Begin/End with at least one captured draw
|
||||
// recorded; selects counter-buffer resume on the next captured draw.
|
||||
Bool m_xfbCountersValid = false;
|
||||
Uint64 m_xfbLastSeenGeneration = 0;
|
||||
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
||||
// when GL transform feedback is active; binds capture buffers on demand.
|
||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
||||
// query is active. Returns whether a slot was begun (End must mirror it).
|
||||
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
|
||||
Bool m_occlusionQueryPreciseEnabled = false;
|
||||
Bool m_hostQueryResetEnabled = false;
|
||||
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
|
||||
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kOcclusionQuerySlots = 8192;
|
||||
Uint32 m_occlusionSlotCursor = 0;
|
||||
Bool m_occlusionCaptureActive = false;
|
||||
Vector<Uint32> m_occlusionActiveSlots;
|
||||
// Transform feedback primitive queries: one pool slot per captured draw yields
|
||||
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
|
||||
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
|
||||
// unlike the CPU fallback accounting.
|
||||
Bool m_xfbQueriesSupported = false;
|
||||
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
|
||||
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
|
||||
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
|
||||
static constexpr Uint32 kXfbQuerySlots = 8192;
|
||||
Uint32 m_xfbQuerySlotCursor = 0;
|
||||
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
|
||||
public:
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
|
||||
VkBufferManager m_bufferManager;
|
||||
@@ -455,14 +557,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
||||
// state is unchanged from the previous draw. The key provably covers every pipeline field.
|
||||
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
||||
Bool m_lastPipelineValid = false;
|
||||
GLenum m_lastPipelineMode = 0;
|
||||
Uint64 m_lastPipelineProgramHash = 0;
|
||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
||||
Uint m_lastPipelineRenderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
||||
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||
// single-entry memo thrashed on draw sequences that alternate a few
|
||||
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||
// payload-hash lookup per draw; eight entries cover such working sets
|
||||
// while keeping the hit path a trivial linear scan.
|
||||
struct PipelineMemoEntry {
|
||||
GLenum mode = 0;
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint renderStateVersion = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 0;
|
||||
Uint32 m_pipelineMemoNext = 0;
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed.
|
||||
void InvalidatePipelineMemo() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
}
|
||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||
UniquePtr<ProgramFactory> m_programFactory;
|
||||
UniquePtr<UniformManager> m_uniformManager;
|
||||
@@ -491,9 +609,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
||||
|
||||
// Memo for the per-draw explicit-LOD-0 eligibility probe
|
||||
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
|
||||
// sampled-set memo, plus the sampled textures' params-version sum so a
|
||||
// level-range or filter change re-probes. On a hit the resolved
|
||||
// transform flags are reused, which also collapses the two
|
||||
// GetOrCreateProgram lookups into one.
|
||||
Bool m_lastLodDecisionValid = false;
|
||||
Uint64 m_lastLodProgramLifetimeId = 0;
|
||||
Uint32 m_lastLodProgramVersion = 0;
|
||||
Uint64 m_lastLodBindGeneration = 0;
|
||||
Uint64 m_lastLodParamsSum = 0;
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
// protected by the command-buffer-boundary reset plus the mid-frame
|
||||
// pipeline-destruction resets, and monotonic epochs guard everything
|
||||
// that can be destroyed or recreated between draws.
|
||||
struct SetupDrawSnapshot {
|
||||
Bool valid = false;
|
||||
Uint8 aspects = 0;
|
||||
GLenum mode = 0;
|
||||
Uint64 programLifetimeId = 0;
|
||||
Uint32 programVersion = 0;
|
||||
const void* vao = nullptr;
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
Uint16 fboVersion = 0;
|
||||
Bool drawFboIsDefault = false;
|
||||
Uint renderStateVersion = 0;
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
Uint64 textureImageEpoch = 0;
|
||||
Uint64 renderbufferImageEpoch = 0;
|
||||
Uint64 sampledContentSum = 0;
|
||||
Uint64 sampledParamsSum = 0;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
||||
|
||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||
// draw call and must not allocate.
|
||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||
// first sampled-texture loop: the resolved backend resources, so the
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
|
||||
@@ -52,19 +52,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
|
||||
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
|
||||
// attachment view (and render pass format) therefore drops to the UNORM twin
|
||||
// whenever the capability is off. Sampled views keep the sRGB format (decode on
|
||||
// sample is unconditional in GL).
|
||||
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
|
||||
if (framebufferSrgbEnabled) return format;
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
default:
|
||||
return format;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||
// call: appending its format to the base format while its arguments precede the base
|
||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
if (_vk_verify_result != VK_SUCCESS) { \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
|
||||
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
|
||||
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||
_vk_verify_result, __FILE__, __LINE__); \
|
||||
} while (0)
|
||||
|
||||
#define XXHASH_VERIFY(expr, ...) \
|
||||
do { \
|
||||
XXH_errorcode _xxh_verify_result = (expr); \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
|
||||
if (_xxh_verify_result != XXH_OK) { \
|
||||
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
|
||||
} \
|
||||
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
|
||||
__LINE__); \
|
||||
} while (0)
|
||||
|
||||
@@ -1351,6 +1351,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||
@@ -1384,6 +1392,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer bindings cannot change while transform "
|
||||
"feedback is active."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||
|
||||
@@ -60,6 +60,11 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||
}
|
||||
if (target == BufferTarget::TransformFeedback) {
|
||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||
pointCount = std::min<SizeT>(pointCount, 4);
|
||||
}
|
||||
|
||||
if (index < pointCount) {
|
||||
return true;
|
||||
|
||||
@@ -48,6 +48,73 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Primitives a draw of `count` vertices in `mode` assembles (0 for
|
||||
// incomplete primitives). Used for the CPU-side transform feedback
|
||||
// primitive accounting.
|
||||
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
|
||||
if (count <= 0) return 0;
|
||||
switch (mode) {
|
||||
case GL_POINTS: return static_cast<Uint64>(count);
|
||||
case GL_LINES: return static_cast<Uint64>(count / 2);
|
||||
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
|
||||
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
|
||||
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate the transform feedback primitive counter for a captured draw.
|
||||
// Draws without a geometry stage write exactly the primitives they assemble,
|
||||
// clamped by the capture buffers' remaining capacity (a full buffer stops
|
||||
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
|
||||
// Geometry amplification is not modelled here.
|
||||
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
|
||||
Uint64 primitives = CountPrimitivesForDraw(mode, count);
|
||||
if (primitives == 0) return;
|
||||
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
|
||||
|
||||
Uint64 verticesPerPrimitive = 1;
|
||||
switch (mode) {
|
||||
case GL_LINES:
|
||||
case GL_LINE_STRIP:
|
||||
case GL_LINE_LOOP:
|
||||
verticesPerPrimitive = 2;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
case GL_TRIANGLE_STRIP:
|
||||
case GL_TRIANGLE_FAN:
|
||||
verticesPerPrimitive = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
if (program != nullptr) {
|
||||
// Capacity in captured vertices = the tightest bound buffer.
|
||||
Uint64 capacityVertices = ~0ull;
|
||||
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
if (stride == 0) continue;
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
const Range1D range = point.GetRange();
|
||||
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
|
||||
}
|
||||
if (capacityVertices != ~0ull) {
|
||||
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
|
||||
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
|
||||
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||
}
|
||||
|
||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
@@ -57,15 +124,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -75,6 +133,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// While transform feedback is active the draw's primitive type must match
|
||||
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
||||
// the constraint moves to the shader's output primitive type instead, so
|
||||
// the draw mode itself is unconstrained here.
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||
Bool compatible = false;
|
||||
switch (feedbackMode) {
|
||||
case GL_POINTS:
|
||||
compatible = mode == GL_POINTS;
|
||||
break;
|
||||
case GL_LINES:
|
||||
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||
break;
|
||||
case GL_TRIANGLES:
|
||||
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!compatible) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Primitive mode is incompatible with the active transform feedback primitive mode."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -402,12 +492,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -444,7 +536,147 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
|
||||
return;
|
||||
}
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||
return;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"No program with transform feedback varyings is active."));
|
||||
return;
|
||||
}
|
||||
// Every capture buffer slot the program's mode uses must have a buffer bound.
|
||||
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(i));
|
||||
if (point.GetBoundObject() == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||
beginXfb(primitiveMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
|
||||
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
|
||||
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
|
||||
// lengths the captured records are reordered in place: swap the first two
|
||||
// vertex records of every odd triangle within each emitted strip.
|
||||
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
|
||||
Uint64 inputPrimitives) {
|
||||
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
||||
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
||||
// already produced GL's vertex order, and reordering it again would corrupt it.
|
||||
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& stripTriangles = program->GetGsStripTriangles();
|
||||
|
||||
// Global triangle indices whose leading vertex pair must swap.
|
||||
Vector<Uint64> swapTriangles;
|
||||
Uint64 triangleBase = 0;
|
||||
for (Uint64 input = 0; input < inputPrimitives; ++input) {
|
||||
for (const Uint32 stripLength : stripTriangles) {
|
||||
for (Uint32 t = 1; t < stripLength; t += 2) {
|
||||
swapTriangles.push_back(triangleBase + t);
|
||||
}
|
||||
triangleBase += stripLength;
|
||||
}
|
||||
}
|
||||
if (swapTriangles.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
|
||||
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
|
||||
if (stride == 0) continue;
|
||||
const auto& bindingPoint =
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||
static_cast<Uint>(bufferIndex));
|
||||
const auto& buffer = bindingPoint.GetBoundObject();
|
||||
if (buffer == nullptr) continue;
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
// The geometry stage amplifies, so the CPU vertex counter does not bound
|
||||
// the capture; the binding range's whole-triangle capacity does.
|
||||
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
|
||||
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
|
||||
|
||||
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
|
||||
// (winding preserved by swapping the trailing pair); GL wants
|
||||
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
|
||||
Vector<Uint8> scratch(stride);
|
||||
for (const Uint64 triangle : swapTriangles) {
|
||||
if (triangle >= capturedTriangles) break;
|
||||
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
|
||||
const SizeT v1Offset = v0Offset + stride;
|
||||
const SizeT v2Offset = v1Offset + stride;
|
||||
Memcpy(scratch.data(), mapped + v2Offset, stride);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
|
||||
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
|
||||
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EndTransformFeedback(void) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
|
||||
return;
|
||||
}
|
||||
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
|
||||
// Closed while the capture state is still active: a backend that captures
|
||||
// through its own driver reads the capture program and buffer bindings here.
|
||||
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||
endXfb();
|
||||
}
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||
// End; the capture targets are host-coherent GPU memory, so completing
|
||||
// the GPU work is all that is required.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
void BeginTransformFeedback(GLenum primitiveMode);
|
||||
void EndTransformFeedback(void);
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
|
||||
@@ -236,12 +236,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
|
||||
|
||||
@@ -22,9 +22,21 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
Bool IsActiveBackendDirectVulkan() {
|
||||
// GL only requires support for framebuffers whose depth and stencil attachments
|
||||
// are the same image; anything else may be reported GL_FRAMEBUFFER_UNSUPPORTED.
|
||||
// DirectVulkan cannot form two separate attachments at all, and the real ES
|
||||
// drivers behind DirectGLES answer UNSUPPORTED for it too - so saying COMPLETE
|
||||
// and then rendering into a framebuffer the driver refuses produced silently
|
||||
// empty results (KHR-GL3x.packed_depth_stencil.verify_mixed_attachments).
|
||||
Bool ActiveBackendRejectsDistinctDepthStencil() {
|
||||
auto* activeBackend = MG_Backend::pActiveBackendObject.get();
|
||||
return activeBackend != nullptr && activeBackend->GetBackendType() == BackendType::DirectVulkan;
|
||||
if (activeBackend == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (activeBackend->GetBackendType() == BackendType::DirectVulkan) {
|
||||
return true;
|
||||
}
|
||||
return !activeBackend->GetDynamicParameters().SupportsDistinctDepthStencilAttachments;
|
||||
}
|
||||
|
||||
Bool HasDistinctCompleteDepthStencilTextureAttachments(
|
||||
@@ -45,10 +57,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
|
||||
}
|
||||
|
||||
Bool IsUnsupportedFramebufferForDirectVulkan(
|
||||
// Mirrors the renderer-side gate: distinct depth/stencil renderbuffers (or a
|
||||
// renderbuffer paired with a texture) cannot form one Vulkan depth-stencil
|
||||
// attachment, and GL permits reporting such framebuffers as UNSUPPORTED.
|
||||
Bool HasDistinctCompleteDepthStencilRenderbufferAttachments(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
if (framebufferObject.GetExternalIndex() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth);
|
||||
const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) {
|
||||
return false;
|
||||
}
|
||||
if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) {
|
||||
return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get();
|
||||
}
|
||||
return (depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) &&
|
||||
(depthAttachment.IsTexture() || stencilAttachment.IsTexture());
|
||||
}
|
||||
|
||||
Bool HasUnsupportedDistinctDepthStencilAttachments(
|
||||
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject);
|
||||
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) ||
|
||||
HasDistinctCompleteDepthStencilRenderbufferAttachments(framebufferObject);
|
||||
}
|
||||
|
||||
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
@@ -67,7 +101,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
|
||||
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
|
||||
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
|
||||
//
|
||||
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
||||
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
||||
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
||||
// widens three-channel formats to four channels to keep them renderable as *multisample*
|
||||
// storage, and a format that survives only through that substitution is still texture-only
|
||||
// on every ordinary target.
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
||||
@@ -79,8 +120,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Backend::FormatCapability::Creatable);
|
||||
}
|
||||
if (cachePopulated) {
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
++targetIndex) {
|
||||
const Bool singleTarget = capabilityTargetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
const SizeT firstTarget = singleTarget ? capabilityTargetIndex : 0;
|
||||
const SizeT lastTarget =
|
||||
singleTarget ? capabilityTargetIndex + 1 : MG_Backend::kFormatCapabilityTargetCount;
|
||||
for (SizeT targetIndex = firstTarget; targetIndex < lastTarget; ++targetIndex) {
|
||||
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
||||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
||||
@@ -129,12 +173,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& attachment = attachments[i];
|
||||
if (!attachment.IsValid()) continue;
|
||||
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
||||
SizeT capabilityTargetIndex = MG_Backend::kFormatCapabilityTargetCount;
|
||||
if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
format = attachment.GetTexture()->GetFormat();
|
||||
capabilityTargetIndex =
|
||||
MG_Backend::GetFormatCapabilityTargetIndex(attachment.GetTexture()->GetTarget());
|
||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
||||
capabilityTargetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
}
|
||||
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
|
||||
if (format != TextureInternalFormat::Unknown &&
|
||||
!IsColorInternalFormatRenderable(format, capabilityTargetIndex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -147,6 +196,148 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
|
||||
}
|
||||
|
||||
GLint ClassifyAttachmentComponentType(TextureInternalFormat internalFormat,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// A stencil value is an unsigned integer index regardless of the depth half
|
||||
// of a packed format.
|
||||
if (attachmentType == FramebufferAttachmentType::Stencil) return GL_UNSIGNED_INT;
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return GL_FLOAT;
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return GL_INT;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return GL_UNSIGNED_INT;
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return GL_SIGNED_NORMALIZED;
|
||||
case TextureInternalFormat::Unknown:
|
||||
return GL_NONE;
|
||||
default:
|
||||
return GL_UNSIGNED_NORMALIZED;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
|
||||
// and its DSA variant. Returns true when pname was one of them.
|
||||
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
|
||||
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
|
||||
GLenum pname, GLint* params, const char* caller) {
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
|
||||
// The depth and stencil components have different types, so the combined
|
||||
// attachment name has no single answer.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"No image is attached to the queried attachment point."));
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
if (attachmentObject->IsTexture() && attachmentObject->GetTexture()) {
|
||||
internalFormat = attachmentObject->GetTexture()->GetFormat();
|
||||
} else if (attachmentObject->IsRenderbuffer() && attachmentObject->GetRenderbuffer()) {
|
||||
internalFormat = attachmentObject->GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
const auto sizes = MG_Util::GetComponentSizesForInternalFormat(internalFormat);
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
|
||||
*params = sizes.Red;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
|
||||
*params = sizes.Green;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
|
||||
*params = sizes.Blue;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
|
||||
*params = sizes.Alpha;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
|
||||
*params = sizes.Depth;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
|
||||
*params = sizes.Stencil;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
|
||||
*params = (internalFormat == TextureInternalFormat::SRGB8 ||
|
||||
internalFormat == TextureInternalFormat::SRGB8Alpha8)
|
||||
? GL_SRGB
|
||||
: GL_LINEAR;
|
||||
break;
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
|
||||
*params = ClassifyAttachmentComponentType(internalFormat, attachmentType);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
|
||||
TextureUploadTarget& outUploadTarget,
|
||||
Bool& outLayered) {
|
||||
@@ -412,6 +603,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
|
||||
// Default-framebuffer attachment names (GL_DEPTH, GL_STENCIL, GL_FRONT/GL_BACK
|
||||
// variants) alias onto the equivalent attachment points.
|
||||
switch (attachment) {
|
||||
case GL_DEPTH:
|
||||
attachment = GL_DEPTH_ATTACHMENT;
|
||||
break;
|
||||
case GL_STENCIL:
|
||||
attachment = GL_STENCIL_ATTACHMENT;
|
||||
break;
|
||||
case GL_FRONT:
|
||||
case GL_FRONT_LEFT:
|
||||
case GL_FRONT_RIGHT:
|
||||
case GL_BACK:
|
||||
case GL_BACK_LEFT:
|
||||
case GL_BACK_RIGHT:
|
||||
attachment = GL_COLOR_ATTACHMENT0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
|
||||
FramebufferAttachmentType attachmentType = depthStencilAlias
|
||||
? FramebufferAttachmentType::Depth
|
||||
@@ -428,20 +640,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -505,6 +737,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
"GetFramebufferAttachmentParameteriv_State")) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -1422,8 +1658,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
return GL_FRAMEBUFFER_COMPLETE;
|
||||
@@ -1450,8 +1686,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
return GL_FRAMEBUFFER_COMPLETE;
|
||||
@@ -1468,20 +1704,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
|
||||
Bool depthStencilMismatch = false;
|
||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||
if (!depthStencilAlias) {
|
||||
return &framebufferObject->GetAttachment(attachmentType);
|
||||
}
|
||||
|
||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
||||
|
||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
|
||||
|
||||
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
|
||||
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
|
||||
if (depthLive && stencilLive) {
|
||||
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
|
||||
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
|
||||
(!depthAttachment.IsRenderbuffer() ||
|
||||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
|
||||
depthStencilMismatch = !sameObject;
|
||||
} else {
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
|
||||
depthStencilMismatch = depthLive != stencilLive;
|
||||
}
|
||||
if (depthLive) return &depthAttachment;
|
||||
if (stencilLive) return &stencilAttachment;
|
||||
return nullptr;
|
||||
}();
|
||||
|
||||
if (depthStencilMismatch) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
|
||||
"attachment images."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||
@@ -1527,6 +1783,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GL_FALSE;
|
||||
break;
|
||||
default:
|
||||
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||
caller)) {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
|
||||
@@ -213,8 +213,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
@@ -465,6 +470,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_STENCIL_TEST:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -520,6 +533,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
|
||||
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
|
||||
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
|
||||
} else {
|
||||
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_CLEAR_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetClearDepth();
|
||||
return;
|
||||
@@ -1901,6 +1927,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateComponents;
|
||||
break;
|
||||
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
|
||||
// whether the extension is advertised and requires no GL error; desktop
|
||||
// drivers all accept them, so answer with the separate-attrib capacity and
|
||||
// the single vertex stream the backends provide.
|
||||
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||
*params = 0;
|
||||
break;
|
||||
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
||||
*params = dynamicParameters.MaxTextureImageUnits;
|
||||
break;
|
||||
@@ -1957,6 +1999,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SUBPIXEL_BITS:
|
||||
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
|
||||
break;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
*params = dynamicParameters.FragmentInterpolationOffsetBits;
|
||||
break;
|
||||
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||
break;
|
||||
|
||||
@@ -49,10 +49,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckProgramNameValidity(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
// Programs and shaders share one name space: a name that exists but
|
||||
// belongs to a shader is INVALID_OPERATION, a name GL never handed
|
||||
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
|
||||
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) + " is not a valid name."));
|
||||
std::to_string(program) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a program object."
|
||||
: " is not a valid name.")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -605,6 +613,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
*params = programObject->GetTransformFeedbackVaryingMaxLength();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -624,9 +644,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
case GL_PROGRAM_BINARY_LENGTH:
|
||||
|
||||
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYINGS:
|
||||
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
@@ -827,19 +844,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsProgram_State(GLuint program) {
|
||||
/* FIXME: Handle situations that:
|
||||
* A program object marked for deletion with glDeleteProgram but still in use as part of current
|
||||
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
|
||||
*/
|
||||
// Deletion-flagged names stay valid while the object is still GL-visible (program in
|
||||
// use, shader attached), so name validity is exactly the Is* answer.
|
||||
if (program == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
GLboolean IsShader_State(GLuint shader) {
|
||||
/* FIXME: Handle situations that:
|
||||
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
|
||||
* considered a shader object and glIsShader will return GL_TRUE.
|
||||
*/
|
||||
if (shader == 0) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
@@ -849,6 +860,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject) return;
|
||||
MGLOG_D("%s: linking program %d", __func__, program);
|
||||
|
||||
// Relinking the program an active transform feedback captures from would
|
||||
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"The program used by active transform feedback cannot be relinked."));
|
||||
return;
|
||||
}
|
||||
|
||||
static Bool allowVSOnlyPrograms;
|
||||
static Bool initialized = false;
|
||||
if (!initialized) {
|
||||
@@ -892,6 +915,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void UseProgram_State(GLuint program) {
|
||||
MGLOG_D("UseProgram_State: program=%u", program);
|
||||
|
||||
// GL 3.3 core 2.11.3: the program in use may not change while transform
|
||||
// feedback is active (there is no pause in 3.3).
|
||||
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"The current program cannot change while transform feedback is active."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (program == 0) {
|
||||
MG_State::pGLContext->UseProgram(0);
|
||||
return;
|
||||
@@ -2217,4 +2250,66 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ValidateProgram(GLuint program) {
|
||||
ValidateProgram_State(program);
|
||||
}
|
||||
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
|
||||
return;
|
||||
}
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
|
||||
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
|
||||
return;
|
||||
}
|
||||
Vector<String> names;
|
||||
names.reserve(static_cast<SizeT>(count));
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
|
||||
}
|
||||
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
|
||||
}
|
||||
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) + " has not been successfully linked."));
|
||||
return;
|
||||
}
|
||||
const auto* varying = programObject->GetTransformFeedbackVarying(index);
|
||||
if (varying == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"index is not an active transform feedback varying of the program."));
|
||||
return;
|
||||
}
|
||||
if (size != nullptr) *size = varying->size;
|
||||
if (type != nullptr) *type = varying->type;
|
||||
GLsizei written = 0;
|
||||
if (name != nullptr && bufSize > 0) {
|
||||
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
|
||||
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
|
||||
name[written] = '\0';
|
||||
}
|
||||
if (length != nullptr) *length = written;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -138,4 +138,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
void ValidateProgram(GLuint program);
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Bool ended = false;
|
||||
Bool resultCached = false;
|
||||
Uint64 cachedResult = 0;
|
||||
// Transform feedback primitive counter at BeginQuery time.
|
||||
Uint64 counterSnapshot = 0;
|
||||
};
|
||||
|
||||
// Query calls may arrive from any thread (launchers migrate the context
|
||||
@@ -41,6 +43,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_nextQueryId = 1;
|
||||
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
||||
GLuint g_activeTimeElapsedQueryId = 0;
|
||||
// Ids of the queries active on the transform feedback targets (0 = none).
|
||||
GLuint g_activePrimitivesWrittenQueryId = 0;
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -126,6 +133,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
outValue = 0;
|
||||
return true;
|
||||
}
|
||||
// ANY_SAMPLES_PASSED* report a boolean.
|
||||
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
result = result != 0 ? 1 : 0;
|
||||
}
|
||||
// Final value produced (or no GetQueryResult64 hook: the
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
@@ -180,7 +192,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
QueryObject* queryObject = it->second;
|
||||
if (queryObject->active) {
|
||||
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
|
||||
// Implicitly end before deletion, releasing the matching active slot.
|
||||
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
||||
? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId) = 0;
|
||||
} else {
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
}
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
@@ -204,10 +233,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void BeginQuery(GLenum target, GLuint id) {
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
|
||||
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
|
||||
// BeginQuery target either.
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
@@ -221,9 +255,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
if (g_activeTimeElapsedQueryId != 0) {
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on GL_TIME_ELAPSED.");
|
||||
"A query is already active on this target.");
|
||||
return;
|
||||
}
|
||||
if (queryObject->active) {
|
||||
@@ -239,25 +277,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
g_activeTimeElapsedQueryId = id;
|
||||
if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||
} else if (isOcclusionQuery) {
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
activeQueryId = id;
|
||||
}
|
||||
|
||||
void EndQuery(GLenum target) {
|
||||
if (target != GL_TIME_ELAPSED) {
|
||||
const Bool isTransformFeedbackQuery =
|
||||
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
if (g_activeTimeElapsedQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
return;
|
||||
}
|
||||
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
|
||||
auto* queryObject = FindQueryObjectLocked(activeQueryId);
|
||||
if (!queryObject) {
|
||||
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
// Result comes from the GPU query at read time.
|
||||
} else {
|
||||
queryObject->cachedResult =
|
||||
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
||||
queryObject->resultCached = true;
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
EndTimeElapsedQueryLocked(queryObject);
|
||||
@@ -303,9 +388,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
switch (pname) {
|
||||
case GL_CURRENT_QUERY: {
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
|
||||
// never are, and other targets remain unimplemented.
|
||||
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
|
||||
switch (target) {
|
||||
case GL_TIME_ELAPSED:
|
||||
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
|
||||
break;
|
||||
case GL_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
|
||||
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
|
||||
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
|
||||
break;
|
||||
case GL_PRIMITIVES_GENERATED:
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
*params = 0;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GL_QUERY_COUNTER_BITS: {
|
||||
@@ -313,7 +414,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins. Non-timer targets remain unimplemented and report 0.
|
||||
// wins.
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||
return;
|
||||
}
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
const Bool supported =
|
||||
|
||||
@@ -185,6 +185,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static thread_local Vector<GLuint> names;
|
||||
MG_State::pGLContext->GenSamplerNames(count, names);
|
||||
Memcpy(samplers, names.data(), count * sizeof(GLuint));
|
||||
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
|
||||
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->CreateSamplerObject(names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
|
||||
|
||||
@@ -2820,7 +2820,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"The attachment specified by the read buffer is incomplete.")); \
|
||||
return false; \
|
||||
}
|
||||
if (isDepth) {
|
||||
if (isDepth && isStencil) {
|
||||
// A combined internalformat copies both halves, so the read framebuffer
|
||||
// must populate both attachment points.
|
||||
const auto& stencilAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
const auto& depthAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (!depthAttachment.IsValid() || depthAttachment.IsEmpty() || !stencilAttachment.IsValid() ||
|
||||
stencilAttachment.IsEmpty()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "CopyTexImage2D_State",
|
||||
"DEPTH_STENCIL copy requires both depth and stencil attachments in the read framebuffer."));
|
||||
return false;
|
||||
}
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
|
||||
} else if (isDepth) {
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
|
||||
} else if (isStencil) {
|
||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
|
||||
|
||||
@@ -231,6 +231,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_resource.Bytes();
|
||||
}
|
||||
|
||||
Bool BufferObject::EnsureGpuResidentStorage() {
|
||||
if (m_resource.IsGpuResident()) {
|
||||
return true;
|
||||
}
|
||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||
return false;
|
||||
}
|
||||
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
|
||||
if (base == nullptr) {
|
||||
return false;
|
||||
}
|
||||
m_resource.AdoptPersistentMap(base);
|
||||
return true;
|
||||
}
|
||||
|
||||
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
|
||||
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
||||
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
||||
|
||||
@@ -133,6 +133,11 @@ namespace MobileGL {
|
||||
|
||||
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
|
||||
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
|
||||
// Adopt backend host-visible coherent GPU storage as the source of truth
|
||||
// (used for GPU-written targets like transform feedback capture, so
|
||||
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
|
||||
// resident or when the backend declines.
|
||||
Bool EnsureGpuResidentStorage();
|
||||
void ReleaseMemory();
|
||||
void FlushMemoryRange(SizeT offset, SizeT length);
|
||||
|
||||
|
||||
@@ -211,6 +211,47 @@ namespace MobileGL {
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
|
||||
// Transform feedback (GL 3.0 core Begin/End; no feedback objects yet)
|
||||
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
|
||||
m_transformFeedbackActive = true;
|
||||
m_transformFeedbackPrimitiveMode = primitiveMode;
|
||||
m_transformFeedbackProgram = program;
|
||||
++m_transformFeedbackGeneration;
|
||||
m_transformFeedbackCapturedVertices = 0;
|
||||
m_transformFeedbackInputPrimitives = 0;
|
||||
}
|
||||
void EndTransformFeedback() {
|
||||
m_transformFeedbackActive = false;
|
||||
m_transformFeedbackProgram.reset();
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
|
||||
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
return m_transformFeedbackProgram;
|
||||
}
|
||||
// Bumped on every BeginTransformFeedback; the backend uses it to
|
||||
// distinguish "resume appending" from "fresh capture".
|
||||
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
|
||||
// CPU-side primitive accounting for the transform feedback queries:
|
||||
// every captured draw adds its primitive count (draws without a
|
||||
// geometry stage write exactly what they generate).
|
||||
void AddTransformFeedbackPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackPrimitiveCounter += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
|
||||
// Vertices already captured since BeginTransformFeedback (drives the
|
||||
// buffer-capacity clamp on the primitives-written accounting).
|
||||
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
|
||||
m_transformFeedbackCapturedVertices += vertices;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
|
||||
// Raw assembled input primitives fed to the capture stage since Begin
|
||||
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
|
||||
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
|
||||
m_transformFeedbackInputPrimitives += primitives;
|
||||
}
|
||||
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
|
||||
|
||||
// Framebuffer
|
||||
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||
@@ -243,6 +284,13 @@ namespace MobileGL {
|
||||
BufferState m_bufferState;
|
||||
VertexArrayState m_vertexArrayState;
|
||||
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
Uint64 m_transformFeedbackGeneration = 0;
|
||||
Uint64 m_transformFeedbackPrimitiveCounter = 0;
|
||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||
Uint64 m_transformFeedbackInputPrimitives = 0;
|
||||
TextureState m_textureState;
|
||||
ProgramState m_programState;
|
||||
RenderState m_renderState;
|
||||
|
||||
@@ -170,9 +170,230 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_uniformNameMaxLength = 0;
|
||||
m_attribInNameMaxLength = 0;
|
||||
m_uniformBlockNameMaxLength = 0;
|
||||
m_xfbVaryings.clear();
|
||||
m_xfbStrides.clear();
|
||||
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
m_xfbVaryingNameMaxLength = 0;
|
||||
m_linkStatus = false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL type enum for a vertex-stage output symbol captured by transform
|
||||
// feedback. Covers the scalar/vector/matrix float+integer types transform
|
||||
// feedback may legally capture in GL 3.3.
|
||||
Bool ResolveXfbSymbolType(const glslang::TType& type, GLenum& outType, GLint& outArraySize,
|
||||
Uint32& outBytesPerElement) {
|
||||
outArraySize = type.isArray() ? type.getOuterArraySize() : 1;
|
||||
const Int columns = type.isMatrix() ? type.getMatrixCols() : 1;
|
||||
const Int components = type.isMatrix() ? type.getMatrixRows()
|
||||
: (type.isVector() ? type.getVectorSize() : 1);
|
||||
const glslang::TBasicType basic = type.getBasicType();
|
||||
static constexpr GLenum kFloatTypes[5] = {0, GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4};
|
||||
static constexpr GLenum kIntTypes[5] = {0, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4};
|
||||
static constexpr GLenum kUintTypes[5] = {0, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
|
||||
GL_UNSIGNED_INT_VEC4};
|
||||
if (type.isMatrix()) {
|
||||
if (basic != glslang::EbtFloat) return false;
|
||||
static constexpr GLenum kMatTypes[5][5] = {
|
||||
{}, {},
|
||||
{0, 0, GL_FLOAT_MAT2, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4},
|
||||
{0, 0, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3, GL_FLOAT_MAT3x4},
|
||||
{0, 0, GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_FLOAT_MAT4},
|
||||
};
|
||||
if (columns < 2 || columns > 4 || components < 2 || components > 4) return false;
|
||||
outType = kMatTypes[columns][components];
|
||||
} else if (components >= 1 && components <= 4) {
|
||||
switch (basic) {
|
||||
case glslang::EbtFloat: outType = kFloatTypes[components]; break;
|
||||
case glslang::EbtInt: outType = kIntTypes[components]; break;
|
||||
case glslang::EbtUint: outType = kUintTypes[components]; break;
|
||||
default: return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
outBytesPerElement = static_cast<Uint32>(columns * components) * 4u;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProgramObject::ResolveTransformFeedbackVaryings() {
|
||||
m_xfbVaryings.clear();
|
||||
m_xfbStrides.clear();
|
||||
m_xfbBufferMode = m_requestedXfbBufferMode;
|
||||
m_xfbVaryingNameMaxLength = 0;
|
||||
if (m_requestedXfbVaryings.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capture happens at the last vertex-processing stage (geometry, then
|
||||
// tessellation evaluation, then vertex).
|
||||
const glslang::TIntermediate* captureIntermediate = nullptr;
|
||||
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
|
||||
captureIntermediate = m_program->getIntermediate(stage);
|
||||
if (captureIntermediate != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (captureIntermediate == nullptr) {
|
||||
m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage.";
|
||||
return false;
|
||||
}
|
||||
const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects();
|
||||
|
||||
const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS;
|
||||
Uint32 interleavedOffset = 0;
|
||||
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
|
||||
const String& name = m_requestedXfbVaryings[i];
|
||||
for (SizeT j = 0; j < i; ++j) {
|
||||
if (m_requestedXfbVaryings[j] == name) {
|
||||
m_infoLog = "Transform feedback varying '" + name + "' is specified more than once.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
XfbVarying varying;
|
||||
varying.name = name;
|
||||
Uint32 bytesPerElement = 0;
|
||||
Bool resolved = false;
|
||||
if (name == "gl_Position") {
|
||||
varying.type = GL_FLOAT_VEC4;
|
||||
varying.size = 1;
|
||||
bytesPerElement = 16;
|
||||
resolved = true;
|
||||
} else if (name == "gl_PointSize") {
|
||||
varying.type = GL_FLOAT;
|
||||
varying.size = 1;
|
||||
bytesPerElement = 4;
|
||||
resolved = true;
|
||||
} else if (linkerObjects != nullptr) {
|
||||
for (const auto* node : linkerObjects->getSequence()) {
|
||||
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
||||
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
||||
continue;
|
||||
}
|
||||
if (symbol->getName() != name.c_str()) {
|
||||
continue;
|
||||
}
|
||||
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!resolved) {
|
||||
m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage.";
|
||||
return false;
|
||||
}
|
||||
|
||||
varying.byteSize = bytesPerElement * static_cast<Uint32>(varying.size);
|
||||
if (interleaved) {
|
||||
varying.bufferIndex = 0;
|
||||
varying.offsetBytes = interleavedOffset;
|
||||
interleavedOffset += varying.byteSize;
|
||||
} else {
|
||||
varying.bufferIndex = static_cast<Uint32>(i);
|
||||
varying.offsetBytes = 0;
|
||||
}
|
||||
m_xfbVaryingNameMaxLength =
|
||||
std::max(m_xfbVaryingNameMaxLength, static_cast<Int>(name.size()) + 1);
|
||||
m_xfbVaryings.push_back(Move(varying));
|
||||
}
|
||||
|
||||
constexpr Uint32 kMaxSeparateAttribs = 4;
|
||||
constexpr Uint32 kMaxSeparateComponents = 4;
|
||||
constexpr Uint32 kMaxInterleavedComponents = 64;
|
||||
if (interleaved) {
|
||||
if (interleavedOffset > kMaxInterleavedComponents * 4) {
|
||||
m_infoLog = "Transform feedback interleaved capture exceeds "
|
||||
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides.assign(1, interleavedOffset);
|
||||
} else {
|
||||
if (m_xfbVaryings.size() > kMaxSeparateAttribs) {
|
||||
m_infoLog = "Transform feedback separate capture exceeds "
|
||||
"GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides.resize(m_xfbVaryings.size());
|
||||
for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) {
|
||||
if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) {
|
||||
m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name +
|
||||
"' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS.";
|
||||
return false;
|
||||
}
|
||||
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
|
||||
}
|
||||
}
|
||||
|
||||
ResolveGsTriangleStripCapture(captureIntermediate);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
|
||||
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
|
||||
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
|
||||
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
|
||||
// the static strip lengths the capture buffer can be reordered after EndTF.
|
||||
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
|
||||
public:
|
||||
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
|
||||
if (node->getOp() == glslang::EOpEmitVertex) {
|
||||
++emitCount;
|
||||
hasEmit = true;
|
||||
} else if (node->getOp() == glslang::EOpEndPrimitive) {
|
||||
FlushStrip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
|
||||
inControlFlow = true;
|
||||
return true;
|
||||
}
|
||||
void FlushStrip() {
|
||||
if (emitCount >= 3) {
|
||||
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
|
||||
}
|
||||
emitCount = 0;
|
||||
}
|
||||
|
||||
Vector<Uint32> stripTriangles;
|
||||
Uint32 emitCount = 0;
|
||||
Bool hasEmit = false;
|
||||
Bool inControlFlow = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
|
||||
m_gsStripTriangles.clear();
|
||||
m_gsStripCaptureFixup = false;
|
||||
if (captureIntermediate == nullptr || m_program == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
|
||||
return;
|
||||
}
|
||||
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
|
||||
return;
|
||||
}
|
||||
GsEmitSequenceTraverser traverser;
|
||||
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
|
||||
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
|
||||
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
|
||||
return;
|
||||
}
|
||||
m_gsStripTriangles = Move(traverser.stripTriangles);
|
||||
m_gsStripCaptureFixup = true;
|
||||
}
|
||||
|
||||
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
|
||||
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
|
||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
|
||||
@@ -327,6 +548,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!ValidateFragmentOutputLocations()) {
|
||||
return;
|
||||
}
|
||||
if (!ResolveTransformFeedbackVaryings()) {
|
||||
m_linkStatus = false;
|
||||
MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex,
|
||||
m_infoLog.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
|
||||
GenerateBinary();
|
||||
|
||||
@@ -465,6 +465,39 @@ namespace MobileGL::MG_State::GLState {
|
||||
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
|
||||
}
|
||||
|
||||
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
|
||||
// the NEXT link; the linked snapshot below is what draws and queries see).
|
||||
struct XfbVarying {
|
||||
String name;
|
||||
GLenum type = GL_FLOAT;
|
||||
GLint size = 1; // array element count
|
||||
Uint32 bufferIndex = 0; // capture buffer slot
|
||||
Uint32 offsetBytes = 0; // offset within the capture buffer
|
||||
Uint32 byteSize = 0; // bytes captured per vertex for this varying
|
||||
};
|
||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||
m_requestedXfbVaryings = Move(names);
|
||||
m_requestedXfbBufferMode = bufferMode;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
|
||||
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
||||
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
|
||||
}
|
||||
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
|
||||
// Stride of one captured vertex in the given capture buffer slot.
|
||||
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
|
||||
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
|
||||
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
|
||||
// True when the capture stage is a triangle-strip geometry shader with a
|
||||
// statically-known emit sequence: the Vulkan capture order then needs the GL
|
||||
// odd-triangle vertex swap after EndTransformFeedback.
|
||||
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
|
||||
// Triangles per strip, in emission order, for ONE geometry invocation.
|
||||
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
||||
// name (external index), which is freed to a LIFO list and immediately handed back by
|
||||
@@ -475,6 +508,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
private:
|
||||
void ResetLinkArtifacts();
|
||||
void DoReflection();
|
||||
// Resolves the requested transform feedback varyings against the linked
|
||||
// vertex stage; fails the link (GL semantics) on unknown or duplicate
|
||||
// names or exceeded capture limits.
|
||||
Bool ResolveTransformFeedbackVaryings();
|
||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||
void GenerateBinary();
|
||||
void WaitUntilGenerationCompleted() const;
|
||||
void AddDefaultFragmentShaderIfMissing();
|
||||
@@ -558,5 +596,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||
Uint32 m_uboContentVersion = 0;
|
||||
Uint32 m_linkVersion = 0;
|
||||
|
||||
// Transform feedback: request (applies at next link) and linked snapshot.
|
||||
Vector<String> m_requestedXfbVaryings;
|
||||
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Vector<XfbVarying> m_xfbVaryings;
|
||||
Vector<Uint32> m_xfbStrides;
|
||||
Vector<Uint32> m_gsStripTriangles;
|
||||
Bool m_gsStripCaptureFixup = false;
|
||||
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int m_xfbVaryingNameMaxLength = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -29,17 +29,25 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||
auto& programObject = m_programObjects[program];
|
||||
if (programObject != nullptr) {
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject->MarkAsDeleted();
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
}
|
||||
// A program in use is only FLAGGED: its name (and every program query) stays
|
||||
// valid until it stops being current, at which point UseProgram finishes the job.
|
||||
if (programObject == m_currentProgram) return;
|
||||
DestroyProgramSlot(program);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramState::DestroyProgramSlot(const Uint program) {
|
||||
auto& programObject = m_programObjects[program];
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,10 +57,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void ProgramState::UseProgram(Uint program) {
|
||||
const SharedPtr<ProgramObject> previous = m_currentProgram;
|
||||
|
||||
if (program == 0) m_currentProgram.reset();
|
||||
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return;
|
||||
m_currentProgram = m_programObjects[program];
|
||||
if (CheckIndexAvail(program, m_programObjects)) {
|
||||
m_currentProgram = m_programObjects[program];
|
||||
}
|
||||
|
||||
// A deletion flagged while the program was current takes effect the moment it
|
||||
// stops being current.
|
||||
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
|
||||
const Uint previousName = previous->GetExternalIndex();
|
||||
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
|
||||
DestroyProgramSlot(previousName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint ProgramState::CreateShader(ShaderStage stage) {
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
private:
|
||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
||||
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
||||
// of glDeleteProgram (deferred while the program is current).
|
||||
void DestroyProgramSlot(Uint program);
|
||||
|
||||
template <typename T>
|
||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
||||
|
||||
@@ -169,6 +169,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
}
|
||||
|
||||
const std::optional<String> reservedError =
|
||||
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
|
||||
if (reservedError) {
|
||||
m_compileStatus = false;
|
||||
m_shader.reset();
|
||||
m_infoLog = *reservedError;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compile for OpenGL here, so that we can do validation and link
|
||||
// like a real OpenGL driver at linking stage
|
||||
// Will compile for other backends later.
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace MobileGL {
|
||||
DepthComponent32F,
|
||||
Depth24Stencil8,
|
||||
Depth32FStencil8,
|
||||
StencilIndex8,
|
||||
|
||||
DepthComponent,
|
||||
DepthStencil,
|
||||
|
||||
@@ -104,6 +104,23 @@ namespace MobileGL {
|
||||
m_backendHashMemoVersion = m_configVersion;
|
||||
}
|
||||
|
||||
// Backend-owned resolved-state memo: an opaque pointer into the
|
||||
// backend's vertex-input-state cache plus the cache's eviction
|
||||
// epoch, valid while the config version matches. Lets the
|
||||
// per-draw path skip the content hash AND the cache lookup; the
|
||||
// epoch guards against the cache evicting the pointee.
|
||||
Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const {
|
||||
if (m_backendStateMemoVersion != m_configVersion) return false;
|
||||
outState = m_backendStateMemo;
|
||||
outEpoch = m_backendStateMemoEpoch;
|
||||
return true;
|
||||
}
|
||||
void SetBackendStateMemo(const void* state, Uint64 epoch) const {
|
||||
m_backendStateMemo = state;
|
||||
m_backendStateMemoEpoch = epoch;
|
||||
m_backendStateMemoVersion = m_configVersion;
|
||||
}
|
||||
|
||||
private:
|
||||
void BumpAttributeFormatVersion(Uint index);
|
||||
void BumpAttributeBufferVersion(Uint index);
|
||||
@@ -137,6 +154,9 @@ namespace MobileGL {
|
||||
Uint32 m_configVersion = 0;
|
||||
mutable Uint64 m_backendHashMemo = 0;
|
||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||
mutable const void* m_backendStateMemo = nullptr;
|
||||
mutable Uint64 m_backendStateMemoEpoch = 0;
|
||||
mutable Uint32 m_backendStateMemoVersion = ~0u;
|
||||
};
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace {
|
||||
GLint maxFragmentImageUniforms = 4;
|
||||
GLint maxComputeImageUniforms = 5;
|
||||
bool maxGeometryImageUniformsQueried = false;
|
||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||
GLint fragmentInterpolationOffsetBits = 6;
|
||||
bool fragmentInterpolationLimitsQueried = false;
|
||||
bool fragmentInterpolationQueryRaisesError = false;
|
||||
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
||||
// baseInstance word and exposes it through gl_InstanceID.
|
||||
bool drawLeaksBaseInstanceWord = false;
|
||||
@@ -108,6 +113,14 @@ namespace {
|
||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxComputeImageUniforms;
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.fragmentInterpolationOffsetBits;
|
||||
}
|
||||
break;
|
||||
// FillInGLESCapabilities reads the context version before running the
|
||||
// baseInstance probe, which requires ES >= 3.1.
|
||||
case GL_MAJOR_VERSION:
|
||||
@@ -155,6 +168,22 @@ namespace {
|
||||
g_fake.maxTextureMaxAnisotropyQueried = true;
|
||||
data[0] = g_fake.maxTextureMaxAnisotropy;
|
||||
break;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
data[0] = g_fake.minFragmentInterpolationOffset;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
data[0] = g_fake.maxFragmentInterpolationOffset;
|
||||
}
|
||||
break;
|
||||
// Two-component range queries.
|
||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
@@ -462,6 +491,52 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
|
||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities unsupportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(unsupportedCaps, funcs));
|
||||
EXPECT_FALSE(unsupportedCaps.SupportsShaderMultisampleInterpolation);
|
||||
EXPECT_FALSE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(unsupportedCaps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(unsupportedCaps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(unsupportedCaps.FragmentInterpolationOffsetBits, 4);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
|
||||
// A stale error from an earlier capability probe must not make the optional
|
||||
// interpolation query look like it failed.
|
||||
g_fake.pendingError = GL_INVALID_OPERATION;
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsShaderMultisampleInterpolation);
|
||||
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(supportedCaps.MinFragmentInterpolationOffset, g_fake.minFragmentInterpolationOffset);
|
||||
EXPECT_FLOAT_EQ(supportedCaps.MaxFragmentInterpolationOffset, g_fake.maxFragmentInterpolationOffset);
|
||||
EXPECT_EQ(supportedCaps.FragmentInterpolationOffsetBits, g_fake.fragmentInterpolationOffsetBits);
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueryErrorIsDrainedAndFallsBackToCoreMinimums) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
|
||||
g_fake.fragmentInterpolationQueryRaisesError = true;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
|
||||
EXPECT_FLOAT_EQ(caps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(caps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(caps.FragmentInterpolationOffsetBits, 4);
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||
|
||||
@@ -197,13 +197,13 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
|
||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
||||
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),
|
||||
@@ -397,13 +397,13 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
|
||||
MobileGL::IntVec2(512, 512));
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
||||
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),
|
||||
@@ -516,6 +516,50 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
||||
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_External::GLESCapabilities glesCaps;
|
||||
glesCaps.MinFragmentInterpolationOffset = -0.75f;
|
||||
glesCaps.MaxFragmentInterpolationOffset = 0.625f;
|
||||
glesCaps.FragmentInterpolationOffsetBits = 6;
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
|
||||
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
|
||||
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.75f);
|
||||
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.625f);
|
||||
EXPECT_EQ(glesBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 6);
|
||||
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
// A common Vulkan limit pair: max is one representable 4-bit step below 0.5.
|
||||
properties.limits.minInterpolationOffset = -0.5f;
|
||||
properties.limits.maxInterpolationOffset = 0.4375f;
|
||||
properties.limits.subPixelInterpolationOffsetBits = 4;
|
||||
MG_External::VulkanCapabilities vkCaps;
|
||||
MG_Util::BackendLoader::FillInVulkanCapabilities(vkCaps, properties);
|
||||
EXPECT_FLOAT_EQ(vkCaps.MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(vkCaps.MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(vkCaps.FragmentInterpolationOffsetBits, 4);
|
||||
|
||||
vkCaps.MinFragmentInterpolationOffset = -0.875f;
|
||||
vkCaps.MaxFragmentInterpolationOffset = 0.75f;
|
||||
vkCaps.FragmentInterpolationOffsetBits = 7;
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
|
||||
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.875f);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.75f);
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 7);
|
||||
|
||||
// Invalid/zero host data cannot under-advertise the OpenGL 4 minimums.
|
||||
MG_External::VulkanCapabilities invalidCaps;
|
||||
invalidCaps.MinFragmentInterpolationOffset = 0.0f;
|
||||
invalidCaps.MaxFragmentInterpolationOffset = 0.0f;
|
||||
invalidCaps.FragmentInterpolationOffsetBits = 0;
|
||||
vkBackend.ApplyVulkanCapabilitiesForTesting(invalidCaps);
|
||||
EXPECT_LE(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.5f);
|
||||
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.4375f);
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -638,6 +682,48 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
|
||||
MG_State::pGLContext.reset();
|
||||
}
|
||||
|
||||
TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MinFragmentInterpolationOffset = -0.75f;
|
||||
params.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
params.FragmentInterpolationOffsetBits = 6;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLfloat floatValue = 0.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, -0.75f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, 0.4375f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &floatValue);
|
||||
EXPECT_FLOAT_EQ(floatValue, 6.0f);
|
||||
|
||||
GLint intValue = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
|
||||
EXPECT_EQ(intValue, -1);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
|
||||
EXPECT_EQ(intValue, 0);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &intValue);
|
||||
EXPECT_EQ(intValue, 6);
|
||||
|
||||
GLboolean boolValue = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &boolValue);
|
||||
EXPECT_EQ(boolValue, GL_TRUE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Loader.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
#if defined(_WIN32)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN 1
|
||||
@@ -823,6 +824,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
|
||||
caps.SupportsNorm16Texture = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
|
||||
caps.SupportsRenderSnorm = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
|
||||
caps.SupportsSrgbWriteControl = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
|
||||
caps.SupportsTextureFilterAnisotropy = true;
|
||||
}
|
||||
@@ -838,8 +845,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||
caps.SupportsNoperspectiveInterpolation = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
|
||||
caps.SupportsShaderMultisampleInterpolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
caps.SupportsShaderMultisampleInterpolation =
|
||||
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
|
||||
// Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES
|
||||
// 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
|
||||
@@ -900,6 +913,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLint maxColorAttachments = 8;
|
||||
GLint maxClipDistances = 8;
|
||||
GLint maxViewports = 16;
|
||||
GLfloat minFragmentInterpolationOffset = -0.5f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
||||
GLint fragmentInterpolationOffsetBits = 4;
|
||||
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
||||
@@ -919,6 +935,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
|
||||
// MobileGL's sample-mask state only stores a single 32-bit word (see
|
||||
// RenderState::SampleMaskValue) and SampleMaski_State() rejects any
|
||||
// maskNumber other than 0. Advertising the real driver's value here (e.g.
|
||||
// NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
|
||||
// dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
|
||||
// raise GL_INVALID_VALUE and aborts the whole glcts process after every
|
||||
// single test case. 1 is a spec-legal value (the minimum required), so cap
|
||||
// to what is actually implemented instead of forwarding the raw driver limit.
|
||||
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
|
||||
@@ -950,6 +975,29 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
|
||||
const auto drainErrors = [&glesFuncs]() {
|
||||
Bool hadError = false;
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||
}
|
||||
return hadError;
|
||||
};
|
||||
|
||||
// Isolate these optional queries from errors raised by preceding capability
|
||||
// probes, then consume any query error so initialization never leaks it into
|
||||
// the application's first glGetError call.
|
||||
drainErrors();
|
||||
glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset);
|
||||
glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset);
|
||||
glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits);
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums");
|
||||
minFragmentInterpolationOffset = -0.5f;
|
||||
maxFragmentInterpolationOffset = 0.4375f;
|
||||
fragmentInterpolationOffsetBits = 4;
|
||||
}
|
||||
}
|
||||
// Only legal to query once the extension has been seen in the loop above, hence not batched
|
||||
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
|
||||
if (caps.SupportsTextureFilterAnisotropy) {
|
||||
@@ -1007,6 +1055,19 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
||||
? minFragmentInterpolationOffset
|
||||
: -0.5f;
|
||||
caps.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
caps.FragmentInterpolationOffsetBits = 4;
|
||||
if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) {
|
||||
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits);
|
||||
if (maxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||
caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset;
|
||||
caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits;
|
||||
}
|
||||
}
|
||||
MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
|
||||
caps.AliasedLineWidthRangeMax);
|
||||
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
|
||||
|
||||
@@ -1031,6 +1031,13 @@ namespace MobileGL {
|
||||
String GLESShadingLanguageVersionString;
|
||||
Bool SupportsPersistentMapping = false;
|
||||
Bool SupportsNorm16Texture = false;
|
||||
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
|
||||
// (and usable as multisample texture storage) rather than texture-only.
|
||||
Bool SupportsRenderSnorm = false;
|
||||
// GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off.
|
||||
// GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded,
|
||||
// while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw.
|
||||
Bool SupportsSrgbWriteControl = false;
|
||||
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
|
||||
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
|
||||
Bool SupportsTextureFilterAnisotropy = false;
|
||||
@@ -1055,6 +1062,9 @@ namespace MobileGL {
|
||||
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||
Bool SupportsNoperspectiveInterpolation = false;
|
||||
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
||||
// interpolateAtOffset and the three fragment-offset limit queries.
|
||||
Bool SupportsShaderMultisampleInterpolation = false;
|
||||
// GL_RENDERER contains "ANGLE".
|
||||
Bool IsAngleRenderer = false;
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
@@ -1120,6 +1130,9 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
};
|
||||
} // namespace MG_External
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Loader.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Util::BackendLoader {
|
||||
namespace {
|
||||
@@ -40,6 +41,25 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
return MaxSampleCountFromFlags(commonFlags);
|
||||
}
|
||||
|
||||
void FillFragmentInterpolationLimits(MG_External::VulkanCapabilities& caps,
|
||||
const VkPhysicalDeviceLimits& limits) {
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(limits.minInterpolationOffset) && limits.minInterpolationOffset <= -0.5f
|
||||
? limits.minInterpolationOffset
|
||||
: -0.5f;
|
||||
|
||||
caps.MaxFragmentInterpolationOffset = 0.4375f;
|
||||
caps.FragmentInterpolationOffsetBits = 4;
|
||||
const Int bits = static_cast<Int>(limits.subPixelInterpolationOffsetBits);
|
||||
if (bits >= 4 && std::isfinite(limits.maxInterpolationOffset)) {
|
||||
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -bits);
|
||||
if (limits.maxInterpolationOffset >= requiredMaxOffset) {
|
||||
caps.MaxFragmentInterpolationOffset = limits.maxInterpolationOffset;
|
||||
caps.FragmentInterpolationOffsetBits = bits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VulkanDynamicFunctions LoadVulkanDynamicFunctions(VkInstance instance) {
|
||||
VulkanDynamicFunctions loaded{};
|
||||
if (instance == VK_NULL_HANDLE) {
|
||||
@@ -171,6 +191,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
||||
FillFragmentInterpolationLimits(caps, p.limits);
|
||||
|
||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||
@@ -261,6 +282,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||
caps.SupportsWideLines = false;
|
||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||
// stage writes disabled rather than inferring them from descriptor limits alone.
|
||||
|
||||
@@ -68,6 +68,9 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
Float MinFragmentInterpolationOffset = -0.5f;
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// Storage-image descriptors are limited per stage by
|
||||
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace MobileGL {
|
||||
|
||||
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
|
||||
switch (internalformat) {
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
case TextureInternalFormat::Depth24Stencil8:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -251,6 +251,8 @@ namespace MobileGL {
|
||||
return TextureInternalFormat::Depth24Stencil8;
|
||||
case GL_DEPTH32F_STENCIL8:
|
||||
return TextureInternalFormat::Depth32FStencil8;
|
||||
case GL_STENCIL_INDEX8:
|
||||
return TextureInternalFormat::StencilIndex8;
|
||||
case GL_DEPTH_COMPONENT:
|
||||
return TextureInternalFormat::DepthComponent;
|
||||
case GL_DEPTH_STENCIL:
|
||||
|
||||
@@ -233,6 +233,8 @@ namespace MobileGL {
|
||||
return GL_DEPTH24_STENCIL8;
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return GL_DEPTH32F_STENCIL8;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return GL_STENCIL_INDEX8;
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
return GL_DEPTH_COMPONENT32;
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -234,6 +234,8 @@ namespace MobileGL {
|
||||
return "Depth24Stencil8";
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return "Depth32FStencil8";
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return "StencilIndex8";
|
||||
case TextureInternalFormat::Red:
|
||||
return "Red";
|
||||
case TextureInternalFormat::RG:
|
||||
|
||||
@@ -25,6 +25,19 @@ namespace MobileGL {
|
||||
case GL_TRIANGLE_FAN:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
|
||||
case GL_LINE_LOOP:
|
||||
// DrawArrays/DrawElements rewrite line loops into closed indexed
|
||||
// strips; entry points without that rewrite (instanced/indirect)
|
||||
// degrade to an open strip, which only misses the closing segment.
|
||||
MGLOG_W("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||
case GL_LINES_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
|
||||
case GL_LINE_STRIP_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY;
|
||||
case GL_TRIANGLES_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
|
||||
default:
|
||||
MGLOG_W("Unrecognized primitive topology");
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
|
||||
@@ -226,6 +226,8 @@ namespace MobileGL {
|
||||
return VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
return VK_FORMAT_S8_UINT;
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
return VK_FORMAT_D32_SFLOAT;
|
||||
case TextureInternalFormat::DepthStencil:
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::Red: // UNorm8 shadow layout
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
case TextureInternalFormat::R8UI:
|
||||
return 1;
|
||||
|
||||
@@ -43,8 +44,11 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::SRGB8:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
return 3;
|
||||
// Canonical depth shadow is a full 32-bit unorm word (see PixelStoreProcessor),
|
||||
// converted at upload to the image's own 24/32-bit layout.
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
return 4;
|
||||
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
@@ -91,6 +95,9 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
// Shadow bytes hold the GL_FLOAT_32_UNSIGNED_INT_24_8_REV wire format
|
||||
// (float depth + a word whose low 8 bits are stencil), 8 bytes/texel.
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return 8;
|
||||
|
||||
case TextureInternalFormat::RGB32F:
|
||||
@@ -101,7 +108,6 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::Depth32FStencil8:
|
||||
return 16;
|
||||
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
@@ -253,8 +259,10 @@ namespace MobileGL {
|
||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||
case TexturePixelDataType::UnsignedInt248:
|
||||
case TexturePixelDataType::Float32UnsignedInt248Rev:
|
||||
return 4;
|
||||
case TexturePixelDataType::Float32UnsignedInt248Rev:
|
||||
// A 32-bit float depth word followed by a 32-bit word holding stencil.
|
||||
return 8;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -501,9 +509,16 @@ namespace MobileGL {
|
||||
s.Depth = 32;
|
||||
s.Stencil = 8;
|
||||
break;
|
||||
case TextureInternalFormat::StencilIndex8:
|
||||
s.Stencil = 8;
|
||||
break;
|
||||
case TextureInternalFormat::Unknown:
|
||||
// Queried for attachments that have no storage yet (e.g. framebuffer
|
||||
// parameter queries on the initial state); every size stays 0.
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
||||
static_cast<Int>(internal));
|
||||
MGLOG_W("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
||||
static_cast<Int>(internal));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -220,11 +220,14 @@ namespace MobileGL {
|
||||
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
|
||||
if (result) return result;
|
||||
|
||||
// Legacy desktop sources are normalized to "#version 330 core", which parses under
|
||||
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
|
||||
// using e.g. layout(binding=...) without the matching #extension line compiles on real
|
||||
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
|
||||
// broken shader fails both attempts and keeps its original diagnostics.
|
||||
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
|
||||
// directive), which parses under stricter rules than the 460 they used to be forced
|
||||
// to: a shader declaring 110-150 while using e.g. layout(binding=...) without the
|
||||
// matching #extension line compiles on real drivers but is rejected here. Retry once
|
||||
// at 460 before reporting failure; a genuinely broken shader fails both attempts and
|
||||
// keeps its original diagnostics. Application-declared 330+ sources carry no marker
|
||||
// and keep their declared version's strict rules (the GL CTS negative-compile cases
|
||||
// depend on that).
|
||||
String retrySource = source;
|
||||
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
|
||||
return result;
|
||||
@@ -360,6 +363,86 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ...
|
||||
constexpr SizeT kTypeImageDimWordIndex = 3;
|
||||
constexpr SizeT kTypeImageMinWordCount = 9;
|
||||
outputBinary.clear();
|
||||
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<SizeT> rectDimWordOffsets;
|
||||
Vector<SizeT> rectCapabilityWordOffsets;
|
||||
Bool hasNormalizedCoordinateLookup = false;
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||
const Uint32 instructionWord = inputBinary[offset];
|
||||
const SizeT wordCount = instructionWord >> 16u;
|
||||
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) {
|
||||
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
|
||||
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
|
||||
}
|
||||
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
|
||||
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
|
||||
if (capability == spv::Capability::SampledRect ||
|
||||
capability == spv::Capability::ImageRect) {
|
||||
rectCapabilityWordOffsets.push_back(offset + 1);
|
||||
}
|
||||
} else {
|
||||
switch (opcode) {
|
||||
// Everything that takes normalized coordinates. Tracing each one back to
|
||||
// its image type would let a module mix a normalized 2D lookup with a
|
||||
// rectangle fetch, but the extra reach is not worth the risk of getting
|
||||
// the trace wrong: decline the whole module instead.
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
hasNormalizedCoordinateLookup = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outputBinary.assign(inputBinary.begin(), inputBinary.end());
|
||||
for (const SizeT dimWordOffset : rectDimWordOffsets) {
|
||||
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is always
|
||||
// declared by a graphics module, so restating it keeps the word count intact
|
||||
// without leaving a capability SPIRV-Cross would key off.
|
||||
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
|
||||
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -43,6 +43,16 @@ namespace MobileGL {
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
|
||||
// for them at all - it refuses outright ("Rectangle textures are not supported on
|
||||
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
|
||||
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
|
||||
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
|
||||
// lookup would also need its coordinates divided by the texture size, so the pass
|
||||
// declines those modules instead of emitting something subtly wrong. Returns false
|
||||
// when it changed nothing or cannot safely convert. DirectGLES only.
|
||||
static bool LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
|
||||
@@ -688,6 +688,10 @@ namespace {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Stamped onto the normalized directive when a legacy (or absent) desktop
|
||||
// version was rewritten to 330; consumed by RetargetLegacyVersionDirectiveTo460.
|
||||
constexpr const char* kNormalizedLegacyMarker = "/*mobilegl-normalized-legacy*/";
|
||||
|
||||
MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) {
|
||||
if (info.profile == MobileGL::ShaderProfile::ES) {
|
||||
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
|
||||
@@ -702,9 +706,23 @@ namespace {
|
||||
return "#version 460 compatibility\n";
|
||||
}
|
||||
|
||||
// An explicitly declared modern core version keeps its number: the GL CTS
|
||||
// negative-compile cases (reserved names, layout-qualifier forms, missing
|
||||
// overloads) rely on the declared version's rules, and raising it would
|
||||
// silently legalize them. gpu_shader5 opt-ins keep the 460 escalation -
|
||||
// Vulkan glslang's ARB_gpu_shader5 support is not complete enough alone.
|
||||
if (info.hasValidVersionDirective && info.version >= 330 && !info.enablesGpuShader5) {
|
||||
return "#version " + std::to_string(info.version) + " core\n";
|
||||
}
|
||||
|
||||
const bool useLegacyDesktopVersion =
|
||||
info.version < 400 && !info.enablesGpuShader5;
|
||||
return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n";
|
||||
// The trailing marker records that this 330 came from a legacy declaration
|
||||
// (or none at all), so the compile-failure retry may re-raise it to 460.
|
||||
// An application's own "#version 330" never carries it and keeps strict
|
||||
// 3.30 semantics.
|
||||
return useLegacyDesktopVersion ? MobileGL::String("#version 330 core ") + kNormalizedLegacyMarker + "\n"
|
||||
: "#version 460 core\n";
|
||||
}
|
||||
|
||||
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
||||
@@ -1306,12 +1324,123 @@ namespace MobileGL {
|
||||
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
||||
// compatibility shaders keep whatever they declared.
|
||||
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
||||
// Only rescue MobileGL's own legacy normalization (marked on the directive line).
|
||||
// An application-declared "#version 330" keeps strict 3.30 semantics: raising it
|
||||
// would re-legalize the CTS negative-compile cases (reserved names, arrays of
|
||||
// arrays, missing overloads).
|
||||
SizeT lineEnd = source.find('\n', info.versionDirectiveStart);
|
||||
if (lineEnd == MobileGL::String::npos) {
|
||||
lineEnd = source.size();
|
||||
}
|
||||
const SizeT markerPos = source.find(kNormalizedLegacyMarker, info.versionDirectiveStart);
|
||||
if (markerPos == MobileGL::String::npos || markerPos > lineEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
||||
"#version 460 core\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<String> FindReservedIdentifierViolation(const String& source) {
|
||||
// Reserved anywhere; glslang accepts them as plain identifiers.
|
||||
static constexpr const char* kAlwaysReserved[] = {
|
||||
"image1DShadow",
|
||||
"image2DShadow",
|
||||
"image1DArrayShadow",
|
||||
"image2DArrayShadow",
|
||||
};
|
||||
// Keywords legal only inside a layout(...) qualifier list.
|
||||
static constexpr const char* kLayoutOnlyKeywords[] = {
|
||||
"packed",
|
||||
"row_major",
|
||||
};
|
||||
|
||||
const auto isIdentChar = [](char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
|
||||
};
|
||||
|
||||
const SizeT length = source.size();
|
||||
SizeT i = 0;
|
||||
Int layoutParenDepth = 0; // >0 while inside layout(...)
|
||||
Bool pendingLayoutParen = false; // saw "layout", awaiting its '('
|
||||
while (i < length) {
|
||||
const char c = source[i];
|
||||
// Comments.
|
||||
if (c == '/' && i + 1 < length && source[i + 1] == '/') {
|
||||
while (i < length && source[i] != '\n') ++i;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < length && source[i + 1] == '*') {
|
||||
i += 2;
|
||||
while (i + 1 < length && !(source[i] == '*' && source[i + 1] == '/')) ++i;
|
||||
i = (i + 1 < length) ? i + 2 : length;
|
||||
continue;
|
||||
}
|
||||
// Preprocessor lines stay out of scope (macro names may shadow anything).
|
||||
if (c == '#' && (i == 0 || source[i - 1] == '\n' ||
|
||||
source.find_last_not_of(" \t", i - 1) == MobileGL::String::npos ||
|
||||
source[source.find_last_not_of(" \t", i - 1)] == '\n')) {
|
||||
while (i < length && source[i] != '\n') {
|
||||
if (source[i] == '\\' && i + 1 < length && source[i + 1] == '\n') ++i;
|
||||
++i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '(') {
|
||||
if (pendingLayoutParen) {
|
||||
layoutParenDepth = 1;
|
||||
pendingLayoutParen = false;
|
||||
} else if (layoutParenDepth > 0) {
|
||||
++layoutParenDepth;
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (c == ')') {
|
||||
if (layoutParenDepth > 0) --layoutParenDepth;
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (isIdentChar(c) && !(c >= '0' && c <= '9')) {
|
||||
const SizeT start = i;
|
||||
while (i < length && isIdentChar(source[i])) ++i;
|
||||
const StringView word(source.data() + start, i - start);
|
||||
if (word == "layout") {
|
||||
pendingLayoutParen = true;
|
||||
continue;
|
||||
}
|
||||
pendingLayoutParen = false;
|
||||
for (const char* reserved : kAlwaysReserved) {
|
||||
if (word == reserved) {
|
||||
return String("ERROR: reserved identifier '") + reserved + "' may not be used.";
|
||||
}
|
||||
}
|
||||
if (layoutParenDepth == 0) {
|
||||
for (const char* keyword : kLayoutOnlyKeywords) {
|
||||
if (word == keyword) {
|
||||
return String("ERROR: '") + keyword +
|
||||
"' is a keyword and may not be used as an identifier.";
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isIdentChar(c)) { // digit-led token: skip the whole number/identifier tail
|
||||
while (i < length && isIdentChar(source[i])) ++i;
|
||||
pendingLayoutParen = false;
|
||||
continue;
|
||||
}
|
||||
pendingLayoutParen = false;
|
||||
++i;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace MobileGL {
|
||||
// uses 420-era syntax without the matching #extension line, which real drivers tend to
|
||||
// accept - can be retried instead of failing to compile.
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
||||
|
||||
// GLSL reserves a few names glslang happily accepts as identifiers ("packed",
|
||||
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the
|
||||
// compile-error text for the first violation, or nullopt for a clean source.
|
||||
std::optional<String> FindReservedIdentifierViolation(const String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -95,6 +95,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
Int32,
|
||||
Half,
|
||||
Float32,
|
||||
UNorm32, // 32-bit fixed-point depth shadow
|
||||
};
|
||||
|
||||
struct InternalShadowLayout {
|
||||
@@ -123,6 +124,21 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
|
||||
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
|
||||
switch (internal) {
|
||||
// Depth shadows follow TextureFormatProcessor::NormalizePixelFormat: 16-bit
|
||||
// unorm for DEPTH_COMPONENT16, 32-bit unorm for the 24/32-bit fixed-point
|
||||
// depths, float for DEPTH_COMPONENT32F.
|
||||
case TextureInternalFormat::DepthComponent16:
|
||||
out = {1, ShadowComponent::UNorm16, false};
|
||||
return true;
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
case TextureInternalFormat::DepthComponent32:
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
out = {1, ShadowComponent::UNorm32, false};
|
||||
return true;
|
||||
case TextureInternalFormat::DepthComponent32F:
|
||||
out = {1, ShadowComponent::Float32, false};
|
||||
return true;
|
||||
|
||||
case TextureInternalFormat::R8:
|
||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RG8:
|
||||
@@ -299,8 +315,10 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case TextureInputFormat::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true;
|
||||
case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; return true;
|
||||
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
// A depth value converts like a single normalized/float channel.
|
||||
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
|
||||
default:
|
||||
return false; // depth / stencil / unknown
|
||||
return false; // stencil / packed depth-stencil / unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,8 +364,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16;
|
||||
return true;
|
||||
case TexturePixelDataType::UnsignedInt:
|
||||
if (!isInteger) return false; // no 32-bit normalized shadow layout
|
||||
out = ShadowComponent::UInt32;
|
||||
out = isInteger ? ShadowComponent::UInt32 : ShadowComponent::UNorm32;
|
||||
return true;
|
||||
case TexturePixelDataType::Int:
|
||||
if (!isInteger) return false;
|
||||
@@ -617,6 +634,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
case ShadowComponent::Float32:
|
||||
Memcpy(dst, &v, sizeof(v));
|
||||
break;
|
||||
case ShadowComponent::UNorm32: {
|
||||
const auto out = static_cast<Uint32>(
|
||||
std::llround(static_cast<double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break; // integer components never reach the float encoder
|
||||
}
|
||||
@@ -771,6 +794,64 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
const Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height;
|
||||
const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment);
|
||||
|
||||
// GL_DEPTH_COMPONENT client data may populate a packed depth-stencil internal
|
||||
// format (the stencil half becomes zero); the generic channel converter cannot
|
||||
// express the packed shadow words, so convert here.
|
||||
const Bool packedDepthStencilInternal = targetInternalFormat == TextureInternalFormat::Depth24Stencil8 ||
|
||||
targetInternalFormat == TextureInternalFormat::DepthStencil ||
|
||||
targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
|
||||
if (!isBitmap && packedDepthStencilInternal && textureInputFormat == TextureInputFormat::DepthComponent &&
|
||||
(inputDataType == TexturePixelDataType::Float || inputDataType == TexturePixelDataType::UnsignedInt ||
|
||||
inputDataType == TexturePixelDataType::UnsignedShort)) {
|
||||
const Bool floatShadow = targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
|
||||
const SizeT outPixelSize = floatShadow ? 8 : 4;
|
||||
outSize = static_cast<SizeT>(width) * height * std::max(depth, 1) * outPixelSize;
|
||||
Uint8* outputPixels = static_cast<Uint8*>(malloc(outSize));
|
||||
if (!outputPixels) {
|
||||
outSize = 0;
|
||||
return nullptr;
|
||||
}
|
||||
const Uint8* srcBase = static_cast<const Uint8*>(inputPixels) +
|
||||
static_cast<SizeT>(params.SkipImages) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
|
||||
static_cast<SizeT>(params.SkipRows) * inputRowStride +
|
||||
static_cast<SizeT>(params.SkipPixels) * pixelSize;
|
||||
Uint8* dst = outputPixels;
|
||||
for (Int z = 0; z < std::max(depth, 1); ++z) {
|
||||
for (Int y = 0; y < height; ++y) {
|
||||
const Uint8* srcRow = srcBase +
|
||||
static_cast<SizeT>(z) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
|
||||
static_cast<SizeT>(y) * inputRowStride;
|
||||
for (Int x = 0; x < width; ++x) {
|
||||
Float depthValue = 0.0f;
|
||||
if (inputDataType == TexturePixelDataType::Float) {
|
||||
Memcpy(&depthValue, srcRow + static_cast<SizeT>(x) * 4, sizeof(depthValue));
|
||||
} else if (inputDataType == TexturePixelDataType::UnsignedInt) {
|
||||
Uint32 raw = 0;
|
||||
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 4, sizeof(raw));
|
||||
depthValue = static_cast<Float>(static_cast<double>(raw) / 4294967295.0);
|
||||
} else {
|
||||
Uint16 raw = 0;
|
||||
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 2, sizeof(raw));
|
||||
depthValue = static_cast<Float>(raw) / 65535.0f;
|
||||
}
|
||||
if (floatShadow) {
|
||||
const Uint32 stencilWord = 0;
|
||||
Memcpy(dst, &depthValue, sizeof(depthValue));
|
||||
Memcpy(dst + 4, &stencilWord, sizeof(stencilWord));
|
||||
dst += 8;
|
||||
} else {
|
||||
const Uint32 depth24 = static_cast<Uint32>(
|
||||
std::llround(static_cast<double>(std::clamp(depthValue, 0.0f, 1.0f)) * 16777215.0));
|
||||
const Uint32 word = depth24 << 8;
|
||||
Memcpy(dst, &word, sizeof(word));
|
||||
dst += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputPixels;
|
||||
}
|
||||
|
||||
UnpackConversionSpec conversion{};
|
||||
const Bool needConversion =
|
||||
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
|
||||
@@ -972,6 +1053,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::UNorm32: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(static_cast<double>(v) / 4294967295.0);
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,16 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat)
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
break;
|
||||
case GL_RGB16_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
break;
|
||||
case GL_RGBA16_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
@@ -46,6 +51,9 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
break;
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
@@ -66,7 +74,15 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
switch (internalFormat) {
|
||||
case GL_DEPTH_COMPONENT32:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
||||
*outInternalFormat = GL_DEPTH_COMPONENT;
|
||||
// The unsized GL_DEPTH_COMPONENT base format is not a legal
|
||||
// glTexStorage/glRenderbufferStorage internal format on ES, which left
|
||||
// the attachment with no storage at all (KHR-GL3x.framebuffer_blit's
|
||||
// GL_DEPTH_COMPONENT32 config then read an incomplete framebuffer).
|
||||
// GL_DEPTH_COMPONENT24 is the nearest sized ES format that keeps the
|
||||
// same fixed-point encoding, so the GL_UNSIGNED_INT transfer type below
|
||||
// still describes the data; GL_DEPTH_COMPONENT32F would need a float
|
||||
// conversion the upload path does not apply.
|
||||
*outInternalFormat = GL_DEPTH_COMPONENT24;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
@@ -79,6 +95,13 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB16:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
// GL_RGB32F is a legal ES texture format but is not colour-renderable, so
|
||||
// glTexStorage2DMultisample rejects it and the attachment ends up with no
|
||||
// storage at all.
|
||||
*outInternalFormat = GL_RGBA32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRgb16)) {
|
||||
*outInternalFormat = GL_RGB32F;
|
||||
@@ -109,6 +132,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
|
||||
// signed-normalized encoding whenever the driver can render to it.
|
||||
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
? GL_RGBA16F
|
||||
: GL_RGBA16_SNORM;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
@@ -142,6 +173,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
break;
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
*outInternalFormat = GL_RGB16F;
|
||||
break;
|
||||
@@ -552,6 +587,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outType = GL_UNSIGNED_INT;
|
||||
break;
|
||||
case GL_DEPTH_COMPONENT32:
|
||||
// Follows the internal-format normalization above: ES only accepts
|
||||
// GL_FLOAT data for a GL_DEPTH_COMPONENT32F store.
|
||||
*outType = GL_UNSIGNED_INT;
|
||||
break;
|
||||
case GL_DEPTH_COMPONENT32F:
|
||||
|
||||
@@ -18,6 +18,17 @@ namespace MobileGL {
|
||||
NoDepthComponent32 = 1 << 4,
|
||||
NoRGBA8Snorm = 1 << 5,
|
||||
NoRGB16Snorm = 1 << 6,
|
||||
// The target must be colour-renderable and ES has no renderable three-channel
|
||||
// form of the requested format, so it has to be widened to the four-channel one.
|
||||
// Only meaningful for multisample textures: those can never be uploaded to, only
|
||||
// rendered into, so the extra alpha comes from the draw (1.0 for an RGB source)
|
||||
// and no transfer path has to expand three-channel client data.
|
||||
NoThreeChannelRenderTarget = 1 << 7,
|
||||
// Pairs with the bit above: the widened four-channel format has to stay renderable AND
|
||||
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and
|
||||
// EXT_render_snorm. Without them the only renderable widening left is a half float, whose
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
||||
NoSnorm16RenderTarget = 1 << 8,
|
||||
None = 0,
|
||||
};
|
||||
namespace MG_Util::TextureFormatProcessor {
|
||||
|
||||
@@ -181,7 +181,19 @@ namespace MobileGL {
|
||||
explicit BindingSlotRange1D(TargetEnum target, const Range1D& range = Range1D())
|
||||
: BindingSlot<ObjectType>(target), m_range(range) {}
|
||||
|
||||
Range1D GetRange() const { return m_range; }
|
||||
// The range the binding actually covers right now. A whole-buffer binding
|
||||
// (glBindBufferBase) does not freeze anything: GL resolves it against the
|
||||
// object's size at every use, so a glBufferData issued after the bind has to
|
||||
// be visible here - binding an empty buffer and giving it storage afterwards
|
||||
// is ordinary application code. Only glBindBufferRange pins a fixed window.
|
||||
Range1D GetRange() const {
|
||||
if (!m_hasExplicitRange) {
|
||||
if (const auto& object = this->GetBoundObject()) {
|
||||
return Range1D(0, object->GetSize());
|
||||
}
|
||||
}
|
||||
return m_range;
|
||||
}
|
||||
|
||||
Bool HasExplicitRange() const { return m_hasExplicitRange; }
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ val releaseSigningReady = signingStoreFile.exists()
|
||||
val debuggableRelease = (findProperty("mobilegl.debuggableRelease") ?: "false").toString().toBoolean()
|
||||
|
||||
val mobileGlVersionMajor = 26
|
||||
val mobileGlVersionMinor = 7
|
||||
val mobileGlVersionMinor = 8
|
||||
val mobileGlGitShortHash = runGit("rev-parse", "--short=7", "HEAD") ?: "nogit"
|
||||
val mobileGlMonthlyRevision = runGit(
|
||||
"rev-list",
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# MobileGL POST Format Capability Tables
|
||||
|
||||
## Goal
|
||||
|
||||
Expose the format-capability results used during MobileGL backend startup in the Android plugin's driver POST screen. The screen must show the exact `Full`, `Caveat`, or `None` result for every backend, target, internal format, and capability without duplicating the backend's detection rules.
|
||||
|
||||
## Existing Architecture
|
||||
|
||||
- `DriverPost.cpp` probes the device GLES and Vulkan drivers before `MobileGL::Initialize()` and returns a `BackendPostReport` for each backend.
|
||||
- `DriverPostJni.cpp` serializes those reports to JSON for `PostActivity`.
|
||||
- `PostActivity` uses platform Android views and already supports collapsible check details and a collapsible raw report.
|
||||
- Backend startup fills a `FormatCapabilityCache` in the DirectGLES and DirectVulkan `InitCapabilities()` paths. `FullCaps` takes precedence over `CaveatCaps`; an absent bit means `None`.
|
||||
- The capability matrix contains 12 targets, 75 internal formats, and 14 capability columns per backend.
|
||||
|
||||
## Selected Approach
|
||||
|
||||
Extract callable format-probe entry points from the existing DirectGLES and DirectVulkan implementations. Backend startup and the POST will call these same functions, so their results cannot drift.
|
||||
|
||||
The POST will run each probe while its temporary driver resources are still valid:
|
||||
|
||||
- DirectGLES: after the GLES function table and capabilities have been populated, while the 1x1 pbuffer context is current.
|
||||
- DirectVulkan: after selecting the physical device, while the Vulkan instance and physical device handles remain valid.
|
||||
|
||||
The resulting optional `FormatCapabilityCache` will be stored in each `BackendPostReport`. Failure to obtain a format table will not discard the existing POST checks or change their verdict; the UI will instead report that the format table is unavailable.
|
||||
|
||||
## JSON Contract
|
||||
|
||||
The JNI report will add an optional `formatCapabilities` object to each backend. To avoid repeating tens of thousands of status strings, the object will contain:
|
||||
|
||||
- one ordered capability-name array;
|
||||
- one entry per target;
|
||||
- one compact row per internal format containing the format name, a Full bitmask, and a Caveat bitmask.
|
||||
|
||||
Java resolves each cell in this order:
|
||||
|
||||
1. Full bit present: `Full`.
|
||||
2. Otherwise Caveat bit present: `Caveat`.
|
||||
3. Otherwise: `None`.
|
||||
|
||||
This preserves the backend's current precedence and keeps the raw JSON reasonably small.
|
||||
|
||||
## Android UI
|
||||
|
||||
Each backend section keeps its existing verdict, renderer string, and check table. A new `Format capabilities` subsection follows it.
|
||||
|
||||
- Each of the 11 texture targets and `Renderbuffer` is a separate, initially collapsed table.
|
||||
- Target headers can be expanded independently.
|
||||
- Table content is created on expansion and removed when collapsed, preventing the activity from retaining roughly 27,000 status views.
|
||||
- Each expanded table is placed in a horizontal scroll container.
|
||||
- The first column contains internal-format names. The remaining columns use the ordered capability names from the JSON report.
|
||||
- Every status cell displays its status text and uses the conventional color mapping:
|
||||
- `Full`: green background with white text.
|
||||
- `Caveat`: yellow background with black text.
|
||||
- `None`: red background with white text.
|
||||
- Header and format-name cells use neutral dark backgrounds consistent with the existing POST theme.
|
||||
- The existing raw-report toggle remains available at the end of the screen.
|
||||
|
||||
## Performance and Lifecycle
|
||||
|
||||
- The existing single-flight native POST and cached JSON behavior remains unchanged.
|
||||
- Format tables are lazily materialized and discarded on collapse.
|
||||
- The JSON carries bitmasks rather than repeated `Full`, `Caveat`, and `None` strings.
|
||||
- Existing configuration-change handling remains unchanged.
|
||||
|
||||
## Validation
|
||||
|
||||
1. Run focused source checks and `git diff --check`.
|
||||
2. Build the Android plugin APK with the repository's current Gradle workflow.
|
||||
3. If an Android target is connected, install the APK and open `PostActivity`.
|
||||
4. Verify both backend sections, all target toggles, horizontal scrolling, visible cell text, and the green/yellow/red mapping.
|
||||
5. Confirm collapsing a table removes its generated content and expanding it recreates the same values.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Changing the meanings of `Full`, `Caveat`, or `None`.
|
||||
- Changing POST verdict rules.
|
||||
- Displaying sample-count vectors in this iteration.
|
||||
- Replacing the existing platform-view UI with Compose, AppCompat, or WebView.
|
||||
+28
-1
@@ -1,4 +1,31 @@
|
||||
# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android
|
||||
# Running the OpenGL CTS against MobileGL
|
||||
|
||||
This directory contains two supported paths:
|
||||
|
||||
- Android arm64 / MobileGL EGL: the KHR-GL33 workflow documented below and in
|
||||
`skills/gl-cts-on-mobilegl/SKILL.md`.
|
||||
- Windows x64 / MobileGL WGL: the GL30-GL46 pipeline in
|
||||
`scripts/wgl_glcts_pipeline.py`, documented by
|
||||
`skills/wgl-gl-cts-on-mobilegl/SKILL.md`.
|
||||
|
||||
Windows prerequisites are Git, Python 3.9+, CMake, Visual Studio 2022's Desktop
|
||||
C++ workload, and a Vulkan SDK visible to CMake. DirectVulkan also needs a
|
||||
working Vulkan loader plus a GPU-vendor ICD and driver; the SDK alone is not a
|
||||
GPU driver.
|
||||
|
||||
For Windows, start with:
|
||||
|
||||
```powershell
|
||||
python tools\cts\scripts\wgl_glcts_pipeline.py --help
|
||||
```
|
||||
|
||||
The pipeline builds MobileGL as a drop-in `opengl32.dll`, builds or reuses
|
||||
`glcts.exe`, checks that WGL loaded MobileGL rather than the system driver,
|
||||
resumes individual suites after crashes/timeouts, and writes Markdown plus JSON
|
||||
reports below the printed `runs/<first-16-of-run-fingerprint>` directory. Its
|
||||
manifest records provenance and the runner settings used to validate a resume.
|
||||
|
||||
## Android KHR-GL33 workflow
|
||||
|
||||
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
|
||||
passes, separately for each backend (`DirectGLES`, `DirectVulkan`).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
* dEQP platform port for MobileGL on Android
|
||||
* dEQP platform port for MobileGL (Android and desktop Linux)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,14 +28,17 @@
|
||||
* - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with
|
||||
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
|
||||
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
|
||||
* - Window surfaces are backed by an AImageReader rather than an Activity,
|
||||
* which is what lets the suite run as a plain adb-shell binary. DirectVulkan
|
||||
* needs this: its pbuffer path requires VK_EXT_headless_surface, which
|
||||
* Adreno's Android driver does not expose.
|
||||
* - On Android, window surfaces are backed by an AImageReader rather than an
|
||||
* Activity, which is what lets the suite run as a plain adb-shell binary.
|
||||
* DirectVulkan needs this: its pbuffer path requires VK_EXT_headless_surface,
|
||||
* which Adreno's Android driver does not expose.
|
||||
* - On desktop Linux, only pbuffer surfaces are offered. DirectVulkan's
|
||||
* pbuffer path works there because desktop Vulkan loaders expose
|
||||
* VK_EXT_headless_surface.
|
||||
*
|
||||
* Environment:
|
||||
* MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so)
|
||||
* MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer"
|
||||
* MOBILEGL_CTS_SURFACE "window" (Android default) or "pbuffer" (desktop default/only)
|
||||
* MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching
|
||||
*//*--------------------------------------------------------------------*/
|
||||
|
||||
@@ -58,9 +61,11 @@
|
||||
#include "tcuPlatform.hpp"
|
||||
#include "tcuRenderTarget.hpp"
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <android/native_window.h>
|
||||
#include <media/NdkImageReader.h>
|
||||
#endif
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
@@ -88,13 +93,24 @@ static string getLibraryName(void)
|
||||
return (env && env[0]) ? string(env) : string("libMobileGL.so");
|
||||
}
|
||||
|
||||
//! Window surfaces default on: they are the only kind DirectVulkan can use.
|
||||
#if defined(__ANDROID__)
|
||||
//! Window surfaces default on: they are the only kind DirectVulkan can use
|
||||
//! on Android (its pbuffer path needs VK_EXT_headless_surface).
|
||||
static bool useWindowSurface(void)
|
||||
{
|
||||
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
|
||||
return !(env && string(env) == "pbuffer");
|
||||
}
|
||||
#else
|
||||
//! Desktop: pbuffer only. VK_EXT_headless_surface is available there and no
|
||||
//! Activity-free native window abstraction exists.
|
||||
static bool useWindowSurface(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
/*--------------------------------------------------------------------*//*!
|
||||
* \brief A real ANativeWindow with no Activity behind it.
|
||||
*
|
||||
@@ -160,6 +176,12 @@ private:
|
||||
AImageReader *m_reader;
|
||||
ANativeWindow *m_window;
|
||||
};
|
||||
#else
|
||||
//! Never instantiated on desktop; keeps EglRenderContext's member deletable.
|
||||
class ImageReaderWindow
|
||||
{
|
||||
};
|
||||
#endif
|
||||
|
||||
class GetProcFuncLoader : public glw::FunctionLoader
|
||||
{
|
||||
@@ -352,6 +374,7 @@ EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::C
|
||||
|
||||
if (wantWindow)
|
||||
{
|
||||
#if defined(__ANDROID__)
|
||||
m_window = new ImageReaderWindow(width, height);
|
||||
|
||||
eglw::EGLint visualId = 0;
|
||||
@@ -361,6 +384,9 @@ EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::C
|
||||
m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig,
|
||||
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
|
||||
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
|
||||
#else
|
||||
throw tcu::NotSupportedError("Window surfaces are not supported by the desktop MobileGL platform");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
#!/usr/bin/env python
|
||||
"""Build a GL 3.0--3.3 CTS conformance matrix from dEQP QPA logs.
|
||||
|
||||
The report deliberately scores against the unique cases in each supplied
|
||||
caselist. A case that has not produced a result therefore cannot disappear
|
||||
from the denominator and make a partial run look conformant.
|
||||
|
||||
QPA parsing and crash/hang sidecar handling follow :mod:`qpa_report`:
|
||||
|
||||
* a later QPA observation of a case wins;
|
||||
* ``crashed.txt`` upgrades a missing/incomplete result to ``Crash``;
|
||||
* ``hung.txt`` upgrades a missing/incomplete/crash result to ``DeviceHang``.
|
||||
|
||||
Example::
|
||||
|
||||
python cts_matrix_report.py \
|
||||
--gl30-caselist gl30-main.txt --gl30-results runs/gl30 \
|
||||
--gl31-caselist gl31-main.txt --gl31-results runs/gl31 \
|
||||
--gl32-caselist gl32-main.txt --gl32-results runs/gl32 \
|
||||
--gl33-caselist gl33-main.txt --gl33-results runs/gl33 \
|
||||
--json runs/cts-matrix.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Iterable, Optional, Sequence
|
||||
|
||||
try: # Works both as a directly executed script and as a package import.
|
||||
from . import qpa_report
|
||||
except ImportError: # pragma: no cover - exercised by the command-line tests
|
||||
import qpa_report
|
||||
|
||||
|
||||
VERSIONS = ("gl30", "gl31", "gl32", "gl33")
|
||||
ACCEPTED_STATUSES = (
|
||||
"Pass",
|
||||
"NotSupported",
|
||||
"QualityWarning",
|
||||
"CompatibilityWarning",
|
||||
"Waiver",
|
||||
)
|
||||
ACCEPTED = frozenset(ACCEPTED_STATUSES)
|
||||
CHUNK_QPA = re.compile(r"^chunk(\d+)\.qpa$", re.IGNORECASE)
|
||||
|
||||
|
||||
class ReportInputError(ValueError):
|
||||
"""An input path cannot be used to construct a meaningful report."""
|
||||
|
||||
|
||||
def _read_non_comment_lines(path: str) -> list[str]:
|
||||
try:
|
||||
# Match run_cts_windows.py: Khronos lists are UTF-8 and may carry a BOM.
|
||||
with open(path, "r", encoding="utf-8-sig", errors="strict") as fh:
|
||||
return [
|
||||
line.strip()
|
||||
for line in fh
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
]
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise ReportInputError(f"cannot read {path}: {exc}") from exc
|
||||
|
||||
|
||||
def read_caselist(path: str) -> tuple[list[str], dict[str, int]]:
|
||||
"""Return unique cases in file order and repeated caselist entries.
|
||||
|
||||
The mustpass files consumed by glcts and ``run_cts.py`` are one case per
|
||||
non-empty, non-comment line, so this intentionally uses the same syntax.
|
||||
"""
|
||||
|
||||
entries = _read_non_comment_lines(path)
|
||||
counts = Counter(entries)
|
||||
unique = list(dict.fromkeys(entries))
|
||||
duplicates = {case: count for case, count in counts.items() if count > 1}
|
||||
return unique, duplicates
|
||||
|
||||
|
||||
def _collect_qpa_files(paths: Sequence[str]) -> list[str]:
|
||||
missing = [path for path in paths if not os.path.exists(path)]
|
||||
if missing:
|
||||
raise ReportInputError(
|
||||
"result path(s) do not exist: " + ", ".join(sorted(missing))
|
||||
)
|
||||
|
||||
# qpa_report.collect provides the established directory-recursion rules.
|
||||
# De-duplicate aliases so specifying the same directory twice does not
|
||||
# manufacture duplicate observations.
|
||||
files = qpa_report.collect(paths)
|
||||
by_identity: dict[str, str] = {}
|
||||
for path in files:
|
||||
if not os.path.isfile(path):
|
||||
raise ReportInputError(f"QPA input is not a file: {path}")
|
||||
absolute = os.path.abspath(path)
|
||||
by_identity.setdefault(os.path.normcase(absolute), absolute)
|
||||
def order_key(value: str) -> tuple[str, str, int, str]:
|
||||
absolute = os.path.abspath(value)
|
||||
directory = os.path.normcase(os.path.dirname(absolute))
|
||||
filename = os.path.normcase(os.path.basename(absolute))
|
||||
match = CHUNK_QPA.fullmatch(filename)
|
||||
if match:
|
||||
# run_cts_windows.py uses a minimum width of four digits, not a
|
||||
# fixed width. Numeric ordering is therefore required once a run
|
||||
# reaches chunk10000; lexical ordering would put it before
|
||||
# chunk9999 and break the later-observation-wins rule.
|
||||
return directory, "chunk", int(match.group(1)), filename
|
||||
# Preserve a deterministic, name-based position for foreign/legacy
|
||||
# QPA files while grouping numeric runner chunks at the lexical
|
||||
# position occupied by the "chunk" basename.
|
||||
return directory, filename, -1, filename
|
||||
|
||||
return sorted(by_identity.values(), key=order_key)
|
||||
|
||||
|
||||
def _ratio(numerator: int, denominator: int) -> float:
|
||||
return numerator / denominator if denominator else 0.0
|
||||
|
||||
|
||||
def _sidecar(paths: Sequence[str], name: str) -> set[str]:
|
||||
"""Load a run_cts.py sidecar with qpa_report-compatible lookup rules."""
|
||||
|
||||
return qpa_report.load_sidecar(paths, name)
|
||||
|
||||
|
||||
def build_version_report(
|
||||
version: str, caselist: str, result_paths: Sequence[str]
|
||||
) -> dict:
|
||||
"""Build the serialisable report for one GL mustpass version."""
|
||||
|
||||
expected_cases, expected_duplicates = read_caselist(caselist)
|
||||
expected = set(expected_cases)
|
||||
qpa_files = _collect_qpa_files(result_paths)
|
||||
|
||||
results: dict[str, str] = {}
|
||||
observation_history: dict[str, list[dict[str, str]]] = defaultdict(list)
|
||||
for qpa_file in qpa_files:
|
||||
for case, status in qpa_report.parse_qpa(qpa_file):
|
||||
observation_history[case].append(
|
||||
{"file": qpa_file, "status": status}
|
||||
)
|
||||
results[case] = status
|
||||
|
||||
crashed = _sidecar(result_paths, "crashed.txt")
|
||||
hung = _sidecar(result_paths, "hung.txt")
|
||||
explicit_unrun = _sidecar(result_paths, "unrun.txt")
|
||||
skipped = _sidecar(result_paths, "skipped.txt")
|
||||
|
||||
# Keep this order and these guards in lock-step with qpa_report.py.
|
||||
for case in crashed:
|
||||
if results.get(case, "Incomplete") == "Incomplete":
|
||||
results[case] = "Crash"
|
||||
for case in hung:
|
||||
if results.get(case, "Incomplete") in ("Incomplete", "Crash"):
|
||||
results[case] = "DeviceHang"
|
||||
|
||||
# A begin/end pair without <Result>, or a QPA truncated mid-case, is not a
|
||||
# completed observation. Sidecars above may upgrade it to Crash/Hang;
|
||||
# anything still Incomplete must stay in the expected denominator as unrun.
|
||||
incomplete_results = {
|
||||
case for case, status in results.items() if status == "Incomplete"
|
||||
}
|
||||
for case in incomplete_results:
|
||||
del results[case]
|
||||
|
||||
expected_results = {
|
||||
case: status for case, status in results.items() if case in expected
|
||||
}
|
||||
unexpected_results = {
|
||||
case: status for case, status in results.items() if case not in expected
|
||||
}
|
||||
|
||||
# Missing cases are inferred from the caselist even if unrun.txt itself is
|
||||
# missing or stale. This is the invariant that prevents partial-run rate
|
||||
# inflation.
|
||||
unrun_cases = expected - set(expected_results)
|
||||
declared_not_measured = explicit_unrun | skipped
|
||||
undeclared_unrun = unrun_cases - declared_not_measured
|
||||
stale_unrun = (explicit_unrun | skipped) & set(expected_results)
|
||||
|
||||
counts = Counter(expected_results.values())
|
||||
strict_pass = counts["Pass"]
|
||||
accepted = sum(counts[status] for status in ACCEPTED)
|
||||
result_count = len(expected_results)
|
||||
expected_count = len(expected)
|
||||
crash_count = counts["Crash"]
|
||||
hang_count = counts["DeviceHang"]
|
||||
|
||||
duplicate_cases = {
|
||||
case: {
|
||||
"observations": len(history),
|
||||
"extra_observations": len(history) - 1,
|
||||
"final_status": results.get(case, "Incomplete"),
|
||||
"history": history,
|
||||
}
|
||||
for case, history in sorted(observation_history.items())
|
||||
if len(history) > 1
|
||||
}
|
||||
duplicate_observations = sum(
|
||||
item["extra_observations"] for item in duplicate_cases.values()
|
||||
)
|
||||
|
||||
sidecar_unknown = {
|
||||
name: sorted(cases - expected)
|
||||
for name, cases in (
|
||||
("crashed.txt", crashed),
|
||||
("hung.txt", hung),
|
||||
("unrun.txt", explicit_unrun),
|
||||
("skipped.txt", skipped),
|
||||
)
|
||||
if cases - expected
|
||||
}
|
||||
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if not expected_count:
|
||||
errors.append("caselist has no cases")
|
||||
if expected_duplicates:
|
||||
errors.append(
|
||||
f"caselist has {sum(n - 1 for n in expected_duplicates.values())} "
|
||||
"duplicate entry/entries"
|
||||
)
|
||||
if not qpa_files:
|
||||
errors.append("no .qpa files found")
|
||||
if unexpected_results:
|
||||
errors.append(
|
||||
f"{len(unexpected_results)} result case(s) are absent from the caselist"
|
||||
)
|
||||
if sidecar_unknown:
|
||||
errors.append("one or more sidecars name cases absent from the caselist")
|
||||
if undeclared_unrun:
|
||||
errors.append(
|
||||
f"{len(undeclared_unrun)} missing result case(s) are not declared by "
|
||||
"unrun.txt/skipped.txt"
|
||||
)
|
||||
if stale_unrun:
|
||||
warnings.append(
|
||||
f"{len(stale_unrun)} case(s) declared unrun/skipped also have a result"
|
||||
)
|
||||
if duplicate_observations:
|
||||
warnings.append(
|
||||
f"{duplicate_observations} duplicate QPA observation(s); last result wins"
|
||||
)
|
||||
if incomplete_results:
|
||||
warnings.append(
|
||||
f"{len(incomplete_results)} QPA case(s) ended without a final result and were treated as unrun"
|
||||
)
|
||||
|
||||
if errors:
|
||||
state = "ERROR"
|
||||
elif unrun_cases:
|
||||
state = "INCOMPLETE"
|
||||
else:
|
||||
state = "OK"
|
||||
|
||||
return {
|
||||
"version": version,
|
||||
"inputs": {
|
||||
"caselist": os.path.abspath(caselist),
|
||||
"result_paths": [os.path.abspath(path) for path in result_paths],
|
||||
"qpa_files": qpa_files,
|
||||
},
|
||||
"expected": expected_count,
|
||||
"result": result_count,
|
||||
"pass": strict_pass,
|
||||
"accepted": accepted,
|
||||
"crash": crash_count,
|
||||
"hang": hang_count,
|
||||
"unrun": len(unrun_cases),
|
||||
"duplicate": duplicate_observations,
|
||||
"counts": dict(sorted(counts.items())),
|
||||
"coverage": {
|
||||
"numerator": result_count,
|
||||
"denominator": expected_count,
|
||||
"rate": _ratio(result_count, expected_count),
|
||||
},
|
||||
"rates": {
|
||||
# These are the report's conformance rates. Expected, not merely
|
||||
# measured results, is the denominator.
|
||||
"denominator": "expected",
|
||||
"strict_pass_only": _ratio(strict_pass, expected_count),
|
||||
"conformance_accepted": _ratio(accepted, expected_count),
|
||||
# Useful for comparison with qpa_report.py, whose denominator is
|
||||
# cases with a result. Never presented as the conformance rate.
|
||||
"measured_only_strict_pass": _ratio(strict_pass, result_count),
|
||||
"measured_only_conformance_accepted": _ratio(
|
||||
accepted, result_count
|
||||
),
|
||||
},
|
||||
"strict_pass_rate": _ratio(strict_pass, expected_count),
|
||||
"conformance_accepted_rate": _ratio(accepted, expected_count),
|
||||
"validation": {
|
||||
"state": state,
|
||||
"ok": state == "OK",
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"invariant_expected_equals_result_plus_unrun": (
|
||||
expected_count == result_count + len(unrun_cases)
|
||||
),
|
||||
"undeclared_unrun": sorted(undeclared_unrun),
|
||||
"stale_unrun_or_skipped": sorted(stale_unrun),
|
||||
"sidecar_cases_absent_from_caselist": sidecar_unknown,
|
||||
},
|
||||
"cases": {
|
||||
"results": dict(sorted(expected_results.items())),
|
||||
"unrun": sorted(unrun_cases),
|
||||
"unexpected_results": dict(sorted(unexpected_results.items())),
|
||||
"incomplete_results": sorted(incomplete_results),
|
||||
"duplicate_results": duplicate_cases,
|
||||
"duplicate_caselist_entries": dict(sorted(expected_duplicates.items())),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_matrix(suites: dict[str, tuple[str, Sequence[str]]]) -> dict:
|
||||
"""Build all four version reports and their case-weighted aggregate."""
|
||||
|
||||
version_reports = {
|
||||
version: build_version_report(version, *suites[version])
|
||||
for version in VERSIONS
|
||||
}
|
||||
|
||||
totals = {
|
||||
key: sum(report[key] for report in version_reports.values())
|
||||
for key in (
|
||||
"expected",
|
||||
"result",
|
||||
"pass",
|
||||
"accepted",
|
||||
"crash",
|
||||
"hang",
|
||||
"unrun",
|
||||
"duplicate",
|
||||
)
|
||||
}
|
||||
status_counts: Counter[str] = Counter()
|
||||
for report in version_reports.values():
|
||||
status_counts.update(report["counts"])
|
||||
|
||||
states = {report["validation"]["state"] for report in version_reports.values()}
|
||||
if "ERROR" in states:
|
||||
overall_state = "ERROR"
|
||||
elif "INCOMPLETE" in states:
|
||||
overall_state = "INCOMPLETE"
|
||||
else:
|
||||
overall_state = "OK"
|
||||
|
||||
overall = {
|
||||
**totals,
|
||||
"counts": dict(sorted(status_counts.items())),
|
||||
"aggregation": "weighted_by_expected_cases",
|
||||
"coverage": {
|
||||
"numerator": totals["result"],
|
||||
"denominator": totals["expected"],
|
||||
"rate": _ratio(totals["result"], totals["expected"]),
|
||||
},
|
||||
"rates": {
|
||||
"denominator": "expected",
|
||||
"strict_pass_only": _ratio(totals["pass"], totals["expected"]),
|
||||
"conformance_accepted": _ratio(
|
||||
totals["accepted"], totals["expected"]
|
||||
),
|
||||
"measured_only_strict_pass": _ratio(
|
||||
totals["pass"], totals["result"]
|
||||
),
|
||||
"measured_only_conformance_accepted": _ratio(
|
||||
totals["accepted"], totals["result"]
|
||||
),
|
||||
},
|
||||
"strict_pass_rate": _ratio(totals["pass"], totals["expected"]),
|
||||
"conformance_accepted_rate": _ratio(
|
||||
totals["accepted"], totals["expected"]
|
||||
),
|
||||
"validation": {
|
||||
"state": overall_state,
|
||||
"ok": overall_state == "OK",
|
||||
"invariant_expected_equals_result_plus_unrun": (
|
||||
totals["expected"] == totals["result"] + totals["unrun"]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"accepted_statuses": list(ACCEPTED_STATUSES),
|
||||
"rate_policy": {
|
||||
"denominator": "unique expected cases from each caselist",
|
||||
"unrun_cases": "included in the denominator and never accepted",
|
||||
"duplicate_results": "last QPA result wins, matching qpa_report.py",
|
||||
},
|
||||
"versions": version_reports,
|
||||
"overall": overall,
|
||||
}
|
||||
|
||||
|
||||
def _percent(numerator: int, denominator: int) -> str:
|
||||
if not denominator:
|
||||
return "n/a"
|
||||
return f"{100.0 * numerator / denominator:.2f}% ({numerator}/{denominator})"
|
||||
|
||||
|
||||
def render_markdown(report: dict) -> str:
|
||||
"""Render the compact terminal-facing conformance table."""
|
||||
|
||||
header = (
|
||||
"| Suite | Expected | Result | Pass | Accepted | Crash | Hang | Unrun | "
|
||||
"Duplicate | Coverage | Strict Pass-only | Conformance-accepted | Validation |"
|
||||
)
|
||||
separator = (
|
||||
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|:---:|"
|
||||
)
|
||||
rows = [header, separator]
|
||||
for version in VERSIONS:
|
||||
item = report["versions"][version]
|
||||
rows.append(
|
||||
"| {version} | {expected} | {result} | {pass_count} | {accepted} | "
|
||||
"{crash} | {hang} | {unrun} | {duplicate} | {coverage} | {strict} | "
|
||||
"{accepted_rate} | {state} |".format(
|
||||
version=version.upper(),
|
||||
expected=item["expected"],
|
||||
result=item["result"],
|
||||
pass_count=item["pass"],
|
||||
accepted=item["accepted"],
|
||||
crash=item["crash"],
|
||||
hang=item["hang"],
|
||||
unrun=item["unrun"],
|
||||
duplicate=item["duplicate"],
|
||||
coverage=_percent(item["result"], item["expected"]),
|
||||
strict=_percent(item["pass"], item["expected"]),
|
||||
accepted_rate=_percent(item["accepted"], item["expected"]),
|
||||
state=item["validation"]["state"],
|
||||
)
|
||||
)
|
||||
|
||||
overall = report["overall"]
|
||||
rows.append(
|
||||
"| **Overall (weighted)** | **{expected}** | **{result}** | **{pass_count}** | "
|
||||
"**{accepted}** | **{crash}** | **{hang}** | **{unrun}** | **{duplicate}** | "
|
||||
"**{coverage}** | **{strict}** | **{accepted_rate}** | **{state}** |".format(
|
||||
expected=overall["expected"],
|
||||
result=overall["result"],
|
||||
pass_count=overall["pass"],
|
||||
accepted=overall["accepted"],
|
||||
crash=overall["crash"],
|
||||
hang=overall["hang"],
|
||||
unrun=overall["unrun"],
|
||||
duplicate=overall["duplicate"],
|
||||
coverage=_percent(overall["result"], overall["expected"]),
|
||||
strict=_percent(overall["pass"], overall["expected"]),
|
||||
accepted_rate=_percent(overall["accepted"], overall["expected"]),
|
||||
state=overall["validation"]["state"],
|
||||
)
|
||||
)
|
||||
rows.extend(
|
||||
(
|
||||
"",
|
||||
"Rates use unique **Expected** caselist cases as the denominator; unrun cases "
|
||||
"remain in that denominator and are not accepted.",
|
||||
"Accepted statuses: " + ", ".join(f"`{s}`" for s in ACCEPTED_STATUSES) + ".",
|
||||
"Duplicate is the number of extra QPA observations; the last observation wins.",
|
||||
)
|
||||
)
|
||||
|
||||
details: list[str] = []
|
||||
for version in VERSIONS:
|
||||
validation = report["versions"][version]["validation"]
|
||||
messages = validation["errors"] + validation["warnings"]
|
||||
if messages:
|
||||
details.append(
|
||||
f"- **{version.upper()} {validation['state']}**: " + "; ".join(messages)
|
||||
)
|
||||
if details:
|
||||
rows.extend(("", "Validation details:", "", *details))
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def _write_json(path: str, report: dict) -> None:
|
||||
parent = os.path.dirname(os.path.abspath(path))
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
||||
json.dump(report, fh, indent=2, sort_keys=True)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
for version in VERSIONS:
|
||||
parser.add_argument(
|
||||
f"--{version}-caselist",
|
||||
f"--{version}-case-list",
|
||||
required=True,
|
||||
help=f"{version.upper()} mustpass caselist",
|
||||
)
|
||||
parser.add_argument(
|
||||
f"--{version}-results",
|
||||
f"--{version}-result-dir",
|
||||
f"--{version}-results-dir",
|
||||
action="append",
|
||||
required=True,
|
||||
help=f"{version.upper()} result directory or QPA file (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
dest="json_out",
|
||||
default="cts_matrix_report.json",
|
||||
help="JSON output path (default: ./cts_matrix_report.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-incomplete",
|
||||
action="store_true",
|
||||
help="return success even when validation is ERROR/INCOMPLETE",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
args = _parser().parse_args(argv)
|
||||
suites = {
|
||||
version: (
|
||||
getattr(args, f"{version}_caselist"),
|
||||
getattr(args, f"{version}_results"),
|
||||
)
|
||||
for version in VERSIONS
|
||||
}
|
||||
try:
|
||||
report = build_matrix(suites)
|
||||
_write_json(args.json_out, report)
|
||||
except (OSError, ReportInputError) as exc:
|
||||
print(f"cts_matrix_report: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(render_markdown(report))
|
||||
print(f"\nJSON: {os.path.abspath(args.json_out)}")
|
||||
if not args.allow_incomplete and not report["overall"]["validation"]["ok"]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,540 @@
|
||||
#!/usr/bin/env python
|
||||
"""Summarise any number of GL CTS suites and MobileGL backends.
|
||||
|
||||
Each repeatable suite specification consists of four values: backend, label,
|
||||
caselist, and result directory. For example::
|
||||
|
||||
python cts_multi_report.py \
|
||||
--suite DirectGLES gl30 gl30-main.txt runs/gles/gl30 \
|
||||
--suite DirectVulkan gl30 gl30-main.txt runs/vulkan/gl30 \
|
||||
--markdown cts-summary.md --json cts-summary.json
|
||||
|
||||
A compact comma form is accepted as well::
|
||||
|
||||
--suite=DirectGLES,gl31,gl31-main.txt,runs/gles/gl31
|
||||
|
||||
Per-suite parsing and validation deliberately delegate to
|
||||
``cts_matrix_report`` so QPA ordering, sidecar upgrades, accepted statuses,
|
||||
unrun handling, and duplicate-result semantics cannot drift between reports.
|
||||
All conformance rates use unique expected caselist cases as their denominator.
|
||||
Backend subtotals and the overall total are therefore case-weighted, not an
|
||||
unweighted average of suite percentages.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Iterable, Optional, Sequence
|
||||
|
||||
try: # Direct script execution and package imports are both supported.
|
||||
from . import cts_matrix_report
|
||||
except ImportError: # pragma: no cover - covered through CLI-style tests
|
||||
import cts_matrix_report
|
||||
|
||||
|
||||
SUPPORTED_BACKENDS = ("DirectGLES", "DirectVulkan")
|
||||
SUM_FIELDS = (
|
||||
"expected",
|
||||
"result",
|
||||
"pass",
|
||||
"accepted",
|
||||
"crash",
|
||||
"hang",
|
||||
"unrun",
|
||||
"duplicate",
|
||||
)
|
||||
|
||||
|
||||
class MultiReportInputError(ValueError):
|
||||
"""Suite specifications cannot produce an unambiguous report."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SuiteSpec:
|
||||
backend: str
|
||||
label: str
|
||||
caselist: str
|
||||
result_dir: str
|
||||
|
||||
@property
|
||||
def suite_id(self) -> str:
|
||||
return f"{self.backend}/{self.label}"
|
||||
|
||||
|
||||
def _ratio(numerator: int, denominator: int) -> float:
|
||||
return numerator / denominator if denominator else 0.0
|
||||
|
||||
|
||||
def _validation_state(items: Sequence[dict]) -> str:
|
||||
states = {item["validation"]["state"] for item in items}
|
||||
if "ERROR" in states:
|
||||
return "ERROR"
|
||||
if "INCOMPLETE" in states:
|
||||
return "INCOMPLETE"
|
||||
return "OK"
|
||||
|
||||
|
||||
def aggregate_reports(items: Sequence[dict], suite_state_keys: Sequence[str]) -> dict:
|
||||
"""Return an expected-case-weighted aggregate for suite reports."""
|
||||
|
||||
if len(items) != len(suite_state_keys):
|
||||
raise MultiReportInputError("internal suite/state key count mismatch")
|
||||
|
||||
totals = {
|
||||
field: sum(int(item[field]) for item in items)
|
||||
for field in SUM_FIELDS
|
||||
}
|
||||
status_counts: Counter[str] = Counter()
|
||||
for item in items:
|
||||
status_counts.update(item["counts"])
|
||||
|
||||
state = _validation_state(items)
|
||||
expected = totals["expected"]
|
||||
result = totals["result"]
|
||||
suite_states = {
|
||||
key: item["validation"]["state"]
|
||||
for key, item in zip(suite_state_keys, items)
|
||||
}
|
||||
return {
|
||||
**totals,
|
||||
"suite_count": len(items),
|
||||
"counts": dict(sorted(status_counts.items())),
|
||||
"aggregation": "weighted_by_expected_cases",
|
||||
"coverage": {
|
||||
"numerator": result,
|
||||
"denominator": expected,
|
||||
"rate": _ratio(result, expected),
|
||||
},
|
||||
"rates": {
|
||||
"denominator": "expected",
|
||||
"strict_pass_only": _ratio(totals["pass"], expected),
|
||||
"conformance_accepted": _ratio(totals["accepted"], expected),
|
||||
"measured_only_strict_pass": _ratio(totals["pass"], result),
|
||||
"measured_only_conformance_accepted": _ratio(
|
||||
totals["accepted"], result
|
||||
),
|
||||
},
|
||||
# Keep the convenient aliases used by cts_matrix_report consumers.
|
||||
"strict_pass_rate": _ratio(totals["pass"], expected),
|
||||
"conformance_accepted_rate": _ratio(totals["accepted"], expected),
|
||||
"validation": {
|
||||
"state": state,
|
||||
"ok": state == "OK",
|
||||
"suite_states": suite_states,
|
||||
"invariant_expected_equals_result_plus_unrun": (
|
||||
expected == result + totals["unrun"]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _caselist_fingerprint(path: str) -> tuple[str, int]:
|
||||
cases, _duplicates = cts_matrix_report.read_caselist(path)
|
||||
payload = "\n".join(cases).encode("utf-8") + b"\n"
|
||||
return hashlib.sha256(payload).hexdigest(), len(cases)
|
||||
|
||||
|
||||
def _read_provenance(
|
||||
spec: SuiteSpec,
|
||||
require_run_state: bool,
|
||||
expected_run_identity: Optional[str],
|
||||
) -> dict:
|
||||
path = os.path.join(spec.result_dir, "run_state.json")
|
||||
if not os.path.isfile(path):
|
||||
if require_run_state or expected_run_identity is not None:
|
||||
raise MultiReportInputError(
|
||||
"suite result directory has no run_state.json; invocation provenance "
|
||||
f"cannot be verified: {spec.result_dir}"
|
||||
)
|
||||
return {"state": "UNVERIFIED", "run_state": None}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
state = json.load(handle)
|
||||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise MultiReportInputError(f"cannot read suite run identity {path}: {exc}") from exc
|
||||
if not isinstance(state, dict):
|
||||
raise MultiReportInputError(f"suite run identity must be a JSON object: {path}")
|
||||
if state.get("backend") != spec.backend:
|
||||
raise MultiReportInputError(
|
||||
f"suite {spec.suite_id} is labelled {spec.backend}, but run_state.json "
|
||||
f"records {state.get('backend')!r}"
|
||||
)
|
||||
fingerprint, case_count = _caselist_fingerprint(spec.caselist)
|
||||
if state.get("caselist_sha256") != fingerprint or state.get("case_count") != case_count:
|
||||
raise MultiReportInputError(
|
||||
f"suite {spec.suite_id} run_state.json belongs to a different caselist"
|
||||
)
|
||||
invocation_identity = state.get("invocation_identity")
|
||||
if (
|
||||
expected_run_identity is not None
|
||||
and invocation_identity != expected_run_identity
|
||||
):
|
||||
raise MultiReportInputError(
|
||||
f"suite {spec.suite_id} run_state.json belongs to a different CTS invocation"
|
||||
)
|
||||
return {
|
||||
"state": "VERIFIED",
|
||||
"run_state": os.path.abspath(path),
|
||||
"invocation_identity": invocation_identity,
|
||||
}
|
||||
|
||||
|
||||
def _validate_specs(
|
||||
specs: Sequence[SuiteSpec],
|
||||
require_run_state: bool,
|
||||
expected_run_identity: Optional[str],
|
||||
) -> dict[str, dict]:
|
||||
if not specs:
|
||||
raise MultiReportInputError("at least one --suite specification is required")
|
||||
|
||||
seen: set[tuple[str, str]] = set()
|
||||
seen_result_dirs: list[tuple[str, str]] = []
|
||||
provenance: dict[str, dict] = {}
|
||||
for spec in specs:
|
||||
if spec.backend not in SUPPORTED_BACKENDS:
|
||||
raise MultiReportInputError(
|
||||
f"unsupported backend {spec.backend!r}; expected one of "
|
||||
+ ", ".join(SUPPORTED_BACKENDS)
|
||||
)
|
||||
if not spec.label.strip():
|
||||
raise MultiReportInputError("suite label cannot be empty")
|
||||
identity = (spec.backend, spec.label)
|
||||
if identity in seen:
|
||||
raise MultiReportInputError(
|
||||
f"duplicate suite specification for {spec.backend}/{spec.label}"
|
||||
)
|
||||
seen.add(identity)
|
||||
if not os.path.isdir(spec.result_dir):
|
||||
raise MultiReportInputError(
|
||||
f"suite result directory does not exist: {spec.result_dir}"
|
||||
)
|
||||
physical_result_dir = os.path.normcase(
|
||||
os.path.realpath(os.path.abspath(spec.result_dir))
|
||||
)
|
||||
for previous_dir, previous_suite in seen_result_dirs:
|
||||
try:
|
||||
common_dir = os.path.commonpath(
|
||||
[previous_dir, physical_result_dir]
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
if common_dir in (previous_dir, physical_result_dir):
|
||||
raise MultiReportInputError(
|
||||
f"suite {spec.suite_id} uses a result directory which overlaps "
|
||||
f"{previous_suite}: {spec.result_dir}"
|
||||
)
|
||||
seen_result_dirs.append((physical_result_dir, spec.suite_id))
|
||||
provenance[spec.suite_id] = _read_provenance(
|
||||
spec, require_run_state, expected_run_identity
|
||||
)
|
||||
return provenance
|
||||
|
||||
|
||||
def build_report(
|
||||
specs: Sequence[SuiteSpec],
|
||||
require_run_state: bool = True,
|
||||
expected_run_identity: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Build suite, per-backend, and overall serialisable reports."""
|
||||
|
||||
provenance = _validate_specs(
|
||||
specs, require_run_state, expected_run_identity
|
||||
)
|
||||
suite_reports: list[dict] = []
|
||||
backend_order: list[str] = []
|
||||
|
||||
for spec in specs:
|
||||
if spec.backend not in backend_order:
|
||||
backend_order.append(spec.backend)
|
||||
item = cts_matrix_report.build_version_report(
|
||||
spec.label, spec.caselist, [spec.result_dir]
|
||||
)
|
||||
# ``version`` is the generic label argument in build_version_report;
|
||||
# expose explicit multi-report terminology while retaining all of its
|
||||
# validation and case-level evidence.
|
||||
item.pop("version", None)
|
||||
item["backend"] = spec.backend
|
||||
item["label"] = spec.label
|
||||
item["suite_id"] = spec.suite_id
|
||||
item["provenance"] = provenance[spec.suite_id]
|
||||
if item["provenance"]["state"] == "UNVERIFIED":
|
||||
item["validation"]["warnings"].append(
|
||||
"result directory has no run_state.json; backend provenance is unverified"
|
||||
)
|
||||
suite_reports.append(item)
|
||||
|
||||
backends: dict[str, dict] = {}
|
||||
for backend in backend_order:
|
||||
backend_items = [
|
||||
item for item in suite_reports if item["backend"] == backend
|
||||
]
|
||||
labels = [item["label"] for item in backend_items]
|
||||
aggregate = aggregate_reports(backend_items, labels)
|
||||
aggregate["backend"] = backend
|
||||
aggregate["suite_labels"] = labels
|
||||
backends[backend] = aggregate
|
||||
|
||||
overall = aggregate_reports(
|
||||
suite_reports, [item["suite_id"] for item in suite_reports]
|
||||
)
|
||||
overall["backend_count"] = len(backends)
|
||||
overall["backends"] = backend_order
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"accepted_statuses": list(cts_matrix_report.ACCEPTED_STATUSES),
|
||||
"rate_policy": {
|
||||
"denominator": "unique expected cases from each suite caselist",
|
||||
"unrun_cases": "included in the denominator and never accepted",
|
||||
"backend_aggregation": "weighted by expected cases",
|
||||
"overall_aggregation": "weighted by expected cases across backend-suite pairs",
|
||||
"duplicate_results": "last QPA result wins, matching qpa_report.py",
|
||||
},
|
||||
"suites": suite_reports,
|
||||
"backends": backends,
|
||||
"overall": overall,
|
||||
}
|
||||
|
||||
|
||||
def _percent(numerator: int, denominator: int) -> str:
|
||||
if not denominator:
|
||||
return "n/a"
|
||||
return f"{100.0 * numerator / denominator:.2f}% ({numerator}/{denominator})"
|
||||
|
||||
|
||||
def _markdown_cell(value: object) -> str:
|
||||
return str(value).replace("|", r"\|").replace("\r", " ").replace("\n", " ")
|
||||
|
||||
|
||||
def _table_row(backend: str, label: str, item: dict, bold: bool = False) -> str:
|
||||
values = [
|
||||
backend,
|
||||
label,
|
||||
str(item["expected"]),
|
||||
str(item["result"]),
|
||||
str(item["pass"]),
|
||||
str(item["accepted"]),
|
||||
str(item["crash"]),
|
||||
str(item["hang"]),
|
||||
str(item["unrun"]),
|
||||
str(item["duplicate"]),
|
||||
_percent(item["result"], item["expected"]),
|
||||
_percent(item["pass"], item["expected"]),
|
||||
_percent(item["accepted"], item["expected"]),
|
||||
item["validation"]["state"],
|
||||
]
|
||||
values = [_markdown_cell(value) for value in values]
|
||||
if bold:
|
||||
values = [f"**{value}**" for value in values]
|
||||
return "| " + " | ".join(values) + " |"
|
||||
|
||||
|
||||
def render_markdown(report: dict) -> str:
|
||||
lines = [
|
||||
"# GL CTS multi-suite conformance report",
|
||||
"",
|
||||
(
|
||||
"| Backend | Suite | Expected | Result | Pass | Accepted | Crash | Hang | "
|
||||
"Unrun | Duplicate | Coverage | Strict Pass-only | Conformance-accepted | Validation |"
|
||||
),
|
||||
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|:---:|",
|
||||
]
|
||||
|
||||
for backend in report["backends"]:
|
||||
for item in report["suites"]:
|
||||
if item["backend"] == backend:
|
||||
lines.append(_table_row(backend, item["label"], item))
|
||||
subtotal = report["backends"][backend]
|
||||
lines.append(
|
||||
_table_row(backend, f"{backend} weighted subtotal", subtotal, bold=True)
|
||||
)
|
||||
|
||||
lines.append(
|
||||
_table_row(
|
||||
"All backends",
|
||||
"Overall weighted",
|
||||
report["overall"],
|
||||
bold=True,
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
(
|
||||
"Rates use unique **Expected** caselist cases as the denominator. "
|
||||
"Unrun cases remain in the denominator and are not accepted."
|
||||
),
|
||||
"Accepted statuses: "
|
||||
+ ", ".join(
|
||||
f"`{status}`" for status in report["accepted_statuses"]
|
||||
)
|
||||
+ ".",
|
||||
(
|
||||
"Duplicate is the number of extra QPA observations; the final "
|
||||
"observation wins."
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
details: list[str] = []
|
||||
for item in report["suites"]:
|
||||
validation = item["validation"]
|
||||
messages = validation["errors"] + validation["warnings"]
|
||||
if messages:
|
||||
details.append(
|
||||
f"- **{_markdown_cell(item['suite_id'])} {validation['state']}**: "
|
||||
+ "; ".join(_markdown_cell(message) for message in messages)
|
||||
)
|
||||
if details:
|
||||
lines.extend(["", "## Validation details", "", *details])
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _write_text(path: str, contents: str) -> None:
|
||||
absolute = os.path.abspath(path)
|
||||
os.makedirs(os.path.dirname(absolute), exist_ok=True)
|
||||
with open(absolute, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(contents)
|
||||
|
||||
|
||||
def _write_json(path: str, report: dict) -> None:
|
||||
_write_text(path, json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def _normalise_compact_suite_args(argv: Sequence[str]) -> list[str]:
|
||||
"""Expand ``--suite=b,l,c,r`` into the four-value argparse form."""
|
||||
|
||||
result: list[str] = []
|
||||
index = 0
|
||||
while index < len(argv):
|
||||
token = argv[index]
|
||||
if token.startswith("--suite="):
|
||||
compact = token.split("=", 1)[1]
|
||||
parts = compact.split(",", 3)
|
||||
if len(parts) != 4:
|
||||
raise MultiReportInputError(
|
||||
"compact --suite expects backend,label,caselist,result-dir"
|
||||
)
|
||||
result.extend(["--suite", *parts])
|
||||
index += 1
|
||||
continue
|
||||
if token == "--suite" and index + 1 < len(argv) and argv[index + 1].count(",") >= 3:
|
||||
parts = argv[index + 1].split(",", 3)
|
||||
result.extend(["--suite", *parts])
|
||||
index += 2
|
||||
continue
|
||||
result.append(token)
|
||||
index += 1
|
||||
return result
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--suite",
|
||||
action="append",
|
||||
nargs=4,
|
||||
required=True,
|
||||
metavar=("BACKEND", "LABEL", "CASELIST", "RESULT_DIR"),
|
||||
help=(
|
||||
"suite specification; repeat for every backend/suite pair "
|
||||
f"(backends: {', '.join(SUPPORTED_BACKENDS)})"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--markdown",
|
||||
default="cts_multi_report.md",
|
||||
help="Markdown output path (default: ./cts_multi_report.md)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
dest="json_out",
|
||||
default="cts_multi_report.json",
|
||||
help="JSON output path (default: ./cts_multi_report.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-incomplete",
|
||||
action="store_true",
|
||||
help="return success even when one or more suites are ERROR/INCOMPLETE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--adopt-legacy",
|
||||
dest="allow_unverified_provenance",
|
||||
action="store_true",
|
||||
help="accept legacy result directories without run_state.json (provenance remains unverified)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-unverified-provenance",
|
||||
dest="allow_unverified_provenance",
|
||||
action="store_true",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-run-identity",
|
||||
help="require every suite run_state.json to contain this controller fingerprint",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def _specs_from_args(values: Sequence[Sequence[str]]) -> list[SuiteSpec]:
|
||||
return [
|
||||
SuiteSpec(
|
||||
backend=backend.strip(),
|
||||
label=label.strip(),
|
||||
caselist=caselist,
|
||||
result_dir=result_dir,
|
||||
)
|
||||
for backend, label, caselist, result_dir in values
|
||||
]
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
raw_argv = list(argv) if argv is not None else sys.argv[1:]
|
||||
try:
|
||||
normalised = _normalise_compact_suite_args(raw_argv)
|
||||
except MultiReportInputError as exc:
|
||||
print(f"cts_multi_report: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
args = _parser().parse_args(normalised)
|
||||
|
||||
if os.path.normcase(os.path.abspath(args.markdown)) == os.path.normcase(
|
||||
os.path.abspath(args.json_out)
|
||||
):
|
||||
print("cts_multi_report: Markdown and JSON paths must differ", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
report = build_report(
|
||||
_specs_from_args(args.suite),
|
||||
require_run_state=not args.allow_unverified_provenance,
|
||||
expected_run_identity=args.expected_run_identity,
|
||||
)
|
||||
markdown = render_markdown(report)
|
||||
_write_text(args.markdown, markdown)
|
||||
_write_json(args.json_out, report)
|
||||
except (
|
||||
OSError,
|
||||
MultiReportInputError,
|
||||
cts_matrix_report.ReportInputError,
|
||||
) as exc:
|
||||
print(f"cts_multi_report: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(markdown, end="")
|
||||
print(f"\nMarkdown: {os.path.abspath(args.markdown)}")
|
||||
print(f"JSON: {os.path.abspath(args.json_out)}")
|
||||
if not args.allow_incomplete and not report["overall"]["validation"]["ok"]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python
|
||||
"""Drive a glcts run on the local host, resuming across crashes.
|
||||
|
||||
Local-host counterpart of run_cts.py: MobileGL crashes on some cases and glcts
|
||||
takes the whole process down with it, so a single invocation stops at the first
|
||||
crash. This runner re-invokes glcts with only the cases that have no result
|
||||
yet, records the case that was open when the process died as "Crash" (or
|
||||
"Hang" on a timeout), and repeats until the list is exhausted.
|
||||
|
||||
Usage:
|
||||
python run_cts_local.py --backend DirectVulkan \\
|
||||
--glcts <path-to-glcts-binary> --lib <path-to-libMobileGL.so> \\
|
||||
--caselist <mustpass.txt> --outdir <dir> [--env K=V ...]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import resource
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
|
||||
CASE_END = re.compile(r"^#endTestCaseResult")
|
||||
CASE_TERM = re.compile(r"^#terminateTestCaseResult")
|
||||
|
||||
|
||||
def completed_cases(qpa_path):
|
||||
"""Return (finished_case_names, last_started_case_or_None)."""
|
||||
finished = []
|
||||
current = None
|
||||
if not os.path.exists(qpa_path):
|
||||
return finished, None
|
||||
with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
m = CASE_START.match(line)
|
||||
if m:
|
||||
current = m.group(1)
|
||||
continue
|
||||
if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)):
|
||||
finished.append(current)
|
||||
current = None
|
||||
return finished, current
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"])
|
||||
ap.add_argument("--glcts", required=True, help="path to the glcts binary")
|
||||
ap.add_argument("--lib", required=True, help="path to libMobileGL.so")
|
||||
ap.add_argument("--caselist", required=True)
|
||||
ap.add_argument("--outdir", required=True)
|
||||
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
||||
# Without an explicit size, dEQP's FboRenderContext sizes the wrapper FBO to
|
||||
# GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test allocations
|
||||
# explode (a 4-sample 16K depth texture alone is 4 GiB).
|
||||
ap.add_argument("--surface-size", type=int, default=256,
|
||||
help="--deqp-surface-width/height value")
|
||||
# With DONT_CARE depth/stencil bits dEQP's FboRenderContext picks the first entry of
|
||||
# its own format list, GL_DEPTH32F_STENCIL8. framebuffer_blit meanwhile hardcodes
|
||||
# GL_DEPTH24_STENCIL8 for its own buffers whenever it detects an FBO surface, then
|
||||
# blits depth between the two - which the spec forbids for mismatched formats, so a
|
||||
# conformant driver has to fail it. Asking for a config the test agrees with avoids
|
||||
# the contradiction instead of papering over it.
|
||||
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
|
||||
help="--deqp-gl-config-name value (empty string to leave it unset)")
|
||||
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||
ap.add_argument("--max-empty-streak", type=int, default=64,
|
||||
help="abort after this many consecutive chunks that produce no log at all")
|
||||
ap.add_argument("--chunk-timeout", type=int, default=1800,
|
||||
help="seconds before killing one glcts invocation (a wedged case never returns)")
|
||||
# dEQP's watchdog aborts the process when a single case exceeds a hardcoded 30s
|
||||
# (framework/common/tcuApp.hpp), which is not a hang on a CPU rasterizer - some
|
||||
# texture_swizzle cases legitimately take ~17s each and cross it once the process is
|
||||
# warm, so they came back as spurious Timeouts. dEQP's own default is off; the
|
||||
# chunk-timeout above is what actually rescues a genuinely wedged case.
|
||||
ap.add_argument("--watchdog", default="disable", choices=["enable", "disable"],
|
||||
help="--deqp-watchdog value")
|
||||
ap.add_argument("--skip-file", default=None,
|
||||
help="file of case names to exclude, e.g. cases known to wedge the host")
|
||||
ap.add_argument("--waiver-file", default=None,
|
||||
help="--deqp-waiver-file value, e.g. tools/cts/waivers/mobilegl-fbo-harness.xml")
|
||||
ap.add_argument("--env", action="append", default=[], metavar="K=V",
|
||||
help="extra environment variable for glcts (repeatable)")
|
||||
args = ap.parse_args()
|
||||
|
||||
os.makedirs(args.outdir, exist_ok=True)
|
||||
glcts = os.path.abspath(args.glcts)
|
||||
lib = os.path.abspath(args.lib)
|
||||
# glcts resolves its gl_cts data tree relative to the binary's directory.
|
||||
workdir = os.path.dirname(glcts)
|
||||
|
||||
with open(args.caselist, "r", encoding="utf-8") as fh:
|
||||
remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
|
||||
|
||||
skipped = []
|
||||
if args.skip_file and os.path.isfile(args.skip_file):
|
||||
with open(args.skip_file, "r", encoding="utf-8") as fh:
|
||||
skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")}
|
||||
skipped = [c for c in remaining if c in skip]
|
||||
remaining = [c for c in remaining if c not in skip]
|
||||
print(f"[run_cts_local] skipping {len(skipped)} case(s) from {args.skip_file}")
|
||||
|
||||
total = len(remaining)
|
||||
print(f"[run_cts_local] {args.backend}: {total} cases")
|
||||
|
||||
env = dict(os.environ)
|
||||
env["MOBILEGL_BACKEND_TYPE"] = args.backend
|
||||
env["MOBILEGL_CTS_LIB"] = lib
|
||||
for kv in args.env:
|
||||
k, _, v = kv.partition("=")
|
||||
env[k] = v
|
||||
|
||||
crashed = []
|
||||
hung = []
|
||||
done = set()
|
||||
chunk = 0
|
||||
started = time.time()
|
||||
empty_streak = 0
|
||||
|
||||
# Resume: results in chunk files from an interrupted run still count. The
|
||||
# case that was open when that run died is re-tried rather than assumed bad.
|
||||
prior_chunks = sorted(glob.glob(os.path.join(args.outdir, "chunk*.qpa")))
|
||||
for prior in prior_chunks:
|
||||
finished, _ = completed_cases(prior)
|
||||
done.update(finished)
|
||||
if prior_chunks:
|
||||
chunk = int(re.search(r"chunk(\d+)\.qpa$", prior_chunks[-1]).group(1)) + 1
|
||||
remaining = [c for c in remaining if c not in done]
|
||||
print(f"[run_cts_local] resuming: {len(done)} case(s) already measured, "
|
||||
f"{len(remaining)} to go")
|
||||
|
||||
while remaining and chunk < args.max_rounds:
|
||||
listfile = os.path.abspath(os.path.join(args.outdir, "remaining.txt"))
|
||||
with open(listfile, "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n".join(remaining) + "\n")
|
||||
|
||||
qpa = os.path.abspath(os.path.join(args.outdir, f"chunk{chunk:04d}.qpa"))
|
||||
cmd = [
|
||||
glcts,
|
||||
f"--deqp-caselist-file={listfile}",
|
||||
f"--deqp-surface-type={args.surface}",
|
||||
f"--deqp-surface-width={args.surface_size}",
|
||||
f"--deqp-surface-height={args.surface_size}",
|
||||
"--deqp-terminate-on-device-lost=disable",
|
||||
f"--deqp-watchdog={args.watchdog}",
|
||||
"--deqp-log-images=disable",
|
||||
"--deqp-log-shader-sources=disable",
|
||||
f"--deqp-log-filename={qpa}",
|
||||
]
|
||||
if args.gl_config_name:
|
||||
cmd.append(f"--deqp-gl-config-name={args.gl_config_name}")
|
||||
if args.waiver_file:
|
||||
cmd.append(f"--deqp-waiver-file={os.path.abspath(args.waiver_file)}")
|
||||
timed_out = False
|
||||
try:
|
||||
# RLIMIT_CORE=0: MobileGL asserts abort with a core dump, and writing
|
||||
# a multi-GB glcts core image after every crash dominates wall time.
|
||||
subprocess.run(cmd, cwd=workdir, env=env, timeout=args.chunk_timeout,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
print(f"[run_cts_local] chunk {chunk:04d} timed out after {args.chunk_timeout}s",
|
||||
file=sys.stderr)
|
||||
|
||||
finished, in_flight = completed_cases(qpa)
|
||||
for c in finished:
|
||||
done.add(c)
|
||||
|
||||
progressed = len(finished)
|
||||
if progressed > 0:
|
||||
empty_streak = 0
|
||||
if in_flight is not None:
|
||||
if timed_out:
|
||||
print(f"[run_cts_local] HANG in {in_flight} - quarantining it")
|
||||
hung.append(in_flight)
|
||||
else:
|
||||
crashed.append(in_flight)
|
||||
done.add(in_flight)
|
||||
progressed += 1
|
||||
elif progressed == 0:
|
||||
empty_streak += 1
|
||||
if empty_streak >= args.max_empty_streak:
|
||||
print(f"[run_cts_local] ABORTING: {empty_streak} consecutive chunks produced no "
|
||||
f"output. Something systemic is wrong; refusing to label the rest of the "
|
||||
f"suite as crashes.", file=sys.stderr)
|
||||
break
|
||||
victim = remaining[0]
|
||||
label = "Hang" if timed_out else "Crash"
|
||||
print(f"[run_cts_local] no output at all; recording {victim} as {label}")
|
||||
(hung if timed_out else crashed).append(victim)
|
||||
done.add(victim)
|
||||
progressed = 1
|
||||
|
||||
remaining = [c for c in remaining if c not in done]
|
||||
elapsed = time.time() - started
|
||||
print(
|
||||
f"[run_cts_local] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, "
|
||||
f"crashes {len(crashed)}, hangs {len(hung)}, {elapsed / 60:.1f} min)"
|
||||
)
|
||||
chunk += 1
|
||||
|
||||
with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n".join(crashed) + ("\n" if crashed else ""))
|
||||
with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n".join(hung) + ("\n" if hung else ""))
|
||||
with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n".join(remaining) + ("\n" if remaining else ""))
|
||||
if skipped:
|
||||
with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n".join(skipped) + "\n")
|
||||
|
||||
if remaining:
|
||||
print(f"[run_cts_local] WARNING: {len(remaining)} cases were never run (see unrun.txt)",
|
||||
file=sys.stderr)
|
||||
print(f"[run_cts_local] finished: {len(done)}/{total} cases, {len(crashed)} crashes, "
|
||||
f"{len(hung)} hangs, {chunk} invocations")
|
||||
print(f"[run_cts_local] qpa chunks in {args.outdir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,878 @@
|
||||
#!/usr/bin/env python
|
||||
"""Run a local Windows glcts executable and resume across process failures.
|
||||
|
||||
The desktop CTS normally runs a complete caselist in one process. That is a
|
||||
poor fit for testing a developing OpenGL implementation: one access violation
|
||||
or GPU hang prevents every later case from running. This driver gives each
|
||||
invocation the cases which have not produced a result yet, preserves one QPA
|
||||
and stdout/stderr pair per invocation, and starts another process after a
|
||||
crash.
|
||||
|
||||
Existing ``chunkNNNN.qpa`` files and ``crashed.txt``/``hung.txt`` sidecars are
|
||||
read on startup, so invoking the same command and output directory resumes an
|
||||
interrupted run. A timeout is based on *idle QPA time*, not total process wall
|
||||
time: a healthy invocation may legitimately run thousands of cases for hours.
|
||||
|
||||
Example (values beginning with ``--`` use argparse's ``=`` spelling)::
|
||||
|
||||
py run_cts_windows.py \
|
||||
--exe D:\\glcts\\glcts.exe --workdir D:\\glcts \
|
||||
--caselist D:\\glcts\\mustpass\\gl30.txt --outdir D:\\results\\gl30 \
|
||||
--backend DirectVulkan \
|
||||
--deqp-arg=--deqp-surface-type=window
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Iterable, Optional, Sequence
|
||||
|
||||
|
||||
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
|
||||
CASE_END = re.compile(r"^#endTestCaseResult(?:\s|$)")
|
||||
CASE_TERM = re.compile(r"^#terminateTestCaseResult(?:\s|$)")
|
||||
CASE_RESULT = re.compile(r'<Result\s+StatusCode="[^"]+"')
|
||||
CHUNK_ARTIFACT = re.compile(r"^chunk(\d+)(?:\.|$)", re.IGNORECASE)
|
||||
CHUNK_META = re.compile(r"^chunk(\d+)\.meta\.json$", re.IGNORECASE)
|
||||
RECOVERY_SIDECAR_NAMES = frozenset(
|
||||
{"crashed.txt", "hung.txt", "unrun.txt", "skipped.txt", "remaining.txt"}
|
||||
)
|
||||
CONTROLLED_DEQP_OPTIONS = {
|
||||
"--deqp-caselist-file",
|
||||
"--deqp-log-filename",
|
||||
}
|
||||
ATOMIC_REPLACE_ATTEMPTS = 8
|
||||
ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS = 0.025
|
||||
ATOMIC_REPLACE_MAX_BACKOFF_SECONDS = 0.2
|
||||
|
||||
|
||||
class RunnerError(Exception):
|
||||
"""A user/configuration error which should not be attributed to a case."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class QpaProgress:
|
||||
"""Cases recorded by a QPA and its unterminated tail, if any."""
|
||||
|
||||
recorded: list[str]
|
||||
in_flight: Optional[str]
|
||||
begin_count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessOutcome:
|
||||
returncode: Optional[int]
|
||||
duration_seconds: float
|
||||
timed_out: bool = False
|
||||
timeout_reason: Optional[str] = None
|
||||
interrupted: bool = False
|
||||
launch_error: Optional[str] = None
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def read_caselist(path: Path) -> list[str]:
|
||||
"""Read a dEQP text caselist, preserving order and removing duplicates."""
|
||||
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8-sig", errors="strict").splitlines()
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise RunnerError(f"cannot read caselist {path}: {exc}") from exc
|
||||
|
||||
cases: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in lines:
|
||||
case = raw.strip()
|
||||
if not case or case.startswith("#") or case in seen:
|
||||
continue
|
||||
cases.append(case)
|
||||
seen.add(case)
|
||||
if not cases:
|
||||
raise RunnerError(f"caselist contains no test cases: {path}")
|
||||
return cases
|
||||
|
||||
|
||||
def read_name_set(path: Path) -> set[str]:
|
||||
if not path.is_file():
|
||||
return set()
|
||||
try:
|
||||
return {
|
||||
line.strip()
|
||||
for line in path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
}
|
||||
except OSError as exc:
|
||||
raise RunnerError(f"cannot read recovery file {path}: {exc}") from exc
|
||||
|
||||
|
||||
def _replace_with_retry(source: Path, destination: Path) -> None:
|
||||
"""Replace a state file, tolerating brief Windows access-denied races.
|
||||
|
||||
Antivirus/indexing tools can momentarily open ``remaining.txt`` without
|
||||
delete sharing. Windows then reports either ``PermissionError`` or a
|
||||
generic ``OSError`` carrying ``winerror == 5``. Retry only those cases;
|
||||
disk, path, and programming errors remain immediately visible.
|
||||
"""
|
||||
|
||||
for attempt in range(ATOMIC_REPLACE_ATTEMPTS):
|
||||
try:
|
||||
os.replace(source, destination)
|
||||
return
|
||||
except OSError as exc:
|
||||
retryable = isinstance(exc, PermissionError) or getattr(exc, "winerror", None) == 5
|
||||
if not retryable or attempt + 1 >= ATOMIC_REPLACE_ATTEMPTS:
|
||||
raise
|
||||
delay = min(
|
||||
ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS * (2**attempt),
|
||||
ATOMIC_REPLACE_MAX_BACKOFF_SECONDS,
|
||||
)
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def atomic_write_text(path: Path, text: str) -> None:
|
||||
"""Replace a small state file without exposing a partially-written copy."""
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
|
||||
temporary_path = Path(temporary)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
_replace_with_retry(temporary_path, path)
|
||||
finally:
|
||||
try:
|
||||
temporary_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, value: object) -> None:
|
||||
atomic_write_text(path, json.dumps(value, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def write_case_file(path: Path, cases: Iterable[str]) -> None:
|
||||
values = list(cases)
|
||||
atomic_write_text(path, "\n".join(values) + ("\n" if values else ""))
|
||||
|
||||
|
||||
def scan_qpa(path: Path) -> QpaProgress:
|
||||
"""Return cases with a final result and the unfinished tail, if any.
|
||||
|
||||
``#terminateTestCaseResult`` is a completed result (usually Crash or
|
||||
Timeout). ``#endTestCaseResult`` only completes a case when its XML carried
|
||||
a ``<Result StatusCode=...>``. A truncated case that already wrote Result is
|
||||
also recoverable; a case with no Result remains eligible for a retry.
|
||||
"""
|
||||
|
||||
if not path.is_file():
|
||||
return QpaProgress([], None, 0)
|
||||
|
||||
recorded: list[str] = []
|
||||
current: Optional[str] = None
|
||||
has_result = False
|
||||
begin_count = 0
|
||||
try:
|
||||
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
for raw_line in handle:
|
||||
line = raw_line.lstrip("\ufeff")
|
||||
match = CASE_START.match(line)
|
||||
if match:
|
||||
if current is not None and has_result:
|
||||
recorded.append(current)
|
||||
current = match.group(1)
|
||||
has_result = False
|
||||
begin_count += 1
|
||||
continue
|
||||
if current is not None and CASE_RESULT.search(line):
|
||||
has_result = True
|
||||
continue
|
||||
if current is not None and CASE_TERM.match(line):
|
||||
recorded.append(current)
|
||||
current = None
|
||||
has_result = False
|
||||
continue
|
||||
if current is not None and CASE_END.match(line):
|
||||
if has_result:
|
||||
recorded.append(current)
|
||||
current = None
|
||||
has_result = False
|
||||
except OSError as exc:
|
||||
raise RunnerError(f"cannot read QPA {path}: {exc}") from exc
|
||||
if current is not None and has_result:
|
||||
recorded.append(current)
|
||||
current = None
|
||||
return QpaProgress(recorded, current, begin_count)
|
||||
|
||||
|
||||
def numbered_files(outdir: Path, pattern: re.Pattern[str]) -> list[tuple[int, Path]]:
|
||||
found: list[tuple[int, Path]] = []
|
||||
try:
|
||||
children = list(outdir.iterdir())
|
||||
except OSError as exc:
|
||||
raise RunnerError(f"cannot list output directory {outdir}: {exc}") from exc
|
||||
for path in children:
|
||||
match = pattern.match(path.name)
|
||||
if match:
|
||||
found.append((int(match.group(1)), path))
|
||||
found.sort(key=lambda item: item[0])
|
||||
return found
|
||||
|
||||
|
||||
def next_chunk_number(outdir: Path) -> int:
|
||||
numbers = [number for number, _path in numbered_files(outdir, CHUNK_ARTIFACT)]
|
||||
return max(numbers, default=-1) + 1
|
||||
|
||||
|
||||
def load_meta_classifications(outdir: Path, expected: set[str]) -> tuple[set[str], set[str]]:
|
||||
"""Recover an atomic classification written just before sidecar updates."""
|
||||
|
||||
crashed: set[str] = set()
|
||||
hung: set[str] = set()
|
||||
for _number, path in numbered_files(outdir, CHUNK_META):
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
# A damaged metadata file is diagnostic only. QPA and sidecars are
|
||||
# authoritative and must still allow recovery.
|
||||
continue
|
||||
if not isinstance(value, dict):
|
||||
continue
|
||||
case = value.get("classified_case")
|
||||
classification = value.get("classification")
|
||||
if not isinstance(case, str) or case not in expected:
|
||||
continue
|
||||
if classification == "DeviceHang":
|
||||
hung.add(case)
|
||||
elif classification == "Crash":
|
||||
crashed.add(case)
|
||||
crashed.difference_update(hung)
|
||||
return crashed, hung
|
||||
|
||||
|
||||
def result_qpa_files(outdir: Path) -> list[Path]:
|
||||
"""Return every QPA a directory-based report would consume."""
|
||||
|
||||
found: list[Path] = []
|
||||
try:
|
||||
for root, directories, names in os.walk(outdir):
|
||||
directories.sort(key=str.casefold)
|
||||
for name in sorted(names, key=str.casefold):
|
||||
if name.casefold().endswith(".qpa"):
|
||||
found.append(Path(root) / name)
|
||||
except OSError as exc:
|
||||
raise RunnerError(f"cannot scan output directory {outdir}: {exc}") from exc
|
||||
return found
|
||||
|
||||
|
||||
def recover_results(outdir: Path, expected: set[str]) -> tuple[set[str], set[str], set[str]]:
|
||||
recorded: set[str] = set()
|
||||
for path in result_qpa_files(outdir):
|
||||
progress = scan_qpa(path)
|
||||
recorded.update(case for case in progress.recorded if case in expected)
|
||||
|
||||
crashed = read_name_set(outdir / "crashed.txt") & expected
|
||||
hung = read_name_set(outdir / "hung.txt") & expected
|
||||
meta_crashed, meta_hung = load_meta_classifications(outdir, expected)
|
||||
crashed.update(meta_crashed)
|
||||
hung.update(meta_hung)
|
||||
crashed.difference_update(hung)
|
||||
return recorded, crashed, hung
|
||||
|
||||
|
||||
def caselist_fingerprint(cases: Sequence[str]) -> str:
|
||||
payload = "\n".join(cases).encode("utf-8") + b"\n"
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def recovery_artifacts(outdir: Path) -> list[Path]:
|
||||
"""Return prior-run evidence which must not be adopted implicitly."""
|
||||
|
||||
found = set(result_qpa_files(outdir))
|
||||
try:
|
||||
children = list(outdir.iterdir())
|
||||
except OSError as exc:
|
||||
raise RunnerError(f"cannot list output directory {outdir}: {exc}") from exc
|
||||
found.update(
|
||||
path
|
||||
for path in children
|
||||
if CHUNK_ARTIFACT.match(path.name)
|
||||
or path.name.casefold() in RECOVERY_SIDECAR_NAMES
|
||||
)
|
||||
return sorted(
|
||||
found,
|
||||
key=lambda path: str(path.relative_to(outdir)).casefold(),
|
||||
)
|
||||
|
||||
|
||||
def check_run_identity(
|
||||
outdir: Path,
|
||||
backend: str,
|
||||
cases: Sequence[str],
|
||||
invocation_identity: Optional[str] = None,
|
||||
adopt_legacy: bool = False,
|
||||
) -> None:
|
||||
"""Refuse to silently mix different suites/backends in one result dir."""
|
||||
|
||||
path = outdir / "run_state.json"
|
||||
fingerprint = caselist_fingerprint(cases)
|
||||
if path.is_file():
|
||||
try:
|
||||
state = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise RunnerError(f"cannot read run identity {path}: {exc}") from exc
|
||||
if not isinstance(state, dict):
|
||||
raise RunnerError(f"run identity must be a JSON object: {path}")
|
||||
if state.get("backend") != backend:
|
||||
raise RunnerError(
|
||||
f"output directory belongs to backend {state.get('backend')!r}, not {backend!r}: {outdir}"
|
||||
)
|
||||
if state.get("caselist_sha256") != fingerprint:
|
||||
raise RunnerError(f"output directory belongs to a different caselist: {outdir}")
|
||||
stored_invocation_identity = state.get("invocation_identity")
|
||||
if (
|
||||
stored_invocation_identity is not None
|
||||
or invocation_identity is not None
|
||||
) and stored_invocation_identity != invocation_identity:
|
||||
raise RunnerError(f"output directory belongs to a different CTS invocation: {outdir}")
|
||||
return
|
||||
|
||||
legacy_artifacts = recovery_artifacts(outdir)
|
||||
if legacy_artifacts and not adopt_legacy:
|
||||
examples = ", ".join(path.name for path in legacy_artifacts[:3])
|
||||
raise RunnerError(
|
||||
"output directory contains CTS recovery artifacts but no run_state.json; "
|
||||
f"refusing to adopt unverified legacy results ({examples}). Re-run with "
|
||||
"--adopt-legacy only after verifying the backend, caselist, and invocation."
|
||||
)
|
||||
|
||||
atomic_write_json(
|
||||
path,
|
||||
{
|
||||
"version": 1,
|
||||
"backend": backend,
|
||||
"case_count": len(cases),
|
||||
"caselist_sha256": fingerprint,
|
||||
"invocation_identity": invocation_identity,
|
||||
"adopted_legacy": bool(legacy_artifacts),
|
||||
"created_utc": utc_now(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def persist_sidecars(
|
||||
outdir: Path,
|
||||
ordered_cases: Sequence[str],
|
||||
crashed: set[str],
|
||||
hung: set[str],
|
||||
remaining: Sequence[str],
|
||||
) -> None:
|
||||
write_case_file(outdir / "crashed.txt", (case for case in ordered_cases if case in crashed))
|
||||
write_case_file(outdir / "hung.txt", (case for case in ordered_cases if case in hung))
|
||||
write_case_file(outdir / "unrun.txt", remaining)
|
||||
write_case_file(outdir / "remaining.txt", remaining)
|
||||
|
||||
|
||||
def parse_environment(values: Sequence[str]) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for value in values:
|
||||
if "=" not in value:
|
||||
raise RunnerError(f"--env expects NAME=VALUE, got {value!r}")
|
||||
name, contents = value.split("=", 1)
|
||||
if not name or "\x00" in name or "=" in name:
|
||||
raise RunnerError(f"invalid environment variable name in {value!r}")
|
||||
result[name] = contents
|
||||
return result
|
||||
|
||||
|
||||
def validate_deqp_args(values: Sequence[str]) -> None:
|
||||
for value in values:
|
||||
option = value.split("=", 1)[0].lower()
|
||||
if option in CONTROLLED_DEQP_OPTIONS:
|
||||
raise RunnerError(f"{option} is controlled by this runner and cannot be supplied via --deqp-arg")
|
||||
|
||||
|
||||
def resolve_paths(
|
||||
exe_value: str,
|
||||
workdir_value: Optional[str],
|
||||
caselist_value: str,
|
||||
outdir_value: str,
|
||||
) -> tuple[Path, Path, Path, Path]:
|
||||
launch_dir = Path.cwd()
|
||||
requested_exe = Path(exe_value).expanduser()
|
||||
|
||||
if workdir_value:
|
||||
workdir = Path(workdir_value).expanduser().resolve()
|
||||
elif requested_exe.is_absolute():
|
||||
workdir = requested_exe.resolve().parent
|
||||
else:
|
||||
workdir = launch_dir
|
||||
|
||||
if requested_exe.is_absolute():
|
||||
exe = requested_exe.resolve()
|
||||
else:
|
||||
in_workdir = (workdir / requested_exe).resolve()
|
||||
in_launch_dir = (launch_dir / requested_exe).resolve()
|
||||
exe = in_workdir if in_workdir.is_file() else in_launch_dir
|
||||
|
||||
caselist = Path(caselist_value).expanduser().resolve()
|
||||
outdir = Path(outdir_value).expanduser().resolve()
|
||||
if not exe.is_file():
|
||||
raise RunnerError(f"glcts executable does not exist: {exe}")
|
||||
if not workdir.is_dir():
|
||||
raise RunnerError(f"working directory does not exist: {workdir}")
|
||||
if not caselist.is_file():
|
||||
raise RunnerError(f"caselist does not exist: {caselist}")
|
||||
return exe, workdir, caselist, outdir
|
||||
|
||||
|
||||
def qpa_signature(path: Path) -> Optional[tuple[int, int]]:
|
||||
try:
|
||||
stat = path.stat()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError:
|
||||
# A transient sharing violation must not kill a healthy process. The
|
||||
# next poll will retry and the idle clock retains its previous value.
|
||||
return None
|
||||
return stat.st_size, stat.st_mtime_ns
|
||||
|
||||
|
||||
def kill_process_tree(process: subprocess.Popen[bytes]) -> None:
|
||||
"""Force-stop the process and descendants, with a parent-only fallback."""
|
||||
|
||||
if process.poll() is not None:
|
||||
return
|
||||
|
||||
if os.name == "nt":
|
||||
# /T is essential: CTS/platform helpers can outlive the top-level
|
||||
# process, retain the QPA/DLL, and poison the next continuation round.
|
||||
taskkill = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "taskkill.exe"
|
||||
command = [str(taskkill), "/PID", str(process.pid), "/T", "/F"]
|
||||
try:
|
||||
subprocess.run(
|
||||
command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=20,
|
||||
check=False,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
return
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
try:
|
||||
process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
|
||||
def run_process(
|
||||
command: Sequence[str],
|
||||
workdir: Path,
|
||||
environment: dict[str, str],
|
||||
qpa_path: Path,
|
||||
stdout_path: Path,
|
||||
stderr_path: Path,
|
||||
idle_timeout: float,
|
||||
max_round_seconds: float,
|
||||
poll_seconds: float,
|
||||
) -> ProcessOutcome:
|
||||
"""Run one CTS chunk, killing its tree only after QPA progress stalls."""
|
||||
|
||||
started = time.monotonic()
|
||||
with stdout_path.open("wb") as stdout_handle, stderr_path.open("wb") as stderr_handle:
|
||||
popen_options: dict[str, object] = {
|
||||
"cwd": str(workdir),
|
||||
"env": environment,
|
||||
"stdin": subprocess.DEVNULL,
|
||||
"stdout": stdout_handle,
|
||||
"stderr": stderr_handle,
|
||||
}
|
||||
if os.name == "nt":
|
||||
popen_options["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
||||
else:
|
||||
popen_options["start_new_session"] = True
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(list(command), **popen_options) # type: ignore[arg-type]
|
||||
except OSError as exc:
|
||||
message = f"failed to launch {command[0]}: {exc}\n"
|
||||
stderr_handle.write(message.encode("utf-8", errors="replace"))
|
||||
stderr_handle.flush()
|
||||
return ProcessOutcome(None, time.monotonic() - started, launch_error=str(exc))
|
||||
|
||||
last_signature = qpa_signature(qpa_path)
|
||||
last_progress = time.monotonic()
|
||||
timed_out = False
|
||||
timeout_reason: Optional[str] = None
|
||||
interrupted = False
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
returncode = process.wait(timeout=poll_seconds)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
now = time.monotonic()
|
||||
signature = qpa_signature(qpa_path)
|
||||
if signature is not None and signature != last_signature:
|
||||
last_signature = signature
|
||||
last_progress = now
|
||||
|
||||
if idle_timeout > 0 and now - last_progress >= idle_timeout:
|
||||
timed_out = True
|
||||
timeout_reason = "qpa-idle"
|
||||
kill_process_tree(process)
|
||||
returncode = process.poll()
|
||||
break
|
||||
if max_round_seconds > 0 and now - started >= max_round_seconds:
|
||||
timed_out = True
|
||||
timeout_reason = "max-round"
|
||||
kill_process_tree(process)
|
||||
returncode = process.poll()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
interrupted = True
|
||||
kill_process_tree(process)
|
||||
returncode = process.poll()
|
||||
|
||||
return ProcessOutcome(
|
||||
returncode=returncode,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
timed_out=timed_out,
|
||||
timeout_reason=timeout_reason,
|
||||
interrupted=interrupted,
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run Windows glcts against MobileGL, resuming across crashes and GPU hangs."
|
||||
)
|
||||
parser.add_argument("--exe", required=True, help="path to glcts.exe")
|
||||
parser.add_argument(
|
||||
"--workdir",
|
||||
help="glcts working directory (default: executable directory, or current directory for a relative exe)",
|
||||
)
|
||||
parser.add_argument("--caselist", required=True, help="mustpass/caselist text file")
|
||||
parser.add_argument("--outdir", required=True, help="persistent result directory")
|
||||
parser.add_argument("--backend", required=True, choices=("DirectGLES", "DirectVulkan"))
|
||||
parser.add_argument(
|
||||
"--run-identity",
|
||||
help="controller fingerprint for executable, data, arguments, and environment",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--adopt-legacy",
|
||||
action="store_true",
|
||||
help=(
|
||||
"adopt existing chunk/sidecar results which predate run_state.json; "
|
||||
"disabled by default because their provenance cannot be verified"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME=VALUE",
|
||||
help="extra child environment variable (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deqp-arg",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="ARG",
|
||||
help="extra glcts argument; repeat and use --deqp-arg=--option=value for leading dashes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--idle-timeout",
|
||||
type=float,
|
||||
default=300.0,
|
||||
metavar="SECONDS",
|
||||
help="kill a chunk after this many seconds with no QPA size/mtime change (0 disables; default: 300)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-round-seconds",
|
||||
type=float,
|
||||
default=0.0,
|
||||
metavar="SECONDS",
|
||||
help="optional total wall limit for one invocation (0 disables; default: 0)",
|
||||
)
|
||||
parser.add_argument("--poll-seconds", type=float, default=1.0, help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--max-rounds",
|
||||
type=int,
|
||||
default=10000,
|
||||
help="maximum glcts invocations in this runner process (default: 10000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-empty-streak",
|
||||
type=int,
|
||||
default=3,
|
||||
help="abort after this many invocations record no case at all; no case is blamed (default: 3)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def execute(args: argparse.Namespace) -> int:
|
||||
if args.idle_timeout < 0 or args.max_round_seconds < 0:
|
||||
raise RunnerError("timeout values must be non-negative")
|
||||
if args.poll_seconds <= 0:
|
||||
raise RunnerError("--poll-seconds must be greater than zero")
|
||||
if args.max_rounds <= 0 or args.max_empty_streak <= 0:
|
||||
raise RunnerError("--max-rounds and --max-empty-streak must be greater than zero")
|
||||
validate_deqp_args(args.deqp_arg)
|
||||
extra_environment = parse_environment(args.env)
|
||||
exe, workdir, caselist_path, outdir = resolve_paths(
|
||||
args.exe, args.workdir, args.caselist, args.outdir
|
||||
)
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cases = read_caselist(caselist_path)
|
||||
expected = set(cases)
|
||||
check_run_identity(
|
||||
outdir,
|
||||
args.backend,
|
||||
cases,
|
||||
args.run_identity,
|
||||
adopt_legacy=args.adopt_legacy,
|
||||
)
|
||||
recorded, crashed, hung = recover_results(outdir, expected)
|
||||
accounted = recorded | crashed | hung
|
||||
remaining = [case for case in cases if case not in accounted]
|
||||
persist_sidecars(outdir, cases, crashed, hung, remaining)
|
||||
|
||||
existing_qpas = len(result_qpa_files(outdir))
|
||||
print(
|
||||
f"[run_cts_windows] {args.backend}: expected {len(cases)}, recovered {len(accounted)} "
|
||||
f"({existing_qpas} QPA chunk(s), {len(crashed)} crash, {len(hung)} hang)"
|
||||
)
|
||||
if not remaining:
|
||||
print(f"[run_cts_windows] complete: all {len(cases)} expected cases are accounted")
|
||||
return 0
|
||||
|
||||
environment = os.environ.copy()
|
||||
environment.update(extra_environment)
|
||||
# --backend is authoritative even if the inherited or extra environment
|
||||
# already contains a different value.
|
||||
environment["MOBILEGL_BACKEND_TYPE"] = args.backend
|
||||
|
||||
common_arguments = [
|
||||
"--deqp-terminate-on-device-lost=disable",
|
||||
"--deqp-log-images=disable",
|
||||
"--deqp-log-shader-sources=disable",
|
||||
]
|
||||
|
||||
chunk_number = next_chunk_number(outdir)
|
||||
rounds = 0
|
||||
empty_streak = 0
|
||||
interrupted = False
|
||||
fatal_launch_error = False
|
||||
started_all = time.monotonic()
|
||||
|
||||
while remaining and rounds < args.max_rounds:
|
||||
prefix = f"chunk{chunk_number:04d}"
|
||||
remaining_path = outdir / "remaining.txt"
|
||||
qpa_path = outdir / f"{prefix}.qpa"
|
||||
stdout_path = outdir / f"{prefix}.stdout.log"
|
||||
stderr_path = outdir / f"{prefix}.stderr.log"
|
||||
meta_path = outdir / f"{prefix}.meta.json"
|
||||
|
||||
# The number allocator considers every chunk artifact, so these should
|
||||
# be new. Refuse to truncate evidence if a foreign file races us.
|
||||
for artifact in (qpa_path, stdout_path, stderr_path, meta_path):
|
||||
if artifact.exists():
|
||||
raise RunnerError(f"refusing to overwrite existing chunk artifact: {artifact}")
|
||||
write_case_file(remaining_path, remaining)
|
||||
|
||||
command = [
|
||||
str(exe),
|
||||
f"--deqp-caselist-file={remaining_path}",
|
||||
f"--deqp-log-filename={qpa_path}",
|
||||
*common_arguments,
|
||||
*args.deqp_arg,
|
||||
]
|
||||
print(
|
||||
f"[run_cts_windows] {prefix}: launching {len(remaining)} remaining case(s); "
|
||||
f"idle timeout {args.idle_timeout:g}s"
|
||||
)
|
||||
chunk_started_utc = utc_now()
|
||||
outcome = run_process(
|
||||
command,
|
||||
workdir,
|
||||
environment,
|
||||
qpa_path,
|
||||
stdout_path,
|
||||
stderr_path,
|
||||
args.idle_timeout,
|
||||
args.max_round_seconds,
|
||||
args.poll_seconds,
|
||||
)
|
||||
progress = scan_qpa(qpa_path)
|
||||
|
||||
before = set(accounted)
|
||||
for case in progress.recorded:
|
||||
if case in expected:
|
||||
recorded.add(case)
|
||||
accounted.add(case)
|
||||
|
||||
classification: Optional[str] = None
|
||||
classified_case: Optional[str] = None
|
||||
in_flight = progress.in_flight if progress.in_flight in expected else None
|
||||
if not outcome.interrupted and in_flight is not None and in_flight not in accounted:
|
||||
classified_case = in_flight
|
||||
if outcome.timed_out:
|
||||
classification = "DeviceHang"
|
||||
hung.add(in_flight)
|
||||
crashed.discard(in_flight)
|
||||
else:
|
||||
classification = "Crash"
|
||||
crashed.add(in_flight)
|
||||
accounted.add(in_flight)
|
||||
|
||||
new_accounted = len(accounted - before)
|
||||
if new_accounted:
|
||||
empty_streak = 0
|
||||
elif progress.begin_count == 0:
|
||||
# No #begin marker means there is no evidence that the first
|
||||
# remaining case was reached. Retry the identical caselist, then
|
||||
# abort rather than manufacturing a string of false Crash results.
|
||||
empty_streak += 1
|
||||
else:
|
||||
# A log containing only already-accounted cases is also no forward
|
||||
# progress, but it is a different failure mode. Bound it with the
|
||||
# same guard while retaining the QPA evidence.
|
||||
empty_streak += 1
|
||||
|
||||
remaining = [case for case in cases if case not in accounted]
|
||||
metadata = {
|
||||
"version": 1,
|
||||
"chunk": chunk_number,
|
||||
"started_utc": chunk_started_utc,
|
||||
"finished_utc": utc_now(),
|
||||
"duration_seconds": round(outcome.duration_seconds, 3),
|
||||
"returncode": outcome.returncode,
|
||||
"timed_out": outcome.timed_out,
|
||||
"timeout_reason": outcome.timeout_reason,
|
||||
"interrupted": outcome.interrupted,
|
||||
"launch_error": outcome.launch_error,
|
||||
"qpa_begin_count": progress.begin_count,
|
||||
"qpa_recorded_count": len(progress.recorded),
|
||||
"in_flight": progress.in_flight,
|
||||
"classification": classification,
|
||||
"classified_case": classified_case,
|
||||
"new_accounted": new_accounted,
|
||||
"remaining": len(remaining),
|
||||
}
|
||||
# Metadata is committed first. If the runner itself dies between this
|
||||
# write and the sidecars, recovery can reconstruct the classification.
|
||||
atomic_write_json(meta_path, metadata)
|
||||
persist_sidecars(outdir, cases, crashed, hung, remaining)
|
||||
|
||||
rounds += 1
|
||||
elapsed_minutes = (time.monotonic() - started_all) / 60.0
|
||||
detail = ""
|
||||
if classification:
|
||||
detail = f", {classification}={classified_case}"
|
||||
if outcome.timed_out:
|
||||
detail += f", timeout={outcome.timeout_reason}"
|
||||
print(
|
||||
f"[run_cts_windows] {prefix}: +{new_accounted}, accounted "
|
||||
f"{len(accounted)}/{len(cases)}, remaining {len(remaining)}{detail} "
|
||||
f"({elapsed_minutes:.1f} min)"
|
||||
)
|
||||
|
||||
chunk_number += 1
|
||||
if outcome.interrupted:
|
||||
interrupted = True
|
||||
print("[run_cts_windows] interrupted; process tree stopped and state preserved", file=sys.stderr)
|
||||
break
|
||||
if outcome.launch_error:
|
||||
fatal_launch_error = True
|
||||
print(
|
||||
f"[run_cts_windows] launch failed; see {stderr_path.name}: {outcome.launch_error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
break
|
||||
if empty_streak >= args.max_empty_streak:
|
||||
print(
|
||||
f"[run_cts_windows] aborting after {empty_streak} consecutive chunks made no "
|
||||
"case progress; no unobserved case was labelled Crash/Hang",
|
||||
file=sys.stderr,
|
||||
)
|
||||
break
|
||||
|
||||
# Recompute from the persisted evidence so the final completeness claim is
|
||||
# subject to the exact same recovery path as a later invocation.
|
||||
final_recorded, final_crashed, final_hung = recover_results(outdir, expected)
|
||||
final_accounted = final_recorded | final_crashed | final_hung
|
||||
final_remaining = [case for case in cases if case not in final_accounted]
|
||||
persist_sidecars(outdir, cases, final_crashed, final_hung, final_remaining)
|
||||
|
||||
if not final_remaining and final_accounted == expected:
|
||||
print(
|
||||
f"[run_cts_windows] complete: all {len(cases)} expected cases are accounted "
|
||||
f"({len(final_crashed)} crash, {len(final_hung)} hang, {rounds} new invocation(s))"
|
||||
)
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"[run_cts_windows] INCOMPLETE: {len(final_accounted)}/{len(cases)} accounted; "
|
||||
f"{len(final_remaining)} listed in {outdir / 'unrun.txt'}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if interrupted:
|
||||
return 130
|
||||
if fatal_launch_error:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
return execute(args)
|
||||
except RunnerError as exc:
|
||||
print(f"[run_cts_windows] ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
except OSError as exc:
|
||||
print(f"[run_cts_windows] ERROR: filesystem/process operation failed: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -21,6 +21,7 @@ CTS_TOOLS = os.path.dirname(HERE)
|
||||
COPIES = [
|
||||
(os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None),
|
||||
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]),
|
||||
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl-desktop", ["mobilegl-desktop.cmake"]),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from . import cts_matrix_report as report
|
||||
except ImportError: # Allows `python test_cts_matrix_report.py`.
|
||||
import cts_matrix_report as report
|
||||
|
||||
|
||||
def qpa_case(case, status):
|
||||
return (
|
||||
f"#beginTestCaseResult {case}\n"
|
||||
f'<Result StatusCode="{status}"/>\n'
|
||||
"#endTestCaseResult\n"
|
||||
)
|
||||
|
||||
|
||||
class MatrixReportTests(unittest.TestCase):
|
||||
def test_utf8_bom_caselist_matches_runner_semantics(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_bytes(b"\xef\xbb\xbfcase.a\n")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "run.qpa").write_text(
|
||||
qpa_case("case.a", "Pass"), encoding="utf-8"
|
||||
)
|
||||
|
||||
item = report.build_version_report("gl30", str(caselist), [str(results)])
|
||||
|
||||
self.assertEqual(1, item["expected"])
|
||||
self.assertEqual({"case.a": "Pass"}, item["cases"]["results"])
|
||||
self.assertEqual("OK", item["validation"]["state"])
|
||||
|
||||
def test_incomplete_qpa_is_unrun_not_a_completed_result(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("case.a\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "run.qpa").write_text(
|
||||
"#beginTestCaseResult case.a\n#endTestCaseResult\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(results / "unrun.txt").write_text("case.a\n", encoding="utf-8")
|
||||
|
||||
item = report.build_version_report("gl46", str(caselist), [str(results)])
|
||||
|
||||
self.assertEqual(0, item["result"])
|
||||
self.assertEqual(1, item["unrun"])
|
||||
self.assertEqual(["case.a"], item["cases"]["incomplete_results"])
|
||||
self.assertEqual("INCOMPLETE", item["validation"]["state"])
|
||||
|
||||
def test_incomplete_qpa_is_upgraded_by_crash_sidecar(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("case.a\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "run.qpa").write_text(
|
||||
"#beginTestCaseResult case.a\n", encoding="utf-8"
|
||||
)
|
||||
(results / "crashed.txt").write_text("case.a\n", encoding="utf-8")
|
||||
|
||||
item = report.build_version_report("gl46", str(caselist), [str(results)])
|
||||
|
||||
self.assertEqual("Crash", item["cases"]["results"]["case.a"])
|
||||
self.assertEqual([], item["cases"]["incomplete_results"])
|
||||
self.assertEqual("OK", item["validation"]["state"])
|
||||
|
||||
def test_chunk_numbers_above_four_digits_use_numeric_order(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("case.a\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "alpha.qpa").write_text("# no results\n", encoding="utf-8")
|
||||
(results / "chunk9999.qpa").write_text(
|
||||
qpa_case("case.a", "Fail"), encoding="utf-8"
|
||||
)
|
||||
(results / "chunk10000.qpa").write_text(
|
||||
qpa_case("case.a", "Pass"), encoding="utf-8"
|
||||
)
|
||||
(results / "zeta.qpa").write_text("# no results\n", encoding="utf-8")
|
||||
|
||||
item = report.build_version_report(
|
||||
"gl46", str(caselist), [str(results)]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
["alpha.qpa", "chunk9999.qpa", "chunk10000.qpa", "zeta.qpa"],
|
||||
[Path(path).name for path in item["inputs"]["qpa_files"]],
|
||||
)
|
||||
self.assertEqual("Pass", item["cases"]["results"]["case.a"])
|
||||
self.assertEqual(1, item["duplicate"])
|
||||
|
||||
def test_qpa_sidecars_duplicates_and_expected_denominator(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "gl30.txt"
|
||||
caselist.write_text("\n".join("abcdefg") + "\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "chunk0000.qpa").write_text(
|
||||
qpa_case("a", "Fail") + "#beginTestCaseResult e\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(results / "chunk0001.qpa").write_text(
|
||||
qpa_case("a", "Pass")
|
||||
+ qpa_case("b", "NotSupported")
|
||||
+ qpa_case("c", "QualityWarning")
|
||||
+ qpa_case("d", "Fail"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(results / "crashed.txt").write_text("e\n", encoding="utf-8")
|
||||
(results / "hung.txt").write_text("f\n", encoding="utf-8")
|
||||
(results / "unrun.txt").write_text("g\n", encoding="utf-8")
|
||||
|
||||
item = report.build_version_report(
|
||||
"gl30", str(caselist), [str(results)]
|
||||
)
|
||||
|
||||
self.assertEqual(item["expected"], 7)
|
||||
self.assertEqual(item["result"], 6)
|
||||
self.assertEqual(item["pass"], 1)
|
||||
self.assertEqual(item["accepted"], 3)
|
||||
self.assertEqual(item["crash"], 1)
|
||||
self.assertEqual(item["hang"], 1)
|
||||
self.assertEqual(item["unrun"], 1)
|
||||
self.assertEqual(item["duplicate"], 1)
|
||||
self.assertEqual(item["cases"]["results"]["a"], "Pass")
|
||||
self.assertEqual(item["cases"]["results"]["e"], "Crash")
|
||||
self.assertEqual(item["cases"]["results"]["f"], "DeviceHang")
|
||||
self.assertAlmostEqual(item["strict_pass_rate"], 1 / 7)
|
||||
self.assertAlmostEqual(item["conformance_accepted_rate"], 3 / 7)
|
||||
self.assertAlmostEqual(
|
||||
item["rates"]["measured_only_conformance_accepted"], 3 / 6
|
||||
)
|
||||
self.assertEqual(item["validation"]["state"], "INCOMPLETE")
|
||||
self.assertEqual(item["validation"]["errors"], [])
|
||||
self.assertTrue(
|
||||
item["validation"]["invariant_expected_equals_result_plus_unrun"]
|
||||
)
|
||||
|
||||
def test_missing_result_is_inferred_and_rejected_when_not_declared(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("a\nb\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "run.qpa").write_text(qpa_case("a", "Pass"), encoding="utf-8")
|
||||
|
||||
item = report.build_version_report(
|
||||
"gl31", str(caselist), [str(results)]
|
||||
)
|
||||
|
||||
self.assertEqual(item["unrun"], 1)
|
||||
self.assertEqual(item["cases"]["unrun"], ["b"])
|
||||
self.assertEqual(item["validation"]["state"], "ERROR")
|
||||
self.assertEqual(item["validation"]["undeclared_unrun"], ["b"])
|
||||
self.assertIn("not declared", item["validation"]["errors"][0])
|
||||
self.assertEqual(item["strict_pass_rate"], 0.5)
|
||||
|
||||
def test_cli_emits_markdown_json_and_weighted_overall(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
statuses = {
|
||||
"gl30": "Pass",
|
||||
"gl31": "Fail",
|
||||
"gl32": "NotSupported",
|
||||
"gl33": None,
|
||||
}
|
||||
argv = []
|
||||
for version, status in statuses.items():
|
||||
caselist = root / f"{version}.txt"
|
||||
caselist.write_text(f"{version}.case\n", encoding="utf-8")
|
||||
result_dir = root / f"{version}-results"
|
||||
result_dir.mkdir()
|
||||
qpa = result_dir / "run.qpa"
|
||||
qpa.write_text(
|
||||
qpa_case(f"{version}.case", status) if status else "# empty run\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if status is None:
|
||||
(result_dir / "unrun.txt").write_text(
|
||||
f"{version}.case\n", encoding="utf-8"
|
||||
)
|
||||
argv.extend(
|
||||
[
|
||||
f"--{version}-caselist",
|
||||
str(caselist),
|
||||
f"--{version}-results",
|
||||
str(result_dir),
|
||||
]
|
||||
)
|
||||
json_path = root / "matrix.json"
|
||||
argv.extend(["--json", str(json_path)])
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
rc = report.main(argv)
|
||||
|
||||
self.assertEqual(rc, 1) # GL33 is explicitly incomplete.
|
||||
markdown = stdout.getvalue()
|
||||
self.assertIn("| Suite | Expected | Result", markdown)
|
||||
self.assertIn("| **Overall (weighted)**", markdown)
|
||||
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
overall = payload["overall"]
|
||||
self.assertEqual(overall["expected"], 4)
|
||||
self.assertEqual(overall["result"], 3)
|
||||
self.assertEqual(overall["pass"], 1)
|
||||
self.assertEqual(overall["accepted"], 2)
|
||||
self.assertEqual(overall["unrun"], 1)
|
||||
self.assertEqual(overall["strict_pass_rate"], 0.25)
|
||||
self.assertEqual(overall["conformance_accepted_rate"], 0.5)
|
||||
self.assertEqual(overall["aggregation"], "weighted_by_expected_cases")
|
||||
self.assertEqual(overall["validation"]["state"], "INCOMPLETE")
|
||||
|
||||
def test_duplicate_caselist_and_unexpected_result_are_validation_errors(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("a\na\n", encoding="utf-8")
|
||||
results = root / "results"
|
||||
results.mkdir()
|
||||
(results / "run.qpa").write_text(
|
||||
qpa_case("a", "Pass") + qpa_case("outside", "Pass"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
item = report.build_version_report(
|
||||
"gl32", str(caselist), [str(results)]
|
||||
)
|
||||
|
||||
self.assertEqual(item["cases"]["duplicate_caselist_entries"], {"a": 2})
|
||||
self.assertEqual(item["cases"]["unexpected_results"], {"outside": "Pass"})
|
||||
self.assertEqual(item["validation"]["state"], "ERROR")
|
||||
self.assertEqual(len(item["validation"]["errors"]), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,342 @@
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from . import cts_multi_report as report
|
||||
except ImportError: # Allows `python test_cts_multi_report.py`.
|
||||
import cts_multi_report as report
|
||||
|
||||
|
||||
def qpa_case(case: str, status: str) -> str:
|
||||
return (
|
||||
f"#beginTestCaseResult {case}\n"
|
||||
f'<Result StatusCode="{status}"/>\n'
|
||||
"#endTestCaseResult\n"
|
||||
)
|
||||
|
||||
|
||||
def write_run_state(
|
||||
caselist: Path,
|
||||
result_dir: Path,
|
||||
backend: str,
|
||||
invocation_identity=None,
|
||||
) -> None:
|
||||
fingerprint, case_count = report._caselist_fingerprint(str(caselist))
|
||||
(result_dir / "run_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"backend": backend,
|
||||
"case_count": case_count,
|
||||
"caselist_sha256": fingerprint,
|
||||
"invocation_identity": invocation_identity,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def make_inputs(
|
||||
root: Path,
|
||||
name: str,
|
||||
cases: list[str],
|
||||
qpa: str,
|
||||
backend: str = "DirectGLES",
|
||||
):
|
||||
caselist = root / f"{name}.txt"
|
||||
caselist.write_text("\n".join(cases) + "\n", encoding="utf-8")
|
||||
result_dir = root / f"{name}-results"
|
||||
result_dir.mkdir()
|
||||
(result_dir / "chunk0000.qpa").write_text(qpa, encoding="utf-8")
|
||||
write_run_state(caselist, result_dir, backend)
|
||||
return caselist, result_dir
|
||||
|
||||
|
||||
class MultiReportTests(unittest.TestCase):
|
||||
def test_backend_aggregate_is_weighted_by_expected_cases(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
small_cases, small_results = make_inputs(
|
||||
root, "small", ["small.pass"], qpa_case("small.pass", "Pass")
|
||||
)
|
||||
large_cases, large_results = make_inputs(
|
||||
root,
|
||||
"large",
|
||||
["large.pass", "large.crash", "large.hang"],
|
||||
qpa_case("large.pass", "Pass"),
|
||||
)
|
||||
(large_results / "crashed.txt").write_text(
|
||||
"large.crash\n", encoding="utf-8"
|
||||
)
|
||||
(large_results / "hung.txt").write_text(
|
||||
"large.hang\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
payload = report.build_report(
|
||||
[
|
||||
report.SuiteSpec(
|
||||
"DirectGLES", "small", str(small_cases), str(small_results)
|
||||
),
|
||||
report.SuiteSpec(
|
||||
"DirectGLES", "large", str(large_cases), str(large_results)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
aggregate = payload["backends"]["DirectGLES"]
|
||||
self.assertEqual(4, aggregate["expected"])
|
||||
self.assertEqual(4, aggregate["result"])
|
||||
self.assertEqual(2, aggregate["pass"])
|
||||
self.assertEqual(2, aggregate["accepted"])
|
||||
self.assertEqual(1, aggregate["crash"])
|
||||
self.assertEqual(1, aggregate["hang"])
|
||||
self.assertEqual(0, aggregate["unrun"])
|
||||
# (1 accepted + 1 accepted) / (1 expected + 3 expected), not
|
||||
# the unweighted mean of 100% and 33.3%.
|
||||
self.assertEqual(0.5, aggregate["conformance_accepted_rate"])
|
||||
self.assertEqual("weighted_by_expected_cases", aggregate["aggregation"])
|
||||
self.assertEqual("OK", aggregate["validation"]["state"])
|
||||
|
||||
def test_declared_unrun_is_incomplete_and_cli_returns_nonzero(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root, "missing", ["case.a", "case.b"], qpa_case("case.a", "Pass")
|
||||
)
|
||||
(result_dir / "unrun.txt").write_text("case.b\n", encoding="utf-8")
|
||||
markdown_path = root / "report.md"
|
||||
json_path = root / "report.json"
|
||||
argv = [
|
||||
"--suite",
|
||||
"DirectGLES",
|
||||
"gl30",
|
||||
str(caselist),
|
||||
str(result_dir),
|
||||
"--markdown",
|
||||
str(markdown_path),
|
||||
"--json",
|
||||
str(json_path),
|
||||
]
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
returncode = report.main(argv)
|
||||
|
||||
self.assertEqual(1, returncode)
|
||||
self.assertTrue(markdown_path.is_file())
|
||||
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
suite = payload["suites"][0]
|
||||
self.assertEqual(2, suite["expected"])
|
||||
self.assertEqual(1, suite["result"])
|
||||
self.assertEqual(1, suite["unrun"])
|
||||
self.assertEqual("INCOMPLETE", suite["validation"]["state"])
|
||||
self.assertEqual("INCOMPLETE", payload["overall"]["validation"]["state"])
|
||||
self.assertEqual(0.5, payload["overall"]["conformance_accepted_rate"])
|
||||
|
||||
def test_dual_backend_cli_outputs_markdown_and_json(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist = root / "gl30.txt"
|
||||
caselist.write_text("gl30.case\n", encoding="utf-8")
|
||||
gles = root / "gles"
|
||||
vulkan = root / "vulkan"
|
||||
gles.mkdir()
|
||||
vulkan.mkdir()
|
||||
(gles / "run.qpa").write_text(
|
||||
qpa_case("gl30.case", "Pass"), encoding="utf-8"
|
||||
)
|
||||
(vulkan / "run.qpa").write_text(
|
||||
qpa_case("gl30.case", "Fail"), encoding="utf-8"
|
||||
)
|
||||
write_run_state(caselist, gles, "DirectGLES")
|
||||
write_run_state(caselist, vulkan, "DirectVulkan")
|
||||
markdown_path = root / "dual.md"
|
||||
json_path = root / "dual.json"
|
||||
argv = [
|
||||
f"--suite=DirectGLES,gl30,{caselist},{gles}",
|
||||
"--suite",
|
||||
"DirectVulkan",
|
||||
"gl30",
|
||||
str(caselist),
|
||||
str(vulkan),
|
||||
"--markdown",
|
||||
str(markdown_path),
|
||||
"--json",
|
||||
str(json_path),
|
||||
]
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
returncode = report.main(argv)
|
||||
|
||||
self.assertEqual(0, returncode)
|
||||
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual({"DirectGLES", "DirectVulkan"}, set(payload["backends"]))
|
||||
self.assertEqual(1, payload["backends"]["DirectGLES"]["accepted"])
|
||||
self.assertEqual(0, payload["backends"]["DirectVulkan"]["accepted"])
|
||||
self.assertEqual(2, payload["overall"]["expected"])
|
||||
self.assertEqual(1, payload["overall"]["accepted"])
|
||||
self.assertEqual(0.5, payload["overall"]["conformance_accepted_rate"])
|
||||
markdown = markdown_path.read_text(encoding="utf-8")
|
||||
self.assertIn("DirectGLES weighted subtotal", markdown)
|
||||
self.assertIn("DirectVulkan weighted subtotal", markdown)
|
||||
self.assertIn("Overall weighted", markdown)
|
||||
self.assertIn("Markdown:", stdout.getvalue())
|
||||
|
||||
def test_duplicate_qpa_result_uses_last_observation(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root,
|
||||
"duplicate",
|
||||
["case.a"],
|
||||
qpa_case("case.a", "Fail"),
|
||||
backend="DirectVulkan",
|
||||
)
|
||||
(result_dir / "chunk0001.qpa").write_text(
|
||||
qpa_case("case.a", "Pass"), encoding="utf-8"
|
||||
)
|
||||
|
||||
payload = report.build_report(
|
||||
[
|
||||
report.SuiteSpec(
|
||||
"DirectVulkan", "gl33", str(caselist), str(result_dir)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
suite = payload["suites"][0]
|
||||
self.assertEqual("Pass", suite["cases"]["results"]["case.a"])
|
||||
self.assertEqual(1, suite["duplicate"])
|
||||
self.assertEqual(1, payload["overall"]["duplicate"])
|
||||
self.assertEqual(1.0, payload["overall"]["strict_pass_rate"])
|
||||
self.assertEqual("OK", suite["validation"]["state"])
|
||||
self.assertIn("last result wins", suite["validation"]["warnings"][0])
|
||||
|
||||
def test_backend_provenance_mismatch_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root,
|
||||
"provenance",
|
||||
["case.a"],
|
||||
qpa_case("case.a", "Pass"),
|
||||
backend="DirectVulkan",
|
||||
)
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report(
|
||||
[report.SuiteSpec("DirectGLES", "gl30", str(caselist), str(result_dir))]
|
||||
)
|
||||
|
||||
def test_missing_provenance_requires_explicit_legacy_opt_in(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root, "legacy", ["case.a"], qpa_case("case.a", "Pass")
|
||||
)
|
||||
(result_dir / "run_state.json").unlink()
|
||||
spec = report.SuiteSpec("DirectGLES", "gl30", str(caselist), str(result_dir))
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report([spec])
|
||||
payload = report.build_report([spec], require_run_state=False)
|
||||
self.assertEqual("UNVERIFIED", payload["suites"][0]["provenance"]["state"])
|
||||
|
||||
def test_expected_run_identity_accepts_match_and_rejects_mismatch(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root, "identity", ["case.a"], qpa_case("case.a", "Pass")
|
||||
)
|
||||
write_run_state(
|
||||
caselist, result_dir, "DirectGLES", invocation_identity="identity-a"
|
||||
)
|
||||
spec = report.SuiteSpec(
|
||||
"DirectGLES", "gl30", str(caselist), str(result_dir)
|
||||
)
|
||||
|
||||
payload = report.build_report(
|
||||
[spec], expected_run_identity="identity-a"
|
||||
)
|
||||
self.assertEqual(
|
||||
"identity-a",
|
||||
payload["suites"][0]["provenance"]["invocation_identity"],
|
||||
)
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report([spec], expected_run_identity="identity-b")
|
||||
|
||||
def test_expected_identity_rejects_legacy_state_and_missing_state(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root, "legacy-identity", ["case.a"], qpa_case("case.a", "Pass")
|
||||
)
|
||||
spec = report.SuiteSpec(
|
||||
"DirectGLES", "gl30", str(caselist), str(result_dir)
|
||||
)
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report([spec], expected_run_identity="identity-a")
|
||||
|
||||
(result_dir / "run_state.json").unlink()
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report(
|
||||
[spec],
|
||||
require_run_state=False,
|
||||
expected_run_identity="identity-a",
|
||||
)
|
||||
|
||||
def test_duplicate_physical_result_directory_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist, result_dir = make_inputs(
|
||||
root, "duplicate-dir", ["case.a"], qpa_case("case.a", "Pass")
|
||||
)
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report(
|
||||
[
|
||||
report.SuiteSpec(
|
||||
"DirectGLES", "gl30", str(caselist), str(result_dir)
|
||||
),
|
||||
report.SuiteSpec(
|
||||
"DirectGLES",
|
||||
"gl31",
|
||||
str(caselist),
|
||||
str(result_dir / "."),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def test_ancestor_and_descendant_result_directories_are_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist = root / "cases.txt"
|
||||
caselist.write_text("case.a\n", encoding="utf-8")
|
||||
parent = root / "results"
|
||||
child = parent / "nested"
|
||||
child.mkdir(parents=True)
|
||||
(parent / "chunk0000.qpa").write_text(
|
||||
qpa_case("case.a", "Pass"), encoding="utf-8"
|
||||
)
|
||||
(child / "chunk0000.qpa").write_text(
|
||||
qpa_case("case.a", "Fail"), encoding="utf-8"
|
||||
)
|
||||
write_run_state(caselist, parent, "DirectGLES")
|
||||
write_run_state(caselist, child, "DirectVulkan")
|
||||
|
||||
with self.assertRaises(report.MultiReportInputError):
|
||||
report.build_report(
|
||||
[
|
||||
report.SuiteSpec(
|
||||
"DirectGLES", "gl30", str(caselist), str(parent)
|
||||
),
|
||||
report.SuiteSpec(
|
||||
"DirectVulkan", "gl30", str(caselist), str(child)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,401 @@
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import run_cts_windows as runner
|
||||
|
||||
|
||||
def qpa_closed(case: str, status: str = "Pass") -> str:
|
||||
return (
|
||||
f"#beginTestCaseResult {case}\n"
|
||||
f'<Result StatusCode="{status}">ok</Result>\n'
|
||||
"#endTestCaseResult\n"
|
||||
)
|
||||
|
||||
|
||||
def command_path(command, option):
|
||||
prefix = option + "="
|
||||
return Path(next(value[len(prefix) :] for value in command if value.startswith(prefix)))
|
||||
|
||||
|
||||
class AtomicWriteTests(unittest.TestCase):
|
||||
def test_access_denied_retries_then_replace_succeeds(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
target = Path(temporary) / "remaining.txt"
|
||||
real_replace = runner.os.replace
|
||||
attempts = 0
|
||||
|
||||
def flaky_replace(source, destination):
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
if attempts == 1:
|
||||
raise PermissionError(13, "temporarily denied", str(destination))
|
||||
if attempts == 2:
|
||||
error = OSError("temporary WinError 5")
|
||||
error.winerror = 5
|
||||
raise error
|
||||
real_replace(source, destination)
|
||||
|
||||
with mock.patch.object(runner.os, "replace", side_effect=flaky_replace), mock.patch.object(
|
||||
runner.time, "sleep"
|
||||
) as sleep:
|
||||
runner.atomic_write_text(target, "case.a\n")
|
||||
|
||||
self.assertEqual(3, attempts)
|
||||
self.assertEqual("case.a\n", target.read_text(encoding="utf-8"))
|
||||
self.assertEqual(2, sleep.call_count)
|
||||
self.assertEqual(
|
||||
[
|
||||
mock.call(runner.ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS),
|
||||
mock.call(runner.ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS * 2),
|
||||
],
|
||||
sleep.call_args_list,
|
||||
)
|
||||
self.assertEqual([], list(target.parent.glob(".remaining.txt.*.tmp")))
|
||||
|
||||
def test_permanent_access_denied_stops_after_bounded_attempts(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
target = Path(temporary) / "remaining.txt"
|
||||
|
||||
def always_denied(_source, _destination):
|
||||
error = OSError("persistent WinError 5")
|
||||
error.winerror = 5
|
||||
raise error
|
||||
|
||||
with mock.patch.object(
|
||||
runner.os, "replace", side_effect=always_denied
|
||||
) as replace, mock.patch.object(runner.time, "sleep") as sleep:
|
||||
with self.assertRaises(OSError) as raised:
|
||||
runner.atomic_write_text(target, "case.a\n")
|
||||
|
||||
self.assertEqual(5, raised.exception.winerror)
|
||||
self.assertEqual(runner.ATOMIC_REPLACE_ATTEMPTS, replace.call_count)
|
||||
self.assertEqual(runner.ATOMIC_REPLACE_ATTEMPTS - 1, sleep.call_count)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual([], list(target.parent.glob(".remaining.txt.*.tmp")))
|
||||
|
||||
def test_non_access_error_is_not_retried(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
target = Path(temporary) / "remaining.txt"
|
||||
error = OSError(28, "disk full")
|
||||
with mock.patch.object(
|
||||
runner.os, "replace", side_effect=error
|
||||
) as replace, mock.patch.object(runner.time, "sleep") as sleep:
|
||||
with self.assertRaises(OSError):
|
||||
runner.atomic_write_text(target, "case.a\n")
|
||||
|
||||
self.assertEqual(1, replace.call_count)
|
||||
sleep.assert_not_called()
|
||||
|
||||
|
||||
class QpaParsingTests(unittest.TestCase):
|
||||
def test_terminate_is_a_completed_result(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / "chunk0000.qpa"
|
||||
path.write_text(
|
||||
"#beginTestCaseResult KHR-GL30.a\n"
|
||||
"#terminateTestCaseResult Crash\n"
|
||||
"#beginTestCaseResult KHR-GL30.b\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
progress = runner.scan_qpa(path)
|
||||
self.assertEqual(["KHR-GL30.a"], progress.recorded)
|
||||
self.assertEqual("KHR-GL30.b", progress.in_flight)
|
||||
self.assertEqual(2, progress.begin_count)
|
||||
|
||||
def test_end_without_result_is_not_accounted(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / "chunk0000.qpa"
|
||||
path.write_text(
|
||||
"#beginTestCaseResult KHR-GL46.incomplete\n"
|
||||
"#endTestCaseResult\n"
|
||||
+ qpa_closed("KHR-GL46.complete"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
progress = runner.scan_qpa(path)
|
||||
self.assertEqual(["KHR-GL46.complete"], progress.recorded)
|
||||
self.assertIsNone(progress.in_flight)
|
||||
|
||||
def test_result_written_before_truncated_eof_is_recovered(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / "chunk0000.qpa"
|
||||
path.write_text(
|
||||
"#beginTestCaseResult KHR-GL46.complete\n"
|
||||
'<Result StatusCode="Pass">ok</Result>\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
progress = runner.scan_qpa(path)
|
||||
self.assertEqual(["KHR-GL46.complete"], progress.recorded)
|
||||
self.assertIsNone(progress.in_flight)
|
||||
|
||||
|
||||
class RunIdentityTests(unittest.TestCase):
|
||||
def test_non_object_run_state_is_a_controlled_error(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
(outdir / "run_state.json").write_text("null\n", encoding="utf-8")
|
||||
with self.assertRaises(runner.RunnerError):
|
||||
runner.check_run_identity(outdir, "DirectVulkan", ["case.a"])
|
||||
|
||||
def test_controller_identity_prevents_mixed_invocations(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
|
||||
)
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
|
||||
)
|
||||
with self.assertRaises(runner.RunnerError):
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-b"
|
||||
)
|
||||
|
||||
def test_controller_identity_cannot_be_downgraded_by_omission(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
|
||||
)
|
||||
with self.assertRaises(runner.RunnerError):
|
||||
runner.check_run_identity(outdir, "DirectVulkan", ["case.a"])
|
||||
|
||||
def test_legacy_artifacts_require_explicit_adoption(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
(outdir / "chunk0000.qpa").write_text(
|
||||
qpa_closed("case.a"), encoding="utf-8"
|
||||
)
|
||||
with self.assertRaises(runner.RunnerError):
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
|
||||
)
|
||||
self.assertFalse((outdir / "run_state.json").exists())
|
||||
|
||||
runner.check_run_identity(
|
||||
outdir,
|
||||
"DirectVulkan",
|
||||
["case.a"],
|
||||
invocation_identity="identity-a",
|
||||
adopt_legacy=True,
|
||||
)
|
||||
state = runner.json.loads(
|
||||
(outdir / "run_state.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertTrue(state["adopted_legacy"])
|
||||
|
||||
def test_foreign_nested_qpa_and_skipped_sidecar_are_legacy_evidence(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
nested = outdir / "old"
|
||||
nested.mkdir()
|
||||
qpa = nested / "legacy.qpa"
|
||||
qpa.write_text(qpa_closed("case.a"), encoding="utf-8")
|
||||
skipped = outdir / "skipped.txt"
|
||||
skipped.write_text("case.b\n", encoding="utf-8")
|
||||
|
||||
self.assertEqual(
|
||||
{qpa, skipped}, set(runner.recovery_artifacts(outdir))
|
||||
)
|
||||
with self.assertRaises(runner.RunnerError):
|
||||
runner.check_run_identity(
|
||||
outdir, "DirectVulkan", ["case.a", "case.b"]
|
||||
)
|
||||
recorded, crashed, hung = runner.recover_results(
|
||||
outdir, {"case.a", "case.b"}
|
||||
)
|
||||
self.assertEqual({"case.a"}, recorded)
|
||||
self.assertEqual(set(), crashed)
|
||||
self.assertEqual(set(), hung)
|
||||
|
||||
def test_non_object_or_non_string_meta_classification_is_ignored(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
outdir = Path(temporary)
|
||||
(outdir / "chunk0000.meta.json").write_text("null\n", encoding="utf-8")
|
||||
(outdir / "chunk0001.meta.json").write_text("[]\n", encoding="utf-8")
|
||||
(outdir / "chunk0002.meta.json").write_text(
|
||||
'{"classified_case": [], "classification": "Crash"}\n', encoding="utf-8"
|
||||
)
|
||||
self.assertEqual(
|
||||
(set(), set()), runner.load_meta_classifications(outdir, {"case.a"})
|
||||
)
|
||||
|
||||
|
||||
class RunnerRecoveryTests(unittest.TestCase):
|
||||
def run_args(self, root: Path, caselist: Path, outdir: Path, *extra: str):
|
||||
return [
|
||||
"--exe",
|
||||
sys.executable,
|
||||
"--workdir",
|
||||
str(root),
|
||||
"--caselist",
|
||||
str(caselist),
|
||||
"--outdir",
|
||||
str(outdir),
|
||||
"--backend",
|
||||
"DirectVulkan",
|
||||
*extra,
|
||||
]
|
||||
|
||||
def test_crash_tail_is_quarantined_and_next_chunk_resumes(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist = root / "cases.txt"
|
||||
outdir = root / "results"
|
||||
caselist.write_text("KHR-GL30.a\nKHR-GL30.b\nKHR-GL30.c\n", encoding="utf-8")
|
||||
seen_remaining = []
|
||||
|
||||
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
|
||||
del workdir, timeouts
|
||||
seen_remaining.append(
|
||||
command_path(command, "--deqp-caselist-file")
|
||||
.read_text(encoding="utf-8")
|
||||
.splitlines()
|
||||
)
|
||||
stdout_path.write_text("fake stdout\n", encoding="utf-8")
|
||||
stderr_path.write_text("fake stderr\n", encoding="utf-8")
|
||||
self.assertEqual("DirectVulkan", environment["MOBILEGL_BACKEND_TYPE"])
|
||||
if len(seen_remaining) == 1:
|
||||
qpa_path.write_text(
|
||||
qpa_closed("KHR-GL30.a") + "#beginTestCaseResult KHR-GL30.b\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return runner.ProcessOutcome(0xC0000005, 0.1)
|
||||
qpa_path.write_text(qpa_closed("KHR-GL30.c"), encoding="utf-8")
|
||||
return runner.ProcessOutcome(0, 0.1)
|
||||
|
||||
with mock.patch.object(runner, "run_process", side_effect=fake_run):
|
||||
result = runner.main(self.run_args(root, caselist, outdir))
|
||||
|
||||
self.assertEqual(0, result)
|
||||
self.assertEqual(
|
||||
[
|
||||
["KHR-GL30.a", "KHR-GL30.b", "KHR-GL30.c"],
|
||||
["KHR-GL30.c"],
|
||||
],
|
||||
seen_remaining,
|
||||
)
|
||||
self.assertEqual("KHR-GL30.b\n", (outdir / "crashed.txt").read_text(encoding="utf-8"))
|
||||
self.assertEqual("", (outdir / "hung.txt").read_text(encoding="utf-8"))
|
||||
self.assertEqual("", (outdir / "unrun.txt").read_text(encoding="utf-8"))
|
||||
|
||||
def test_existing_qpa_and_sidecar_are_recovered(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist = root / "cases.txt"
|
||||
outdir = root / "results"
|
||||
outdir.mkdir()
|
||||
caselist.write_text("KHR-GL31.a\nKHR-GL31.b\nKHR-GL31.c\n", encoding="utf-8")
|
||||
(outdir / "chunk0000.qpa").write_text(qpa_closed("KHR-GL31.a"), encoding="utf-8")
|
||||
(outdir / "crashed.txt").write_text("KHR-GL31.b\n", encoding="utf-8")
|
||||
seen_remaining = []
|
||||
|
||||
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
|
||||
del workdir, environment, stdout_path, stderr_path, timeouts
|
||||
seen_remaining.extend(
|
||||
command_path(command, "--deqp-caselist-file")
|
||||
.read_text(encoding="utf-8")
|
||||
.splitlines()
|
||||
)
|
||||
qpa_path.write_text(qpa_closed("KHR-GL31.c"), encoding="utf-8")
|
||||
return runner.ProcessOutcome(0, 0.1)
|
||||
|
||||
with mock.patch.object(runner, "run_process", side_effect=fake_run):
|
||||
result = runner.main(
|
||||
self.run_args(root, caselist, outdir, "--adopt-legacy")
|
||||
)
|
||||
|
||||
self.assertEqual(0, result)
|
||||
self.assertEqual(["KHR-GL31.c"], seen_remaining)
|
||||
self.assertTrue((outdir / "chunk0001.qpa").is_file())
|
||||
|
||||
def test_repeated_no_output_aborts_without_false_case_blame(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
caselist = root / "cases.txt"
|
||||
outdir = root / "results"
|
||||
caselist.write_text("KHR-GL32.a\nKHR-GL32.b\n", encoding="utf-8")
|
||||
seen_remaining = []
|
||||
|
||||
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
|
||||
del workdir, environment, stdout_path, stderr_path, timeouts
|
||||
seen_remaining.append(
|
||||
command_path(command, "--deqp-caselist-file")
|
||||
.read_text(encoding="utf-8")
|
||||
.splitlines()
|
||||
)
|
||||
qpa_path.write_text("#sessionInfo releaseName fake\n", encoding="utf-8")
|
||||
return runner.ProcessOutcome(
|
||||
1, 0.1, timed_out=True, timeout_reason="qpa-idle"
|
||||
)
|
||||
|
||||
with mock.patch.object(runner, "run_process", side_effect=fake_run):
|
||||
result = runner.main(
|
||||
self.run_args(root, caselist, outdir, "--max-empty-streak", "2")
|
||||
)
|
||||
|
||||
self.assertEqual(4, result)
|
||||
self.assertEqual(
|
||||
[["KHR-GL32.a", "KHR-GL32.b"], ["KHR-GL32.a", "KHR-GL32.b"]],
|
||||
seen_remaining,
|
||||
)
|
||||
self.assertEqual("", (outdir / "crashed.txt").read_text(encoding="utf-8"))
|
||||
self.assertEqual("", (outdir / "hung.txt").read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
"KHR-GL32.a\nKHR-GL32.b\n",
|
||||
(outdir / "unrun.txt").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
class ProcessTimeoutTests(unittest.TestCase):
|
||||
def test_qpa_activity_prevents_idle_timeout(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
qpa = root / "active.qpa"
|
||||
helper = (
|
||||
"import pathlib,sys,time\n"
|
||||
"path=pathlib.Path(sys.argv[1])\n"
|
||||
"for size in range(1, 9):\n"
|
||||
" path.write_text('x' * size, encoding='utf-8')\n"
|
||||
" time.sleep(0.08)\n"
|
||||
)
|
||||
outcome = runner.run_process(
|
||||
[sys.executable, "-c", helper, str(qpa)],
|
||||
root,
|
||||
dict(runner.os.environ),
|
||||
qpa,
|
||||
root / "stdout.log",
|
||||
root / "stderr.log",
|
||||
idle_timeout=0.2,
|
||||
max_round_seconds=0,
|
||||
poll_seconds=0.03,
|
||||
)
|
||||
self.assertFalse(outcome.timed_out)
|
||||
self.assertEqual(0, outcome.returncode)
|
||||
|
||||
def test_idle_timeout_really_stops_process(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
started = time.monotonic()
|
||||
outcome = runner.run_process(
|
||||
[sys.executable, "-c", "import time; time.sleep(30)"],
|
||||
root,
|
||||
dict(runner.os.environ),
|
||||
root / "never-created.qpa",
|
||||
root / "stdout.log",
|
||||
root / "stderr.log",
|
||||
idle_timeout=0.2,
|
||||
max_round_seconds=0,
|
||||
poll_seconds=0.05,
|
||||
)
|
||||
elapsed = time.monotonic() - started
|
||||
self.assertTrue(outcome.timed_out)
|
||||
self.assertEqual("qpa-idle", outcome.timeout_reason)
|
||||
self.assertLess(elapsed, 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,294 @@
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import wgl_glcts_pipeline as pipeline
|
||||
|
||||
|
||||
def write_fake_pe(path: Path, machine: int = pipeline.PE_MACHINE_AMD64, payload: bytes = b"") -> None:
|
||||
data = bytearray(0x88)
|
||||
data[0:2] = b"MZ"
|
||||
struct.pack_into("<I", data, 0x3C, 0x80)
|
||||
data[0x80:0x84] = b"PE\0\0"
|
||||
struct.pack_into("<H", data, 0x84, machine)
|
||||
path.write_bytes(bytes(data) + payload)
|
||||
|
||||
|
||||
class ArgumentTests(unittest.TestCase):
|
||||
def test_versions_accept_gl_and_dotted_spellings(self):
|
||||
self.assertEqual("30", pipeline.normalize_version("GL30"))
|
||||
self.assertEqual("46", pipeline.normalize_version("4.6"))
|
||||
with self.assertRaises(argparse.ArgumentTypeError):
|
||||
pipeline.normalize_version("4.7")
|
||||
|
||||
def test_environment_assignment_validation(self):
|
||||
self.assertEqual(("MOBILEGL_TEST", "a=b"), pipeline.parse_assignment("MOBILEGL_TEST=a=b"))
|
||||
with self.assertRaises(argparse.ArgumentTypeError):
|
||||
pipeline.parse_assignment("9BAD=value")
|
||||
|
||||
def test_windows_environment_keys_are_canonical_and_last_wins(self):
|
||||
self.assertEqual(
|
||||
{"FOO": "x", "PATH": "second"},
|
||||
pipeline.canonicalize_windows_environment(
|
||||
[("Path", "first"), ("FOO", "x"), ("pAtH", "second")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CommandTests(unittest.TestCase):
|
||||
def test_visual_studio_configure_commands_are_x64_and_wgl_default(self):
|
||||
mobilegl = pipeline.mobilegl_configure_command(
|
||||
Path("C:/src/MobileGL"), Path("D:/work/mg"), "Visual Studio 17 2022", "x64", []
|
||||
)
|
||||
self.assertIn("-A", mobilegl)
|
||||
self.assertIn("x64", mobilegl)
|
||||
self.assertIn("-DMOBILEGL_BUILD_TEST=OFF", mobilegl)
|
||||
|
||||
cts = pipeline.cts_configure_command(
|
||||
Path("D:/src/VK-GL-CTS"), Path("D:/work/cts"), "Visual Studio 17 2022", "x64", []
|
||||
)
|
||||
self.assertIn("-DDEQP_TARGET=default", cts)
|
||||
self.assertNotIn("-DDEQP_TARGET=mobilegl", cts)
|
||||
|
||||
def test_runner_command_contains_identity_preserving_wgl_flags(self):
|
||||
command = pipeline.runner_command(
|
||||
Path("runner.py"),
|
||||
Path("runtime/glcts.exe"),
|
||||
Path("cts/modules"),
|
||||
Path("gl46-main.txt"),
|
||||
Path("results/gl46"),
|
||||
"DirectVulkan",
|
||||
300,
|
||||
0,
|
||||
10000,
|
||||
{"MOBILEGL_LOG_FILE_PATH": "result/mobilegl.log"},
|
||||
pipeline.DEFAULT_DEQP_ARGS,
|
||||
"run-fingerprint",
|
||||
)
|
||||
self.assertIn("--backend", command)
|
||||
self.assertIn("DirectVulkan", command)
|
||||
self.assertIn("--deqp-arg=--deqp-gl-context-type=wgl", command)
|
||||
self.assertIn("--deqp-arg=--deqp-surface-type=fbo", command)
|
||||
self.assertIn("--env", command)
|
||||
self.assertIn("MOBILEGL_LOG_FILE_PATH=result/mobilegl.log", command)
|
||||
self.assertIn("--run-identity", command)
|
||||
self.assertIn("run-fingerprint", command)
|
||||
|
||||
def test_report_command_requires_the_pipeline_run_identity(self):
|
||||
command = pipeline.report_command(
|
||||
Path("report.py"),
|
||||
[("DirectVulkan", "gl46", Path("gl46.txt"), Path("results/gl46"))],
|
||||
Path("summary.md"),
|
||||
Path("summary.json"),
|
||||
False,
|
||||
"run-fingerprint",
|
||||
)
|
||||
self.assertIn("--expected-run-identity", command)
|
||||
self.assertIn("run-fingerprint", command)
|
||||
|
||||
|
||||
class RuntimeTests(unittest.TestCase):
|
||||
def test_pe_machine_rejects_non_x64(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / "x86.dll"
|
||||
write_fake_pe(path, machine=0x14C)
|
||||
self.assertEqual(0x14C, pipeline.pe_machine(path))
|
||||
with self.assertRaises(pipeline.PipelineError):
|
||||
pipeline.require_x64_pe(path, "test DLL")
|
||||
|
||||
def test_runtime_is_hash_keyed_and_copies_only_declared_files(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
glcts = root / "source-glcts.exe"
|
||||
mobilegl = root / "source-MobileGL.dll"
|
||||
write_fake_pe(glcts, payload=b"glcts")
|
||||
write_fake_pe(mobilegl, payload=b"mobilegl")
|
||||
sources = {"glcts.exe": glcts, "opengl32.dll": mobilegl}
|
||||
fingerprint, hashes = pipeline.runtime_fingerprint(sources)
|
||||
|
||||
runtime = pipeline.assemble_runtime(root / "work", sources, fingerprint, hashes)
|
||||
|
||||
self.assertEqual(fingerprint[:16], runtime.name)
|
||||
self.assertEqual(hashes["glcts.exe"], pipeline.sha256_file(runtime / "glcts.exe"))
|
||||
self.assertEqual(hashes["opengl32.dll"], pipeline.sha256_file(runtime / "opengl32.dll"))
|
||||
manifest = json.loads((runtime / "manifest.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(fingerprint, manifest["fingerprint"])
|
||||
self.assertFalse((runtime / "libEGL.dll").exists())
|
||||
|
||||
def test_run_fingerprint_changes_with_execution_semantics(self):
|
||||
base = pipeline.run_fingerprint(
|
||||
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
|
||||
)
|
||||
self.assertEqual(
|
||||
base,
|
||||
pipeline.run_fingerprint(
|
||||
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
|
||||
),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base,
|
||||
pipeline.run_fingerprint(
|
||||
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=window"], {"FLAG": "1"}
|
||||
),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base,
|
||||
pipeline.run_fingerprint(
|
||||
"runtime", "data", {"runner": "tool"}, {"30": "different"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
|
||||
),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base,
|
||||
pipeline.run_fingerprint(
|
||||
"runtime", "different-data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
|
||||
),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base,
|
||||
pipeline.run_fingerprint(
|
||||
"runtime", "data", {"runner": "different-tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
|
||||
),
|
||||
)
|
||||
|
||||
timeout_baseline = pipeline.run_fingerprint(
|
||||
"runtime",
|
||||
"data",
|
||||
{"runner": "tool"},
|
||||
{"30": "caselist"},
|
||||
["--deqp-surface-type=fbo"],
|
||||
{"FLAG": "1"},
|
||||
{"idle_timeout_seconds": 300.0, "max_round_seconds": 0.0},
|
||||
)
|
||||
idle_changed = pipeline.run_fingerprint(
|
||||
"runtime",
|
||||
"data",
|
||||
{"runner": "tool"},
|
||||
{"30": "caselist"},
|
||||
["--deqp-surface-type=fbo"],
|
||||
{"FLAG": "1"},
|
||||
{"idle_timeout_seconds": 1.0, "max_round_seconds": 0.0},
|
||||
)
|
||||
max_round_changed = pipeline.run_fingerprint(
|
||||
"runtime",
|
||||
"data",
|
||||
{"runner": "tool"},
|
||||
{"30": "caselist"},
|
||||
["--deqp-surface-type=fbo"],
|
||||
{"FLAG": "1"},
|
||||
{"idle_timeout_seconds": 300.0, "max_round_seconds": 60.0},
|
||||
)
|
||||
self.assertNotEqual(timeout_baseline, idle_changed)
|
||||
self.assertNotEqual(timeout_baseline, max_round_changed)
|
||||
|
||||
def test_tracked_environment_is_case_insensitive_and_narrow(self):
|
||||
ambient, effective = pipeline.tracked_run_environment(
|
||||
{
|
||||
"Path": "ambient-path",
|
||||
"mobilegl_debug": "0",
|
||||
"LibGL_Driver": "ambient-libgl",
|
||||
"vK_iCd_fIlEnAmEs": "ambient-icd",
|
||||
"ANGLE_DEFAULT_PLATFORM": "vulkan",
|
||||
"Egl_Test": "1",
|
||||
"D3D_Feature": "1",
|
||||
"DxVk_Config": "ambient-dxvk",
|
||||
"HOME": "ignored",
|
||||
"PATH_EXTRA": "ignored",
|
||||
"MOBILEGL": "ignored",
|
||||
},
|
||||
{"pAtH": "explicit-path", "vk_icd_filenames": "explicit-icd", "CUSTOM": "kept"},
|
||||
)
|
||||
self.assertEqual("ambient-path", ambient["PATH"])
|
||||
self.assertNotIn("HOME", ambient)
|
||||
self.assertNotIn("PATH_EXTRA", ambient)
|
||||
self.assertNotIn("MOBILEGL", ambient)
|
||||
self.assertEqual("explicit-path", effective["PATH"])
|
||||
self.assertEqual("explicit-icd", effective["VK_ICD_FILENAMES"])
|
||||
self.assertEqual("kept", effective["CUSTOM"])
|
||||
|
||||
def test_reserved_environment_names_cannot_hide_behind_case(self):
|
||||
overrides = pipeline.canonicalize_windows_environment(
|
||||
[("mobilegl_backend_type", "DirectGLES")]
|
||||
)
|
||||
self.assertEqual(
|
||||
{"MOBILEGL_BACKEND_TYPE"},
|
||||
pipeline.CONTROLLED_ENVIRONMENT_NAMES & set(overrides),
|
||||
)
|
||||
|
||||
def test_directgles_requires_complete_angle_runtime(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
glcts = root / "glcts.exe"
|
||||
mobilegl = root / "MobileGL.dll"
|
||||
write_fake_pe(glcts)
|
||||
write_fake_pe(mobilegl)
|
||||
with self.assertRaises(pipeline.PipelineError):
|
||||
pipeline.runtime_source_files(glcts, mobilegl, ["DirectGLES"], root / "angle")
|
||||
|
||||
angle = root / "angle"
|
||||
angle.mkdir()
|
||||
for name in pipeline.ANGLE_REQUIRED_DLLS:
|
||||
write_fake_pe(angle / name, payload=name.encode("ascii"))
|
||||
files = pipeline.runtime_source_files(glcts, mobilegl, ["DirectGLES"], angle)
|
||||
self.assertEqual(
|
||||
{"glcts.exe", "opengl32.dll", *pipeline.ANGLE_REQUIRED_DLLS}, set(files)
|
||||
)
|
||||
|
||||
|
||||
class CaselistTests(unittest.TestCase):
|
||||
def test_preflight_prefers_small_buffer_case(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
caselist = Path(temporary) / "gl30-main.txt"
|
||||
caselist.write_text(
|
||||
"KHR-GL30.api.coverage\nKHR-GL30.buffer_objects.gen_buffers\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.assertEqual("KHR-GL30.buffer_objects.gen_buffers", pipeline.choose_preflight_case(caselist))
|
||||
|
||||
|
||||
class PreflightTests(unittest.TestCase):
|
||||
def write_identity(self, root: Path, renderer: str, version: str = "4.6"):
|
||||
qpa = root / "chunk0000.qpa"
|
||||
qpa.write_text(
|
||||
'#sessionInfo vendor "MobileGL-Dev"\n'
|
||||
f'#sessionInfo renderer "{renderer}"\n'
|
||||
'#sessionInfo commandLineParameters "--deqp-gl-context-type=wgl --deqp-surface-type=fbo"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
log = root / "mobilegl.log"
|
||||
log.write_text(f"Target OpenGL Version: {version}\n", encoding="utf-8")
|
||||
return qpa, log
|
||||
|
||||
def test_identity_accepts_both_mobilegl_renderers(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
qpa, log = self.write_identity(root, "Magma (MobileGL Core)")
|
||||
identity = pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
|
||||
self.assertEqual("4.6", identity["target_gl_version"])
|
||||
|
||||
qpa, log = self.write_identity(root, "Espryt (MobileGL Core)")
|
||||
identity = pipeline.parse_preflight_identity([qpa], log, "DirectGLES", (3, 3))
|
||||
self.assertIn("Espryt", identity["renderer"])
|
||||
|
||||
def test_identity_rejects_system_driver_or_low_version(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
qpa, log = self.write_identity(root, "NVIDIA GeForce RTX", version="4.6")
|
||||
qpa.write_text(
|
||||
'#sessionInfo vendor "NVIDIA Corporation"\n'
|
||||
'#sessionInfo renderer "NVIDIA GeForce RTX"\n'
|
||||
'#sessionInfo commandLineParameters "--deqp-gl-context-type=wgl"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(pipeline.PipelineError):
|
||||
pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
|
||||
|
||||
qpa, log = self.write_identity(root, "Magma (MobileGL Core)", version="4.5")
|
||||
with self.assertRaises(pipeline.PipelineError):
|
||||
pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,914 @@
|
||||
#!/usr/bin/env python
|
||||
"""Build MobileGL's Windows WGL shim and run Khronos OpenGL CTS suites.
|
||||
|
||||
The pipeline intentionally keeps the build, runtime, results, and reports in an
|
||||
explicit work root. Each run is keyed by the hashes of glcts.exe, opengl32.dll,
|
||||
and (for DirectGLES) the ANGLE runtime, so resuming can never silently combine
|
||||
results from different binaries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Iterable, Mapping, Optional, Sequence
|
||||
|
||||
|
||||
SUPPORTED_VERSIONS = ("30", "31", "32", "33", "40", "41", "42", "43", "44", "45", "46")
|
||||
SUPPORTED_BACKENDS = ("DirectGLES", "DirectVulkan")
|
||||
ANGLE_REQUIRED_DLLS = ("libEGL.dll", "libGLESv2.dll", "d3dcompiler_47.dll")
|
||||
ANGLE_OPTIONAL_DLLS = ("dxcompiler.dll", "dxil.dll")
|
||||
PE_MACHINE_AMD64 = 0x8664
|
||||
TRACKED_ENVIRONMENT_NAMES = frozenset({"PATH"})
|
||||
TRACKED_ENVIRONMENT_PREFIXES = (
|
||||
"MOBILEGL_",
|
||||
"LIBGL_",
|
||||
"VK_",
|
||||
"ANGLE_",
|
||||
"EGL_",
|
||||
"D3D_",
|
||||
"DXVK_",
|
||||
)
|
||||
CONTROLLED_ENVIRONMENT_NAMES = frozenset(
|
||||
{"MOBILEGL_BACKEND_TYPE", "MOBILEGL_LOG_FILE_PATH"}
|
||||
)
|
||||
|
||||
DEFAULT_DEQP_ARGS = (
|
||||
"--deqp-gl-context-type=wgl",
|
||||
"--deqp-surface-type=fbo",
|
||||
"--deqp-gl-config-name=rgba8888d24s8",
|
||||
"--deqp-surface-width=64",
|
||||
"--deqp-surface-height=-1",
|
||||
"--deqp-base-seed=3",
|
||||
"--deqp-visibility=hidden",
|
||||
"--deqp-watchdog=enable",
|
||||
"--deqp-crashhandler=enable",
|
||||
)
|
||||
|
||||
SESSION_VENDOR = re.compile(r'^#sessionInfo vendor "([^"]*)"', re.MULTILINE)
|
||||
SESSION_RENDERER = re.compile(r'^#sessionInfo renderer "([^"]*)"', re.MULTILINE)
|
||||
SESSION_COMMAND_LINE = re.compile(r'^#sessionInfo commandLineParameters "([^"]*)"', re.MULTILINE)
|
||||
TARGET_GL_VERSION = re.compile(r"Target OpenGL Version:\s*(\d+)\.(\d+)")
|
||||
|
||||
|
||||
class PipelineError(RuntimeError):
|
||||
"""A configuration, build, or identity error."""
|
||||
|
||||
|
||||
def repository_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def normalize_version(value: str) -> str:
|
||||
normalized = value.strip().lower().removeprefix("gl").replace(".", "")
|
||||
if normalized not in SUPPORTED_VERSIONS:
|
||||
supported = ", ".join(f"gl{version}" for version in SUPPORTED_VERSIONS)
|
||||
raise argparse.ArgumentTypeError(f"unsupported GL suite {value!r}; choose one of: {supported}")
|
||||
return normalized
|
||||
|
||||
|
||||
def gl_version_tuple(version: str) -> tuple[int, int]:
|
||||
return int(version[0]), int(version[1])
|
||||
|
||||
|
||||
def parse_assignment(value: str) -> tuple[str, str]:
|
||||
name, separator, setting = value.partition("=")
|
||||
if not separator or not name or "\x00" in value:
|
||||
raise argparse.ArgumentTypeError(f"expected NAME=VALUE, got {value!r}")
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
||||
raise argparse.ArgumentTypeError(f"invalid environment variable name {name!r}")
|
||||
return name, setting
|
||||
|
||||
|
||||
def canonicalize_windows_environment(
|
||||
items: Iterable[tuple[str, str]],
|
||||
) -> dict[str, str]:
|
||||
"""Canonicalize environment keys using Windows' case-insensitive rules."""
|
||||
|
||||
result: dict[str, str] = {}
|
||||
for name, value in items:
|
||||
result[name.upper()] = value
|
||||
return result
|
||||
|
||||
|
||||
def tracked_run_environment(
|
||||
inherited: Mapping[str, str], overrides: Mapping[str, str]
|
||||
) -> tuple[dict[str, str], dict[str, str]]:
|
||||
"""Return tracked ambient values and the effective values used for identity."""
|
||||
|
||||
canonical_inherited = canonicalize_windows_environment(inherited.items())
|
||||
ambient = {
|
||||
name: value
|
||||
for name, value in canonical_inherited.items()
|
||||
if name in TRACKED_ENVIRONMENT_NAMES
|
||||
or name.startswith(TRACKED_ENVIRONMENT_PREFIXES)
|
||||
}
|
||||
effective = dict(ambient)
|
||||
effective.update(canonicalize_windows_environment(overrides.items()))
|
||||
return dict(sorted(ambient.items())), dict(sorted(effective.items()))
|
||||
|
||||
|
||||
def command_text(command: Sequence[object]) -> str:
|
||||
return subprocess.list2cmdline([str(part) for part in command])
|
||||
|
||||
|
||||
def run_command(
|
||||
command: Sequence[object],
|
||||
*,
|
||||
cwd: Optional[Path] = None,
|
||||
env: Optional[Mapping[str, str]] = None,
|
||||
check: bool = True,
|
||||
capture: bool = False,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
rendered = command_text(command)
|
||||
location = f" (cwd={cwd})" if cwd else ""
|
||||
print(f"[wgl_glcts_pipeline] $ {rendered}{location}", flush=True)
|
||||
completed = subprocess.run(
|
||||
[str(part) for part in command],
|
||||
cwd=str(cwd) if cwd else None,
|
||||
env=dict(env) if env else None,
|
||||
text=True,
|
||||
capture_output=capture,
|
||||
check=False,
|
||||
)
|
||||
if check and completed.returncode != 0:
|
||||
detail = ""
|
||||
if capture:
|
||||
detail = f"\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
|
||||
raise PipelineError(f"command failed with exit code {completed.returncode}: {rendered}{detail}")
|
||||
return completed
|
||||
|
||||
|
||||
def require_file(path: Path, label: str) -> Path:
|
||||
resolved = path.expanduser().resolve()
|
||||
if not resolved.is_file():
|
||||
raise PipelineError(f"{label} does not exist or is not a file: {resolved}")
|
||||
return resolved
|
||||
|
||||
|
||||
def require_directory(path: Path, label: str) -> Path:
|
||||
resolved = path.expanduser().resolve()
|
||||
if not resolved.is_dir():
|
||||
raise PipelineError(f"{label} does not exist or is not a directory: {resolved}")
|
||||
return resolved
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def sha256_directory(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
files = sorted(
|
||||
(candidate for candidate in path.rglob("*") if candidate.is_file()),
|
||||
key=lambda candidate: candidate.relative_to(path).as_posix(),
|
||||
)
|
||||
if not files:
|
||||
raise PipelineError(f"directory contains no files to fingerprint: {path}")
|
||||
for candidate in files:
|
||||
relative = candidate.relative_to(path).as_posix()
|
||||
digest.update(relative.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(sha256_file(candidate).encode("ascii"))
|
||||
digest.update(b"\n")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def pe_machine(path: Path) -> int:
|
||||
with path.open("rb") as stream:
|
||||
if stream.read(2) != b"MZ":
|
||||
raise PipelineError(f"not a PE executable: {path}")
|
||||
stream.seek(0x3C)
|
||||
offset_bytes = stream.read(4)
|
||||
if len(offset_bytes) != 4:
|
||||
raise PipelineError(f"truncated PE header: {path}")
|
||||
pe_offset = struct.unpack("<I", offset_bytes)[0]
|
||||
stream.seek(pe_offset)
|
||||
if stream.read(4) != b"PE\0\0":
|
||||
raise PipelineError(f"invalid PE signature: {path}")
|
||||
machine_bytes = stream.read(2)
|
||||
if len(machine_bytes) != 2:
|
||||
raise PipelineError(f"truncated PE COFF header: {path}")
|
||||
return struct.unpack("<H", machine_bytes)[0]
|
||||
|
||||
|
||||
def require_x64_pe(path: Path, label: str) -> None:
|
||||
machine = pe_machine(path)
|
||||
if machine != PE_MACHINE_AMD64:
|
||||
raise PipelineError(f"{label} must be an x64 PE (machine 0x8664), got 0x{machine:04x}: {path}")
|
||||
|
||||
|
||||
def git_snapshot(path: Path) -> dict[str, object]:
|
||||
snapshot: dict[str, object] = {"path": str(path)}
|
||||
try:
|
||||
head = run_command(
|
||||
["git", "-C", path, "rev-parse", "HEAD"], check=True, capture=True
|
||||
).stdout.strip()
|
||||
status = run_command(
|
||||
["git", "-C", path, "status", "--porcelain"], check=True, capture=True
|
||||
).stdout
|
||||
snapshot.update({"head": head, "dirty": bool(status.strip())})
|
||||
except (OSError, PipelineError):
|
||||
snapshot.update({"head": None, "dirty": None})
|
||||
return snapshot
|
||||
|
||||
|
||||
def generator_arguments(generator: str, architecture: str) -> list[str]:
|
||||
arguments = ["-G", generator]
|
||||
if generator.lower().startswith("visual studio"):
|
||||
arguments.extend(["-A", architecture])
|
||||
return arguments
|
||||
|
||||
|
||||
def mobilegl_configure_command(
|
||||
repo_root: Path,
|
||||
build_dir: Path,
|
||||
generator: str,
|
||||
architecture: str,
|
||||
extra: Iterable[str],
|
||||
) -> list[str]:
|
||||
return [
|
||||
"cmake",
|
||||
"-S",
|
||||
str(repo_root),
|
||||
"-B",
|
||||
str(build_dir),
|
||||
*generator_arguments(generator, architecture),
|
||||
"-DMOBILEGL_BUILD_TEST=OFF",
|
||||
"-DMOBILEGL_BUILD_BENCHMARK=OFF",
|
||||
"-DMOBILEGL_BUILD_TRACE_REPLAY=OFF",
|
||||
"-DMOBILEGL_ENABLE_TRACY=OFF",
|
||||
"-DMOBILEGL_FORCE_RELEASE_OPT=ON",
|
||||
*extra,
|
||||
]
|
||||
|
||||
|
||||
def cts_configure_command(
|
||||
cts_source: Path,
|
||||
build_dir: Path,
|
||||
generator: str,
|
||||
architecture: str,
|
||||
extra: Iterable[str],
|
||||
) -> list[str]:
|
||||
return [
|
||||
"cmake",
|
||||
"-S",
|
||||
str(cts_source),
|
||||
"-B",
|
||||
str(build_dir),
|
||||
*generator_arguments(generator, architecture),
|
||||
"-DDEQP_TARGET=default",
|
||||
"-DDEQP_SUPPORT_DRM=OFF",
|
||||
*extra,
|
||||
]
|
||||
|
||||
|
||||
def build_command(build_dir: Path, configuration: str, target: str, jobs: int) -> list[str]:
|
||||
command = ["cmake", "--build", str(build_dir), "--config", configuration, "--target", target]
|
||||
if jobs > 0:
|
||||
command.extend(["--parallel", str(jobs)])
|
||||
return command
|
||||
|
||||
|
||||
def verify_mobilegl_sources(repo_root: Path) -> None:
|
||||
required = (
|
||||
repo_root / "CMakeLists.txt",
|
||||
repo_root / "MobileGL" / "MG_Impl" / "WGLImpl" / "WGLImpl.cpp",
|
||||
repo_root / "3rdparty" / "glslang" / "CMakeLists.txt",
|
||||
repo_root / "3rdparty" / "SPIRV-Cross" / "CMakeLists.txt",
|
||||
repo_root / "3rdparty" / "Vulkan-Headers" / "CMakeLists.txt",
|
||||
)
|
||||
missing = [str(path) for path in required if not path.is_file()]
|
||||
if missing:
|
||||
raise PipelineError(
|
||||
"MobileGL source/submodules are incomplete:\n "
|
||||
+ "\n ".join(missing)
|
||||
+ f"\nRun: git -C {repo_root} submodule update --init --recursive"
|
||||
)
|
||||
|
||||
|
||||
def verify_cts_sources(cts_source: Path) -> None:
|
||||
required = (
|
||||
cts_source / "CMakeLists.txt",
|
||||
cts_source / "external" / "openglcts" / "CMakeLists.txt",
|
||||
)
|
||||
missing = [str(path) for path in required if not path.is_file()]
|
||||
if missing:
|
||||
raise PipelineError(
|
||||
"VK-GL-CTS source/external packages are incomplete:\n "
|
||||
+ "\n ".join(missing)
|
||||
+ f"\nRun: {sys.executable} {cts_source / 'external' / 'fetch_sources.py'}"
|
||||
)
|
||||
|
||||
|
||||
def discover_mobilegl_dll(build_dir: Path, configuration: str) -> Path:
|
||||
preferred = (
|
||||
build_dir / configuration / "opengl32.dll",
|
||||
build_dir / "MobileGL" / configuration / "opengl32.dll",
|
||||
build_dir / "opengl32.dll",
|
||||
)
|
||||
for candidate in preferred:
|
||||
if candidate.is_file():
|
||||
return candidate.resolve()
|
||||
candidates = sorted({path.resolve() for path in build_dir.rglob("opengl32.dll") if path.is_file()})
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
if not candidates:
|
||||
raise PipelineError(f"MobileGL build produced no opengl32.dll under {build_dir}")
|
||||
raise PipelineError("multiple opengl32.dll candidates; pass --mobilegl-dll explicitly:\n " + "\n ".join(map(str, candidates)))
|
||||
|
||||
|
||||
def discover_glcts_exe(build_dir: Path, configuration: str) -> Path:
|
||||
preferred = (
|
||||
build_dir / "external" / "openglcts" / "modules" / configuration / "glcts.exe",
|
||||
build_dir / "external" / "openglcts" / "modules" / "glcts.exe",
|
||||
)
|
||||
for candidate in preferred:
|
||||
if candidate.is_file():
|
||||
return candidate.resolve()
|
||||
candidates = sorted({path.resolve() for path in build_dir.rglob("glcts.exe") if path.is_file()})
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
if not candidates:
|
||||
raise PipelineError(f"CTS build produced no glcts.exe under {build_dir}")
|
||||
raise PipelineError("multiple glcts.exe candidates; pass --glcts-exe explicitly:\n " + "\n ".join(map(str, candidates)))
|
||||
|
||||
|
||||
def default_cts_modules_dir(cts_build_dir: Path) -> Path:
|
||||
return cts_build_dir / "external" / "openglcts" / "modules"
|
||||
|
||||
|
||||
def find_caselist_root(cts_modules_dir: Path, cts_source: Path) -> Path:
|
||||
relative = Path("gl_cts/data/mustpass/gl/khronos_mustpass/main")
|
||||
candidates = (cts_modules_dir / relative, cts_source / "external" / "openglcts" / "modules" / relative)
|
||||
for candidate in candidates:
|
||||
if candidate.is_dir():
|
||||
return candidate.resolve()
|
||||
raise PipelineError("Khronos GL mustpass directory was not found; checked:\n " + "\n ".join(map(str, candidates)))
|
||||
|
||||
|
||||
def caselist_for(caselist_root: Path, version: str) -> Path:
|
||||
return require_file(caselist_root / f"gl{version}-main.txt", f"GL{version} mustpass caselist")
|
||||
|
||||
|
||||
def runtime_source_files(
|
||||
glcts_exe: Path,
|
||||
mobilegl_dll: Path,
|
||||
backends: Sequence[str],
|
||||
angle_dir: Optional[Path],
|
||||
) -> dict[str, Path]:
|
||||
files = {"glcts.exe": glcts_exe, "opengl32.dll": mobilegl_dll}
|
||||
if "DirectGLES" in backends:
|
||||
if angle_dir is None:
|
||||
raise PipelineError("--angle-dir is required when DirectGLES is selected")
|
||||
angle_dir = require_directory(angle_dir, "ANGLE runtime directory")
|
||||
for name in ANGLE_REQUIRED_DLLS:
|
||||
files[name] = require_file(angle_dir / name, f"ANGLE {name}")
|
||||
for name in ANGLE_OPTIONAL_DLLS:
|
||||
candidate = angle_dir / name
|
||||
if candidate.is_file():
|
||||
files[name] = candidate.resolve()
|
||||
return files
|
||||
|
||||
|
||||
def runtime_fingerprint(files: Mapping[str, Path]) -> tuple[str, dict[str, str]]:
|
||||
hashes = {name: sha256_file(path) for name, path in sorted(files.items())}
|
||||
digest = hashlib.sha256()
|
||||
for name, file_hash in hashes.items():
|
||||
digest.update(f"{name}\0{file_hash}\n".encode("utf-8"))
|
||||
return digest.hexdigest(), hashes
|
||||
|
||||
|
||||
def run_fingerprint(
|
||||
runtime_hash: str,
|
||||
cts_data_hash: str,
|
||||
tool_hashes: Mapping[str, str],
|
||||
caselist_hashes: Mapping[str, str],
|
||||
deqp_args: Sequence[str],
|
||||
environment: Mapping[str, str],
|
||||
result_semantics: Optional[Mapping[str, object]] = None,
|
||||
) -> str:
|
||||
identity = {
|
||||
"version": 2,
|
||||
"runtime_fingerprint": runtime_hash,
|
||||
"cts_data_sha256": cts_data_hash,
|
||||
"tool_hashes": dict(sorted(tool_hashes.items())),
|
||||
"caselist_hashes": dict(sorted(caselist_hashes.items())),
|
||||
"deqp_args": list(deqp_args),
|
||||
"environment": dict(sorted(environment.items())),
|
||||
"result_semantics": dict(sorted((result_semantics or {}).items())),
|
||||
}
|
||||
return hashlib.sha256(
|
||||
json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def assemble_runtime(
|
||||
work_root: Path,
|
||||
sources: Mapping[str, Path],
|
||||
fingerprint: str,
|
||||
hashes: Mapping[str, str],
|
||||
) -> Path:
|
||||
runtime_dir = work_root / "runtime" / fingerprint[:16]
|
||||
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name, source in sources.items():
|
||||
require_x64_pe(source, name)
|
||||
destination = runtime_dir / name
|
||||
if source.resolve() != destination.resolve():
|
||||
shutil.copy2(source, destination)
|
||||
if sha256_file(destination) != hashes[name]:
|
||||
raise PipelineError(f"runtime copy hash mismatch: {destination}")
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"created_utc": utc_now(),
|
||||
"fingerprint": fingerprint,
|
||||
"files": {
|
||||
name: {"source": str(source), "sha256": hashes[name]}
|
||||
for name, source in sorted(sources.items())
|
||||
},
|
||||
}
|
||||
write_json(runtime_dir / "manifest.json", manifest)
|
||||
return runtime_dir
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def read_cases(caselist: Path) -> list[str]:
|
||||
cases: list[str] = []
|
||||
for raw_line in caselist.read_text(encoding="utf-8-sig").splitlines():
|
||||
line = raw_line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
cases.append(line)
|
||||
if not cases:
|
||||
raise PipelineError(f"caselist contains no cases: {caselist}")
|
||||
return cases
|
||||
|
||||
|
||||
def choose_preflight_case(caselist: Path) -> str:
|
||||
cases = read_cases(caselist)
|
||||
preferred_suffixes = (
|
||||
".buffer_objects.gen_buffers",
|
||||
".CommonBugs.CommonBug_GetProgramivActiveUniformBlockMaxNameLength",
|
||||
)
|
||||
for suffix in preferred_suffixes:
|
||||
for case in cases:
|
||||
if case.endswith(suffix):
|
||||
return case
|
||||
return cases[0]
|
||||
|
||||
|
||||
def runner_command(
|
||||
runner: Path,
|
||||
runtime_exe: Path,
|
||||
workdir: Path,
|
||||
caselist: Path,
|
||||
outdir: Path,
|
||||
backend: str,
|
||||
idle_timeout: float,
|
||||
max_round_seconds: float,
|
||||
max_rounds: int,
|
||||
environment: Mapping[str, str],
|
||||
deqp_args: Sequence[str],
|
||||
run_identity: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = [
|
||||
sys.executable,
|
||||
str(runner),
|
||||
"--exe",
|
||||
str(runtime_exe),
|
||||
"--workdir",
|
||||
str(workdir),
|
||||
"--caselist",
|
||||
str(caselist),
|
||||
"--outdir",
|
||||
str(outdir),
|
||||
"--backend",
|
||||
backend,
|
||||
"--idle-timeout",
|
||||
str(idle_timeout),
|
||||
"--max-round-seconds",
|
||||
str(max_round_seconds),
|
||||
"--max-rounds",
|
||||
str(max_rounds),
|
||||
]
|
||||
if run_identity is not None:
|
||||
command.extend(["--run-identity", run_identity])
|
||||
for name, value in sorted(environment.items()):
|
||||
command.extend(["--env", f"{name}={value}"])
|
||||
command.extend(f"--deqp-arg={argument}" for argument in deqp_args)
|
||||
return command
|
||||
|
||||
|
||||
def parse_preflight_identity(
|
||||
qpa_files: Sequence[Path],
|
||||
mobilegl_log: Path,
|
||||
backend: str,
|
||||
minimum_version: tuple[int, int],
|
||||
) -> dict[str, object]:
|
||||
if not qpa_files:
|
||||
raise PipelineError(f"{backend} preflight produced no QPA file")
|
||||
qpa_text = "\n".join(path.read_text(encoding="utf-8", errors="replace") for path in qpa_files)
|
||||
vendors = SESSION_VENDOR.findall(qpa_text)
|
||||
renderers = SESSION_RENDERER.findall(qpa_text)
|
||||
command_lines = SESSION_COMMAND_LINE.findall(qpa_text)
|
||||
if not vendors or "MobileGL" not in vendors[-1]:
|
||||
raise PipelineError(f"{backend} preflight did not load MobileGL (vendor={vendors[-1] if vendors else None!r})")
|
||||
expected_renderer = "Espryt" if backend == "DirectGLES" else "Magma"
|
||||
if not renderers or expected_renderer not in renderers[-1]:
|
||||
raise PipelineError(
|
||||
f"{backend} preflight renderer mismatch: expected {expected_renderer!r}, "
|
||||
f"got {renderers[-1] if renderers else None!r}"
|
||||
)
|
||||
if not command_lines or "--deqp-gl-context-type=wgl" not in command_lines[-1]:
|
||||
raise PipelineError(f"{backend} preflight did not record a WGL context")
|
||||
if not mobilegl_log.is_file():
|
||||
raise PipelineError(f"{backend} preflight did not create MobileGL log: {mobilegl_log}")
|
||||
log_text = mobilegl_log.read_text(encoding="utf-8", errors="replace")
|
||||
versions = [(int(major), int(minor)) for major, minor in TARGET_GL_VERSION.findall(log_text)]
|
||||
if not versions:
|
||||
raise PipelineError(f"{backend} preflight log contains no target OpenGL version")
|
||||
actual_version = versions[-1]
|
||||
if actual_version < minimum_version:
|
||||
raise PipelineError(
|
||||
f"{backend} reports GL {actual_version[0]}.{actual_version[1]}, "
|
||||
f"but selected suites require at least {minimum_version[0]}.{minimum_version[1]}"
|
||||
)
|
||||
return {
|
||||
"backend": backend,
|
||||
"vendor": vendors[-1],
|
||||
"renderer": renderers[-1],
|
||||
"target_gl_version": f"{actual_version[0]}.{actual_version[1]}",
|
||||
"qpa_files": [str(path) for path in qpa_files],
|
||||
"mobilegl_log": str(mobilegl_log),
|
||||
}
|
||||
|
||||
|
||||
def run_preflight(
|
||||
*,
|
||||
runner: Path,
|
||||
runtime_exe: Path,
|
||||
cts_modules_dir: Path,
|
||||
caselist: Path,
|
||||
preflight_root: Path,
|
||||
backend: str,
|
||||
idle_timeout: float,
|
||||
environment: Mapping[str, str],
|
||||
deqp_args: Sequence[str],
|
||||
minimum_version: tuple[int, int],
|
||||
run_identity: str,
|
||||
) -> dict[str, object]:
|
||||
outdir = preflight_root / backend.lower()
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
case_file = outdir / "case.txt"
|
||||
case_file.write_text(choose_preflight_case(caselist) + "\n", encoding="utf-8")
|
||||
log_path = outdir / "mobilegl.log"
|
||||
child_environment = dict(environment)
|
||||
child_environment["MOBILEGL_LOG_FILE_PATH"] = str(log_path)
|
||||
command = runner_command(
|
||||
runner,
|
||||
runtime_exe,
|
||||
cts_modules_dir,
|
||||
case_file,
|
||||
outdir,
|
||||
backend,
|
||||
min(idle_timeout, 120.0) if idle_timeout > 0 else 120.0,
|
||||
180.0,
|
||||
1,
|
||||
child_environment,
|
||||
deqp_args,
|
||||
run_identity,
|
||||
)
|
||||
# A developing driver may fail the chosen case or terminate during deinit.
|
||||
# Identity is the gate: the QPA and MobileGL log must prove which WGL driver ran.
|
||||
run_command(command, cwd=repository_root(), check=False)
|
||||
identity = parse_preflight_identity(sorted(outdir.glob("chunk*.qpa")), log_path, backend, minimum_version)
|
||||
print(
|
||||
f"[wgl_glcts_pipeline] preflight {backend}: {identity['renderer']} | "
|
||||
f"GL {identity['target_gl_version']}"
|
||||
)
|
||||
return identity
|
||||
|
||||
|
||||
def report_command(
|
||||
reporter: Path,
|
||||
suites: Sequence[tuple[str, str, Path, Path]],
|
||||
markdown: Path,
|
||||
json_out: Path,
|
||||
allow_incomplete: bool,
|
||||
expected_run_identity: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = [sys.executable, str(reporter)]
|
||||
for backend, label, caselist, results in suites:
|
||||
command.extend(["--suite", backend, label, str(caselist), str(results)])
|
||||
command.extend(["--markdown", str(markdown), "--json", str(json_out)])
|
||||
if expected_run_identity is not None:
|
||||
command.extend(["--expected-run-identity", expected_run_identity])
|
||||
if allow_incomplete:
|
||||
command.append("--allow-incomplete")
|
||||
return command
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build MobileGL WGL, assemble a local glcts runtime, run GL30-GL46, and report results."
|
||||
)
|
||||
parser.add_argument("--repo-root", type=Path, default=repository_root(), help="MobileGL worktree root")
|
||||
parser.add_argument("--cts-source", type=Path, required=True, help="VK-GL-CTS source checkout")
|
||||
parser.add_argument("--work-root", type=Path, required=True, help="build/result root (kept outside source)")
|
||||
parser.add_argument("--angle-dir", type=Path, help="x64 ANGLE directory for DirectGLES")
|
||||
parser.add_argument("--backends", nargs="+", choices=SUPPORTED_BACKENDS, default=list(SUPPORTED_BACKENDS))
|
||||
parser.add_argument("--versions", nargs="+", type=normalize_version, default=list(SUPPORTED_VERSIONS))
|
||||
parser.add_argument("--configuration", default="Release")
|
||||
parser.add_argument("--generator", default="Visual Studio 17 2022")
|
||||
parser.add_argument("--architecture", default="x64")
|
||||
parser.add_argument("--jobs", type=int, default=max(1, os.cpu_count() or 1))
|
||||
parser.add_argument("--mobilegl-build-dir", type=Path)
|
||||
parser.add_argument("--cts-build-dir", type=Path)
|
||||
parser.add_argument("--mobilegl-dll", type=Path, help="reuse an existing MobileGL/opengl32 DLL")
|
||||
parser.add_argument("--glcts-exe", type=Path, help="reuse an existing glcts.exe")
|
||||
parser.add_argument("--cts-modules-dir", type=Path, help="glcts working directory containing gl_cts data")
|
||||
parser.add_argument("--skip-mobilegl-build", action="store_true")
|
||||
parser.add_argument("--skip-cts-build", action="store_true")
|
||||
parser.add_argument("--skip-preflight", action="store_true")
|
||||
parser.add_argument("--skip-run", action="store_true")
|
||||
parser.add_argument("--skip-report", action="store_true")
|
||||
parser.add_argument("--allow-incomplete-report", action="store_true")
|
||||
parser.add_argument("--continue-on-suite-error", action="store_true")
|
||||
parser.add_argument("--idle-timeout", type=float, default=300.0)
|
||||
parser.add_argument("--max-round-seconds", type=float, default=0.0)
|
||||
parser.add_argument("--max-rounds", type=int, default=10000)
|
||||
parser.add_argument("--env", action="append", type=parse_assignment, default=[], metavar="NAME=VALUE")
|
||||
parser.add_argument(
|
||||
"--deqp-arg", action="append", default=[], metavar="ARG", help="extra glcts option; use --deqp-arg=--x=y"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mobilegl-cmake-arg", action="append", default=[], metavar="ARG", help="extra MobileGL configure option"
|
||||
)
|
||||
parser.add_argument("--cts-cmake-arg", action="append", default=[], metavar="ARG", help="extra CTS configure option")
|
||||
return parser
|
||||
|
||||
|
||||
def execute(args: argparse.Namespace) -> int:
|
||||
if os.name != "nt":
|
||||
raise PipelineError("this pipeline builds and exercises the Windows WGL target and must run on Windows")
|
||||
if shutil.which("cmake") is None and (not args.skip_mobilegl_build or not args.skip_cts_build):
|
||||
raise PipelineError("cmake was not found on PATH")
|
||||
if args.jobs < 0 or args.max_rounds <= 0:
|
||||
raise PipelineError("--jobs must be >= 0 and --max-rounds must be > 0")
|
||||
|
||||
repo_root = require_directory(args.repo_root, "MobileGL worktree")
|
||||
cts_source = require_directory(args.cts_source, "VK-GL-CTS checkout")
|
||||
work_root = args.work_root.expanduser().resolve()
|
||||
work_root.mkdir(parents=True, exist_ok=True)
|
||||
versions = list(dict.fromkeys(args.versions))
|
||||
backends = list(dict.fromkeys(args.backends))
|
||||
minimum_version = max(gl_version_tuple(version) for version in versions)
|
||||
extra_environment = canonicalize_windows_environment(args.env)
|
||||
reserved_environment = CONTROLLED_ENVIRONMENT_NAMES & set(extra_environment)
|
||||
if reserved_environment:
|
||||
raise PipelineError("the pipeline controls these environment variables: " + ", ".join(sorted(reserved_environment)))
|
||||
|
||||
configuration = args.configuration
|
||||
mobilegl_build_dir = (args.mobilegl_build_dir or work_root / f"mobilegl-build-{configuration.lower()}").resolve()
|
||||
cts_build_dir = (args.cts_build_dir or work_root / f"cts-build-wgl-{configuration.lower()}").resolve()
|
||||
|
||||
if not args.skip_mobilegl_build:
|
||||
verify_mobilegl_sources(repo_root)
|
||||
mobilegl_build_dir.mkdir(parents=True, exist_ok=True)
|
||||
run_command(
|
||||
mobilegl_configure_command(
|
||||
repo_root, mobilegl_build_dir, args.generator, args.architecture, args.mobilegl_cmake_arg
|
||||
),
|
||||
cwd=repo_root,
|
||||
)
|
||||
run_command(build_command(mobilegl_build_dir, configuration, "MobileGL", args.jobs), cwd=repo_root)
|
||||
mobilegl_dll = (
|
||||
require_file(args.mobilegl_dll, "MobileGL DLL")
|
||||
if args.mobilegl_dll
|
||||
else discover_mobilegl_dll(mobilegl_build_dir, configuration)
|
||||
)
|
||||
|
||||
if not args.skip_cts_build:
|
||||
verify_cts_sources(cts_source)
|
||||
cts_build_dir.mkdir(parents=True, exist_ok=True)
|
||||
run_command(
|
||||
cts_configure_command(cts_source, cts_build_dir, args.generator, args.architecture, args.cts_cmake_arg),
|
||||
cwd=cts_source,
|
||||
)
|
||||
run_command(build_command(cts_build_dir, configuration, "glcts", args.jobs), cwd=cts_source)
|
||||
glcts_exe = (
|
||||
require_file(args.glcts_exe, "glcts executable")
|
||||
if args.glcts_exe
|
||||
else discover_glcts_exe(cts_build_dir, configuration)
|
||||
)
|
||||
cts_modules_dir = require_directory(
|
||||
args.cts_modules_dir or default_cts_modules_dir(cts_build_dir), "CTS modules/working directory"
|
||||
)
|
||||
cts_data_dir = require_directory(cts_modules_dir / "gl_cts" / "data", "CTS gl_cts data directory")
|
||||
cts_data_hash = sha256_directory(cts_data_dir)
|
||||
caselist_root = find_caselist_root(cts_modules_dir, cts_source)
|
||||
caselists = {version: caselist_for(caselist_root, version) for version in versions}
|
||||
|
||||
runtime_sources = runtime_source_files(glcts_exe, mobilegl_dll, backends, args.angle_dir)
|
||||
fingerprint, hashes = runtime_fingerprint(runtime_sources)
|
||||
runtime_dir = assemble_runtime(work_root, runtime_sources, fingerprint, hashes)
|
||||
runtime_exe = runtime_dir / "glcts.exe"
|
||||
runner = require_file(repo_root / "tools" / "cts" / "scripts" / "run_cts_windows.py", "Windows CTS runner")
|
||||
reporter = require_file(repo_root / "tools" / "cts" / "scripts" / "cts_multi_report.py", "CTS reporter")
|
||||
matrix_reporter = require_file(
|
||||
repo_root / "tools" / "cts" / "scripts" / "cts_matrix_report.py", "CTS matrix reporter"
|
||||
)
|
||||
qpa_reporter = require_file(repo_root / "tools" / "cts" / "scripts" / "qpa_report.py", "QPA parser")
|
||||
tool_hashes = {
|
||||
"wgl_glcts_pipeline.py": sha256_file(Path(__file__).resolve()),
|
||||
"run_cts_windows.py": sha256_file(runner),
|
||||
"cts_multi_report.py": sha256_file(reporter),
|
||||
"cts_matrix_report.py": sha256_file(matrix_reporter),
|
||||
"qpa_report.py": sha256_file(qpa_reporter),
|
||||
}
|
||||
deqp_args = [*DEFAULT_DEQP_ARGS, *args.deqp_arg]
|
||||
caselist_hashes = {version: sha256_file(path) for version, path in caselists.items()}
|
||||
ambient_environment, identity_environment = tracked_run_environment(
|
||||
os.environ, extra_environment
|
||||
)
|
||||
result_semantics = {
|
||||
"idle_timeout_seconds": args.idle_timeout,
|
||||
"max_round_seconds": args.max_round_seconds,
|
||||
}
|
||||
execution_settings = {
|
||||
**result_semantics,
|
||||
"max_rounds": args.max_rounds,
|
||||
"continue_on_suite_error": args.continue_on_suite_error,
|
||||
}
|
||||
execution_fingerprint = run_fingerprint(
|
||||
fingerprint,
|
||||
cts_data_hash,
|
||||
tool_hashes,
|
||||
caselist_hashes,
|
||||
deqp_args,
|
||||
identity_environment,
|
||||
result_semantics,
|
||||
)
|
||||
run_root = work_root / "runs" / execution_fingerprint[:16]
|
||||
report_root = run_root / "reports"
|
||||
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"created_utc": utc_now(),
|
||||
"run_fingerprint": execution_fingerprint,
|
||||
"runtime_fingerprint": fingerprint,
|
||||
"runtime_hashes": hashes,
|
||||
"tool_hashes": tool_hashes,
|
||||
"cts_data": {"path": str(cts_data_dir), "sha256": cts_data_hash},
|
||||
"runtime_dir": str(runtime_dir),
|
||||
"mobilegl": git_snapshot(repo_root),
|
||||
"vk_gl_cts": git_snapshot(cts_source),
|
||||
"configuration": configuration,
|
||||
"generator": args.generator,
|
||||
"architecture": args.architecture,
|
||||
"backends": backends,
|
||||
"versions": versions,
|
||||
"caselists": {version: {"path": str(path), "sha256": caselist_hashes[version]} for version, path in caselists.items()},
|
||||
"deqp_args": deqp_args,
|
||||
"environment_overrides": extra_environment,
|
||||
"ambient_environment": ambient_environment,
|
||||
"identity_environment": identity_environment,
|
||||
"result_semantics": result_semantics,
|
||||
"execution_settings": execution_settings,
|
||||
}
|
||||
write_json(run_root / "manifest.json", manifest)
|
||||
print(f"[wgl_glcts_pipeline] runtime fingerprint: {fingerprint}")
|
||||
print(f"[wgl_glcts_pipeline] run fingerprint: {execution_fingerprint}")
|
||||
print(f"[wgl_glcts_pipeline] run root: {run_root}")
|
||||
|
||||
identities: list[dict[str, object]] = []
|
||||
if not args.skip_preflight:
|
||||
first_caselist = caselists[versions[0]]
|
||||
preflight_root = run_root / "preflight"
|
||||
for backend in backends:
|
||||
identities.append(
|
||||
run_preflight(
|
||||
runner=runner,
|
||||
runtime_exe=runtime_exe,
|
||||
cts_modules_dir=cts_modules_dir,
|
||||
caselist=first_caselist,
|
||||
preflight_root=preflight_root,
|
||||
backend=backend,
|
||||
idle_timeout=args.idle_timeout,
|
||||
environment=extra_environment,
|
||||
deqp_args=deqp_args,
|
||||
minimum_version=minimum_version,
|
||||
run_identity=execution_fingerprint,
|
||||
)
|
||||
)
|
||||
manifest["preflight"] = identities
|
||||
write_json(run_root / "manifest.json", manifest)
|
||||
|
||||
suites: list[tuple[str, str, Path, Path]] = []
|
||||
suite_errors: list[dict[str, object]] = []
|
||||
for backend in backends:
|
||||
for version in versions:
|
||||
label = f"gl{version}"
|
||||
result_dir = run_root / "results" / backend.lower() / label
|
||||
suites.append((backend, label, caselists[version], result_dir))
|
||||
if args.skip_run:
|
||||
continue
|
||||
result_dir.mkdir(parents=True, exist_ok=True)
|
||||
child_environment = dict(extra_environment)
|
||||
child_environment["MOBILEGL_LOG_FILE_PATH"] = str(result_dir / "mobilegl.log")
|
||||
command = runner_command(
|
||||
runner,
|
||||
runtime_exe,
|
||||
cts_modules_dir,
|
||||
caselists[version],
|
||||
result_dir,
|
||||
backend,
|
||||
args.idle_timeout,
|
||||
args.max_round_seconds,
|
||||
args.max_rounds,
|
||||
child_environment,
|
||||
deqp_args,
|
||||
execution_fingerprint,
|
||||
)
|
||||
completed = run_command(command, cwd=repo_root, check=False)
|
||||
if completed.returncode != 0:
|
||||
suite_errors.append({"backend": backend, "suite": label, "returncode": completed.returncode})
|
||||
if not args.continue_on_suite_error:
|
||||
break
|
||||
if suite_errors and not args.continue_on_suite_error:
|
||||
break
|
||||
|
||||
report_returncode: Optional[int] = None
|
||||
if not args.skip_report:
|
||||
report_root.mkdir(parents=True, exist_ok=True)
|
||||
completed = run_command(
|
||||
report_command(
|
||||
reporter,
|
||||
suites,
|
||||
report_root / "gl-cts-summary.md",
|
||||
report_root / "gl-cts-summary.json",
|
||||
args.allow_incomplete_report or bool(suite_errors),
|
||||
execution_fingerprint,
|
||||
),
|
||||
cwd=repo_root,
|
||||
check=False,
|
||||
)
|
||||
report_returncode = completed.returncode
|
||||
|
||||
manifest["suite_errors"] = suite_errors
|
||||
manifest["report_returncode"] = report_returncode
|
||||
manifest["finished_utc"] = utc_now()
|
||||
write_json(run_root / "manifest.json", manifest)
|
||||
if suite_errors:
|
||||
print(f"[wgl_glcts_pipeline] {len(suite_errors)} suite runner(s) incomplete; see manifest/report", file=sys.stderr)
|
||||
returncodes = {int(item["returncode"]) for item in suite_errors}
|
||||
if 130 in returncodes:
|
||||
return 130
|
||||
if 2 in returncodes:
|
||||
return 2
|
||||
if 3 in returncodes:
|
||||
return 3
|
||||
return 4
|
||||
if report_returncode is not None and report_returncode != 0:
|
||||
print(f"[wgl_glcts_pipeline] report validation failed with exit code {report_returncode}", file=sys.stderr)
|
||||
return report_returncode
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
return execute(args)
|
||||
except PipelineError as exc:
|
||||
print(f"[wgl_glcts_pipeline] ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
except OSError as exc:
|
||||
print(f"[wgl_glcts_pipeline] ERROR: filesystem/process operation failed: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -16,3 +16,4 @@ Each skill is a self-contained package, matching the layout used by
|
||||
| Skill | What it does |
|
||||
| --- | --- |
|
||||
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
|
||||
| [wgl-gl-cts-on-mobilegl](wgl-gl-cts-on-mobilegl/SKILL.md) | Build MobileGL's Windows x64 WGL drop-in, run GL30-GL46 core CTS against DirectGLES and DirectVulkan, resume safely, and emit validated reports. |
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: wgl-gl-cts-on-mobilegl
|
||||
description: Build MobileGL as a Windows x64 WGL drop-in opengl32.dll, build or reuse VK-GL-CTS glcts, run Khronos GL30 through GL46 core suites against DirectGLES and DirectVulkan, resume after crashes or idle timeouts, and produce validated Markdown/JSON conformance reports. Use when Codex needs to compile MobileGL's WGL target, connect it to desktop OpenGL CTS on Windows, rerun selected GL core mustpass lists, or verify that CTS loaded MobileGL instead of the system OpenGL driver.
|
||||
---
|
||||
|
||||
# WGL OpenGL CTS on MobileGL
|
||||
|
||||
Use `tools/cts/scripts/wgl_glcts_pipeline.py` as the single entry point. It
|
||||
configures both Visual Studio builds, assembles a private runtime, verifies the
|
||||
loaded WGL implementation, calls the crash-resuming runner, and generates the
|
||||
multi-suite report.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Run on Windows x64 with Git, Python 3.9 or newer, CMake, Visual Studio 2022's
|
||||
Desktop C++ workload, and a Vulkan SDK visible to MobileGL's CMake configure.
|
||||
- Use the already selected MobileGL worktree. Inspect `git status` first and do
|
||||
not create another worktree unless the user explicitly asks.
|
||||
- Initialize MobileGL submodules:
|
||||
|
||||
```powershell
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
- Prepare a VK-GL-CTS checkout at a citable release tag, then fetch its pinned
|
||||
externals:
|
||||
|
||||
```powershell
|
||||
git -C D:\VK-GL-CTS checkout opengl-cts-4.6.8.1
|
||||
python D:\VK-GL-CTS\external\fetch_sources.py
|
||||
```
|
||||
|
||||
- For DirectGLES, provide one x64 ANGLE directory containing matching
|
||||
`libEGL.dll`, `libGLESv2.dll`, and `d3dcompiler_47.dll`. DirectVulkan does not
|
||||
need ANGLE.
|
||||
- For DirectVulkan, provide a working Vulkan loader plus a GPU-vendor ICD and
|
||||
driver. The Vulkan SDK alone does not provide a usable GPU device.
|
||||
|
||||
## Run the full matrix
|
||||
|
||||
From the MobileGL worktree root:
|
||||
|
||||
```powershell
|
||||
python tools\cts\scripts\wgl_glcts_pipeline.py `
|
||||
--cts-source D:\VK-GL-CTS `
|
||||
--work-root D:\MobileGL-WGL-CTS `
|
||||
--angle-dir C:\path\to\angle-x64 `
|
||||
--backends DirectGLES DirectVulkan `
|
||||
--versions gl30 gl31 gl32 gl33 gl40 gl41 gl42 gl43 gl44 gl45 gl46
|
||||
```
|
||||
|
||||
The defaults deliberately reproduce the proven Windows setup:
|
||||
|
||||
- Visual Studio 17 2022, x64, Release;
|
||||
- VK-GL-CTS `DEQP_TARGET=default`, which selects desktop WGL on Windows;
|
||||
- MobileGL copied beside `glcts.exe` as `opengl32.dll`;
|
||||
- WGL context, FBO surface, `rgba8888d24s8`, hidden window, watchdog and crash
|
||||
handler enabled;
|
||||
- `--deqp-terminate-on-device-lost=disable` supplied by the underlying runner.
|
||||
|
||||
Do not replace the FBO surface with the default framebuffer when comparing
|
||||
backends; it changes the readback path and invalidates comparison with the
|
||||
established runs.
|
||||
|
||||
## Mandatory preflight
|
||||
|
||||
Leave preflight enabled for a new binary. It must prove all of the following
|
||||
before the full suite starts:
|
||||
|
||||
- QPA vendor contains `MobileGL`;
|
||||
- DirectGLES renderer contains `Espryt`, or DirectVulkan contains `Magma`;
|
||||
- QPA command line records `--deqp-gl-context-type=wgl`;
|
||||
- MobileGL's log reports a GL version at least as high as the highest selected
|
||||
suite.
|
||||
|
||||
Treat any preflight failure as a hard stop. It commonly means `glcts.exe` loaded
|
||||
the system `opengl32.dll`, ANGLE DLLs have the wrong architecture, or the driver
|
||||
still reports too low a GL version.
|
||||
|
||||
## Common variants
|
||||
|
||||
Run DirectVulkan only, without ANGLE:
|
||||
|
||||
```powershell
|
||||
python tools\cts\scripts\wgl_glcts_pipeline.py `
|
||||
--cts-source D:\VK-GL-CTS --work-root D:\MobileGL-WGL-CTS `
|
||||
--backends DirectVulkan --versions gl43 gl44 gl45 gl46
|
||||
```
|
||||
|
||||
Build and preflight without starting a multi-hour CTS run:
|
||||
|
||||
```powershell
|
||||
python tools\cts\scripts\wgl_glcts_pipeline.py `
|
||||
--cts-source D:\VK-GL-CTS --work-root D:\MobileGL-WGL-CTS `
|
||||
--angle-dir C:\path\to\angle-x64 `
|
||||
--skip-run --skip-report
|
||||
```
|
||||
|
||||
Reuse previously built binaries with `--skip-mobilegl-build`,
|
||||
`--skip-cts-build`, `--mobilegl-dll`, `--glcts-exe`, and
|
||||
`--cts-modules-dir`. Continue to use a modules directory containing the
|
||||
`gl_cts` data tree; the executable directory alone is insufficient.
|
||||
|
||||
Pass extra dEQP options with the equals form so argparse does not consume the
|
||||
leading dashes:
|
||||
|
||||
```powershell
|
||||
--deqp-arg=--deqp-log-images=enable
|
||||
```
|
||||
|
||||
## Resume and artifacts
|
||||
|
||||
Repeat the exact command and `--work-root` to resume. The runner recovers
|
||||
completed QPA cases plus `crashed.txt` and `hung.txt`, then schedules only
|
||||
unaccounted cases.
|
||||
|
||||
The pipeline prints full SHA-256 fingerprints and uses their first 16
|
||||
hexadecimal characters as directory names: `runtime/<runtime-prefix>` and
|
||||
`runs/<run-prefix>`. The run fingerprint covers the CTS data tree,
|
||||
runner/report tools, caselists, dEQP arguments, explicit `--env` values, tracked
|
||||
ambient GL/Vulkan environment, and the timeout settings that determine
|
||||
Crash/Hang classification. The manifest records those inputs plus
|
||||
`max_rounds`, suite-error continuation policy, source commits and dirty state,
|
||||
preflight identity, suite errors, and report status.
|
||||
|
||||
The runner refuses to attach a new `run_state.json` to old QPA or sidecar files
|
||||
by default. Invoke `run_cts_windows.py --adopt-legacy` directly only after
|
||||
verifying that those artifacts match the backend, caselist, binaries, and dEQP
|
||||
arguments. Pipeline reports require every suite state to match the current run
|
||||
fingerprint.
|
||||
|
||||
Read the final outputs at:
|
||||
|
||||
```text
|
||||
<work-root>/runs/<run-prefix>/reports/gl-cts-summary.md
|
||||
<work-root>/runs/<run-prefix>/reports/gl-cts-summary.json
|
||||
```
|
||||
|
||||
Use the exact run root printed by the pipeline.
|
||||
|
||||
Do not claim completeness when the report validation is incomplete or when the
|
||||
manifest records suite errors. Keep `NotSupported` separate from hard failures
|
||||
when prioritizing implementation work.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "WGL GL CTS on MobileGL"
|
||||
short_description: "Build MobileGL WGL and run GL30-GL46 CTS on Windows"
|
||||
default_prompt: "Use $wgl-gl-cts-on-mobilegl to build MobileGL's WGL DLL and run the selected OpenGL CTS suites on Windows."
|
||||
@@ -0,0 +1,29 @@
|
||||
#-------------------------------------------------------------------------
|
||||
# VK-GL-CTS target: MobileGL on desktop Linux
|
||||
#
|
||||
# Builds glcts as a normal host executable that reaches OpenGL exclusively
|
||||
# through libMobileGL.so, loaded at runtime via the eglw dynamic wrapper.
|
||||
# Nothing here links libEGL or libGL: a conformance result must be
|
||||
# unambiguously MobileGL's, never the system GL stack's.
|
||||
#
|
||||
# The platform port only offers pbuffer surfaces on desktop; DirectVulkan's
|
||||
# pbuffer path works because desktop Vulkan exposes VK_EXT_headless_surface.
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
message("*** Using MobileGL desktop target")
|
||||
|
||||
set(DEQP_TARGET_NAME "MobileGL")
|
||||
|
||||
# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support
|
||||
# flag is on but no import library is supplied.
|
||||
set(DEQP_SUPPORT_EGL ON)
|
||||
set(DEQP_EGL_LIBRARIES)
|
||||
set(DEQP_GLES2_LIBRARIES)
|
||||
set(DEQP_GLES3_LIBRARIES)
|
||||
|
||||
set(TCUTIL_PLATFORM_SRCS
|
||||
mobilegl/tcuMobileGLPlatform.cpp
|
||||
mobilegl/tcuMobileGLPlatform.hpp
|
||||
)
|
||||
|
||||
list(APPEND TCUTIL_PLATFORM_LIBS dl pthread)
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<waiver_list>
|
||||
|
||||
<!--
|
||||
Waivers for known false failures caused by this local test harness's
|
||||
fbo-surface-type mode (deqp-surface-type=fbo), not by MobileGL itself.
|
||||
-->
|
||||
|
||||
<waiver vendor="MobileGL-Dev*" url="local-only: fbo-surface-type wrapper-FBO artifact, not a MobileGL conformance bug">
|
||||
<description>
|
||||
dEQP's fbo surface-type mode wraps rendering in its own
|
||||
application level framebuffer object (a real, non-zero-named FBO,
|
||||
not framebuffer 0). ApiCoverageTestCase's ReadBuffer coverage
|
||||
sub-test captures GL_READ_BUFFER while that wrapper FBO is bound
|
||||
(a valid GL_COLOR_ATTACHMENTn value there), then later deletes an
|
||||
unrelated FBO of its own; per the GL spec, deleting a bound FBO
|
||||
implicitly rebinds framebuffer target 0, the TRUE default
|
||||
framebuffer this time, not the wrapper. Restoring the captured
|
||||
GL_COLOR_ATTACHMENTn value against the true default framebuffer
|
||||
correctly raises GL_INVALID_ENUM per spec (only FRONT/BACK style
|
||||
tokens are valid there), so the test fails with a fully
|
||||
spec conformant driver. This can only happen when the harness's
|
||||
"default framebuffer" is a real dEQP created FBO instead of a
|
||||
genuine window/pbuffer backed framebuffer 0, which is unique to
|
||||
this local fbo surface-type setup. Confirmed by tracing
|
||||
framebuffer bindings/external indices across the test's
|
||||
execution (2026-08-01).
|
||||
</description>
|
||||
<renderer_list>
|
||||
<r>Magma*</r>
|
||||
<r>Espryt*</r>
|
||||
</renderer_list>
|
||||
<t>KHR-GL30.api.coverage</t>
|
||||
<t>KHR-GL31.api.coverage</t>
|
||||
<t>KHR-GL32.api.coverage</t>
|
||||
<t>KHR-GL33.api.coverage</t>
|
||||
</waiver>
|
||||
|
||||
</waiver_list>
|
||||
Reference in New Issue
Block a user