mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c3cfc1d6 | ||
|
|
38e04eefae | ||
|
|
fd29cb914e | ||
|
|
38497174c8 | ||
|
|
b95fcb7bca | ||
|
|
5fce287de5 | ||
|
|
ae6949e459 | ||
|
|
5437947240 | ||
|
|
f38dbf018d | ||
|
|
2dcc15bb0e | ||
|
|
ff76af9df7 | ||
|
|
76f37a18e6 | ||
|
|
8d1a734c22 | ||
|
|
7d215028fb | ||
|
|
00534d8bbc | ||
|
|
d81a6a0998 | ||
|
|
9bf23d7ffd | ||
|
|
41e45f7d48 | ||
|
|
3dc6a1b6db | ||
|
|
598c5497b0 | ||
|
|
86c00bdf18 | ||
|
|
2f2f95498f | ||
|
|
7105c2ebdc | ||
|
|
13bab780f2 | ||
|
|
027310f993 | ||
|
|
c741a938bc | ||
|
|
01d0f01d13 | ||
|
|
e45f7ae5d4 | ||
|
|
a687873d32 | ||
|
|
43a43c1180 | ||
|
|
65dbfa6f26 | ||
|
|
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/app/src/trace/jniLibs
|
||||||
/android-plugin/local.properties
|
/android-plugin/local.properties
|
||||||
tools/trace_replay/work/
|
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 ProjectName = "MobileGL";
|
||||||
inline const String CoreName = "MobileGL Core";
|
inline const String CoreName = "MobileGL Core";
|
||||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
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 VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||||
inline const Uint64 CacheVersion = 0;
|
inline const Uint64 CacheVersion = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,31 @@ namespace MobileGL {
|
|||||||
// and leave the query readable later.
|
// and leave the query readable later.
|
||||||
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||||
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
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.
|
||||||
|
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
|
||||||
|
void (*PatchParameteri)(GLenum pname, GLint value);
|
||||||
|
void (*BeginTransformFeedback)(GLenum primitiveMode);
|
||||||
|
void (*EndTransformFeedback)();
|
||||||
|
// ARB_transform_feedback2. A backend that leaves these null keeps the single
|
||||||
|
// implicit capture span the frontend has always modelled; the frontend state
|
||||||
|
// (paused flag, per-object bindings) is tracked either way.
|
||||||
|
void (*PauseTransformFeedback)();
|
||||||
|
void (*ResumeTransformFeedback)();
|
||||||
|
void (*BindTransformFeedback)(GLuint name);
|
||||||
|
void (*DeleteTransformFeedback)(GLuint name);
|
||||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||||
};
|
};
|
||||||
struct GlobalBackendFunctionsTable {
|
struct GlobalBackendFunctionsTable {
|
||||||
@@ -272,6 +297,13 @@ namespace MobileGL {
|
|||||||
Int MaxIntegerSamples = 1;
|
Int MaxIntegerSamples = 1;
|
||||||
Int MaxSamples = 1;
|
Int MaxSamples = 1;
|
||||||
Int MaxSampleMaskWords = 1;
|
Int MaxSampleMaskWords = 1;
|
||||||
|
// Tessellation limits; defaults are the GL 4.0 core minimums.
|
||||||
|
Int MaxPatchVertices = 32;
|
||||||
|
Int MaxTessGenLevel = 64;
|
||||||
|
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
|
||||||
|
// minimums, which every ES 3.1 driver also guarantees.
|
||||||
|
Int MinProgramTextureGatherOffset = -8;
|
||||||
|
Int MaxProgramTextureGatherOffset = 7;
|
||||||
Int MaxTextureImageUnits = 32;
|
Int MaxTextureImageUnits = 32;
|
||||||
Int MaxVertexTextureImageUnits = 32;
|
Int MaxVertexTextureImageUnits = 32;
|
||||||
Int MaxComputeTextureImageUnits = 32;
|
Int MaxComputeTextureImageUnits = 32;
|
||||||
@@ -300,7 +332,21 @@ namespace MobileGL {
|
|||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
Float ViewportBoundsRangeMax = 0.0f;
|
Float ViewportBoundsRangeMax = 0.0f;
|
||||||
Int ViewportSubpixelBits = 0;
|
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;
|
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;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
Uint32 SubgroupSupportedStages = 0;
|
Uint32 SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
@@ -209,6 +210,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
||||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
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;
|
String reason;
|
||||||
for (SizeT i = 0; i < reasons.size(); ++i) {
|
for (SizeT i = 0; i < reasons.size(); ++i) {
|
||||||
@@ -356,6 +363,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return complete;
|
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,
|
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
|
||||||
GLuint renderbuffer,
|
GLuint renderbuffer,
|
||||||
TextureInternalFormat format) {
|
TextureInternalFormat format) {
|
||||||
@@ -519,20 +562,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
||||||
GLESProbeFormatInfo fallbackInfo;
|
GLESProbeFormatInfo outerFallbackInfo;
|
||||||
const Bool hasForcedFallback =
|
const Bool outerHasForcedFallback =
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
|
||||||
if (!hasForcedFallback) {
|
if (!outerHasForcedFallback) {
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||||
const auto target = static_cast<TextureTarget>(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;
|
Bool shouldProbeFallback = hasForcedFallback;
|
||||||
if (!hasForcedFallback) {
|
if (!hasForcedFallback) {
|
||||||
Bool nativeRenderable = false;
|
Bool nativeRenderable = false;
|
||||||
const Bool nativeCreated =
|
const Bool nativeCreated =
|
||||||
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||||
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
||||||
if (nativeCreated) {
|
if (nativeCreated) {
|
||||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||||
@@ -547,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
||||||
Bool fallbackRenderable = false;
|
Bool fallbackRenderable = false;
|
||||||
const Bool fallbackCreated =
|
const Bool fallbackCreated =
|
||||||
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||||
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
||||||
if (fallbackCreated) {
|
if (fallbackCreated) {
|
||||||
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
|
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
|
||||||
@@ -563,8 +636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||||
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
|
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
||||||
if (!hasForcedFallback) {
|
if (!outerHasForcedFallback) {
|
||||||
const Bool nativeRenderbufferComplete =
|
const Bool nativeRenderbufferComplete =
|
||||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||||
if (nativeRenderbufferComplete) {
|
if (nativeRenderbufferComplete) {
|
||||||
@@ -578,16 +651,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
shouldProbeFallbackRenderbuffer = true;
|
shouldProbeFallbackRenderbuffer = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||||
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
|
||||||
}
|
}
|
||||||
const Int maxSamples =
|
const Int maxSamples =
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
||||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
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
|
.ExtraVendor = Nullopt, // Extra vendor
|
||||||
.RendererGLInfo =
|
.RendererGLInfo =
|
||||||
{
|
{
|
||||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
.TargetGLVersion = {3, 3, 0}, // GL target version
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||||
@@ -831,7 +904,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
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_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,
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||||
E_GL_ARB_shader_image_size};
|
// 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,
|
||||||
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
|
// extension explicitly permits. It is also the only thing that
|
||||||
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
|
E_GL_ARB_get_program_binary};
|
||||||
// Only advertised when the device driver actually has usable timer queries
|
// Only advertised when the device driver actually has usable timer queries
|
||||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||||
@@ -934,11 +1017,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||||
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||||
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
|
||||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
|
||||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
|
||||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
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.PatchParameteri = DirectGLES::PatchParameteri;
|
||||||
|
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
|
||||||
|
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
|
||||||
|
funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback;
|
||||||
|
funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback;
|
||||||
|
funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback;
|
||||||
|
funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback;
|
||||||
funcsTableInitialized = true;
|
funcsTableInitialized = true;
|
||||||
}
|
}
|
||||||
return funcsTable;
|
return funcsTable;
|
||||||
@@ -979,6 +1081,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
|
m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
|
||||||
m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
|
m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
|
||||||
m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
|
m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
|
||||||
|
m_dynamicParameters.MaxPatchVertices = m_GLESCapabilities.MaxPatchVertices;
|
||||||
|
m_dynamicParameters.MaxTessGenLevel = m_GLESCapabilities.MaxTessGenLevel;
|
||||||
|
m_dynamicParameters.MinProgramTextureGatherOffset = m_GLESCapabilities.MinProgramTextureGatherOffset;
|
||||||
|
m_dynamicParameters.MaxProgramTextureGatherOffset = m_GLESCapabilities.MaxProgramTextureGatherOffset;
|
||||||
// Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
|
// Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
|
||||||
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
|
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
|
||||||
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
|
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
|
||||||
@@ -1025,6 +1131,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||||
m_dynamicParameters.MaxComputeImageUniforms =
|
m_dynamicParameters.MaxComputeImageUniforms =
|
||||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||||
|
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
|
||||||
|
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
@@ -1034,6 +1142,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
|
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
|
||||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
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_dynamicParameters.SupportsWideLines =
|
||||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
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();
|
BackendQueryHandle BeginTimeElapsedQuery();
|
||||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||||
BackendQueryHandle QueryCounterTimestamp();
|
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);
|
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||||
// null or stale-generation handles IS final: the frontend may cache it
|
// null or stale-generation handles IS final: the frontend may cache it
|
||||||
@@ -154,6 +164,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||||
void DestroyEGLContext();
|
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.
|
||||||
|
void PatchParameteri(GLenum pname, GLint value);
|
||||||
|
|
||||||
|
namespace XfbImpl {
|
||||||
|
Bool AreTransformFeedbacksSupported();
|
||||||
|
void BeginTransformFeedback(GLenum primitiveMode);
|
||||||
|
void EndTransformFeedback();
|
||||||
|
void PauseTransformFeedback();
|
||||||
|
void ResumeTransformFeedback();
|
||||||
|
void BindTransformFeedback(GLuint name);
|
||||||
|
void DeleteTransformFeedback(GLuint name);
|
||||||
|
void OnBackendContextDestroyed();
|
||||||
|
} // namespace XfbImpl
|
||||||
|
|
||||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||||
|
|||||||
@@ -630,6 +630,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A shader wrote this buffer through a storage/atomic-counter binding, so the ES
|
||||||
|
// driver's copy is ahead of the frontend shadow. Pull the whole thing back so
|
||||||
|
// MapBuffer/GetBufferSubData/CopyBufferSubData see the real results.
|
||||||
|
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
|
||||||
|
auto* resource = ResourceOf(bufferObject);
|
||||||
|
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||||
|
if (resource->persistentMapped) return; // shadow already IS the GPU storage
|
||||||
|
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||||
|
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
|
||||||
|
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
||||||
|
if (size == 0) return;
|
||||||
|
|
||||||
|
BindBufferId(TempBufferTarget, resource->id);
|
||||||
|
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||||
|
GL_MAP_READ_BIT);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
MGLOG_E("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bufferObject.WritebackFromBackend({mapped, size}, 0);
|
||||||
|
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
||||||
|
// The shadow now matches the backend byte for byte; without this the next
|
||||||
|
// draw would see a newer change serial and re-upload the readback over it.
|
||||||
|
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||||
|
}
|
||||||
|
|
||||||
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
|
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
|
||||||
if (!resource) return;
|
if (!resource) return;
|
||||||
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
|
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
|
||||||
@@ -662,6 +688,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
.FlushMappedRange = Ops_FlushMappedRange,
|
.FlushMappedRange = Ops_FlushMappedRange,
|
||||||
.OnDestroy = Ops_OnDestroy,
|
.OnDestroy = Ops_OnDestroy,
|
||||||
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
||||||
|
.ReadbackFromGpu = Ops_ReadbackFromGpu,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -2405,7 +2432,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
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) {
|
if (swizzleParams != m_cacheSwizzleParams) {
|
||||||
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
|
||||||
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
|
||||||
@@ -2679,6 +2718,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
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(
|
void BackendFramebufferObject::SyncReadBufferToBackend(
|
||||||
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
||||||
if (!stateFBOObject) {
|
if (!stateFBOObject) {
|
||||||
@@ -3181,6 +3245,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
Uint32 g_snormFallbackClampOutputMask = 0;
|
Uint32 g_snormFallbackClampOutputMask = 0;
|
||||||
|
Uint g_fragColorBroadcastCount = 1;
|
||||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||||
Uint g_lastUsedBackendProgramId = 0;
|
Uint g_lastUsedBackendProgramId = 0;
|
||||||
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
|
||||||
@@ -3232,8 +3297,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
||||||
|
m_backendProgramUsable = true;
|
||||||
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||||
|
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
|
||||||
|
|
||||||
// Detach all existing shaders
|
// Detach all existing shaders
|
||||||
GLint attachedCount = 0;
|
GLint attachedCount = 0;
|
||||||
@@ -3316,6 +3383,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &noperspectiveSpirv;
|
effectiveSpirv = &noperspectiveSpirv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||||
|
// than approximating one. Rewriting the type to 2D is exact for a lookup that
|
||||||
|
// takes integer texel coordinates and needs the coordinate divided by the
|
||||||
|
// texture size for one that does not - see NormalizeRectSamplerCoordinates
|
||||||
|
// below, which the ESSL the transpiler produces goes through. The pass declines
|
||||||
|
// anything neither step can convert.
|
||||||
|
Vector<unsigned int> rectLoweredSpirv;
|
||||||
|
Bool loweredRectImages = false;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
|
||||||
|
rectLoweredSpirv) &&
|
||||||
|
!rectLoweredSpirv.empty()) {
|
||||||
|
effectiveSpirv = &rectLoweredSpirv;
|
||||||
|
loweredRectImages = true;
|
||||||
|
}
|
||||||
|
|
||||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
|
||||||
@@ -3338,6 +3420,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
r.log += spvcSession.GetLastErrorString();
|
r.log += spvcSession.GetLastErrorString();
|
||||||
r.errc = -5;
|
r.errc = -5;
|
||||||
MGLOG_E("%s", r.log.c_str());
|
MGLOG_E("%s", r.log.c_str());
|
||||||
|
m_backendProgramUsable = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3347,6 +3430,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
source = RemoveLayoutBinding(source);
|
source = RemoveLayoutBinding(source);
|
||||||
source = ProcessOutColorLocations(source);
|
source = ProcessOutColorLocations(source);
|
||||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||||
|
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
|
||||||
|
source = EmulateTextureLodBias(source);
|
||||||
|
if (loweredRectImages) {
|
||||||
|
// The image type is 2D now, so the transpiled lookups address [0,1]; the
|
||||||
|
// application wrote them in texels. Only the frontend still knows which
|
||||||
|
// samplers were declared rectangle.
|
||||||
|
Vector<String> rectSamplerNames;
|
||||||
|
const Uint uniformCount = stateProgramObject->GetUniformCount();
|
||||||
|
for (Uint i = 0; i < uniformCount; ++i) {
|
||||||
|
switch (stateProgramObject->GetActiveUniformType(i)) {
|
||||||
|
case GL_SAMPLER_2D_RECT:
|
||||||
|
case GL_SAMPLER_2D_RECT_SHADOW:
|
||||||
|
case GL_INT_SAMPLER_2D_RECT:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
|
||||||
|
rectSamplerNames.push_back(stateProgramObject->GetActiveUniformName(i));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source = NormalizeRectSamplerCoordinates(source, rectSamplerNames);
|
||||||
|
}
|
||||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||||
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
||||||
source = ForceSupporterOutput(source);
|
source = ForceSupporterOutput(source);
|
||||||
@@ -3377,6 +3482,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Vector<GLchar> log(logLength);
|
Vector<GLchar> log(logLength);
|
||||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||||
|
m_backendProgramUsable = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3386,12 +3492,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
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
|
// Link program
|
||||||
MGLOG_D("Linking program %u", m_backendProgramId);
|
MGLOG_D("Linking program %u", m_backendProgramId);
|
||||||
g_GLESFuncs.glLinkProgram(m_backendProgramId);
|
g_GLESFuncs.glLinkProgram(m_backendProgramId);
|
||||||
|
|
||||||
GLint linkStatus;
|
GLint linkStatus;
|
||||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
|
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
|
||||||
|
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
|
||||||
if (linkStatus != GL_TRUE) {
|
if (linkStatus != GL_TRUE) {
|
||||||
GLint logLength;
|
GLint logLength;
|
||||||
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
|
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
|
||||||
@@ -3504,6 +3631,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
binding.backendLocation = backendLoc;
|
binding.backendLocation = backendLoc;
|
||||||
binding.uniformType = uniformType;
|
binding.uniformType = uniformType;
|
||||||
binding.lastAssignedUnit = -1;
|
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);
|
m_samplerUniformBindings.push_back(binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3512,12 +3644,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#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;
|
return;
|
||||||
}
|
}
|
||||||
MGLOG_D("Using program %u", m_backendProgramId);
|
MGLOG_D("Using program %u", programToBind);
|
||||||
g_GLESFuncs.glUseProgram(m_backendProgramId);
|
g_GLESFuncs.glUseProgram(programToBind);
|
||||||
g_lastUsedBackendProgramId = m_backendProgramId;
|
g_lastUsedBackendProgramId = programToBind;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
|
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
|
||||||
|
|||||||
@@ -270,18 +270,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
|
||||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
(void)target;
|
||||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
return true;
|
||||||
// coordinate padding for 1D/1D-array shaders.
|
|
||||||
return target != TextureTarget::TextureRectangle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
|
||||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
// 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) {
|
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case TextureTarget::Texture1D:
|
case TextureTarget::Texture1D:
|
||||||
|
case TextureTarget::TextureRectangle:
|
||||||
return TextureTarget::Texture2D;
|
return TextureTarget::Texture2D;
|
||||||
case TextureTarget::Texture1DArray:
|
case TextureTarget::Texture1DArray:
|
||||||
return TextureTarget::Texture2DArray;
|
return TextureTarget::Texture2DArray;
|
||||||
@@ -297,6 +300,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||||
switch (uploadTarget) {
|
switch (uploadTarget) {
|
||||||
case TextureUploadTarget::Texture1D:
|
case TextureUploadTarget::Texture1D:
|
||||||
|
case TextureUploadTarget::TextureRectangle:
|
||||||
return GL_TEXTURE_2D;
|
return GL_TEXTURE_2D;
|
||||||
case TextureUploadTarget::Texture1DArray:
|
case TextureUploadTarget::Texture1DArray:
|
||||||
return GL_TEXTURE_2D_ARRAY;
|
return GL_TEXTURE_2D_ARRAY;
|
||||||
@@ -438,6 +442,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||||
g_backendFramebufferObjects;
|
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;
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
||||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
// 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
|
// 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;
|
Int backendLocation = -1;
|
||||||
GLenum uniformType = 0;
|
GLenum uniformType = 0;
|
||||||
Int lastAssignedUnit = -1;
|
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();
|
BackendProgramObjectImpl();
|
||||||
@@ -587,9 +604,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetDrawID(Uint32 drawId) const;
|
void SetDrawID(Uint32 drawId) const;
|
||||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
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; }
|
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
||||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||||
|
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||||
|
|
||||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||||
@@ -616,7 +638,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Int m_indirectParamsBinding = -1;
|
Int m_indirectParamsBinding = -1;
|
||||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||||
Uint32 m_unormFallbackClampOutputMask = 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_isInitialized = false;
|
||||||
|
Bool m_backendProgramUsable = false;
|
||||||
|
|
||||||
Int m_globalUboBackendBlockIndex = -1;
|
Int m_globalUboBackendBlockIndex = -1;
|
||||||
Int m_globalUboBackendBlockSize = 0;
|
Int m_globalUboBackendBlockSize = 0;
|
||||||
@@ -629,6 +655,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
extern Uint32 g_snormFallbackClampOutputMask;
|
extern Uint32 g_snormFallbackClampOutputMask;
|
||||||
extern Uint32 g_unormFallbackClampOutputMask;
|
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()
|
// 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
|
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||||
// ES context is recreated.
|
// ES context is recreated.
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
#include <MG_Util/Math/SmallFloat.h>
|
#include <MG_Util/Math/SmallFloat.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -45,15 +48,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
|
Flags<PixelFormatNormalizeOptionBit>
|
||||||
|
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
|
||||||
using namespace MG_Util::TextureFormatProcessor;
|
using namespace MG_Util::TextureFormatProcessor;
|
||||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
|
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
|
||||||
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
|
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
|
||||||
if (forcedOptions) {
|
if (forcedOptions) {
|
||||||
return forcedOptions;
|
return forcedOptions;
|
||||||
}
|
}
|
||||||
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
return GetApplicablePixelFormatNormalizeOptions(
|
||||||
GetDriverPixelFormatNormalizeOptions());
|
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,
|
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||||
@@ -113,7 +141,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
|
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
||||||
|
GetRenderTargetNormalizeOptions(targetIndex));
|
||||||
}
|
}
|
||||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||||
}
|
}
|
||||||
@@ -148,6 +177,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
||||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
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 TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
String ProcessOutColorLocations(const String& glslCode) {
|
String ProcessOutColorLocations(const String& glslCode) {
|
||||||
@@ -276,6 +321,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return glslCode;
|
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) {
|
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -342,6 +436,227 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String NormalizeRectSamplerCoordinates(const String& glslCode,
|
||||||
|
const Vector<String>& rectSamplerNames) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
if (rectSamplerNames.empty() || glslCode.find("texture") == String::npos) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookups whose argument 1 is a plain (non-projective) texel-space coordinate on a
|
||||||
|
// rectangle sampler. texelFetch* is absent on purpose: its coordinates are integer
|
||||||
|
// texels on the 2D target too, so it already lands in the right place.
|
||||||
|
static const char* const kRectCoordinateLookups[] = {
|
||||||
|
"textureGatherOffsets", "textureGatherOffset", "textureGather",
|
||||||
|
"textureOffset", "texture",
|
||||||
|
};
|
||||||
|
|
||||||
|
String result = glslCode;
|
||||||
|
// Right to left, so the offsets of the not-yet-rewritten calls stay valid.
|
||||||
|
for (SizeT scan = result.size(); scan-- > 0;) {
|
||||||
|
if (result[scan] != 't') continue;
|
||||||
|
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||||
|
|
||||||
|
SizeT openParen = 0;
|
||||||
|
Bool matched = false;
|
||||||
|
for (const char* name : kRectCoordinateLookups) {
|
||||||
|
const SizeT nameLength = std::strlen(name);
|
||||||
|
if (result.compare(scan, nameLength, name) != 0) continue;
|
||||||
|
const SizeT after = result.find_first_not_of(" \t", scan + nameLength);
|
||||||
|
if (after == String::npos || result[after] != '(') continue;
|
||||||
|
openParen = after;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!matched) continue;
|
||||||
|
|
||||||
|
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||||
|
if (marks.size() < 2) continue; // needs a sampler and a coordinate
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (std::find(rectSamplerNames.begin(), rectSamplerNames.end(), samplerName) ==
|
||||||
|
rectSamplerNames.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap argument 1: (coord) / vec2(textureSize(sampler, 0)).
|
||||||
|
const SizeT coordStart = marks[0] + 1;
|
||||||
|
const SizeT coordEnd = marks[1];
|
||||||
|
result.insert(coordEnd, String(") / vec2(textureSize(") + samplerName + ", 0)))");
|
||||||
|
result.insert(coordStart, "((");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType);
|
GLenum* outFormat, GLenum* outType);
|
||||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
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);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
@@ -104,7 +109,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
||||||
Uint32 unormOutputMask);
|
Uint32 unormOutputMask);
|
||||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
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);
|
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);
|
||||||
|
// GL_TEXTURE_RECTANGLE is emulated on an ES 2D texture and LowerRectImagesForEssl
|
||||||
|
// rewrites the image type to match, but a rectangle lookup addresses texels
|
||||||
|
// directly while a 2D one addresses [0,1] - so every lookup that takes normalized
|
||||||
|
// coordinates has to divide by the texture's size. `rectSamplerNames` is the set of
|
||||||
|
// samplers the program declared as rectangle; texelFetch is left alone (its
|
||||||
|
// coordinates are unnormalized on both targets) and so is anything projective,
|
||||||
|
// which LowerRectImagesForEssl still declines outright.
|
||||||
|
String NormalizeRectSamplerCoordinates(const String& glslCode,
|
||||||
|
const Vector<String>& rectSamplerNames);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -525,10 +526,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
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_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_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_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_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) {
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
@@ -634,6 +636,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
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;
|
funcsTableInitialized = true;
|
||||||
}
|
}
|
||||||
return funcsTable;
|
return funcsTable;
|
||||||
@@ -796,6 +807,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
||||||
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
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.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||||
|
|||||||
@@ -1250,10 +1250,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pVulkanRenderer->Clear(mask);
|
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) {
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
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{};
|
DrawCmd payload{};
|
||||||
payload.mode = mode;
|
payload.mode = mode;
|
||||||
payload.params.firstVertex = first;
|
payload.params.firstVertex = first;
|
||||||
@@ -1266,6 +1332,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
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{};
|
DrawIndexedCmd payload{};
|
||||||
payload.mode = mode;
|
payload.mode = mode;
|
||||||
payload.indexBufferView.indexType = type;
|
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) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
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{};
|
DrawIndexedCmd payload{};
|
||||||
payload.mode = mode;
|
payload.mode = mode;
|
||||||
payload.indexBufferView.indexType = type;
|
payload.indexBufferView.indexType = type;
|
||||||
@@ -1483,8 +1564,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// records are shared (SharedPtr) with the owning pool's pending list,
|
// records are shared (SharedPtr) with the owning pool's pending list,
|
||||||
// so deleting the query while results are still in flight is safe.
|
// so deleting the query while results are still in flight is safe.
|
||||||
struct VulkanTimerQuery {
|
struct VulkanTimerQuery {
|
||||||
|
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
|
||||||
|
Kind kind = Kind::Timer;
|
||||||
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
||||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
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
|
// Renderer generation the records were written under (see
|
||||||
// g_rendererGeneration). A stale generation resolves as available
|
// g_rendererGeneration). A stale generation resolves as available
|
||||||
// with a final zero result: the records' pool indices and frame
|
// with a final zero result: the records' pool indices and frame
|
||||||
@@ -1493,6 +1578,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// (and, via the SharedPtrs, the records), never pool slots, so
|
// (and, via the SharedPtrs, the records), never pool slots, so
|
||||||
// stale queries are always safe to delete.
|
// stale queries are always safe to delete.
|
||||||
Uint64 rendererGeneration = 0;
|
Uint64 rendererGeneration = 0;
|
||||||
|
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||||
|
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
||||||
|
// capture saw, so a draw made while the span was paused is invisible to it -
|
||||||
|
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
||||||
|
// emitted regardless. The delta closes that gap at result time.
|
||||||
|
Uint64 pausedPrimitiveSnapshot = 0;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1574,6 +1665,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// ever be produced, so resolve with a final 0.
|
// ever be produced, so resolve with a final 0.
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
||||||
|
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||||
|
query->pausedPrimitiveSnapshot;
|
||||||
|
}
|
||||||
|
*outNanoseconds = primitives;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
||||||
// complete until Present submits the commands, so the wait refuses to
|
// complete until Present submits the commands, so the wait refuses to
|
||||||
// block on the current unsubmitted serial. Returning false keeps the
|
// block on the current unsubmitted serial. Returning false keeps the
|
||||||
@@ -1606,6 +1721,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
delete static_cast<VulkanTimerQuery*>(handle);
|
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();
|
||||||
|
query->pausedPrimitiveSnapshot =
|
||||||
|
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||||
|
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() {
|
Int64 GetGpuTimestampNs() {
|
||||||
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
||||||
// exist as vkCmdWriteTimestamp results read back later, and
|
// 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.
|
// only while a live renderer exists whose device can actually time.
|
||||||
Bool IsTimerQuerySupported();
|
Bool IsTimerQuerySupported();
|
||||||
BackendQueryHandle BeginTimeElapsedQuery();
|
BackendQueryHandle BeginTimeElapsedQuery();
|
||||||
|
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||||
|
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||||
|
BackendQueryHandle BeginOcclusionQuery();
|
||||||
|
void EndOcclusionQuery(BackendQueryHandle query);
|
||||||
void EndTimeElapsedQuery(BackendQueryHandle query);
|
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||||
BackendQueryHandle QueryCounterTimestamp();
|
BackendQueryHandle QueryCounterTimestamp();
|
||||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||||
|
|||||||
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_device = device;
|
m_device = device;
|
||||||
m_commandPool = commandPool;
|
m_commandPool = commandPool;
|
||||||
|
|
||||||
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
|
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
|
||||||
VkCommandBufferAllocateInfo allocInfo{};
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||||
allocInfo.commandPool = commandPool;
|
allocInfo.commandPool = commandPool;
|
||||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
allocInfo.commandBufferCount = frameCount;
|
allocInfo.commandBufferCount = frameCount * 2;
|
||||||
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||||
m_frames[i].commandBuffer = commandBuffers[i];
|
m_frames[i].commandBuffer = commandBuffers[i];
|
||||||
|
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||||
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
|
||||||
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
|
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) {
|
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||||
commandBuffers[i] = m_frames[i].commandBuffer;
|
commandBuffers[i] = m_frames[i].commandBuffer;
|
||||||
|
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Uint32 i = 0; i < frameCount; ++i) {
|
for (Uint32 i = 0; i < frameCount; ++i) {
|
||||||
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& frame : m_frames) {
|
for (auto& frame : m_frames) {
|
||||||
FreeRetiredCommandBuffers(frame);
|
FreeRetiredCommandBuffers(frame);
|
||||||
}
|
}
|
||||||
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
|
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
|
||||||
}
|
}
|
||||||
m_frames.clear();
|
m_frames.clear();
|
||||||
currentFrameIndex = 0;
|
currentFrameIndex = 0;
|
||||||
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
|
||||||
GetCurrent().isCommandRecording = false;
|
GetCurrent().isCommandRecording = false;
|
||||||
GetCurrent().hasCommandBufferRecorded = false;
|
GetCurrent().hasCommandBufferRecorded = false;
|
||||||
|
GetCurrent().isPreCommandRecording = false;
|
||||||
|
GetCurrent().hasPreCommandBufferRecorded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
|
||||||
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frame.hasCommandBufferRecorded = true;
|
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) {
|
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
|
||||||
DestroySwapchainSemaphores(device);
|
DestroySwapchainSemaphores(device);
|
||||||
if (swapchainImageCount == 0) {
|
if (swapchainImageCount == 0) {
|
||||||
@@ -202,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 swapchainImageIndex) const {
|
Uint32 swapchainImageIndex) const {
|
||||||
const auto& frame = GetCurrent();
|
const auto& frame = GetCurrent();
|
||||||
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
|
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);
|
AssertValidSwapchainImageIndex(swapchainImageIndex);
|
||||||
SubmitInfoPacket packet{};
|
SubmitInfoPacket packet{};
|
||||||
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
packet.waitSemaphore = frame.imageAvailableSemaphore;
|
||||||
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
|
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.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
|
||||||
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
|
||||||
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
|
||||||
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
|
packet.submitInfo.commandBufferCount = commandBufferCount;
|
||||||
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
|
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
|
||||||
packet.submitInfo.signalSemaphoreCount = 1;
|
packet.submitInfo.signalSemaphoreCount = 1;
|
||||||
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
|
||||||
return packet;
|
return packet;
|
||||||
@@ -276,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_recordingObserver = observer;
|
m_recordingObserver = observer;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkResult FrameContext::RetireCurrentCommandBuffer() {
|
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
|
||||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
|
||||||
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
"RetireCurrentCommandBuffer requires an initialized FrameContext");
|
||||||
auto& frame = GetCurrent();
|
auto& frame = GetCurrent();
|
||||||
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
MOBILEGL_ASSERT(!frame.isCommandRecording,
|
||||||
"RetireCurrentCommandBuffer called while the command buffer is still recording");
|
"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{};
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
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.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
allocInfo.commandBufferCount = 1;
|
allocInfo.commandBufferCount = 1;
|
||||||
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
VkCommandBuffer replacement = VK_NULL_HANDLE;
|
||||||
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
return result;
|
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
|
// lastSubmitIndex was just written by the renderer for the submission
|
||||||
// that carried this command buffer.
|
// that carried this command buffer.
|
||||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
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;
|
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||||
VkSemaphore signalSemaphore = 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};
|
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,10 +54,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
struct FrameData {
|
struct FrameData {
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
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;
|
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||||
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
VkFence imageInFlightFence = VK_NULL_HANDLE;
|
||||||
Bool isCommandRecording = false;
|
Bool isCommandRecording = false;
|
||||||
Bool hasCommandBufferRecorded = false;
|
Bool hasCommandBufferRecorded = false;
|
||||||
|
Bool isPreCommandRecording = false;
|
||||||
|
Bool hasPreCommandBufferRecorded = false;
|
||||||
Bool imageAvailableSemaphoreConsumed = false;
|
Bool imageAvailableSemaphoreConsumed = false;
|
||||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||||
// appended in submit order; freed once their submission is known
|
// appended in submit order; freed once their submission is known
|
||||||
@@ -77,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
|
||||||
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
|
||||||
void EndCommandRecording();
|
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);
|
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
|
||||||
void DestroySwapchainSemaphores(VkDevice device);
|
void DestroySwapchainSemaphores(VkDevice device);
|
||||||
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
|
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
|
// can restart while the submitted buffer is still executing. Retired
|
||||||
// buffers are freed after the slot's fence is next waited, or as soon
|
// buffers are freed after the slot's fence is next waited, or as soon
|
||||||
// as their submission is observed complete.
|
// as their submission is observed complete.
|
||||||
VkResult RetireCurrentCommandBuffer();
|
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
|
||||||
|
|
||||||
// Frees every retired command buffer whose tagged submission index is
|
// Frees every retired command buffer whose tagged submission index is
|
||||||
// known complete. Driven by the renderer's submit tracker on completion
|
// known complete. Driven by the renderer's submit tracker on completion
|
||||||
|
|||||||
@@ -12,10 +12,7 @@
|
|||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <unordered_set>
|
|
||||||
#include <spirv-tools/libspirv.h>
|
#include <spirv-tools/libspirv.h>
|
||||||
#include <spirv-tools/optimizer.hpp>
|
#include <spirv-tools/optimizer.hpp>
|
||||||
#include <source/opt/build_module.h>
|
#include <source/opt/build_module.h>
|
||||||
@@ -926,6 +923,163 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
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
|
// 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
|
// 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
|
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
|
||||||
@@ -1102,374 +1256,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
|
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP-PERFDIAG: measure what fragment-stage fp32 costs on this GPU. Desktop GLSL carries
|
|
||||||
// no precision qualifiers, so everything reaches the driver as full fp32 while Adreno runs
|
|
||||||
// fp16 at twice the rate. Decorating every float-typed result in a fragment entry point
|
|
||||||
// with RelaxedPrecision is the blunt "all mediump" upper bound - it changes results, so it
|
|
||||||
// is a probe, not a shipping transform. Toggled by /sdcard/MG/exp_relaxed_precision.
|
|
||||||
class RelaxedPrecisionProbePass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "relaxed-precision-probe"; }
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
Bool isFragment = false;
|
|
||||||
for (auto& entryPoint : get_module()->entry_points()) {
|
|
||||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
isFragment = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isFragment) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// Every 32-bit-float scalar/vector/matrix type in the module. Anything wider (f64)
|
|
||||||
// or narrower is left alone: RelaxedPrecision only has meaning for 32-bit floats.
|
|
||||||
std::unordered_set<Uint32> relaxableTypes;
|
|
||||||
for (auto& type : get_module()->types_values()) {
|
|
||||||
const Uint32 typeId = type.result_id();
|
|
||||||
if (typeId == 0) continue;
|
|
||||||
switch (type.opcode()) {
|
|
||||||
case spv::Op::OpTypeFloat:
|
|
||||||
if (type.GetSingleWordInOperand(0) == 32) relaxableTypes.insert(typeId);
|
|
||||||
break;
|
|
||||||
case spv::Op::OpTypeVector:
|
|
||||||
case spv::Op::OpTypeMatrix:
|
|
||||||
if (relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
|
|
||||||
relaxableTypes.insert(typeId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (relaxableTypes.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
Vector<Uint32> targets;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0) continue;
|
|
||||||
if (relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
targets.push_back(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (const Uint32 id : targets) {
|
|
||||||
context()->get_decoration_mgr()->AddDecoration(
|
|
||||||
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
|
|
||||||
}
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Relax fragment-stage arithmetic that provably came out of a texture read. Desktop GLSL
|
|
||||||
// has no precision qualifiers, so every fragment value reaches the driver as fp32 while
|
|
||||||
// Adreno runs fp16 at twice the rate - and a texel is at most 8 bits per channel, which
|
|
||||||
// fp16's 11-bit mantissa carries exactly. Seeding at image reads and propagating only
|
|
||||||
// through operations whose every input is already relaxed keeps everything the shader
|
|
||||||
// computes from other sources (screen coordinates, depth, wide-range uniforms) at full
|
|
||||||
// precision, which is where fp16 would actually go wrong: fp16 cannot even represent a
|
|
||||||
// 3044-pixel gl_FragCoord.x exactly.
|
|
||||||
class RelaxTextureDerivedPrecisionPass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "relax-texture-derived-precision"; }
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
if (!IsFragmentEntryPoint()) return Status::SuccessWithoutChange;
|
|
||||||
// A shader that drives depth or coverage itself is out of scope: those values must
|
|
||||||
// stay exact, and proving which computations feed them is not worth it here.
|
|
||||||
if (WritesDepthOrSampleMask()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
CollectRelaxableFloatTypes();
|
|
||||||
if (m_relaxableTypes.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// Whitelisting from texture reads captures nothing in practice: MC's fragment
|
|
||||||
// shaders multiply every texel by an interpolated colour and a UBO value, so one
|
|
||||||
// un-relaxed operand vetoes the whole expression (measured: no fps change).
|
|
||||||
// Taint the few genuinely precision-critical sources instead and relax the rest.
|
|
||||||
std::unordered_set<Uint32> tainted;
|
|
||||||
CollectPrecisionCriticalSeeds(tainted);
|
|
||||||
Bool grew = true;
|
|
||||||
while (grew) {
|
|
||||||
grew = false;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || tainted.count(resultId) != 0) continue;
|
|
||||||
if (!AnyOperandTainted(inst, tainted)) continue;
|
|
||||||
tainted.insert(resultId);
|
|
||||||
grew = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<Uint32> relaxed;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || tainted.count(resultId) != 0) continue;
|
|
||||||
if (m_relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (relaxed.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (const Uint32 id : relaxed) {
|
|
||||||
context()->get_decoration_mgr()->AddDecoration(
|
|
||||||
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
|
|
||||||
}
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unordered_set<Uint32> m_relaxableTypes;
|
|
||||||
|
|
||||||
Bool IsFragmentEntryPoint() const {
|
|
||||||
for (auto& entryPoint : get_module()->entry_points()) {
|
|
||||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool WritesDepthOrSampleMask() const {
|
|
||||||
for (auto& annotation : get_module()->annotations()) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
|
||||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
|
||||||
spv::Decoration::BuiltIn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto builtIn = static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2));
|
|
||||||
if (builtIn == spv::BuiltIn::FragDepth || builtIn == spv::BuiltIn::SampleMask) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectRelaxableFloatTypes() {
|
|
||||||
m_relaxableTypes.clear();
|
|
||||||
for (auto& type : get_module()->types_values()) {
|
|
||||||
const Uint32 typeId = type.result_id();
|
|
||||||
if (typeId == 0) continue;
|
|
||||||
switch (type.opcode()) {
|
|
||||||
case spv::Op::OpTypeFloat:
|
|
||||||
if (type.GetSingleWordInOperand(0) == 32) m_relaxableTypes.insert(typeId);
|
|
||||||
break;
|
|
||||||
case spv::Op::OpTypeVector:
|
|
||||||
if (m_relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
|
|
||||||
m_relaxableTypes.insert(typeId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectImageReadSeeds(std::unordered_set<Uint32>& relaxed) const {
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || m_relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
// Interpolated user varyings seed too, or propagation dies at the
|
|
||||||
// first `texel * vertexColour`: the load of an Input can never be
|
|
||||||
// relaxed by the rule below (its operand is a pointer), so a single
|
|
||||||
// varying vetoes every downstream operation. This is what ESSL's
|
|
||||||
// mediump varyings already mean. Built-ins are excluded - gl_FragCoord
|
|
||||||
// carries pixel coordinates that fp16 cannot represent exactly.
|
|
||||||
if (inst.opcode() == spv::Op::OpLoad && IsNonBuiltInFragmentInput(inst)) {
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
case spv::Op::OpImageSampleImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleExplicitLod:
|
|
||||||
case spv::Op::OpImageSampleProjImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleProjExplicitLod:
|
|
||||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
|
||||||
case spv::Op::OpImageFetch:
|
|
||||||
case spv::Op::OpImageRead:
|
|
||||||
case spv::Op::OpImageGather:
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpLoad straight out of a fragment Input variable that carries no BuiltIn decoration.
|
|
||||||
// Only a direct load counts: a load through an access chain could be indexing a
|
|
||||||
// structure whose other members are not interpolated colour data.
|
|
||||||
Bool IsNonBuiltInFragmentInput(const spvtools::opt::Instruction& load) const {
|
|
||||||
const Uint32 pointerId = load.GetSingleWordInOperand(0);
|
|
||||||
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
|
|
||||||
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bool isBuiltIn = false;
|
|
||||||
context()->get_decoration_mgr()->ForEachDecoration(
|
|
||||||
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
|
|
||||||
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
|
|
||||||
return !isBuiltIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A float constant small enough that fp16 represents it without surprise. Colour math
|
|
||||||
// constants (0, 1, 0.5, 255, gamma exponents) all live here; anything larger is
|
|
||||||
// treated as unknown so it stops propagation.
|
|
||||||
Bool IsBoundedFloatConstant(Uint32 id) const {
|
|
||||||
const auto* constant = context()->get_constant_mgr()->FindDeclaredConstant(id);
|
|
||||||
if (constant == nullptr) return false;
|
|
||||||
if (const auto* scalar = constant->AsFloatConstant()) {
|
|
||||||
const float value = scalar->GetFloat();
|
|
||||||
return std::isfinite(value) && std::fabs(value) <= 1024.0f;
|
|
||||||
}
|
|
||||||
if (const auto* composite = constant->AsVectorConstant()) {
|
|
||||||
for (const auto* component : composite->GetComponents()) {
|
|
||||||
const auto* scalar = component->AsFloatConstant();
|
|
||||||
if (scalar == nullptr) return false;
|
|
||||||
const float value = scalar->GetFloat();
|
|
||||||
if (!std::isfinite(value) || std::fabs(value) > 1024.0f) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Precision-critical sources: a built-in fragment input. gl_FragCoord is the one that
|
|
||||||
// matters - fp16 cannot represent a 3044-pixel x coordinate exactly, and anything
|
|
||||||
// derived from it (screen-space effects, manual depth reconstruction) would visibly
|
|
||||||
// quantise. Everything else a fragment shader reads is colour-range data.
|
|
||||||
void CollectPrecisionCriticalSeeds(std::unordered_set<Uint32>& tainted) const {
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
if (inst.opcode() != spv::Op::OpLoad || inst.result_id() == 0) continue;
|
|
||||||
if (IsBuiltInInputLoad(inst)) tainted.insert(inst.result_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool IsBuiltInInputLoad(const spvtools::opt::Instruction& load) const {
|
|
||||||
const Uint32 pointerId = load.GetSingleWordInOperand(0);
|
|
||||||
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
|
|
||||||
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bool isBuiltIn = false;
|
|
||||||
context()->get_decoration_mgr()->ForEachDecoration(
|
|
||||||
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
|
|
||||||
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
|
|
||||||
return isBuiltIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool AnyOperandTainted(const spvtools::opt::Instruction& inst,
|
|
||||||
const std::unordered_set<Uint32>& tainted) const {
|
|
||||||
const Uint32 operandCount = inst.NumInOperands();
|
|
||||||
for (Uint32 i = 0; i < operandCount; ++i) {
|
|
||||||
const auto& operand = inst.GetInOperand(i);
|
|
||||||
if (!spvIsIdType(operand.type)) continue;
|
|
||||||
if (IsNonNumericOperand(inst, i)) continue;
|
|
||||||
if (tainted.count(operand.words[0]) != 0) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool AllValueOperandsRelaxed(const spvtools::opt::Instruction& inst,
|
|
||||||
const std::unordered_set<Uint32>& relaxed) const {
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
// Pointer-typed plumbing: relaxing the loaded value would say nothing about the
|
|
||||||
// memory it came from, and the pointer operand can never be in the set.
|
|
||||||
case spv::Op::OpLoad:
|
|
||||||
case spv::Op::OpStore:
|
|
||||||
case spv::Op::OpAccessChain:
|
|
||||||
case spv::Op::OpInBoundsAccessChain:
|
|
||||||
case spv::Op::OpFunctionCall:
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool sawValueOperand = false;
|
|
||||||
Bool allRelaxed = true;
|
|
||||||
const Uint32 operandCount = inst.NumInOperands();
|
|
||||||
for (Uint32 i = 0; i < operandCount; ++i) {
|
|
||||||
const auto& operand = inst.GetInOperand(i);
|
|
||||||
if (!spvIsIdType(operand.type)) continue; // literals: selectors, swizzle indices
|
|
||||||
const Uint32 id = operand.words[0];
|
|
||||||
// OpPhi's block labels, OpSelect's condition and OpExtInst's instruction-set id
|
|
||||||
// are ids that carry no numeric precision; skip them rather than let them veto.
|
|
||||||
if (IsNonNumericOperand(inst, i)) continue;
|
|
||||||
sawValueOperand = true;
|
|
||||||
if (relaxed.count(id) != 0) continue;
|
|
||||||
if (IsBoundedFloatConstant(id)) continue;
|
|
||||||
allRelaxed = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return sawValueOperand && allRelaxed;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool IsNonNumericOperand(const spvtools::opt::Instruction& inst, Uint32 index) {
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
case spv::Op::OpPhi:
|
|
||||||
return (index % 2) == 1; // parent block labels
|
|
||||||
case spv::Op::OpSelect:
|
|
||||||
return index == 0; // condition
|
|
||||||
case spv::Op::OpExtInst:
|
|
||||||
return index == 0; // extended instruction set
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TEMP-PERFDIAG: A/B switch between the scoped transform and the all-float upper bound.
|
|
||||||
Bool PerfDiagRelaxAllPrecision() {
|
|
||||||
static const Bool enabled = [] {
|
|
||||||
std::FILE* probe = std::fopen("/sdcard/MG/exp_relaxed_precision_all", "rb");
|
|
||||||
if (probe == nullptr) return false;
|
|
||||||
std::fclose(probe);
|
|
||||||
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision: ALL floats (upper-bound probe)");
|
|
||||||
return true;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEMP-PERFDIAG: lets a run turn the transform off entirely for an A/B baseline.
|
|
||||||
Bool PerfDiagRelaxedPrecisionEnabled() {
|
|
||||||
static const Bool disabled = [] {
|
|
||||||
std::FILE* probe = std::fopen("/sdcard/MG/exp_no_relaxed_precision", "rb");
|
|
||||||
if (probe == nullptr) return false;
|
|
||||||
std::fclose(probe);
|
|
||||||
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision DISABLED");
|
|
||||||
return true;
|
|
||||||
}();
|
|
||||||
return !disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
|
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
output.clear();
|
output.clear();
|
||||||
@@ -1499,32 +1285,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP-PERFDIAG
|
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||||
Bool TransformSpirvForRelaxedPrecisionProbe(const Vector<Uint>& input, Vector<Uint>& output) {
|
const MG_State::GLState::ProgramObject& program) {
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
output.clear();
|
output.clear();
|
||||||
return true;
|
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::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||||
spvtools::OptimizerOptions options;
|
spvtools::OptimizerOptions options;
|
||||||
options.set_run_validator(false);
|
options.set_run_validator(false);
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||||
const char* message) {
|
const char* message) {
|
||||||
MGLOG_E("Vulkan: relaxed-precision probe: %s", message != nullptr ? message : "");
|
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||||
});
|
});
|
||||||
// SSA promotion first: glslang emits function-local variables with stores and loads,
|
optimizer.RegisterPass(spvtools::Optimizer::PassToken(
|
||||||
// and a load can never be relaxed (its operand is a pointer), so without this the
|
MakeUnique<XfbCaptureDecoratePass>(Move(varyings), Move(strides))));
|
||||||
// propagation below dies at the first temporary.
|
|
||||||
optimizer.RegisterPass(spvtools::CreateLocalMultiStoreElimPass());
|
|
||||||
if (PerfDiagRelaxAllPrecision()) {
|
|
||||||
optimizer.RegisterPass(spvtools::Optimizer::PassToken(MakeUnique<RelaxedPrecisionProbePass>()));
|
|
||||||
} else {
|
|
||||||
optimizer.RegisterPass(
|
|
||||||
spvtools::Optimizer::PassToken(MakeUnique<RelaxTextureDerivedPrecisionPass>()));
|
|
||||||
}
|
|
||||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
MGLOG_E("Vulkan: relaxed-precision probe failed; keeping the original module");
|
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
|
||||||
output = input;
|
output = input;
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
@@ -2575,7 +2365,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
// Apply position fixup if needed
|
// Apply position fixup if needed
|
||||||
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
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 {
|
} else {
|
||||||
moduleSpirvs[i] = spv;
|
moduleSpirvs[i] = spv;
|
||||||
}
|
}
|
||||||
@@ -2588,15 +2388,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision) &&
|
|
||||||
PerfDiagRelaxedPrecisionEnabled() && shaders[i] &&
|
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> relaxedSpirv;
|
|
||||||
if (TransformSpirvForRelaxedPrecisionProbe(moduleSpirvs[i], relaxedSpirv)) {
|
|
||||||
moduleSpirvs[i] = Move(relaxedSpirv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
// level, which makes the two forms produce identical texels (the implicit lambda is
|
||||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
||||||
ExplicitLod0Sampling = 1 << 5,
|
ExplicitLod0Sampling = 1 << 5,
|
||||||
// Fragment arithmetic may run at relaxed (fp16) precision. Only requested for draws
|
// Decorates the last vertex-processing stage's captured varyings with
|
||||||
// where every sampled texture and every colour attachment is an 8-bit-or-less
|
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
|
||||||
// normalized format, so nothing the shader reads or writes carries more precision
|
// recorded while GL transform feedback is active, so plain draws keep the
|
||||||
// than fp16 already represents exactly.
|
// undecorated variant.
|
||||||
RelaxedFragmentPrecision = 1 << 6,
|
XfbCapture = 1 << 6,
|
||||||
};
|
};
|
||||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_images.resize(imageCount, VK_NULL_HANDLE);
|
m_images.resize(imageCount, VK_NULL_HANDLE);
|
||||||
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
|
||||||
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
|
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);
|
CreateImageViews(device);
|
||||||
CreateDepthStencilResources(device, physicalDevice);
|
CreateDepthStencilResources(device, physicalDevice);
|
||||||
@@ -433,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
m_images.clear();
|
m_images.clear();
|
||||||
m_imageLayouts.clear();
|
m_imageLayouts.clear();
|
||||||
|
m_imageContentDefined.clear();
|
||||||
|
m_depthStencilContentDefined.clear();
|
||||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
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 {
|
VkImage SwapchainObject::GetImage(Uint32 index) const {
|
||||||
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
|
||||||
return m_images[index];
|
return m_images[index];
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
void SetImageLayout(Uint32 index, VkImageLayout layout);
|
||||||
SizeT GetImageCount() const { return m_images.size(); }
|
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:
|
private:
|
||||||
void CreateImageViews(VkDevice device);
|
void CreateImageViews(VkDevice device);
|
||||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||||
@@ -77,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
Vector<VkDeviceMemory> m_depthStencilImageMemories;
|
||||||
Vector<VkImageView> m_depthStencilImageViews;
|
Vector<VkImageView> m_depthStencilImageViews;
|
||||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||||
|
Vector<Bool> m_imageContentDefined;
|
||||||
|
Vector<Bool> m_depthStencilContentDefined;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include <vulkan/utility/vk_format_utils.h>
|
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -205,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
// 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.
|
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||||
m_hasLastDescriptor = false;
|
m_hasLastDescriptor = false;
|
||||||
|
m_lastBindValid = false;
|
||||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||||
for (auto& memo : m_samplerResolveMemo) {
|
for (auto& memo : m_samplerResolveMemo) {
|
||||||
@@ -447,64 +448,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
// fp16 carries an 11-bit mantissa, so an 8-bit normalized channel round-trips exactly.
|
|
||||||
// Anything wider - 16-bit normalized, half float, full float, and every packed HDR
|
|
||||||
// encoding - holds precision or range that relaxing the arithmetic would throw away.
|
|
||||||
Bool IsLowPrecisionNormalizedFormat(VkFormat format) {
|
|
||||||
if (format == VK_FORMAT_UNDEFINED) return false;
|
|
||||||
if (!vkuFormatIsUNORM(format) && !vkuFormatIsSNORM(format) && !vkuFormatIsSRGB(format)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const struct VKU_FORMAT_INFO info = vkuGetFormatInfo(format);
|
|
||||||
for (Uint32 i = 0; i < info.component_count; ++i) {
|
|
||||||
if (info.components[i].size > 8) return false;
|
|
||||||
}
|
|
||||||
return info.component_count > 0;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Bool UniformManager::DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer) {
|
|
||||||
// Default framebuffer: the swapchain is an 8-bit normalized surface.
|
|
||||||
if (drawFramebuffer == nullptr) return true;
|
|
||||||
|
|
||||||
Bool sawColour = false;
|
|
||||||
for (Int i = static_cast<Int>(FramebufferAttachmentType::Color0);
|
|
||||||
i < static_cast<Int>(FramebufferAttachmentType::FramebufferAttachmentTypeCount);
|
|
||||||
++i) {
|
|
||||||
const auto& attachment =
|
|
||||||
drawFramebuffer->GetAttachment(static_cast<FramebufferAttachmentType>(i));
|
|
||||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
||||||
if (const auto& texture = attachment.GetTexture()) {
|
|
||||||
format = MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
|
|
||||||
} else if (const auto& renderbuffer = attachment.GetRenderbuffer()) {
|
|
||||||
format = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
|
||||||
renderbuffer->GetInternalFormat());
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!IsLowPrecisionNormalizedFormat(format)) return false;
|
|
||||||
sawColour = true;
|
|
||||||
}
|
|
||||||
return sawColour;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ProgramSamplesOnlyLowPrecisionTextures(
|
|
||||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
|
||||||
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
|
||||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
|
||||||
// An unresolvable binding is unknown territory, not licence to relax.
|
|
||||||
if (texture == nullptr) return false;
|
|
||||||
const VkFormat format =
|
|
||||||
MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
|
|
||||||
if (!IsLowPrecisionNormalizedFormat(format)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
||||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
||||||
Bool sawSampler = false;
|
Bool sawSampler = false;
|
||||||
@@ -707,6 +650,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The shader may write this buffer, and those writes land in GPU memory behind the
|
||||||
|
// frontend's CPU shadow - which is what MapBuffer and GetBufferSubData read.
|
||||||
|
// Host-visible coherent GPU residency makes the shadow BE that memory, so the
|
||||||
|
// results are visible without a readback path, exactly as for a capture buffer.
|
||||||
|
bufferObject->EnsureGpuResidentStorage();
|
||||||
|
// ... and the read that follows has to wait for this draw or dispatch to retire.
|
||||||
|
bufferObject->MarkGpuWritten();
|
||||||
|
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||||
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||||
@@ -1248,16 +1199,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bufferInfo.range = ubo.range;
|
bufferInfo.range = ubo.range;
|
||||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||||
} else {
|
} else {
|
||||||
BufferSlice slice{};
|
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
||||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
// uniform bytes re-use the slice already uploaded this frame.
|
||||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
const Bool isGlobalUbo =
|
||||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
|
||||||
binding, element);
|
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
||||||
return false;
|
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);
|
bufferInfos.push_back(bufferInfo);
|
||||||
// Dynamic offsets are consumed in binding order, then array element order,
|
// Dynamic offsets are consumed in binding order, then array element order,
|
||||||
@@ -1396,8 +1378,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_hasLastDescriptor = cacheable;
|
m_hasLastDescriptor = cacheable;
|
||||||
}
|
}
|
||||||
|
|
||||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
// Skip the driver call when this exact binding is already live on the
|
||||||
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
void BeginFrame(Uint32 frameIndex);
|
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
|
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
// 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
|
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||||
@@ -76,15 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||||
// level under a wide level range) merely misses the rewrite.
|
// level under a wide level range) merely misses the rewrite.
|
||||||
// True when every texture this program samples is an 8-bit-or-less normalized format, so
|
|
||||||
// relaxing the fragment stage to fp16 cannot lose a bit the texel ever carried. Says
|
|
||||||
// nothing about the render target - the caller must check that too.
|
|
||||||
static Bool ProgramSamplesOnlyLowPrecisionTextures(const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
|
||||||
// True when every colour attachment the draw writes is an 8-bit-or-less normalized
|
|
||||||
// format (nullptr = default framebuffer, which is). Blending happens at attachment
|
|
||||||
// precision, so a wider target must keep the fragment stage at full precision.
|
|
||||||
static Bool DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer);
|
|
||||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
|
||||||
@@ -194,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 m_lastDescriptorSignature = 0;
|
Uint64 m_lastDescriptorSignature = 0;
|
||||||
Bool m_hasLastDescriptor = false;
|
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
|
// 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
|
// 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
|
// 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 VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) {
|
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 VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
return it->second;
|
return *it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexInputStateBuilder builder;
|
VertexInputStateBuilder builder;
|
||||||
@@ -172,11 +184,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const auto& state = builder.Build();
|
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.hash = hash;
|
||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
entry.attributes = builder.GetAttributes();
|
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.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||||
@@ -205,8 +243,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
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);
|
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 {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
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
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
// 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<VkVertexInputBindingDescription> bindings;
|
||||||
Vector<VkVertexInputAttributeDescription> attributes;
|
Vector<VkVertexInputAttributeDescription> attributes;
|
||||||
Vector<SizeT> bindingBufferKeys;
|
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
|
// 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.
|
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||||
Uint32 unsupportedAttribMask = 0;
|
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{
|
VkPipelineVertexInputStateCreateInfo state{
|
||||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||||
};
|
};
|
||||||
@@ -80,9 +92,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
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.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
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();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "VkBufferManager.h"
|
#include "VkBufferManager.h"
|
||||||
|
#include "../DirectVulkan.h"
|
||||||
|
#include "VulkanRenderer.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -22,6 +24,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
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_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
|
||||||
VK_BUFFER_USAGE_TRANSFER_SRC_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
|
// 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).
|
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
|
||||||
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
|
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
|
||||||
@@ -53,6 +59,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
|
||||||
|
// host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
|
||||||
|
// bound as a shader storage buffer), so nothing needs copying - but coherence only
|
||||||
|
// says the writes are visible once they have happened, so the work has to retire
|
||||||
|
// first.
|
||||||
|
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
|
||||||
|
(void)bufferObject;
|
||||||
|
if (pVulkanRenderer) {
|
||||||
|
pVulkanRenderer->FinishPendingGpuWork();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
|
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
|
||||||
if (g_activeBufferManager) {
|
if (g_activeBufferManager) {
|
||||||
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
|
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
|
||||||
@@ -76,6 +94,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.FlushMappedRange = Ops_FlushMappedRange,
|
.FlushMappedRange = Ops_FlushMappedRange,
|
||||||
.OnDestroy = Ops_OnDestroy,
|
.OnDestroy = Ops_OnDestroy,
|
||||||
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
||||||
|
.ReadbackFromGpu = Ops_ReadbackFromGpu,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -465,7 +484,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// it from the current shadow - MappedData() is still the shadow here because the
|
// it from the current shadow - MappedData() is still the shadow here because the
|
||||||
// frontend adopts (and drops) the shadow only after this returns.
|
// frontend adopts (and drops) the shadow only after this returns.
|
||||||
DeferRelease(std::move(resource->buffer));
|
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->persistentMapped = false;
|
||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->usageFlags = 0;
|
resource->usageFlags = 0;
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
|
||||||
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||||
Bool transientPersistentMapping = false;
|
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).
|
// 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);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_pendingClears.clear();
|
m_pendingClears.clear();
|
||||||
m_aliveObjects.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) {
|
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||||
@@ -127,6 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_pendingClears.erase(key);
|
m_pendingClears.erase(key);
|
||||||
}
|
}
|
||||||
m_aliveObjects.erase(identity);
|
m_aliveObjects.erase(identity);
|
||||||
|
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||||
@@ -221,6 +223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||||
auto& pending = m_pendingClears[key];
|
auto& pending = m_pendingClears[key];
|
||||||
MergeClearPayload(pending, clearPayload);
|
MergeClearPayload(pending, clearPayload);
|
||||||
|
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||||
@@ -238,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||||
auto& pending = m_pendingClears[key];
|
auto& pending = m_pendingClears[key];
|
||||||
MergeClearPayload(pending, clearPayload);
|
MergeClearPayload(pending, clearPayload);
|
||||||
|
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||||
@@ -245,6 +249,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
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 Uint64 lifetimeId = texture->GetLifetimeId();
|
||||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||||
@@ -260,6 +268,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (key.texture == nullptr) {
|
if (key.texture == nullptr) {
|
||||||
return false;
|
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);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||||
@@ -287,6 +298,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (key.texture == nullptr) {
|
if (key.texture == nullptr) {
|
||||||
return false;
|
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);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
if (!LockTextureLocked(key, outTexture)) {
|
if (!LockTextureLocked(key, outTexture)) {
|
||||||
@@ -325,6 +339,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
return false;
|
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 Uint64 lifetimeId = texture->GetLifetimeId();
|
||||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
@@ -345,6 +362,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||||
|
return; // per-draw hot path: nothing pending anywhere
|
||||||
|
}
|
||||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
@@ -361,6 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto it = m_pendingClears.find(key);
|
auto it = m_pendingClears.find(key);
|
||||||
if (it != m_pendingClears.end()) {
|
if (it != m_pendingClears.end()) {
|
||||||
m_pendingClears.erase(it);
|
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 "MG_Util/Math/VectorTypes.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <atomic>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -120,7 +121,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||||
|
|
||||||
Uint8 m_gcCounter = 0;
|
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;
|
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<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,31 +16,21 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||||
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
|
// GL promises "at least the requested samples", so a non-power-of-two
|
||||||
case 1:
|
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||||
|
if (requestedSamples <= 1) {
|
||||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
return true;
|
return true;
|
||||||
case 2:
|
}
|
||||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
if (requestedSamples > 64) {
|
||||||
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:
|
|
||||||
return false;
|
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) {
|
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
|
||||||
@@ -166,6 +156,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (view != VK_NULL_HANDLE) {
|
if (view != VK_NULL_HANDLE) {
|
||||||
vkDestroyImageView(device, view, nullptr);
|
vkDestroyImageView(device, view, nullptr);
|
||||||
}
|
}
|
||||||
|
if (unormTwinView != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyImageView(device, unormTwinView, nullptr);
|
||||||
|
}
|
||||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||||
vmaDestroyImage(allocator, image, allocation);
|
vmaDestroyImage(allocator, image, allocation);
|
||||||
}
|
}
|
||||||
@@ -173,6 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
image = VK_NULL_HANDLE;
|
image = VK_NULL_HANDLE;
|
||||||
allocation = nullptr;
|
allocation = nullptr;
|
||||||
view = VK_NULL_HANDLE;
|
view = VK_NULL_HANDLE;
|
||||||
|
unormTwinView = VK_NULL_HANDLE;
|
||||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
format = VK_FORMAT_UNDEFINED;
|
format = VK_FORMAT_UNDEFINED;
|
||||||
aspect = VK_IMAGE_ASPECT_NONE;
|
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) {
|
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
||||||
return;
|
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.image = VK_NULL_HANDLE;
|
||||||
resource.allocation = nullptr;
|
resource.allocation = nullptr;
|
||||||
resource.view = VK_NULL_HANDLE;
|
resource.view = VK_NULL_HANDLE;
|
||||||
|
resource.unormTwinView = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
||||||
@@ -249,6 +245,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (release.view != VK_NULL_HANDLE) {
|
if (release.view != VK_NULL_HANDLE) {
|
||||||
vkDestroyImageView(m_device, release.view, nullptr);
|
vkDestroyImageView(m_device, release.view, nullptr);
|
||||||
}
|
}
|
||||||
|
if (release.unormTwinView != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
|
||||||
|
}
|
||||||
if (release.image != VK_NULL_HANDLE) {
|
if (release.image != VK_NULL_HANDLE) {
|
||||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
||||||
}
|
}
|
||||||
@@ -304,7 +303,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
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);
|
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
// 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),
|
// 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_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
||||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_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()];
|
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||||
const Bool needsCreate =
|
const Bool needsCreate =
|
||||||
resource.image == VK_NULL_HANDLE ||
|
resource.image == VK_NULL_HANDLE ||
|
||||||
@@ -351,6 +430,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.usage = imageUsage;
|
imageInfo.usage = imageUsage;
|
||||||
imageInfo.samples = sampleCount;
|
imageInfo.samples = sampleCount;
|
||||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
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{};
|
VkImageFormatProperties imageFormatProperties{};
|
||||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
@@ -385,6 +470,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewInfo.subresourceRange.layerCount = 1;
|
viewInfo.subresourceRange.layerCount = 1;
|
||||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
|
||||||
"vkCreateImageView(renderbuffer)");
|
"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.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
resource.format = format;
|
resource.format = format;
|
||||||
@@ -481,12 +571,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
|
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));
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||||
if (isDefaultFbo) {
|
if (isDefaultFbo) {
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
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();
|
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||||
auto readBuffer = fbo.GetReadBuffer();
|
auto readBuffer = fbo.GetReadBuffer();
|
||||||
@@ -560,9 +656,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
attachment <= FramebufferAttachmentType::BackRight);
|
attachment <= FramebufferAttachmentType::BackRight);
|
||||||
if (isDefaultColorAttachment) {
|
if (isDefaultColorAttachment) {
|
||||||
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
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 ||
|
} else if (attachment == FramebufferAttachmentType::Depth ||
|
||||||
attachment == FramebufferAttachmentType::Stencil) {
|
attachment == FramebufferAttachmentType::Stencil) {
|
||||||
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
|
||||||
|
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
|
||||||
|
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||||
@@ -617,14 +721,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
combineFramebufferAttachmentObjHash(drawbuf);
|
combineFramebufferAttachmentObjHash(drawbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
|
// The depth-less default-FBO flavor omits the depth/stencil attachment
|
||||||
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
|
// 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);
|
return XXH64_digest(m_hashState);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
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 {
|
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
|
||||||
const auto& drawBuffers = fbo.GetDrawBuffers();
|
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||||
for (auto attachment : drawBuffers) {
|
for (auto attachment : drawBuffers) {
|
||||||
@@ -674,6 +813,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||||
|
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
|
||||||
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
||||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||||
if (activeIt != m_renderPasses.end()) {
|
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 &&
|
if (activeRenderPass != nullptr &&
|
||||||
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
activeRenderPass->CompatibleWith(compatibilityHash) &&
|
||||||
!hasPendingClearOnFramebuffer()) {
|
!hasPendingClearOnFramebuffer()) {
|
||||||
@@ -699,10 +839,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||||
|
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||||
return activeIt->second;
|
return activeIt->second;
|
||||||
}
|
}
|
||||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||||
auto it = m_renderPasses.find(hash);
|
auto it = m_renderPasses.find(hash);
|
||||||
if (it != m_renderPasses.end()) {
|
if (it != m_renderPasses.end()) {
|
||||||
it->second.lastUsedFrame = m_frameCounter;
|
it->second.lastUsedFrame = m_frameCounter;
|
||||||
@@ -789,8 +930,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
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.flags = 0;
|
||||||
rbDesc.format = rbResource->format;
|
rbDesc.format = rbAttachmentFormat;
|
||||||
rbDesc.samples = rbResource->sampleCount;
|
rbDesc.samples = rbResource->sampleCount;
|
||||||
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
||||||
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
||||||
@@ -825,7 +970,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.finalLayout = rbDesc.finalLayout,
|
.finalLayout = rbDesc.finalLayout,
|
||||||
});
|
});
|
||||||
textureResources.emplace_back(nullptr);
|
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,
|
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||||
|
|
||||||
@@ -894,6 +1040,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
|
||||||
"GetOrCreateRenderPass: swapchain image index out of range");
|
"GetOrCreateRenderPass: swapchain image index out of range");
|
||||||
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
|
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 {
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
.target = TrackedAttachmentTarget::SwapchainColor,
|
.target = TrackedAttachmentTarget::SwapchainColor,
|
||||||
.swapchainImageIndex = swapchainImageIndex,
|
.swapchainImageIndex = swapchainImageIndex,
|
||||||
@@ -907,12 +1060,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(textureResource,
|
MOBILEGL_ASSERT(textureResource,
|
||||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||||
textureResources.emplace_back(textureResource);
|
textureResources.emplace_back(textureResource);
|
||||||
desc.format = textureResource->format;
|
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||||
|
textureResource->format,
|
||||||
|
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||||
attachmentSampleCount = textureResource->sampleCount;
|
attachmentSampleCount = textureResource->sampleCount;
|
||||||
trackedColorLayout = textureResource->layout;
|
trackedColorLayout = textureResource->layout;
|
||||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
.target = TrackedAttachmentTarget::Texture,
|
.target = TrackedAttachmentTarget::Texture,
|
||||||
.texture = att.GetTexture(),
|
.texture = att.GetTexture(),
|
||||||
|
.textureRaw = att.GetTexture().get(),
|
||||||
.textureMipLevel = attachmentMipLevel,
|
.textureMipLevel = attachmentMipLevel,
|
||||||
.finalLayout = desc.finalLayout,
|
.finalLayout = desc.finalLayout,
|
||||||
});
|
});
|
||||||
@@ -976,6 +1132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
|
||||||
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
|
(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 =
|
const Bool hasDistinctDepthAndStencilAttachments =
|
||||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||||
@@ -994,6 +1156,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
VkImageLayout trackedDepthLayout = isDefaultFbo ?
|
||||||
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
|
||||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
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;
|
depthAttachmentDescription.flags = 0;
|
||||||
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
Int depthAttachmentId = 0;
|
Int depthAttachmentId = 0;
|
||||||
@@ -1077,6 +1245,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
.target = TrackedAttachmentTarget::Texture,
|
.target = TrackedAttachmentTarget::Texture,
|
||||||
.texture = selectedDepthStencilAttachment->GetTexture(),
|
.texture = selectedDepthStencilAttachment->GetTexture(),
|
||||||
|
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
|
||||||
.textureMipLevel = attachmentMipLevel,
|
.textureMipLevel = attachmentMipLevel,
|
||||||
.finalLayout = depthAttachmentDescription.finalLayout,
|
.finalLayout = depthAttachmentDescription.finalLayout,
|
||||||
});
|
});
|
||||||
@@ -1122,6 +1291,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
|
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
|
// Subpass
|
||||||
VkSubpassDescription subpassDesc;
|
VkSubpassDescription subpassDesc;
|
||||||
subpassDesc.flags = 0;
|
subpassDesc.flags = 0;
|
||||||
@@ -1330,6 +1515,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
renderPassBeginInfo.pClearValues = clearValues.data();
|
renderPassBeginInfo.pClearValues = clearValues.data();
|
||||||
|
|
||||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
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) {
|
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||||
if (pending.hasInlinePayload) {
|
if (pending.hasInlinePayload) {
|
||||||
if (s_renderPassManager != nullptr) {
|
if (s_renderPassManager != nullptr) {
|
||||||
@@ -1382,11 +1578,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case TrackedAttachmentTarget::SwapchainColor:
|
case TrackedAttachmentTarget::SwapchainColor:
|
||||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
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;
|
break;
|
||||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||||
trackedAttachment.finalLayout);
|
trackedAttachment.finalLayout);
|
||||||
|
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct TrackedAttachmentLayoutInfo {
|
struct TrackedAttachmentLayoutInfo {
|
||||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||||
WeakPtr<MG_State::GLState::ITextureObject> 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;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
Uint32 textureMipLevel = 0;
|
Uint32 textureMipLevel = 0;
|
||||||
Uint32 swapchainImageIndex = 0;
|
Uint32 swapchainImageIndex = 0;
|
||||||
@@ -188,8 +193,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
HashType ComputeHash(
|
HashType ComputeHash(
|
||||||
const MG_State::GLState::FramebufferObject& fbo,
|
const MG_State::GLState::FramebufferObject& fbo,
|
||||||
Uint32 swapchainImageIndex,
|
Uint32 swapchainImageIndex,
|
||||||
Bool includePendingClear = true);
|
Bool includePendingClear = true,
|
||||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
|
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,
|
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||||
@@ -219,6 +238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// image recreation.
|
// image recreation.
|
||||||
Uint64 m_renderbufferImageEpoch = 1;
|
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
|
// 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
|
// 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 /
|
// 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_rpFastTexEpoch = 0;
|
||||||
Uint64 m_rpFastRbEpoch = 0;
|
Uint64 m_rpFastRbEpoch = 0;
|
||||||
Uint64 m_rpFastRenderPassHash = 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:
|
public:
|
||||||
struct RenderbufferResource {
|
struct RenderbufferResource {
|
||||||
@@ -244,6 +274,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VmaAllocation allocation = nullptr;
|
VmaAllocation allocation = nullptr;
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
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;
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||||
@@ -277,12 +310,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VmaAllocation allocation = nullptr;
|
VmaAllocation allocation = nullptr;
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
VkImageView view = VK_NULL_HANDLE;
|
||||||
|
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||||
Uint64 deferredAtFrame = 0;
|
Uint64 deferredAtFrame = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
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(
|
Bool HasPendingRenderbufferClear(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||||
|
|||||||
@@ -120,31 +120,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||||
switch (requestedSamples) {
|
// GL promises "at least the requested samples", so a non-power-of-two
|
||||||
case 1:
|
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
|
||||||
|
if (requestedSamples <= 1) {
|
||||||
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
return true;
|
return true;
|
||||||
case 2:
|
}
|
||||||
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
if (requestedSamples > 64) {
|
||||||
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:
|
|
||||||
return false;
|
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) {
|
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||||
@@ -607,7 +597,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::Shutdown() {
|
void VkTextureManager::Shutdown() {
|
||||||
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
|
ReclaimCompletedUploads(/*waitAll=*/true);
|
||||||
|
}
|
||||||
DestroyDeferredReleases();
|
DestroyDeferredReleases();
|
||||||
|
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
|
||||||
m_textureResources.clear();
|
m_textureResources.clear();
|
||||||
m_aliveObjects.clear();
|
m_aliveObjects.clear();
|
||||||
m_storageImageTextures.clear();
|
m_storageImageTextures.clear();
|
||||||
@@ -629,6 +623,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frameIndex, m_deferredViewReleases.size());
|
frameIndex, m_deferredViewReleases.size());
|
||||||
m_currentFrameIndex = frameIndex;
|
m_currentFrameIndex = frameIndex;
|
||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
|
ReclaimCompletedUploads();
|
||||||
|
|
||||||
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
|
// 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
|
// 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_aliveObjects.erase(identity);
|
||||||
m_storageImageTextures.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) {
|
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
||||||
@@ -714,45 +712,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto aliveIt = m_aliveObjects.find(identity);
|
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
|
||||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
// lookups and the (re)registration path for repeat-bound textures.
|
||||||
EraseTrackedTexture(aliveIt->first);
|
TextureResource* resourcePtr = nullptr;
|
||||||
aliveIt = m_aliveObjects.end();
|
for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) {
|
||||||
}
|
const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i];
|
||||||
|
if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId &&
|
||||||
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
|
memo.eraseEpoch == m_resourceEraseEpoch) {
|
||||||
// aliases can only come into existence through an address reuse, which by
|
resourcePtr = memo.resource;
|
||||||
// construction introduces a new identity. Doing this unconditionally made every
|
break;
|
||||||
// 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 (resourcePtr == nullptr) {
|
||||||
if (it == m_textureResources.end()) {
|
auto aliveIt = m_aliveObjects.find(identity);
|
||||||
TextureResource initial{};
|
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||||
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
|
EraseTrackedTexture(aliveIt->first);
|
||||||
it = insertIt;
|
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());
|
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -766,11 +782,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!recorded) {
|
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) {
|
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
|
||||||
@@ -814,7 +830,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return VK_NULL_HANDLE;
|
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);
|
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,6 +844,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.baseArrayLayer = baseArrayLayer,
|
.baseArrayLayer = baseArrayLayer,
|
||||||
.layerCount = layerCount,
|
.layerCount = layerCount,
|
||||||
.viewType = viewType,
|
.viewType = viewType,
|
||||||
|
.viewFormat = attachmentFormat,
|
||||||
};
|
};
|
||||||
auto it = resource->attachmentViews.find(key);
|
auto it = resource->attachmentViews.find(key);
|
||||||
if (it == resource->attachmentViews.end()) {
|
if (it == resource->attachmentViews.end()) {
|
||||||
@@ -833,7 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return attachmentView;
|
return attachmentView;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
|
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
|
||||||
mipLevel, 1, baseArrayLayer, layerCount);
|
mipLevel, 1, baseArrayLayer, layerCount);
|
||||||
if (attachmentView == VK_NULL_HANDLE) {
|
if (attachmentView == VK_NULL_HANDLE) {
|
||||||
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
|
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;
|
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) {
|
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||||
@@ -1076,6 +1108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
|
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
|
||||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
||||||
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
|
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) {
|
if (resource.layout != newLayout && resource.mipLevels > 1) {
|
||||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
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,
|
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||||
resource->arrayLayers);
|
resource->arrayLayers);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
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;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1189,6 +1225,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||||
texture.GetExternalIndex());
|
texture.GetExternalIndex());
|
||||||
|
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||||
|
StampResourceRecordingUse(*resource);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,6 +1246,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
!it->second.storageUsageResolved;
|
!it->second.storageUsageResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const {
|
||||||
|
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||||
|
const auto it = m_textureResources.find(identity);
|
||||||
|
// No image yet: the first sync sizes the chain from the levels the texture already
|
||||||
|
// defines, so nothing is recreated and there is nothing to order against.
|
||||||
|
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const TextureResource& resource = it->second;
|
||||||
|
const IntVec3 extent = {static_cast<Int>(resource.extent.width), static_cast<Int>(resource.extent.height),
|
||||||
|
static_cast<Int>(resource.depth)};
|
||||||
|
return resource.mipLevels < ComputeFullMipLevelCount(extent);
|
||||||
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||||
const auto it = m_textureResources.find(identity);
|
const auto it = m_textureResources.find(identity);
|
||||||
@@ -1406,11 +1458,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
||||||
TextureResource &resource) {
|
TextureResource &resource) {
|
||||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
const VkFormat format = formatInfo.format;
|
VkFormat format = formatInfo.format;
|
||||||
if (format == VK_FORMAT_UNDEFINED) {
|
if (format == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
|
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
|
||||||
return false;
|
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*/) {
|
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
|
||||||
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
|
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
|
||||||
return false;
|
return false;
|
||||||
@@ -1420,8 +1484,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
|
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 =
|
const Uint32 backingMipLevels =
|
||||||
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
|
isMultisampleTexture ? 1u
|
||||||
|
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
|
||||||
TextureShapeInfo shapeInfo{};
|
TextureShapeInfo shapeInfo{};
|
||||||
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
||||||
MOBILEGL_ASSERT(supportedShape,
|
MOBILEGL_ASSERT(supportedShape,
|
||||||
@@ -1473,6 +1546,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
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 =
|
VkImageUsageFlags desiredUsage =
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||||
@@ -1485,6 +1566,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
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 &&
|
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||||
@@ -1614,8 +1733,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VmaAllocationCreateInfo allocationInfo{};
|
VmaAllocationCreateInfo allocationInfo{};
|
||||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||||
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
|
// Soft failure like the unsupported-sample-count path above: a driver can pass the
|
||||||
"vmaCreateImage(texture)");
|
// 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
|
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||||
|
|
||||||
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
@@ -1683,6 +1815,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_deferredViewReleases[frameIndex].clear();
|
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() {
|
void VkTextureManager::DestroyDeferredReleases() {
|
||||||
for (auto& deferredReleases : m_deferredReleases) {
|
for (auto& deferredReleases : m_deferredReleases) {
|
||||||
deferredReleases.clear();
|
deferredReleases.clear();
|
||||||
@@ -1884,17 +2038,119 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy
|
// Combined depth-stencil images need per-aspect copies (VkBufferImageCopy aspectMask
|
||||||
// aspectMask must have exactly one bit set). Until that is implemented, skip the upload
|
// must have exactly one bit set), so de-interleave the shadow's GL wire format into
|
||||||
// instead of recording an invalid command buffer that kills the process.
|
// a depth plane followed by a stencil plane per upload item.
|
||||||
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
|
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
|
||||||
if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
const Bool isCombinedDepthStencil =
|
||||||
MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d",
|
(uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||||
mipmapTexture.GetExternalIndex());
|
if (isCombinedDepthStencil) {
|
||||||
for (const auto& item : uploadItems) {
|
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
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;
|
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||||
@@ -1947,7 +2203,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
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) {
|
for (const auto& item : uploadItems) {
|
||||||
|
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||||
VkBufferImageCopy copy{};
|
VkBufferImageCopy copy{};
|
||||||
copy.bufferOffset = item.offset;
|
copy.bufferOffset = item.offset;
|
||||||
copy.bufferRowLength = 0;
|
copy.bufferRowLength = 0;
|
||||||
@@ -1955,10 +2218,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
copy.imageSubresource.aspectMask = aspectMask;
|
copy.imageSubresource.aspectMask = aspectMask;
|
||||||
copy.imageSubresource.mipLevel = item.level;
|
copy.imageSubresource.mipLevel = item.level;
|
||||||
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
||||||
copy.imageSubresource.layerCount = 1;
|
copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1;
|
||||||
copy.imageOffset = {0, 0, 0};
|
copy.imageOffset = {0, 0, 0};
|
||||||
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
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,
|
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
1, ©);
|
1, ©);
|
||||||
}
|
}
|
||||||
@@ -1989,11 +2266,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
||||||
|
|
||||||
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
|
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
|
||||||
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
|
// Do NOT wait the fence here: this submit sits behind the previous
|
||||||
vkDestroyFence(m_device, uploadFence, nullptr);
|
// frame's rendering on the queue, so a synchronous wait stalls the CPU
|
||||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
|
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
|
||||||
|
// with animated textures. Ordering against the current frame's draws is
|
||||||
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
|
// 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) {
|
if (!ok) {
|
||||||
MGLOG_D("%s: texture upload cmd failed", __func__);
|
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
|
// 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).
|
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
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 {
|
struct TextureIdentity {
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
@@ -64,12 +67,16 @@ public:
|
|||||||
Uint32 baseArrayLayer = 0;
|
Uint32 baseArrayLayer = 0;
|
||||||
Uint32 layerCount = 1;
|
Uint32 layerCount = 1;
|
||||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
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 {
|
Bool operator==(const AttachmentViewKey& other) const {
|
||||||
return mipLevel == other.mipLevel &&
|
return mipLevel == other.mipLevel &&
|
||||||
baseArrayLayer == other.baseArrayLayer &&
|
baseArrayLayer == other.baseArrayLayer &&
|
||||||
layerCount == other.layerCount &&
|
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>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||||
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
|
||||||
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -172,6 +181,13 @@ public:
|
|||||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||||
Bool storageUsageResolved = false;
|
Bool storageUsageResolved = false;
|
||||||
Uint16 syncedTextureParamsVersion = 0;
|
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;
|
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||||
Uint64 syncedContentVersion = 0;
|
Uint64 syncedContentVersion = 0;
|
||||||
@@ -207,6 +223,7 @@ public:
|
|||||||
std::swap(this->usageFlags, that.usageFlags);
|
std::swap(this->usageFlags, that.usageFlags);
|
||||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||||
|
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
}
|
}
|
||||||
@@ -307,6 +324,21 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(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
|
// 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
|
// 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
|
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||||
@@ -318,6 +350,10 @@ public:
|
|||||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||||
|
// The same ordering question for the other recreate-and-preserve trigger: true when this
|
||||||
|
// texture's live image carries a shorter mip chain than a full one, so defining the missing
|
||||||
|
// levels recreates it and copies the old contents forward.
|
||||||
|
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
|
||||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||||
@@ -364,6 +400,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||||
Uint64 m_textureImageEpoch = 1;
|
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,
|
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
TextureResource &outResource);
|
TextureResource &outResource);
|
||||||
@@ -394,6 +433,11 @@ private:
|
|||||||
void DeferViewRelease(VkImageView view);
|
void DeferViewRelease(VkImageView view);
|
||||||
void CollectDeferredReleases(Uint32 frameIndex);
|
void CollectDeferredReleases(Uint32 frameIndex);
|
||||||
void DestroyDeferredReleases();
|
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);
|
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||||
@@ -423,6 +467,23 @@ private:
|
|||||||
TextureResource* resource = nullptr;
|
TextureResource* resource = nullptr;
|
||||||
};
|
};
|
||||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
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
|
// 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.
|
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||||
@@ -430,7 +491,21 @@ private:
|
|||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
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<TextureResource>> m_deferredReleases;
|
||||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
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
|
} // 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;
|
GLenum indexType = GL_UNSIGNED_SHORT;
|
||||||
SizeT indexByteOffset = 0;
|
SizeT indexByteOffset = 0;
|
||||||
SizeT indexByteSize = 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 {
|
struct DrawIndexedCmd {
|
||||||
@@ -151,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
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,
|
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||||
const RenderPassEntry& compatibleRenderPassEntry);
|
const RenderPassEntry& compatibleRenderPassEntry);
|
||||||
|
|
||||||
@@ -188,6 +200,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
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 SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
@@ -273,6 +304,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkTimerQueryManager::TimestampRecord& end) const;
|
const VkTimerQueryManager::TimestampRecord& end) const;
|
||||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) 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);
|
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
// 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
|
// (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_platformDisplay = nullptr;
|
||||||
void* m_platformLibrary = nullptr;
|
void* m_platformLibrary = nullptr;
|
||||||
void* m_platformCloseDisplay = 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;
|
VulkanRendererConfig m_config;
|
||||||
Bool m_swapchainResizeRequested = false;
|
Bool m_swapchainResizeRequested = false;
|
||||||
// Presentation is suspended while the window is zero-area (minimized): the
|
// Presentation is suspended while the window is zero-area (minimized): the
|
||||||
@@ -443,6 +492,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 stride);
|
Uint32 stride);
|
||||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
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. Transform feedback
|
||||||
|
// objects can each hold an open, paused span at the same time, so the counters are
|
||||||
|
// per object: one group of four slots each, handed out on first use.
|
||||||
|
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||||
|
VkBufferObject m_xfbCounterBuffer;
|
||||||
|
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||||
|
Uint32 m_xfbNextCounterSlot = 0;
|
||||||
|
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||||
|
// counter-buffer resume on the next captured draw of the same span.
|
||||||
|
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||||
|
Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
|
||||||
|
// Counter slot group of the bound transform feedback object.
|
||||||
|
Uint32 CurrentXfbCounterSlot();
|
||||||
|
// 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;
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||||
|
|
||||||
VkBufferManager m_bufferManager;
|
VkBufferManager m_bufferManager;
|
||||||
@@ -455,14 +564,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
// 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.
|
// 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.
|
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
|
||||||
Bool m_lastPipelineValid = false;
|
// Small N-way pipeline-resolution memo (round-robin replacement). A
|
||||||
GLenum m_lastPipelineMode = 0;
|
// single-entry memo thrashed on draw sequences that alternate a few
|
||||||
Uint64 m_lastPipelineProgramHash = 0;
|
// pipelines (GUI text/quad program ping-pong), paying the full
|
||||||
Uint64 m_lastPipelineVertexInputHash = 0;
|
// payload-hash lookup per draw; eight entries cover such working sets
|
||||||
Uint64 m_lastPipelineRenderPassHash = 0;
|
// while keeping the hit path a trivial linear scan.
|
||||||
Uint m_lastPipelineRenderStateVersion = 0;
|
struct PipelineMemoEntry {
|
||||||
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
|
GLenum mode = 0;
|
||||||
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
|
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;
|
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||||
UniquePtr<ProgramFactory> m_programFactory;
|
UniquePtr<ProgramFactory> m_programFactory;
|
||||||
UniquePtr<UniformManager> m_uniformManager;
|
UniquePtr<UniformManager> m_uniformManager;
|
||||||
@@ -491,9 +616,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
|
||||||
Uint64 m_lastSampledSetBindGeneration = 0;
|
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
|
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||||
// draw call and must not allocate.
|
// draw call and must not allocate.
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
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<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
@@ -618,6 +795,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout finalLayout);
|
VkImageLayout finalLayout);
|
||||||
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Submits whatever is recorded and waits for it. The CPU is about to read memory
|
||||||
|
// a shader wrote (a mapped shader storage buffer), and coherent host-visible
|
||||||
|
// storage only guarantees visibility once the work that produced it has retired.
|
||||||
|
Bool FinishPendingGpuWork();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
void ShutdownSwapchain();
|
void ShutdownSwapchain();
|
||||||
|
|
||||||
// Static functions
|
// Static functions
|
||||||
|
|||||||
@@ -52,19 +52,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
} // 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, ...) \
|
#define VK_VERIFY(expr, ...) \
|
||||||
do { \
|
do { \
|
||||||
VkResult _vk_verify_result = (expr); \
|
VkResult _vk_verify_result = (expr); \
|
||||||
if (_vk_verify_result != VK_SUCCESS) { \
|
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), \
|
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
|
||||||
_vk_verify_result, __FILE__, __LINE__); \
|
_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)
|
} while (0)
|
||||||
|
|
||||||
#define XXHASH_VERIFY(expr, ...) \
|
#define XXHASH_VERIFY(expr, ...) \
|
||||||
do { \
|
do { \
|
||||||
XXH_errorcode _xxh_verify_result = (expr); \
|
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)
|
} while (0)
|
||||||
|
|||||||
@@ -330,12 +330,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else if (access & BufferMappingAccessBit::Write) {
|
} else if (access & BufferMappingAccessBit::Write) {
|
||||||
*params = GL_WRITE_ONLY;
|
*params = GL_WRITE_ONLY;
|
||||||
} else {
|
} else {
|
||||||
*params = 0;
|
*params = GL_READ_WRITE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
*params = 0;
|
// Initial value, and what glUnmapBuffer restores (GL 4.6 core table 6.2).
|
||||||
|
*params = GL_READ_WRITE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case GL_BUFFER_ACCESS_FLAGS:
|
||||||
|
// The MapBufferRange flags verbatim; glMapBuffer's access enum has already been
|
||||||
|
// normalised into the same bits. Zero while the buffer is not mapped.
|
||||||
|
*params = bufferObject->IsMapped()
|
||||||
|
? static_cast<GLint>(
|
||||||
|
MG_Util::ConvertBufferMappingAccessToGLEnum(bufferObject->GetMappingAccess()))
|
||||||
|
: 0;
|
||||||
|
break;
|
||||||
case GL_BUFFER_MAPPED:
|
case GL_BUFFER_MAPPED:
|
||||||
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
|
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
|
||||||
break;
|
break;
|
||||||
@@ -878,6 +887,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufferObject->SyncGpuWrites();
|
||||||
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
|
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1351,6 +1361,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) 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);
|
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||||
|
|
||||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||||
@@ -1358,6 +1376,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (buffer == 0) {
|
if (buffer == 0) {
|
||||||
point.Bind(nullptr);
|
point.Bind(nullptr);
|
||||||
point.SetRange(Range1D(0, 0));
|
point.SetRange(Range1D(0, 0));
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1376,6 +1395,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else {
|
} else {
|
||||||
point.ClearRange();
|
point.ClearRange();
|
||||||
}
|
}
|
||||||
|
// The indexed bind also binds to the generic binding point of the same target
|
||||||
|
// (GL 4.6 core 6.1.1). Callers rely on it: the texture_gather tests set up their
|
||||||
|
// SSBO with BindBufferBase and then size it through glBufferData on the generic
|
||||||
|
// target alone, which would otherwise raise GL_INVALID_OPERATION and leave the
|
||||||
|
// buffer with no storage.
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||||
@@ -1384,6 +1409,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) 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);
|
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||||
|
|
||||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||||
@@ -1391,6 +1424,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (buffer == 0) {
|
if (buffer == 0) {
|
||||||
point.Bind(nullptr);
|
point.Bind(nullptr);
|
||||||
point.SetRange(Range1D(0, 0));
|
point.SetRange(Range1D(0, 0));
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1408,6 +1442,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else {
|
} else {
|
||||||
point.ClearRange();
|
point.ClearRange();
|
||||||
}
|
}
|
||||||
|
// Also the generic binding point, exactly as BindBufferBase (GL 4.6 core 6.1.1).
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
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) {
|
if (index < pointCount) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||||
@@ -48,7 +49,109 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
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;
|
||||||
|
// A paused span captures nothing, so a draw made while paused contributes to
|
||||||
|
// PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
||||||
|
// GL_PATCHES for the tessellation pipeline). Anything else is GL_INVALID_ENUM.
|
||||||
|
static Bool IsAcceptedPrimitiveMode(GLenum mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case GL_POINTS:
|
||||||
|
case GL_LINES:
|
||||||
|
case GL_LINE_LOOP:
|
||||||
|
case GL_LINE_STRIP:
|
||||||
|
case GL_LINES_ADJACENCY:
|
||||||
|
case GL_LINE_STRIP_ADJACENCY:
|
||||||
|
case GL_TRIANGLES:
|
||||||
|
case GL_TRIANGLE_STRIP:
|
||||||
|
case GL_TRIANGLE_FAN:
|
||||||
|
case GL_TRIANGLES_ADJACENCY:
|
||||||
|
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||||
|
case GL_PATCHES:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||||
|
if (!IsAcceptedPrimitiveMode(mode)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -57,15 +160,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
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();
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -75,9 +169,133 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A geometry stage only accepts the primitive types that decompose into its declared
|
||||||
|
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||||
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
|
// converted, so it is not constrained here.
|
||||||
|
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
|
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||||
|
Bool compatible = false;
|
||||||
|
switch (gsInput) {
|
||||||
|
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_LINES_ADJACENCY:
|
||||||
|
compatible = mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
|
||||||
|
break;
|
||||||
|
case GL_TRIANGLES:
|
||||||
|
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||||
|
break;
|
||||||
|
case GL_TRIANGLES_ADJACENCY:
|
||||||
|
compatible = mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!compatible) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", functionName,
|
||||||
|
"Primitive mode is incompatible with the geometry shader's input primitive type."));
|
||||||
|
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. A paused span is exempt: it
|
||||||
|
// captures nothing, so there is nothing for the mode to be incompatible with
|
||||||
|
// (GL 4.6 core 13.2.3).
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
|
||||||
|
!(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Byte size of the command structures the indirect draws read (GL 4.6 core 10.3.10).
|
||||||
|
constexpr SizeT kDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32);
|
||||||
|
constexpr SizeT kDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32);
|
||||||
|
|
||||||
|
// Shared preconditions of every *Indirect draw: `indirect` is a byte offset into the
|
||||||
|
// buffer bound to GL_DRAW_INDIRECT_BUFFER, must be 4-byte aligned, and the whole
|
||||||
|
// command has to lie inside that buffer.
|
||||||
|
static Bool ValidateIndirectDrawSource(const char* functionName, const void* indirect, SizeT commandBytes) {
|
||||||
|
const auto offset = reinterpret_cast<uintptr_t>(indirect);
|
||||||
|
if (offset % 4 != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"indirect offset must be a multiple of 4."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& buffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
if (!buffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"No buffer is bound to GL_DRAW_INDIRECT_BUFFER."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + commandBytes > buffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"The indirect command extends past the end of the bound "
|
||||||
|
"GL_DRAW_INDIRECT_BUFFER."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index type accepted by the DrawElements family (GL 4.6 core 10.3.9).
|
||||||
|
static Bool ValidateDrawElementsIndexType(const char* functionName, GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE:
|
||||||
|
case GL_UNSIGNED_SHORT:
|
||||||
|
case GL_UNSIGNED_INT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "type is not an accepted index type."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Clear_Backend(GLbitfield mask) {
|
void Clear_Backend(GLbitfield mask) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -272,6 +490,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PatchParameteri(GLenum pname, GLint value) {
|
||||||
|
if (pname != GL_PATCH_VERTICES) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_VERTICES."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint maxPatchVertices = 32;
|
||||||
|
GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||||
|
if (value <= 0 || value > maxPatchVertices) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"value must be in [1, GL_MAX_PATCH_VERTICES]."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
|
||||||
|
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
|
||||||
|
patchParameteri(pname, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MemoryBarrier(GLbitfield barriers) {
|
void MemoryBarrier(GLbitfield barriers) {
|
||||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||||
if (!memoryBarrier) {
|
if (!memoryBarrier) {
|
||||||
@@ -377,6 +617,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||||
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
|
||||||
DrawElementsIndirect_Backend(mode, type, indirect);
|
DrawElementsIndirect_Backend(mode, type, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,18 +638,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||||
DrawArraysIndirect_Backend(mode, indirect);
|
DrawArraysIndirect_Backend(mode, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
DrawArrays_Backend(mode, first, count);
|
DrawArrays_Backend(mode, first, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,7 +689,321 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
DrawElements_Backend(mode, count, type, indices);
|
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. A slot
|
||||||
|
// of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs
|
||||||
|
// no binding.
|
||||||
|
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
||||||
|
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
||||||
|
if (program->GetTransformFeedbackStride(static_cast<Uint32>(i)) == 0) continue;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PauseTransformFeedback(void) {
|
||||||
|
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||||
|
MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Transform feedback is not active, or is already paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetTransformFeedbackPaused(true);
|
||||||
|
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
|
||||||
|
pauseXfb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResumeTransformFeedback(void) {
|
||||||
|
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetTransformFeedbackPaused(false);
|
||||||
|
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
|
||||||
|
resumeXfb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenTransformFeedbacks(GLsizei n, GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n == 0 || ids == nullptr) return;
|
||||||
|
Vector<Uint> names;
|
||||||
|
MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
|
||||||
|
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ids == nullptr) return;
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
const GLuint id = ids[i];
|
||||||
|
// Unknown names and 0 are silently ignored; an object whose capture span is
|
||||||
|
// still open is not (GL 4.6 core 13.2.1).
|
||||||
|
if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue;
|
||||||
|
if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() &&
|
||||||
|
MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Cannot delete a transform feedback object whose capture is active."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
|
||||||
|
deleteXfb(id);
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindTransformFeedback(GLenum target, GLuint id) {
|
||||||
|
if (target != GL_TRANSFORM_FEEDBACK) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A running capture pins its object; only a paused one may be swapped out.
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Transform feedback is active and not paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
std::to_string(id) + " is not a transform feedback object name."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->BindTransformFeedbackObject(id);
|
||||||
|
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
|
||||||
|
bindXfb(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLboolean IsTransformFeedback(GLuint id) {
|
||||||
|
// Name 0 is the default object, and a name glGenTransformFeedbacks handed out only
|
||||||
|
// becomes the name of an object once it has been bound.
|
||||||
|
return MG_State::pGLContext->IsTransformFeedbackObject(id) ? GL_TRUE : GL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object
|
||||||
|
// captured in its last completed span, as if by glDrawArraysInstanced with that count
|
||||||
|
// (GL 4.6 core 10.3.7).
|
||||||
|
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
|
||||||
|
GLsizei instancecount) {
|
||||||
|
if (!ValidateCurrentProgramForExecution(functionName)) return;
|
||||||
|
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
|
||||||
|
if (instancecount < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
std::to_string(id) + " is not a transform feedback object name."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
|
||||||
|
if (stream != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"stream must be less than GL_MAX_VERTEX_STREAMS."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Drawing from an object whose capture is currently open is legal and deliberate:
|
||||||
|
// it is how a transform feedback result is fed straight back into the next span
|
||||||
|
// (ARB_transform_feedback2 lists no such restriction).
|
||||||
|
if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"glEndTransformFeedback has never been called for this object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
|
||||||
|
if (vertices == 0) return;
|
||||||
|
const auto count = static_cast<GLsizei>(vertices);
|
||||||
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
|
if (instancecount == 1) {
|
||||||
|
DrawArrays_Backend(mode, 0, count);
|
||||||
|
} else {
|
||||||
|
DrawArraysInstanced_Backend(mode, 0, count, instancecount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedback(GLenum mode, GLuint id) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, stream, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -11,8 +11,21 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||||
|
void BeginTransformFeedback(GLenum primitiveMode);
|
||||||
|
void EndTransformFeedback(void);
|
||||||
|
void PauseTransformFeedback(void);
|
||||||
|
void ResumeTransformFeedback(void);
|
||||||
|
void GenTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||||
|
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
|
||||||
|
void BindTransformFeedback(GLenum target, GLuint id);
|
||||||
|
GLboolean IsTransformFeedback(GLuint id);
|
||||||
|
void DrawTransformFeedback(GLenum mode, GLuint id);
|
||||||
|
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount);
|
||||||
|
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream);
|
||||||
|
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
|
||||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||||
void DispatchComputeIndirect(GLintptr indirect);
|
void DispatchComputeIndirect(GLintptr indirect);
|
||||||
|
void PatchParameteri(GLenum pname, GLint value);
|
||||||
void MemoryBarrier(GLbitfield barriers);
|
void MemoryBarrier(GLbitfield barriers);
|
||||||
void MemoryBarrierByRegion(GLbitfield barriers);
|
void MemoryBarrierByRegion(GLbitfield barriers);
|
||||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||||
|
|||||||
@@ -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(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(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_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_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_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, 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, 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_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_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_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, 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, 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, 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)
|
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
|
||||||
@@ -292,21 +292,15 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterfv, GLuint sampler, GLenum pname,
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
DECLARE_GL_FUNCTION_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
||||||
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
|
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsTransformFeedback, id)
|
||||||
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
|
DECLARE_GL_FUNCTION_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PauseTransformFeedback)
|
||||||
// is both truthful and what the spec requires for a name that was never generated.
|
DECLARE_GL_FUNCTION_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ResumeTransformFeedback)
|
||||||
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
|
||||||
return GL_FALSE;
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
|
||||||
}
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
|
||||||
@@ -430,7 +424,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint locatio
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameteri, pname, value)
|
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
|
||||||
@@ -915,24 +909,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4ui, GLenum type, GLuint color) DECLAR
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1d, location, x)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1d, location, x)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2d, location, x, y)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2d, location, x, y)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3d, location, x, y, z)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3d, location, x, y, z)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformdv, program, location, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetUniformdv, program, location, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
|
||||||
@@ -942,28 +936,28 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQueryIndexed, target, index)
|
DECLARE_GL_FUNCTION_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQueryIndexed, target, index)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
|
||||||
@@ -988,8 +982,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLi
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||||
|
|||||||
@@ -22,9 +22,21 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
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();
|
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(
|
Bool HasDistinctCompleteDepthStencilTextureAttachments(
|
||||||
@@ -45,10 +57,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
|
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) {
|
const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||||
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
|
// 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) {
|
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.
|
// 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
|
// 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.
|
// 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);
|
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||||
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
||||||
@@ -79,8 +120,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Backend::FormatCapability::Creatable);
|
MG_Backend::FormatCapability::Creatable);
|
||||||
}
|
}
|
||||||
if (cachePopulated) {
|
if (cachePopulated) {
|
||||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
const Bool singleTarget = capabilityTargetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||||
++targetIndex) {
|
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],
|
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
|
||||||
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
||||||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
||||||
@@ -129,12 +173,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto& attachment = attachments[i];
|
const auto& attachment = attachments[i];
|
||||||
if (!attachment.IsValid()) continue;
|
if (!attachment.IsValid()) continue;
|
||||||
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
||||||
|
SizeT capabilityTargetIndex = MG_Backend::kFormatCapabilityTargetCount;
|
||||||
if (attachment.IsTexture() && attachment.GetTexture()) {
|
if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||||
format = attachment.GetTexture()->GetFormat();
|
format = attachment.GetTexture()->GetFormat();
|
||||||
|
capabilityTargetIndex =
|
||||||
|
MG_Backend::GetFormatCapabilityTargetIndex(attachment.GetTexture()->GetTarget());
|
||||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||||
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
||||||
|
capabilityTargetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||||
}
|
}
|
||||||
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
|
if (format != TextureInternalFormat::Unknown &&
|
||||||
|
!IsColorInternalFormatRenderable(format, capabilityTargetIndex)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +196,148 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
|
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,
|
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
|
||||||
TextureUploadTarget& outUploadTarget,
|
TextureUploadTarget& outUploadTarget,
|
||||||
Bool& outLayered) {
|
Bool& outLayered) {
|
||||||
@@ -412,6 +603,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
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;
|
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
|
||||||
FramebufferAttachmentType attachmentType = depthStencilAlias
|
FramebufferAttachmentType attachmentType = depthStencilAlias
|
||||||
? FramebufferAttachmentType::Depth
|
? FramebufferAttachmentType::Depth
|
||||||
@@ -428,20 +640,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool depthStencilMismatch = false;
|
||||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||||
if (!depthStencilAlias) {
|
if (!depthStencilAlias) {
|
||||||
return &framebufferObject->GetAttachment(attachmentType);
|
return &framebufferObject->GetAttachment(attachmentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
|
||||||
|
|
||||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
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;
|
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) {
|
switch (pname) {
|
||||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||||
@@ -505,6 +737,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: GL_FALSE;
|
: GL_FALSE;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||||
|
"GetFramebufferAttachmentParameteriv_State")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1422,8 +1658,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
if (IsActiveBackendDirectVulkan() &&
|
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
return GL_FRAMEBUFFER_COMPLETE;
|
return GL_FRAMEBUFFER_COMPLETE;
|
||||||
@@ -1450,8 +1686,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
if (IsActiveBackendDirectVulkan() &&
|
if (ActiveBackendRejectsDistinctDepthStencil() &&
|
||||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
|
||||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
return GL_FRAMEBUFFER_COMPLETE;
|
return GL_FRAMEBUFFER_COMPLETE;
|
||||||
@@ -1468,20 +1704,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||||
|
|
||||||
|
Bool depthStencilMismatch = false;
|
||||||
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
|
||||||
if (!depthStencilAlias) {
|
if (!depthStencilAlias) {
|
||||||
return &framebufferObject->GetAttachment(attachmentType);
|
return &framebufferObject->GetAttachment(attachmentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
|
||||||
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
|
|
||||||
|
|
||||||
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
|
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;
|
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) {
|
switch (pname) {
|
||||||
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
|
||||||
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
|
||||||
@@ -1527,6 +1783,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: GL_FALSE;
|
: GL_FALSE;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
|
||||||
|
caller)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
|||||||
@@ -213,8 +213,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
GLint maxSamples = 0;
|
GLint maxSamples = 0;
|
||||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||||
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
|
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
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;
|
return maxSamples;
|
||||||
}
|
}
|
||||||
@@ -465,6 +470,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_STENCIL_TEST:
|
case GL_STENCIL_TEST:
|
||||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
|
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -520,6 +533,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||||
return;
|
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:
|
case GL_DEPTH_CLEAR_VALUE:
|
||||||
params[0] = MG_State::pGLContext->GetClearDepth();
|
params[0] = MG_State::pGLContext->GetClearDepth();
|
||||||
return;
|
return;
|
||||||
@@ -1004,6 +1030,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case GL_DRAW_INDIRECT_BUFFER_BINDING: {
|
||||||
|
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||||
*params = 0; // debug-group entrypoints are stubbed
|
*params = 0; // debug-group entrypoints are stubbed
|
||||||
return;
|
return;
|
||||||
@@ -1886,6 +1917,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_SAMPLE_MASK_WORDS:
|
case GL_MAX_SAMPLE_MASK_WORDS:
|
||||||
*params = dynamicParameters.MaxSampleMaskWords;
|
*params = dynamicParameters.MaxSampleMaskWords;
|
||||||
break;
|
break;
|
||||||
|
case GL_PATCH_VERTICES:
|
||||||
|
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
|
||||||
|
break;
|
||||||
|
case GL_MAX_PATCH_VERTICES:
|
||||||
|
*params = dynamicParameters.MaxPatchVertices;
|
||||||
|
break;
|
||||||
|
case GL_MAX_TESS_GEN_LEVEL:
|
||||||
|
*params = dynamicParameters.MaxTessGenLevel;
|
||||||
|
break;
|
||||||
|
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||||
|
*params = dynamicParameters.MinProgramTextureGatherOffset;
|
||||||
|
break;
|
||||||
|
case GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||||
|
*params = dynamicParameters.MaxProgramTextureGatherOffset;
|
||||||
|
break;
|
||||||
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
||||||
break;
|
break;
|
||||||
@@ -1901,6 +1947,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
|
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
|
||||||
*params = kFrontendMaxTransformFeedbackSeparateComponents;
|
*params = kFrontendMaxTransformFeedbackSeparateComponents;
|
||||||
break;
|
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 = MG_State::pGLContext->IsTransformFeedbackPaused() ? 1 : 0;
|
||||||
|
break;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BINDING:
|
||||||
|
*params = static_cast<GLint>(MG_State::pGLContext->GetBoundTransformFeedbackName());
|
||||||
|
break;
|
||||||
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
||||||
*params = dynamicParameters.MaxTextureImageUnits;
|
*params = dynamicParameters.MaxTextureImageUnits;
|
||||||
break;
|
break;
|
||||||
@@ -1957,6 +2022,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_SUBPIXEL_BITS:
|
case GL_SUBPIXEL_BITS:
|
||||||
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
|
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
|
||||||
break;
|
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:
|
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
|
||||||
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
|
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "GL_Program.h"
|
#include "GL_Program.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
@@ -49,10 +51,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
static bool CheckProgramNameValidity(GLuint program) {
|
static bool CheckProgramNameValidity(GLuint program) {
|
||||||
if (!MG_State::pGLContext->ValidateProgramName(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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
error,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -317,11 +327,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DeleteProgram_State(GLuint program) {
|
void DeleteProgram_State(GLuint program) {
|
||||||
|
// "If program is zero, it is silently ignored" (GL 4.6 core 7.3) - unlike every
|
||||||
|
// other program entry point, where 0 is a name GL never handed out.
|
||||||
|
if (program == 0) return;
|
||||||
if (!CheckProgramNameValidity(program)) return;
|
if (!CheckProgramNameValidity(program)) return;
|
||||||
MG_State::pGLContext->MarkProgramForDeletion(program);
|
MG_State::pGLContext->MarkProgramForDeletion(program);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeleteShader_State(GLuint shader) {
|
void DeleteShader_State(GLuint shader) {
|
||||||
|
// Same silent-zero rule as glDeleteProgram (GL 4.6 core 7.1).
|
||||||
|
if (shader == 0) return;
|
||||||
if (!CheckShaderNameValidity(shader)) return;
|
if (!CheckShaderNameValidity(shader)) return;
|
||||||
MG_State::pGLContext->MarkShaderForDeletion(shader);
|
MG_State::pGLContext->MarkShaderForDeletion(shader);
|
||||||
}
|
}
|
||||||
@@ -605,6 +620,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
|
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
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
|
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||||
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -623,10 +650,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case GL_PROGRAM_BINARY_LENGTH:
|
case GL_PROGRAM_BINARY_LENGTH:
|
||||||
|
// No program binary format is exposed, so a program never has a retrievable
|
||||||
|
// binary and its length is zero (ARB_get_program_binary).
|
||||||
|
*params = 0;
|
||||||
|
break;
|
||||||
|
case GL_PROGRAM_BINARY_RETRIEVABLE_HINT:
|
||||||
|
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
|
||||||
|
break;
|
||||||
|
|
||||||
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_VERTICES_OUT:
|
||||||
case GL_GEOMETRY_INPUT_TYPE:
|
case GL_GEOMETRY_INPUT_TYPE:
|
||||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||||
@@ -801,7 +832,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, GLfloat>) {
|
if constexpr (std::is_same_v<T, GLfloat>) {
|
||||||
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
|
if (ttype->getBasicType() != glslang::EbtDouble && ttype->isMatrix() &&
|
||||||
|
ttype->getMatrixCols() == 3) {
|
||||||
auto* pBase = pUBO + offset;
|
auto* pBase = pUBO + offset;
|
||||||
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
||||||
Memcpy(reinterpret_cast<char*>(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i,
|
Memcpy(reinterpret_cast<char*>(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i,
|
||||||
@@ -811,9 +843,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A double-precision uniform is the one case where the stored component type can
|
||||||
|
// differ from the queried one for a non-opaque uniform, and the difference is not
|
||||||
|
// just a reinterpretation: it is twice as wide, so a raw copy would overrun the
|
||||||
|
// caller's buffer as well as return nonsense. Read component by component and let
|
||||||
|
// GL's conversion rules (7.6: round to nearest for the integer queries) apply.
|
||||||
|
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||||
|
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||||
|
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||||
|
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||||
|
// The slot the linker handed out is exactly `columns` columns wide, so it also
|
||||||
|
// states the column stride - which for a double matrix is not a float's 16 bytes.
|
||||||
|
const SizeT columnStride = columns > 0 ? size / static_cast<SizeT>(columns) : size;
|
||||||
|
for (Int column = 0; column < columns; ++column) {
|
||||||
|
for (Int row = 0; row < rows; ++row) {
|
||||||
|
GLdouble component = 0.0;
|
||||||
|
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble),
|
||||||
|
sizeof(component));
|
||||||
|
if constexpr (std::is_integral_v<T>) {
|
||||||
|
// Rounded to the nearest integer and clamped into the queried type's
|
||||||
|
// range, so a negative double read through glGetUniformuiv is 0
|
||||||
|
// rather than its two's complement.
|
||||||
|
const GLdouble rounded = std::nearbyint(component);
|
||||||
|
const GLdouble lowest = static_cast<GLdouble>(std::numeric_limits<T>::lowest());
|
||||||
|
const GLdouble highest = static_cast<GLdouble>(std::numeric_limits<T>::max());
|
||||||
|
params[column * rows + row] = static_cast<T>(std::clamp(rounded, lowest, highest));
|
||||||
|
} else {
|
||||||
|
params[column * rows + row] = static_cast<T>(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Memcpy(params, pUBO + offset, size);
|
Memcpy(params, pUBO + offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetUniformdv_State(GLuint program, GLint location, GLdouble* params) {
|
||||||
|
GetUniformScalar_State(program, location, params);
|
||||||
|
}
|
||||||
|
|
||||||
void GetUniformfv_State(GLuint program, GLint location, GLfloat* params) {
|
void GetUniformfv_State(GLuint program, GLint location, GLfloat* params) {
|
||||||
GetUniformScalar_State(program, location, params);
|
GetUniformScalar_State(program, location, params);
|
||||||
}
|
}
|
||||||
@@ -827,19 +896,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsProgram_State(GLuint program) {
|
GLboolean IsProgram_State(GLuint program) {
|
||||||
/* FIXME: Handle situations that:
|
// Deletion-flagged names stay valid while the object is still GL-visible (program in
|
||||||
* A program object marked for deletion with glDeleteProgram but still in use as part of current
|
// use, shader attached), so name validity is exactly the Is* answer.
|
||||||
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
|
|
||||||
*/
|
|
||||||
if (program == 0) return GL_FALSE;
|
if (program == 0) return GL_FALSE;
|
||||||
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
|
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsShader_State(GLuint shader) {
|
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;
|
if (shader == 0) return GL_FALSE;
|
||||||
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
|
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -849,6 +912,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
MGLOG_D("%s: linking program %d", __func__, program);
|
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 allowVSOnlyPrograms;
|
||||||
static Bool initialized = false;
|
static Bool initialized = false;
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
@@ -892,6 +967,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void UseProgram_State(GLuint program) {
|
void UseProgram_State(GLuint program) {
|
||||||
MGLOG_D("UseProgram_State: program=%u", program);
|
MGLOG_D("UseProgram_State: program=%u", program);
|
||||||
|
|
||||||
|
// The program in use may not change while transform feedback is active - unless
|
||||||
|
// the capture is paused, which is exactly what ARB_transform_feedback2 added the
|
||||||
|
// pause for (GL 4.6 core 7.3).
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
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) {
|
if (program == 0) {
|
||||||
MG_State::pGLContext->UseProgram(0);
|
MG_State::pGLContext->UseProgram(0);
|
||||||
return;
|
return;
|
||||||
@@ -1014,6 +1101,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared
|
||||||
|
// upload template - it is already typed on the component - but a matrix does: the
|
||||||
|
// column stride the linker used for a double matrix is not the 16 bytes a float one
|
||||||
|
// gets. It is not guessed here; the slot the uniform was given is exactly `columns`
|
||||||
|
// columns wide, so dividing states the stride the rest of the pipeline agreed on.
|
||||||
|
template <typename Program>
|
||||||
|
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value, Int columns, Int rows) {
|
||||||
|
const SizeT slotSize = programObject.GetUniformSizesInBytes(location);
|
||||||
|
const SizeT columnStride = columns > 0 ? slotSize / static_cast<SizeT>(columns) : slotSize;
|
||||||
|
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||||
|
Vector<GLdouble> column(static_cast<SizeT>(rows));
|
||||||
|
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||||
|
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||||
|
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||||
|
RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLdouble* source = value + matrix * componentCount;
|
||||||
|
for (Int c = 0; c < columns; ++c) {
|
||||||
|
for (Int r = 0; r < rows; ++r) {
|
||||||
|
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||||
|
}
|
||||||
|
Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride);
|
||||||
|
for (Int r = 1; r < rows; ++r) {
|
||||||
|
Uniform_State<1>(programObject, location + matrix, column.data() + r,
|
||||||
|
c * columnStride + r * sizeof(GLdouble));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to transpose a 2x2 matrix
|
// Helper function to transpose a 2x2 matrix
|
||||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||||
// Input matrix is in column-major order (OpenGL default)
|
// Input matrix is in column-major order (OpenGL default)
|
||||||
@@ -1822,6 +1941,312 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLuint v[] = {v0, v1, v2, v3};
|
GLuint v[] = {v0, v1, v2, v3};
|
||||||
Uniform4uiv(location, 1, v);
|
Uniform4uiv(location, 1, v);
|
||||||
}
|
}
|
||||||
|
void Uniform1d(GLint location, GLdouble v0) {
|
||||||
|
const GLdouble v[] = {v0};
|
||||||
|
Uniformv_State<1>(location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
Uniformv_State<1>(location, count, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) {
|
||||||
|
const GLdouble v[] = {v0};
|
||||||
|
ProgramUniformv_State<1>(program, location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
ProgramUniformv_State<1>(program, location, count, value);
|
||||||
|
}
|
||||||
|
void Uniform2d(GLint location, GLdouble v0, GLdouble v1) {
|
||||||
|
const GLdouble v[] = {v0, v1};
|
||||||
|
Uniformv_State<2>(location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
Uniformv_State<2>(location, count, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) {
|
||||||
|
const GLdouble v[] = {v0, v1};
|
||||||
|
ProgramUniformv_State<2>(program, location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
ProgramUniformv_State<2>(program, location, count, value);
|
||||||
|
}
|
||||||
|
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||||
|
const GLdouble v[] = {v0, v1, v2};
|
||||||
|
Uniformv_State<3>(location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
Uniformv_State<3>(location, count, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) {
|
||||||
|
const GLdouble v[] = {v0, v1, v2};
|
||||||
|
ProgramUniformv_State<3>(program, location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
ProgramUniformv_State<3>(program, location, count, value);
|
||||||
|
}
|
||||||
|
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||||
|
const GLdouble v[] = {v0, v1, v2, v3};
|
||||||
|
Uniformv_State<4>(location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
Uniformv_State<4>(location, count, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) {
|
||||||
|
const GLdouble v[] = {v0, v1, v2, v3};
|
||||||
|
ProgramUniformv_State<4>(program, location, 1, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) {
|
||||||
|
ProgramUniformv_State<4>(program, location, count, value);
|
||||||
|
}
|
||||||
|
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
|
||||||
|
}
|
||||||
|
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
|
||||||
|
}
|
||||||
|
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
|
||||||
|
}
|
||||||
|
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
|
||||||
|
}
|
||||||
|
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
|
||||||
|
}
|
||||||
|
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
|
||||||
|
}
|
||||||
|
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
|
||||||
|
}
|
||||||
|
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
|
||||||
|
}
|
||||||
|
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||||
|
if (programObject == nullptr) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "There is no current program object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLdouble* value) {
|
||||||
|
if (location == -1) return;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"program " + std::to_string(program) + " is not linked."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
|
||||||
|
}
|
||||||
|
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
|
||||||
|
GetUniformdv_State(program, location, params);
|
||||||
|
}
|
||||||
|
|
||||||
void Uniform1fv(GLint location, GLsizei count, const GLfloat* value) {
|
void Uniform1fv(GLint location, GLsizei count, const GLfloat* value) {
|
||||||
Uniform1fv_State(location, count, value);
|
Uniform1fv_State(location, count, value);
|
||||||
}
|
}
|
||||||
@@ -2217,4 +2642,150 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void ValidateProgram(GLuint program) {
|
void ValidateProgram(GLuint program) {
|
||||||
ValidateProgram_State(program);
|
ValidateProgram_State(program);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_get_program_binary with no supported binary format (GL_NUM_PROGRAM_BINARY_FORMATS
|
||||||
|
// is 0, which the extension explicitly allows). The three entry points below are what an
|
||||||
|
// application - and dEQP's function loader - reach through the extension; without it
|
||||||
|
// glProgramParameteri is not exposed in a 4.0 context at all.
|
||||||
|
void ProgramParameteri(GLuint program, GLenum pname, GLint value) {
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname is not an accepted value."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value != GL_TRUE && value != GL_FALSE) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "value must be GL_TRUE or GL_FALSE."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
programObject->SetBinaryRetrievableHint(value == GL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) {
|
||||||
|
(void)binaryFormat;
|
||||||
|
(void)binary;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (bufSize < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (length) *length = 0;
|
||||||
|
// GL_PROGRAM_BINARY_LENGTH is always zero here, which the spec makes an error to ask for.
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "The program has no retrievable binary."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) {
|
||||||
|
(void)binaryFormat;
|
||||||
|
(void)binary;
|
||||||
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
|
if (!programObject) return;
|
||||||
|
if (length < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "length must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No format is supported, so every binary is rejected - and the program's link status
|
||||||
|
// has to read FALSE afterwards.
|
||||||
|
programObject->MarkLinkFailedByProgramBinary();
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "binaryFormat is not a supported format."));
|
||||||
|
}
|
||||||
|
|
||||||
|
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] : "");
|
||||||
|
}
|
||||||
|
// ARB_transform_feedback3's special names only mean anything in an interleaved
|
||||||
|
// capture, and gl_NextBuffer cannot advance past the last capture buffer.
|
||||||
|
constexpr Uint maxTransformFeedbackBuffers = 4;
|
||||||
|
Uint nextBufferCount = 0;
|
||||||
|
for (const String& name : names) {
|
||||||
|
const Bool isNextBuffer = name == "gl_NextBuffer";
|
||||||
|
const Bool isSkipComponents = name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 &&
|
||||||
|
name[17] >= '1' && name[17] <= '4';
|
||||||
|
if (!isNextBuffer && !isSkipComponents) continue;
|
||||||
|
if (bufferMode != GL_INTERLEAVED_ATTRIBS) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"'" + name + "' requires GL_INTERLEAVED_ATTRIBS."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNextBuffer && ++nextBufferCount >= maxTransformFeedbackBuffers) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"More gl_NextBuffer entries than "
|
||||||
|
"GL_MAX_TRANSFORM_FEEDBACK_BUFFERS allows."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -137,5 +137,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||||
|
void Uniform1d(GLint location, GLdouble v0);
|
||||||
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0);
|
||||||
|
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform2d(GLint location, GLdouble v0, GLdouble v1);
|
||||||
|
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1);
|
||||||
|
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
|
||||||
|
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
|
||||||
|
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
|
||||||
|
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
|
||||||
|
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
|
||||||
void ValidateProgram(GLuint program);
|
void ValidateProgram(GLuint program);
|
||||||
|
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
|
||||||
|
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
|
||||||
|
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
|
||||||
|
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
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "GL_Query.h"
|
#include "GL_Query.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
@@ -27,6 +28,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Bool ended = false;
|
Bool ended = false;
|
||||||
Bool resultCached = false;
|
Bool resultCached = false;
|
||||||
Uint64 cachedResult = 0;
|
Uint64 cachedResult = 0;
|
||||||
|
// Transform feedback primitive counter at BeginQuery time.
|
||||||
|
Uint64 counterSnapshot = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query calls may arrive from any thread (launchers migrate the context
|
// Query calls may arrive from any thread (launchers migrate the context
|
||||||
@@ -41,6 +44,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLuint g_nextQueryId = 1;
|
GLuint g_nextQueryId = 1;
|
||||||
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
||||||
GLuint g_activeTimeElapsedQueryId = 0;
|
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() {
|
Bool TimerQueryDisabled() {
|
||||||
return MG_Config::Features.DisableTimerQuery;
|
return MG_Config::Features.DisableTimerQuery;
|
||||||
@@ -126,6 +134,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
outValue = 0;
|
outValue = 0;
|
||||||
return true;
|
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
|
// Final value produced (or no GetQueryResult64 hook: the
|
||||||
// query degrades to a zero result); the backend handle is
|
// query degrades to a zero result); the backend handle is
|
||||||
// consumed and the value cached for later reads.
|
// consumed and the value cached for later reads.
|
||||||
@@ -180,7 +193,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
QueryObject* queryObject = it->second;
|
QueryObject* queryObject = it->second;
|
||||||
if (queryObject->active) {
|
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 (queryObject->backendHandle) {
|
||||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
@@ -204,10 +234,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BeginQuery(GLenum target, GLuint id) {
|
void BeginQuery(GLenum target, GLuint id) {
|
||||||
if (target != GL_TIME_ELAPSED) {
|
const Bool isTransformFeedbackQuery =
|
||||||
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
|
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
|
||||||
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
|
const Bool isOcclusionQuery =
|
||||||
// BeginQuery target either.
|
(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.");
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,9 +256,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||||
return;
|
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__,
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
"A query is already active on GL_TIME_ELAPSED.");
|
"A query is already active on this target.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (queryObject->active) {
|
if (queryObject->active) {
|
||||||
@@ -239,25 +278,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||||
queryObject->target = target;
|
queryObject->target = target;
|
||||||
queryObject->active = true;
|
queryObject->active = true;
|
||||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
if (isTransformFeedbackQuery) {
|
||||||
queryObject->backendHandle =
|
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||||
g_activeTimeElapsedQueryId = id;
|
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) {
|
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.");
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
if (g_activeTimeElapsedQueryId == 0) {
|
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
|
? (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;
|
return;
|
||||||
}
|
}
|
||||||
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
|
auto* queryObject = FindQueryObjectLocked(activeQueryId);
|
||||||
if (!queryObject) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
EndTimeElapsedQueryLocked(queryObject);
|
EndTimeElapsedQueryLocked(queryObject);
|
||||||
@@ -303,9 +389,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_CURRENT_QUERY: {
|
case GL_CURRENT_QUERY: {
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
|
switch (target) {
|
||||||
// never are, and other targets remain unimplemented.
|
case GL_TIME_ELAPSED:
|
||||||
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
|
*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;
|
return;
|
||||||
}
|
}
|
||||||
case GL_QUERY_COUNTER_BITS: {
|
case GL_QUERY_COUNTER_BITS: {
|
||||||
@@ -313,7 +415,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||||
// entry points / timestamp valid bits at call time, not at table
|
// entry points / timestamp valid bits at call time, not at table
|
||||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
// 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 Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||||
const Bool supported =
|
const Bool supported =
|
||||||
@@ -359,4 +467,42 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
*params = static_cast<GLuint64>(value);
|
*params = static_cast<GLuint64>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The indexed query entry points differ from the plain ones only in the vertex
|
||||||
|
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
|
||||||
|
// for the two transform feedback targets and zero for every other target. With a
|
||||||
|
// single vertex stream both bounds are 1, so a valid call is always index 0 and
|
||||||
|
// forwards to the unindexed implementation.
|
||||||
|
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
|
||||||
|
const Bool perStreamTarget =
|
||||||
|
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||||
|
GLint maxVertexStreams = 1;
|
||||||
|
if (perStreamTarget) {
|
||||||
|
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||||
|
}
|
||||||
|
if (index < static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, function,
|
||||||
|
perStreamTarget ? "index is not less than GL_MAX_VERTEX_STREAMS."
|
||||||
|
: "index must be zero for this query target.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
BeginQuery(target, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndQueryIndexed(GLenum target, GLuint index) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
EndQuery(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
GetQueryiv(target, pname, params);
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void BeginQuery(GLenum target, GLuint id);
|
void BeginQuery(GLenum target, GLuint id);
|
||||||
void EndQuery(GLenum target);
|
void EndQuery(GLenum target);
|
||||||
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
|
||||||
|
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id);
|
||||||
|
void EndQueryIndexed(GLenum target, GLuint index);
|
||||||
|
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params);
|
||||||
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
||||||
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
||||||
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static thread_local Vector<GLuint> names;
|
static thread_local Vector<GLuint> names;
|
||||||
MG_State::pGLContext->GenSamplerNames(count, names);
|
MG_State::pGLContext->GenSamplerNames(count, names);
|
||||||
Memcpy(samplers, names.data(), count * sizeof(GLuint));
|
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) {
|
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.")); \
|
"The attachment specified by the read buffer is incomplete.")); \
|
||||||
return false; \
|
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);
|
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
|
||||||
} else if (isStencil) {
|
} else if (isStencil) {
|
||||||
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
|
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
|
||||||
|
|||||||
@@ -174,6 +174,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BufferObject::MarkGpuWritten() {
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferObject::SyncGpuWrites() {
|
||||||
|
if (!m_gpuWritePending) return;
|
||||||
|
// Cleared unconditionally: without a readback op the shadow can never catch up,
|
||||||
|
// and retrying on every subsequent read would only repeat the same no-op.
|
||||||
|
m_gpuWritePending = false;
|
||||||
|
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_bufferBackendOps->ReadbackFromGpu(*this);
|
||||||
|
}
|
||||||
|
|
||||||
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
|
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
|
||||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
||||||
"Cannot upload sub data while buffer is non-persistently mapped.");
|
"Cannot upload sub data while buffer is non-persistently mapped.");
|
||||||
@@ -204,11 +219,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
"Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset,
|
"Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset,
|
||||||
size, m_size);
|
size, m_size);
|
||||||
|
|
||||||
|
src->SyncGpuWrites();
|
||||||
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
||||||
NotifyContentWrite(dstOffset, size);
|
NotifyContentWrite(dstOffset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
|
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
|
||||||
|
SyncGpuWrites();
|
||||||
if (markMapped) {
|
if (markMapped) {
|
||||||
m_isMapped = true;
|
m_isMapped = true;
|
||||||
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
|
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
|
||||||
@@ -231,10 +248,29 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_resource.Bytes();
|
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) {
|
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
|
||||||
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
||||||
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
||||||
range.end, m_size);
|
range.end, m_size);
|
||||||
|
// The app is about to look at the bytes; a shader may have rewritten them since
|
||||||
|
// the shadow was last authoritative. Also needed for a write map without an
|
||||||
|
// invalidate bit, whose staging copy is seeded from the shadow.
|
||||||
|
SyncGpuWrites();
|
||||||
m_isMapped = true;
|
m_isMapped = true;
|
||||||
m_mappingAccess = access;
|
m_mappingAccess = access;
|
||||||
m_mappedRange = range;
|
m_mappedRange = range;
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ namespace MobileGL {
|
|||||||
// Must be idempotent: a second call for an already-backed buffer returns the
|
// Must be idempotent: a second call for an already-backed buffer returns the
|
||||||
// same base pointer.
|
// same base pointer.
|
||||||
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
|
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
|
||||||
|
// Pulls the backend's current contents for the whole buffer into the shadow
|
||||||
|
// (through WritebackFromBackend). Only ever called for a buffer the GPU may
|
||||||
|
// have written behind the frontend's back - a shader storage or atomic counter
|
||||||
|
// binding of a draw or dispatch - because nothing else can desynchronise the
|
||||||
|
// shadow. Backends that cannot read their storage back leave this null; the
|
||||||
|
// shadow then keeps its pre-dispatch bytes, which is the old behaviour.
|
||||||
|
void (*ReadbackFromGpu)(BufferObject& bufferObject) = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registered by the active backend at init, cleared at shutdown.
|
// Registered by the active backend at init, cleared at shutdown.
|
||||||
@@ -133,6 +140,11 @@ namespace MobileGL {
|
|||||||
|
|
||||||
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
|
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
|
||||||
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
|
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 ReleaseMemory();
|
||||||
void FlushMemoryRange(SizeT offset, SizeT length);
|
void FlushMemoryRange(SizeT offset, SizeT length);
|
||||||
|
|
||||||
@@ -144,6 +156,16 @@ namespace MobileGL {
|
|||||||
// backend op: the backend storage already holds these bytes.
|
// backend op: the backend storage already holds these bytes.
|
||||||
void WritebackFromBackend(DataPtr data, SizeT atOffset);
|
void WritebackFromBackend(DataPtr data, SizeT atOffset);
|
||||||
|
|
||||||
|
// A draw or dispatch just ran with this buffer bound where a shader can write
|
||||||
|
// it (shader storage / atomic counter). The next read has to reconcile with
|
||||||
|
// that: pull the bytes back, or - when the shadow already IS coherent GPU
|
||||||
|
// memory - wait for the work that wrote them to retire. Which of the two is
|
||||||
|
// the backend's business; the flag only says a GPU write is outstanding.
|
||||||
|
void MarkGpuWritten();
|
||||||
|
// Refreshes the shadow from the backend when a GPU write is outstanding. Called
|
||||||
|
// from every path that reads the shadow on the app's behalf.
|
||||||
|
void SyncGpuWrites();
|
||||||
|
|
||||||
Bool IsMapped() const;
|
Bool IsMapped() const;
|
||||||
Bool IsImmutableStorage() const;
|
Bool IsImmutableStorage() const;
|
||||||
SizeT GetSize() const;
|
SizeT GetSize() const;
|
||||||
@@ -191,6 +213,8 @@ namespace MobileGL {
|
|||||||
Bool m_isImmutableStorage = false;
|
Bool m_isImmutableStorage = false;
|
||||||
GLbitfield m_storageFlags = 0;
|
GLbitfield m_storageFlags = 0;
|
||||||
Uint64 m_changeSerial = 0;
|
Uint64 m_changeSerial = 0;
|
||||||
|
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||||
|
Bool m_gpuWritePending = false;
|
||||||
Range1D m_mappedRange;
|
Range1D m_mappedRange;
|
||||||
Vector<Uint8> m_stagingData;
|
Vector<Uint8> m_stagingData;
|
||||||
Bool m_ownsStagingData;
|
Bool m_ownsStagingData;
|
||||||
|
|||||||
@@ -241,6 +241,32 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||||
|
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
|
||||||
|
// that is currently bound acts as if FramebufferTexture* had been called with texture
|
||||||
|
// zero for every attachment point it occupied there. Framebuffers that are NOT bound
|
||||||
|
// keep the orphaned attachment, so only the bound ones are touched.
|
||||||
|
//
|
||||||
|
// Without this the framebuffer object goes on holding the deleted texture alive as its
|
||||||
|
// attachment, and a later read through that framebuffer returns the dead texture's
|
||||||
|
// contents rather than those of whatever the application put in its place - the name
|
||||||
|
// it deleted usually comes straight back from the next glGenTextures, so the two are
|
||||||
|
// indistinguishable from the outside (KHR-GL32.packed_pixels read a stale gradient).
|
||||||
|
if (const auto& textureObject = m_textureState.GetTextureObject(index)) {
|
||||||
|
for (SizeT targetIndex = 0; targetIndex < SizeT(FramebufferTarget::FramebufferTargetCount);
|
||||||
|
++targetIndex) {
|
||||||
|
const auto& framebuffer =
|
||||||
|
GetFramebufferBindingSlot(static_cast<FramebufferTarget>(targetIndex)).GetBoundObject();
|
||||||
|
if (!framebuffer || framebuffer->IsDefaultFramebuffer()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& attachments = framebuffer->GetAllAttachmentObjects();
|
||||||
|
for (SizeT i = 0; i < attachments.size(); ++i) {
|
||||||
|
if (attachments[i].IsTexture() && attachments[i].GetTexture() == textureObject) {
|
||||||
|
framebuffer->Detach(static_cast<FramebufferAttachmentType>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +424,14 @@ namespace MobileGL::MG_State {
|
|||||||
m_renderState.SetPointSize(size);
|
m_renderState.SetPointSize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLContext::SetPatchVertices(Uint vertices) {
|
||||||
|
m_renderState.SetPatchVertices(vertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint GLContext::GetPatchVertices() const {
|
||||||
|
return m_renderState.GetPatchVertices();
|
||||||
|
}
|
||||||
|
|
||||||
Float GLContext::GetPointSize() const {
|
Float GLContext::GetPointSize() const {
|
||||||
return m_renderState.GetPointSize();
|
return m_renderState.GetPointSize();
|
||||||
}
|
}
|
||||||
@@ -719,6 +753,95 @@ namespace MobileGL::MG_State {
|
|||||||
Bool GLContext::ValidateRenderbufferObject(Uint index) const {
|
Bool GLContext::ValidateRenderbufferObject(Uint index) const {
|
||||||
return m_renderbufferState.ValidateRenderbufferObject(index);
|
return m_renderbufferState.ValidateRenderbufferObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLContext::SaveBoundTransformFeedbackState() {
|
||||||
|
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
|
||||||
|
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
|
||||||
|
const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
|
||||||
|
object.bindings[i] = {point.GetBoundObject(), point.GetRange(), point.HasExplicitRange()};
|
||||||
|
}
|
||||||
|
object.active = m_transformFeedbackActive;
|
||||||
|
object.paused = m_transformFeedbackPaused;
|
||||||
|
object.primitiveMode = m_transformFeedbackPrimitiveMode;
|
||||||
|
object.program = m_transformFeedbackProgram;
|
||||||
|
object.generation = m_transformFeedbackGeneration;
|
||||||
|
object.capturedVertices = m_transformFeedbackCapturedVertices;
|
||||||
|
object.inputPrimitives = m_transformFeedbackInputPrimitives;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLContext::RestoreBoundTransformFeedbackState() {
|
||||||
|
const auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
|
||||||
|
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
|
||||||
|
auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, i);
|
||||||
|
point.Bind(object.bindings[i].buffer);
|
||||||
|
if (object.bindings[i].buffer) {
|
||||||
|
point.SetRange(object.bindings[i].range, object.bindings[i].hasExplicitRange);
|
||||||
|
} else {
|
||||||
|
point.ClearRange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_transformFeedbackActive = object.active;
|
||||||
|
m_transformFeedbackPaused = object.paused;
|
||||||
|
m_transformFeedbackPrimitiveMode = object.primitiveMode;
|
||||||
|
m_transformFeedbackProgram = object.program;
|
||||||
|
// The generation identifies one capture span, and a span belongs to the object
|
||||||
|
// that opened it - a backend keys its append state on it, so switching objects
|
||||||
|
// has to bring the right one back.
|
||||||
|
m_transformFeedbackGeneration = object.generation;
|
||||||
|
m_transformFeedbackCapturedVertices = object.capturedVertices;
|
||||||
|
m_transformFeedbackInputPrimitives = object.inputPrimitives;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLContext::GenTransformFeedbackNames(Uint number, Vector<Uint>& ids) {
|
||||||
|
ids.resize(number);
|
||||||
|
if (number == 0) return;
|
||||||
|
m_transformFeedbackNames.Generate(number, ids.data());
|
||||||
|
// A generated name already denotes an object with the default state, so that a
|
||||||
|
// bind never has to distinguish "first use" from any later one.
|
||||||
|
for (const Uint id : ids) {
|
||||||
|
m_transformFeedbackObjects[id] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GLContext::ValidateTransformFeedbackName(Uint index) const {
|
||||||
|
return index == 0 || m_transformFeedbackNames.IsValid(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLContext::BindTransformFeedbackObject(Uint index) {
|
||||||
|
if (index == m_boundTransformFeedback) return;
|
||||||
|
SaveBoundTransformFeedbackState();
|
||||||
|
m_boundTransformFeedback = index;
|
||||||
|
m_transformFeedbackObjects[index].everBound = true;
|
||||||
|
RestoreBoundTransformFeedbackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GLContext::IsTransformFeedbackObject(Uint index) const {
|
||||||
|
if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return false;
|
||||||
|
const auto it = m_transformFeedbackObjects.find(index);
|
||||||
|
return it != m_transformFeedbackObjects.end() && it->second.everBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) {
|
||||||
|
if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return;
|
||||||
|
// Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1);
|
||||||
|
// its state is dropped rather than saved back into the dying object.
|
||||||
|
if (index == m_boundTransformFeedback) {
|
||||||
|
m_boundTransformFeedback = 0;
|
||||||
|
RestoreBoundTransformFeedbackState();
|
||||||
|
}
|
||||||
|
m_transformFeedbackObjects.erase(index);
|
||||||
|
m_transformFeedbackNames.Delete(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 GLContext::GetTransformFeedbackRecordedVertices(Uint index) const {
|
||||||
|
const auto it = m_transformFeedbackObjects.find(index);
|
||||||
|
return it == m_transformFeedbackObjects.end() ? 0 : it->second.recordedVertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GLContext::HasTransformFeedbackCompletedSpan(Uint index) const {
|
||||||
|
const auto it = m_transformFeedbackObjects.find(index);
|
||||||
|
return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan;
|
||||||
|
}
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ namespace MobileGL {
|
|||||||
Float GetLineWidth() const;
|
Float GetLineWidth() const;
|
||||||
void SetPointSize(Float size);
|
void SetPointSize(Float size);
|
||||||
Float GetPointSize() const;
|
Float GetPointSize() const;
|
||||||
|
void SetPatchVertices(Uint vertices);
|
||||||
|
Uint GetPatchVertices() const;
|
||||||
void SetPolygonOffset(Float factor, Float units);
|
void SetPolygonOffset(Float factor, Float units);
|
||||||
Float GetPolygonOffsetFactor() const;
|
Float GetPolygonOffsetFactor() const;
|
||||||
Float GetPolygonOffsetUnits() const;
|
Float GetPolygonOffsetUnits() const;
|
||||||
@@ -211,6 +213,92 @@ namespace MobileGL {
|
|||||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||||
|
|
||||||
|
// Transform feedback. The fields below are the state of the transform
|
||||||
|
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||||
|
// block further down for how a bind swaps them.
|
||||||
|
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
|
||||||
|
m_transformFeedbackActive = true;
|
||||||
|
m_transformFeedbackPaused = false;
|
||||||
|
m_transformFeedbackPrimitiveMode = primitiveMode;
|
||||||
|
m_transformFeedbackProgram = program;
|
||||||
|
m_transformFeedbackGeneration = ++m_transformFeedbackNextGeneration;
|
||||||
|
m_transformFeedbackCapturedVertices = 0;
|
||||||
|
m_transformFeedbackInputPrimitives = 0;
|
||||||
|
}
|
||||||
|
void EndTransformFeedback() {
|
||||||
|
m_transformFeedbackActive = false;
|
||||||
|
m_transformFeedbackPaused = false;
|
||||||
|
m_transformFeedbackProgram.reset();
|
||||||
|
// What glDrawTransformFeedback on this object replays from now on.
|
||||||
|
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
|
||||||
|
object.recordedVertices = m_transformFeedbackCapturedVertices;
|
||||||
|
object.hasCompletedSpan = true;
|
||||||
|
}
|
||||||
|
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
|
||||||
|
Bool IsTransformFeedbackPaused() const { return m_transformFeedbackPaused; }
|
||||||
|
void SetTransformFeedbackPaused(Bool paused) { m_transformFeedbackPaused = paused; }
|
||||||
|
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; }
|
||||||
|
// Primitives a draw assembled while the capture was paused. GL counts those in
|
||||||
|
// PRIMITIVES_GENERATED, but a backend that answers the query with its own
|
||||||
|
// transform feedback counter cannot see them - nothing was being captured.
|
||||||
|
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
|
||||||
|
m_transformFeedbackPausedPrimitiveCounter += primitives;
|
||||||
|
}
|
||||||
|
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
|
||||||
|
return m_transformFeedbackPausedPrimitiveCounter;
|
||||||
|
}
|
||||||
|
// 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; }
|
||||||
|
|
||||||
|
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
||||||
|
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
||||||
|
// binding points are object state, but the context keeps exactly one live
|
||||||
|
// copy of both so that every existing reader - the backends' per-draw sync,
|
||||||
|
// the drawing and getter paths - needs no notion of which object owns them.
|
||||||
|
// A bind therefore saves the live copy into the outgoing object and restores
|
||||||
|
// the incoming one's. Object 0 is the default object and always exists.
|
||||||
|
static constexpr Uint MAX_TRANSFORM_FEEDBACK_BUFFERS = 4;
|
||||||
|
void GenTransformFeedbackNames(Uint number, Vector<Uint>& ids);
|
||||||
|
// A name glGenTransformFeedbacks handed out and glDeleteTransformFeedbacks
|
||||||
|
// has not taken back. Name 0 is always valid.
|
||||||
|
Bool ValidateTransformFeedbackName(Uint index) const;
|
||||||
|
// What glIsTransformFeedback reports: a generated name only becomes the name
|
||||||
|
// of an object once it has been bound at least once (GL 4.6 core 13.2.1).
|
||||||
|
Bool IsTransformFeedbackObject(Uint index) const;
|
||||||
|
void BindTransformFeedbackObject(Uint index);
|
||||||
|
void MarkTransformFeedbackObjectForDeletion(Uint index);
|
||||||
|
Uint GetBoundTransformFeedbackName() const { return m_boundTransformFeedback; }
|
||||||
|
// Vertices the object captured in its last completed span; the vertex count
|
||||||
|
// glDrawTransformFeedback replays.
|
||||||
|
Uint64 GetTransformFeedbackRecordedVertices(Uint index) const;
|
||||||
|
// Whether the object has ever completed a capture span. glDrawTransformFeedback
|
||||||
|
// on an object that has not is INVALID_OPERATION, which a zero vertex count
|
||||||
|
// cannot express: an empty completed span is legal and draws nothing.
|
||||||
|
Bool HasTransformFeedbackCompletedSpan(Uint index) const;
|
||||||
|
|
||||||
// Framebuffer
|
// Framebuffer
|
||||||
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
||||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||||
@@ -243,6 +331,46 @@ namespace MobileGL {
|
|||||||
BufferState m_bufferState;
|
BufferState m_bufferState;
|
||||||
VertexArrayState m_vertexArrayState;
|
VertexArrayState m_vertexArrayState;
|
||||||
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
|
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
|
||||||
|
Bool m_transformFeedbackActive = false;
|
||||||
|
Bool m_transformFeedbackPaused = false;
|
||||||
|
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||||
|
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||||
|
Uint64 m_transformFeedbackGeneration = 0;
|
||||||
|
// Source of the per-span ids above; never rolls back with an object switch.
|
||||||
|
Uint64 m_transformFeedbackNextGeneration = 0;
|
||||||
|
// Not object state: the transform feedback queries snapshot it at BeginQuery
|
||||||
|
// and take the delta at EndQuery, which spans whatever objects were used.
|
||||||
|
Uint64 m_transformFeedbackPrimitiveCounter = 0;
|
||||||
|
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
|
||||||
|
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||||
|
Uint64 m_transformFeedbackInputPrimitives = 0;
|
||||||
|
|
||||||
|
// Everything a transform feedback object owns while it is NOT the bound one.
|
||||||
|
struct TransformFeedbackObjectState {
|
||||||
|
struct SavedBufferBinding {
|
||||||
|
SharedPtr<BufferObject> buffer;
|
||||||
|
Range1D range;
|
||||||
|
Bool hasExplicitRange = false;
|
||||||
|
};
|
||||||
|
Array<SavedBufferBinding, MAX_TRANSFORM_FEEDBACK_BUFFERS> bindings;
|
||||||
|
Bool active = false;
|
||||||
|
Bool paused = false;
|
||||||
|
GLenum primitiveMode = GL_POINTS;
|
||||||
|
SharedPtr<ProgramObject> program;
|
||||||
|
Uint64 generation = 0;
|
||||||
|
Uint64 capturedVertices = 0;
|
||||||
|
Uint64 inputPrimitives = 0;
|
||||||
|
Uint64 recordedVertices = 0;
|
||||||
|
Bool hasCompletedSpan = false;
|
||||||
|
Bool everBound = false;
|
||||||
|
};
|
||||||
|
void SaveBoundTransformFeedbackState();
|
||||||
|
void RestoreBoundTransformFeedbackState();
|
||||||
|
// operator[] materialises an entry with the default state on first touch, so
|
||||||
|
// the default object (name 0) needs no seeding here.
|
||||||
|
UnorderedMap<Uint, TransformFeedbackObjectState> m_transformFeedbackObjects;
|
||||||
|
IndexGenerator<Uint> m_transformFeedbackNames;
|
||||||
|
Uint m_boundTransformFeedback = 0;
|
||||||
TextureState m_textureState;
|
TextureState m_textureState;
|
||||||
ProgramState m_programState;
|
ProgramState m_programState;
|
||||||
RenderState m_renderState;
|
RenderState m_renderState;
|
||||||
|
|||||||
@@ -170,9 +170,265 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_uniformNameMaxLength = 0;
|
m_uniformNameMaxLength = 0;
|
||||||
m_attribInNameMaxLength = 0;
|
m_attribInNameMaxLength = 0;
|
||||||
m_uniformBlockNameMaxLength = 0;
|
m_uniformBlockNameMaxLength = 0;
|
||||||
|
m_xfbVaryings.clear();
|
||||||
|
m_xfbStrides.clear();
|
||||||
|
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||||
|
m_xfbVaryingNameMaxLength = 0;
|
||||||
|
m_xfbNeedsScatteredCapture = false;
|
||||||
|
m_xfbPackedStride = 0;
|
||||||
|
m_gsInputPrimitive = GL_NONE;
|
||||||
m_linkStatus = false;
|
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;
|
||||||
|
m_xfbNeedsScatteredCapture = false;
|
||||||
|
m_xfbPackedStride = 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;
|
||||||
|
// ARB_transform_feedback3 lets an interleaved capture leave holes (gl_SkipComponents1..4)
|
||||||
|
// and move on to the next buffer (gl_NextBuffer). Both only affect where the following
|
||||||
|
// varyings land, so they are consumed here and never become XfbVaryings of their own -
|
||||||
|
// which also keeps them out of the name list a backend declares on its own driver.
|
||||||
|
Uint32 interleavedBufferIndex = 0;
|
||||||
|
Vector<Uint32> interleavedStrides;
|
||||||
|
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
|
||||||
|
const String& name = m_requestedXfbVaryings[i];
|
||||||
|
if (interleaved && name == "gl_NextBuffer") {
|
||||||
|
interleavedStrides.push_back(interleavedOffset);
|
||||||
|
interleavedOffset = 0;
|
||||||
|
++interleavedBufferIndex;
|
||||||
|
m_xfbNeedsScatteredCapture = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (interleaved && name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 &&
|
||||||
|
name[17] >= '1' && name[17] <= '4') {
|
||||||
|
interleavedOffset += static_cast<Uint32>(name[17] - '0') * 4;
|
||||||
|
m_xfbNeedsScatteredCapture = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
varying.packedOffsetBytes = m_xfbPackedStride;
|
||||||
|
m_xfbPackedStride += varying.byteSize;
|
||||||
|
if (interleaved) {
|
||||||
|
varying.bufferIndex = interleavedBufferIndex;
|
||||||
|
varying.offsetBytes = interleavedOffset;
|
||||||
|
interleavedOffset += varying.byteSize;
|
||||||
|
} else {
|
||||||
|
varying.bufferIndex = static_cast<Uint32>(m_xfbVaryings.size());
|
||||||
|
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;
|
||||||
|
constexpr Uint32 kMaxTransformFeedbackBuffers = 4;
|
||||||
|
if (interleaved) {
|
||||||
|
interleavedStrides.push_back(interleavedOffset);
|
||||||
|
if (interleavedStrides.size() > kMaxTransformFeedbackBuffers) {
|
||||||
|
m_infoLog = "Transform feedback capture uses more buffers than "
|
||||||
|
"GL_MAX_TRANSFORM_FEEDBACK_BUFFERS.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const Uint32 stride : interleavedStrides) {
|
||||||
|
if (stride > kMaxInterleavedComponents * 4) {
|
||||||
|
m_infoLog = "Transform feedback interleaved capture exceeds "
|
||||||
|
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_xfbStrides = Move(interleavedStrides);
|
||||||
|
} 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) {
|
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
|
||||||
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
|
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
|
||||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
|
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
|
||||||
@@ -321,12 +577,32 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
|
||||||
|
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
|
||||||
|
m_gsInputPrimitive = GL_NONE;
|
||||||
|
if (const glslang::TIntermediate* gs = m_program->getIntermediate(EShLangGeometry)) {
|
||||||
|
switch (gs->getInputPrimitive()) {
|
||||||
|
case glslang::ElgPoints: m_gsInputPrimitive = GL_POINTS; break;
|
||||||
|
case glslang::ElgLines: m_gsInputPrimitive = GL_LINES; break;
|
||||||
|
case glslang::ElgLinesAdjacency: m_gsInputPrimitive = GL_LINES_ADJACENCY; break;
|
||||||
|
case glslang::ElgTriangles: m_gsInputPrimitive = GL_TRIANGLES; break;
|
||||||
|
case glslang::ElgTrianglesAdjacency: m_gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Starting reflection", m_externalIndex);
|
MGLOG_D("ProgramObject %u: Starting reflection", m_externalIndex);
|
||||||
DoReflection();
|
DoReflection();
|
||||||
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)m_linkStatus);
|
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)m_linkStatus);
|
||||||
if (!ValidateFragmentOutputLocations()) {
|
if (!ValidateFragmentOutputLocations()) {
|
||||||
return;
|
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);
|
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
|
||||||
GenerateBinary();
|
GenerateBinary();
|
||||||
|
|||||||
@@ -377,6 +377,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||||
Bool GetLinkStatus() const { return m_linkStatus; }
|
Bool GetLinkStatus() const { return m_linkStatus; }
|
||||||
|
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format
|
||||||
|
// (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
|
||||||
|
// ARB_get_program_binary requires of it.
|
||||||
|
Bool GetBinaryRetrievableHint() const { return m_binaryRetrievableHint; }
|
||||||
|
void SetBinaryRetrievableHint(Bool hint) { m_binaryRetrievableHint = hint; }
|
||||||
|
// glProgramBinary always fails here (there is no format it could accept) and the
|
||||||
|
// spec then requires the program's LINK_STATUS to read FALSE.
|
||||||
|
void MarkLinkFailedByProgramBinary() {
|
||||||
|
ResetLinkArtifacts();
|
||||||
|
m_infoLog = "No program binary format is supported.";
|
||||||
|
}
|
||||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||||
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
|
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
|
||||||
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
|
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
|
||||||
@@ -465,6 +476,55 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
|
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
|
||||||
|
// Offset within the gap-free record a backend that cannot express the GL
|
||||||
|
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||||
|
Uint32 packedOffsetBytes = 0;
|
||||||
|
};
|
||||||
|
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 layout uses gl_SkipComponents / gl_NextBuffer
|
||||||
|
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every
|
||||||
|
// captured varying into one record with no gaps. A backend that captures through
|
||||||
|
// such a driver has to capture into scratch storage and scatter the records into the
|
||||||
|
// application's buffers itself, using packedOffsetBytes as the source offset and
|
||||||
|
// (bufferIndex, offsetBytes, stride) as the destination.
|
||||||
|
Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; }
|
||||||
|
// Bytes one gap-free captured record occupies.
|
||||||
|
Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; }
|
||||||
|
// 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; }
|
||||||
|
// GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES,
|
||||||
|
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
|
||||||
|
// program has no geometry stage. Draws must present a compatible primitive type.
|
||||||
|
GLenum GetGeometryInputType() const { return m_gsInputPrimitive; }
|
||||||
|
|
||||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
// 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
|
// name (external index), which is freed to a LIFO list and immediately handed back by
|
||||||
@@ -475,6 +535,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
private:
|
private:
|
||||||
void ResetLinkArtifacts();
|
void ResetLinkArtifacts();
|
||||||
void DoReflection();
|
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 GenerateBinary();
|
||||||
void WaitUntilGenerationCompleted() const;
|
void WaitUntilGenerationCompleted() const;
|
||||||
void AddDefaultFragmentShaderIfMissing();
|
void AddDefaultFragmentShaderIfMissing();
|
||||||
@@ -539,6 +604,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
String m_infoLog;
|
String m_infoLog;
|
||||||
Bool m_deleteStatus = false;
|
Bool m_deleteStatus = false;
|
||||||
Bool m_linkStatus = false;
|
Bool m_linkStatus = false;
|
||||||
|
Bool m_binaryRetrievableHint = false;
|
||||||
Bool m_validateStatus = true;
|
Bool m_validateStatus = true;
|
||||||
Uint32 m_backendStateVersion = 0;
|
Uint32 m_backendStateVersion = 0;
|
||||||
|
|
||||||
@@ -558,5 +624,18 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||||
Uint32 m_uboContentVersion = 0;
|
Uint32 m_uboContentVersion = 0;
|
||||||
Uint32 m_linkVersion = 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_gsInputPrimitive = GL_NONE;
|
||||||
|
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||||
|
Int m_xfbVaryingNameMaxLength = 0;
|
||||||
|
Bool m_xfbNeedsScatteredCapture = false;
|
||||||
|
Uint32 m_xfbPackedStride = 0;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // 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
|
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||||
auto& programObject = m_programObjects[program];
|
auto& programObject = m_programObjects[program];
|
||||||
if (programObject != nullptr) {
|
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->MarkAsDeleted();
|
||||||
programObject.reset();
|
// A program in use is only FLAGGED: its name (and every program query) stays
|
||||||
m_programIndexGenerator.Delete(program);
|
// valid until it stops being current, at which point UseProgram finishes the job.
|
||||||
for (const auto& shader : attachedShaders) {
|
if (programObject == m_currentProgram) return;
|
||||||
const Uint shaderName = shader->GetExternalIndex();
|
DestroyProgramSlot(program);
|
||||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
}
|
||||||
ReleaseShaderNameIfOrphaned(shaderName);
|
}
|
||||||
}
|
|
||||||
|
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) {
|
void ProgramState::UseProgram(Uint program) {
|
||||||
|
const SharedPtr<ProgramObject> previous = m_currentProgram;
|
||||||
|
|
||||||
if (program == 0) m_currentProgram.reset();
|
if (program == 0) m_currentProgram.reset();
|
||||||
|
|
||||||
if (!CheckIndexAvail(program, m_programObjects)) return;
|
if (CheckIndexAvail(program, m_programObjects)) {
|
||||||
m_currentProgram = m_programObjects[program];
|
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) {
|
Uint ProgramState::CreateShader(ShaderStage stage) {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
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>
|
template <typename T>
|
||||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
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
|
// Compile for OpenGL here, so that we can do validation and link
|
||||||
// like a real OpenGL driver at linking stage
|
// like a real OpenGL driver at linking stage
|
||||||
// Will compile for other backends later.
|
// Will compile for other backends later.
|
||||||
|
|||||||
@@ -154,6 +154,17 @@ namespace MobileGL {
|
|||||||
return m_parameters.PointSize;
|
return m_parameters.PointSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderState::SetPatchVertices(Uint vertices) {
|
||||||
|
if (m_parameters.PatchVertices == vertices) return;
|
||||||
|
|
||||||
|
m_parameters.PatchVertices = vertices;
|
||||||
|
++m_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint RenderState::GetPatchVertices() const {
|
||||||
|
return m_parameters.PatchVertices;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderState::SetPolygonOffset(Float factor, Float units) {
|
void RenderState::SetPolygonOffset(Float factor, Float units) {
|
||||||
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
|
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ namespace MobileGL {
|
|||||||
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||||
Float LineWidth = 1.0f;
|
Float LineWidth = 1.0f;
|
||||||
Float PointSize = 1.0f;
|
Float PointSize = 1.0f;
|
||||||
|
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||||
|
Uint PatchVertices = 3;
|
||||||
Float PolygonOffsetFactor = 0.0f;
|
Float PolygonOffsetFactor = 0.0f;
|
||||||
Float PolygonOffsetUnits = 0.0f;
|
Float PolygonOffsetUnits = 0.0f;
|
||||||
|
|
||||||
@@ -319,6 +321,8 @@ namespace MobileGL {
|
|||||||
Float GetLineWidth() const;
|
Float GetLineWidth() const;
|
||||||
void SetPointSize(Float size);
|
void SetPointSize(Float size);
|
||||||
Float GetPointSize() const;
|
Float GetPointSize() const;
|
||||||
|
void SetPatchVertices(Uint vertices);
|
||||||
|
Uint GetPatchVertices() const;
|
||||||
void SetPolygonOffset(Float factor, Float units);
|
void SetPolygonOffset(Float factor, Float units);
|
||||||
Float GetPolygonOffsetFactor() const;
|
Float GetPolygonOffsetFactor() const;
|
||||||
Float GetPolygonOffsetUnits() const;
|
Float GetPolygonOffsetUnits() const;
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ namespace MobileGL {
|
|||||||
DepthComponent32F,
|
DepthComponent32F,
|
||||||
Depth24Stencil8,
|
Depth24Stencil8,
|
||||||
Depth32FStencil8,
|
Depth32FStencil8,
|
||||||
|
StencilIndex8,
|
||||||
|
|
||||||
DepthComponent,
|
DepthComponent,
|
||||||
DepthStencil,
|
DepthStencil,
|
||||||
|
|||||||
@@ -24,6 +24,19 @@ namespace MobileGL {
|
|||||||
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
|
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
|
||||||
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
|
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
|
||||||
m_sampler = MakeShared<SamplerObject>(0);
|
m_sampler = MakeShared<SamplerObject>(0);
|
||||||
|
if (target == TextureTarget::TextureRectangle) {
|
||||||
|
// A rectangle texture has no mip chain, so its initial sampler state is not
|
||||||
|
// the shared one: TEXTURE_MIN_FILTER is LINEAR and TEXTURE_WRAP_S/T are
|
||||||
|
// CLAMP_TO_EDGE (GL 4.6 core table 23.15). Leaving the 2D default of
|
||||||
|
// NEAREST_MIPMAP_LINEAR in place makes the texture mipmap-incomplete from
|
||||||
|
// birth, and every lookup that the application never re-filtered reads
|
||||||
|
// (0, 0, 0, 1) instead of its contents.
|
||||||
|
m_sampler->SetMinFilter(SamplerFilterMode::Linear);
|
||||||
|
m_sampler->SetMipmapMode(SamplerMipmapMode::None);
|
||||||
|
m_sampler->SetWrapS(SamplerWrapMode::ClampToEdge);
|
||||||
|
m_sampler->SetWrapT(SamplerWrapMode::ClampToEdge);
|
||||||
|
m_sampler->SetWrapR(SamplerWrapMode::ClampToEdge);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureInternalFormat TextureObjectBase::GetFormat() const {
|
TextureInternalFormat TextureObjectBase::GetFormat() const {
|
||||||
@@ -335,6 +348,58 @@ namespace MobileGL {
|
|||||||
|
|
||||||
// TODO: add other texture types as needed
|
// TODO: add other texture types as needed
|
||||||
|
|
||||||
|
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||||
|
if (texture == nullptr) return true;
|
||||||
|
if (!texture->IsComplete()) return false;
|
||||||
|
if (!mipmapped) return true;
|
||||||
|
|
||||||
|
const auto* mipmapTexture = AsMipmapTexture(texture);
|
||||||
|
if (mipmapTexture == nullptr) return true; // no mip chain to be incomplete about
|
||||||
|
|
||||||
|
const UintVec2& levelRange = texture->GetLevelRange();
|
||||||
|
const Uint baseLevel = levelRange.x();
|
||||||
|
const Uint storedLevels = mipmapTexture->GetMipmapLevelCount();
|
||||||
|
if (baseLevel >= storedLevels) return false;
|
||||||
|
|
||||||
|
// An array texture's layer count is not a dimension of the image: it stays put all
|
||||||
|
// the way down the chain (GL 4.6 core 8.14.3). GetMipmapTexelSize reports it in the
|
||||||
|
// slot after the image's own dimensions.
|
||||||
|
const TextureTarget target = texture->GetTarget();
|
||||||
|
Int shrinkingComponents = 3;
|
||||||
|
if (target == TextureTarget::Texture1DArray) {
|
||||||
|
shrinkingComponents = 1;
|
||||||
|
} else if (target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray) {
|
||||||
|
shrinkingComponents = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto uploadTarget : texture->GetUploadTargets()) {
|
||||||
|
const IntVec3 baseSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, baseLevel);
|
||||||
|
Int largest = 0;
|
||||||
|
for (Int component = 0; component < shrinkingComponents; ++component) {
|
||||||
|
largest = std::max(largest, baseSize[component]);
|
||||||
|
}
|
||||||
|
if (largest <= 0) return false;
|
||||||
|
|
||||||
|
// p = log2 of the largest base dimension: the last level the chain needs
|
||||||
|
// before every dimension has reached 1. TEXTURE_MAX_LEVEL can cut it short.
|
||||||
|
Uint p = 0;
|
||||||
|
for (Int extent = largest; extent > 1; extent >>= 1) ++p;
|
||||||
|
const Uint lastLevel = std::min(baseLevel + p, levelRange.y());
|
||||||
|
|
||||||
|
for (Uint level = baseLevel; level <= lastLevel; ++level) {
|
||||||
|
if (level >= storedLevels) return false;
|
||||||
|
const IntVec3 actual = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
|
||||||
|
for (Int component = 0; component < 3; ++component) {
|
||||||
|
const Int expected = component < shrinkingComponents
|
||||||
|
? std::max(1, baseSize[component] >> (level - baseLevel))
|
||||||
|
: baseSize[component];
|
||||||
|
if (actual[component] != expected) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -156,6 +156,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
? static_cast<TextureObjectMipmap*>(texture)
|
? static_cast<TextureObjectMipmap*>(texture)
|
||||||
: nullptr;
|
: nullptr;
|
||||||
}
|
}
|
||||||
|
// Whether the texture satisfies the mipmap-completeness rules a minification filter
|
||||||
|
// that samples the mip chain imposes (GL 4.6 core 8.17): every level from the base to
|
||||||
|
// the effective max must exist at exactly half the previous one's size. `mipmapped` is
|
||||||
|
// the effective sampler's answer to "does this filter read more than the base level" -
|
||||||
|
// when it is false only base-level completeness matters, which the ordinary
|
||||||
|
// IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
|
||||||
|
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
|
||||||
|
|
||||||
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
|
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
|
||||||
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
|
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
|
||||||
? static_cast<const TextureObjectMipmap*>(texture)
|
? static_cast<const TextureObjectMipmap*>(texture)
|
||||||
|
|||||||
@@ -104,6 +104,23 @@ namespace MobileGL {
|
|||||||
m_backendHashMemoVersion = m_configVersion;
|
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:
|
private:
|
||||||
void BumpAttributeFormatVersion(Uint index);
|
void BumpAttributeFormatVersion(Uint index);
|
||||||
void BumpAttributeBufferVersion(Uint index);
|
void BumpAttributeBufferVersion(Uint index);
|
||||||
@@ -137,6 +154,9 @@ namespace MobileGL {
|
|||||||
Uint32 m_configVersion = 0;
|
Uint32 m_configVersion = 0;
|
||||||
mutable Uint64 m_backendHashMemo = 0;
|
mutable Uint64 m_backendHashMemo = 0;
|
||||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||||
|
mutable const void* m_backendStateMemo = nullptr;
|
||||||
|
mutable Uint64 m_backendStateMemoEpoch = 0;
|
||||||
|
mutable Uint32 m_backendStateMemoVersion = ~0u;
|
||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ namespace {
|
|||||||
GLint maxFragmentImageUniforms = 4;
|
GLint maxFragmentImageUniforms = 4;
|
||||||
GLint maxComputeImageUniforms = 5;
|
GLint maxComputeImageUniforms = 5;
|
||||||
bool maxGeometryImageUniformsQueried = false;
|
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
|
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
||||||
// baseInstance word and exposes it through gl_InstanceID.
|
// baseInstance word and exposes it through gl_InstanceID.
|
||||||
bool drawLeaksBaseInstanceWord = false;
|
bool drawLeaksBaseInstanceWord = false;
|
||||||
@@ -108,6 +113,14 @@ namespace {
|
|||||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||||
*data = g_fake.maxComputeImageUniforms;
|
*data = g_fake.maxComputeImageUniforms;
|
||||||
break;
|
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
|
// FillInGLESCapabilities reads the context version before running the
|
||||||
// baseInstance probe, which requires ES >= 3.1.
|
// baseInstance probe, which requires ES >= 3.1.
|
||||||
case GL_MAJOR_VERSION:
|
case GL_MAJOR_VERSION:
|
||||||
@@ -155,6 +168,22 @@ namespace {
|
|||||||
g_fake.maxTextureMaxAnisotropyQueried = true;
|
g_fake.maxTextureMaxAnisotropyQueried = true;
|
||||||
data[0] = g_fake.maxTextureMaxAnisotropy;
|
data[0] = g_fake.maxTextureMaxAnisotropy;
|
||||||
break;
|
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.
|
// Two-component range queries.
|
||||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||||
@@ -462,6 +491,52 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
|
|||||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
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
|
// 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.
|
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ void main() {
|
|||||||
|
|
||||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 330 core\n"), 0);
|
EXPECT_EQ(source.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(source.find("in vec3 position;"), String::npos);
|
EXPECT_NE(source.find("in vec3 position;"), String::npos);
|
||||||
EXPECT_NE(source.find("out vec2 uv;"), String::npos);
|
EXPECT_NE(source.find("out vec2 uv;"), String::npos);
|
||||||
EXPECT_EQ(source.find("attribute"), String::npos);
|
EXPECT_EQ(source.find("attribute"), String::npos);
|
||||||
@@ -323,7 +323,7 @@ void main() {
|
|||||||
|
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 330 core\n"), 0);
|
EXPECT_EQ(source.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos);
|
EXPECT_NE(source.find("out vec4 mg_FragColor;\n"), String::npos);
|
||||||
EXPECT_NE(source.find("in vec2 uv;"), String::npos);
|
EXPECT_NE(source.find("in vec2 uv;"), String::npos);
|
||||||
EXPECT_NE(source.find("texture(texture0, uv)"), String::npos);
|
EXPECT_NE(source.find("texture(texture0, uv)"), String::npos);
|
||||||
@@ -379,7 +379,7 @@ void main() {
|
|||||||
|
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 330 core\n"), 0);
|
EXPECT_EQ(source.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos);
|
EXPECT_NE(source.find("vec4 sample = texture(DiffuseSampler"), String::npos);
|
||||||
EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos);
|
EXPECT_NE(source.find("totalAlpha = totalAlpha + sample.a;"), String::npos);
|
||||||
EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos);
|
EXPECT_NE(source.find("float totalSamples = 0.0;"), String::npos);
|
||||||
@@ -405,7 +405,7 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Vertex, vertexSource);
|
PreprocessShaderSource(ShaderStage::Vertex, vertexSource);
|
||||||
|
|
||||||
EXPECT_EQ(vertexSource.find("#version 330 core\n"), 0);
|
EXPECT_EQ(vertexSource.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos);
|
EXPECT_NE(vertexSource.find("in vec3 sample;"), String::npos);
|
||||||
|
|
||||||
ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
|
ShaderAttrib vertexAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
|
||||||
@@ -425,7 +425,7 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, fragmentSource);
|
PreprocessShaderSource(ShaderStage::Fragment, fragmentSource);
|
||||||
|
|
||||||
EXPECT_EQ(fragmentSource.find("#version 330 core\n"), 0);
|
EXPECT_EQ(fragmentSource.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos);
|
EXPECT_NE(fragmentSource.find("uniform sampler2D sample;"), String::npos);
|
||||||
EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos);
|
EXPECT_NE(fragmentSource.find("texture(sample, texCoord)"), String::npos);
|
||||||
|
|
||||||
@@ -483,7 +483,9 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 460 core\n"), 0);
|
// An explicitly declared modern core version keeps its number (see "keep declared modern
|
||||||
|
// GLSL versions strict"); only the BOM goes.
|
||||||
|
EXPECT_EQ(source.find(String(inputVersion) + "\n"), 0);
|
||||||
EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos);
|
EXPECT_EQ(source.find("\xef\xbb\xbf"), String::npos);
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
@@ -568,10 +570,12 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
const SizeT versionPos = source.find("#version 330 core\n");
|
const SizeT versionPos = source.find("#version 330 core ");
|
||||||
const SizeT outputPos = source.find("out vec4 mg_FragColor;\n");
|
const SizeT outputPos = source.find("out vec4 mg_FragColor;\n");
|
||||||
EXPECT_NE(versionPos, String::npos);
|
EXPECT_NE(versionPos, String::npos);
|
||||||
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
|
// The normalized directive carries a marker recording that this 330 came from a legacy
|
||||||
|
// declaration, so measure the line rather than assuming its length.
|
||||||
|
EXPECT_EQ(outputPos, source.find('\n', versionPos) + 1);
|
||||||
EXPECT_NE(source.find("// #version 460 core"), String::npos);
|
EXPECT_NE(source.find("// #version 460 core"), String::npos);
|
||||||
// This #line sits ahead of the version directive, where GLSL would never have honoured it, so
|
// This #line sits ahead of the version directive, where GLSL would never have honoured it, so
|
||||||
// it is still dropped. Directives that follow the version line are kept - see
|
// it is still dropped. Directives that follow the version line are kept - see
|
||||||
@@ -684,7 +688,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
|
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtItsDeclaredVersion) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
String source = R"(#version 400 core
|
String source = R"(#version 400 core
|
||||||
@@ -697,7 +701,7 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 460 core\n"), 0);
|
EXPECT_EQ(source.find("#version 400 core\n"), 0);
|
||||||
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
|
EXPECT_NE(source.find("sample in vec4 interpolatedColor;"), String::npos);
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
@@ -746,7 +750,7 @@ void main() {
|
|||||||
|
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
EXPECT_EQ(source.find("#version 330 core\n"), 0);
|
EXPECT_EQ(source.find("#version 330 core "), 0);
|
||||||
EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos);
|
EXPECT_NE(source.find("layout(location = 0) out vec4 mg_FragData[8];\n"), String::npos);
|
||||||
EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos);
|
EXPECT_NE(source.find("mg_FragData[0] = vec4(1.0);"), String::npos);
|
||||||
EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos);
|
EXPECT_NE(source.find("mg_FragData[1].a = 0.5;"), String::npos);
|
||||||
@@ -902,16 +906,17 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
|
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
|
||||||
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
|
// used to be forced to. A legacy shader using 420-era syntax without the matching #extension line
|
||||||
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
|
// is accepted by real drivers, so CompileShader retries the normalized source at 460 rather than
|
||||||
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
|
// failing. Only MobileGL's own normalization is rescued this way - an application-declared
|
||||||
|
// "#version 330" keeps strict 3.30 semantics, which is what the CTS negative-compile cases need.
|
||||||
|
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenNormalizedLegacyVersionRejects420Syntax) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
String source = R"(#version 330
|
String source = R"(#version 130
|
||||||
layout(binding = 0) uniform sampler2D InSampler;
|
layout(binding = 0) uniform sampler2D InSampler;
|
||||||
in vec2 texCoord;
|
varying vec2 texCoord;
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
void main() {
|
||||||
fragColor = texture(InSampler, texCoord);
|
gl_FragColor = texture2D(InSampler, texCoord);
|
||||||
})";
|
})";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
// The normal path still emits 330 - the retry must not become the default.
|
// The normal path still emits 330 - the retry must not become the default.
|
||||||
@@ -952,10 +957,21 @@ void main() {
|
|||||||
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
|
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
String normalized = "#version 330 core\nvoid main() {}\n";
|
// Only MobileGL's own normalization is retargetable, and it is recognised by the marker the
|
||||||
|
// preprocessor leaves on the directive line - so normalize a legacy source rather than
|
||||||
|
// hand-writing the directive the marker belongs to.
|
||||||
|
String normalized = "#version 130\nvoid main() {}\n";
|
||||||
|
PreprocessShaderSource(ShaderStage::Vertex, normalized);
|
||||||
|
ASSERT_EQ(normalized.find("#version 330 core "), 0u);
|
||||||
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
|
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
|
||||||
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
|
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
|
||||||
|
|
||||||
|
// An application that declared 330 itself keeps strict 3.30 semantics: raising it would
|
||||||
|
// re-legalize the CTS negative-compile cases.
|
||||||
|
String declared330 = "#version 330 core\nvoid main() {}\n";
|
||||||
|
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(declared330));
|
||||||
|
EXPECT_EQ(declared330.find("#version 330 core"), 0u);
|
||||||
|
|
||||||
// Already modern: nothing to retarget.
|
// Already modern: nothing to retarget.
|
||||||
String modern = "#version 460 core\nvoid main() {}\n";
|
String modern = "#version 460 core\nvoid main() {}\n";
|
||||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
|
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
|
||||||
|
|||||||
@@ -197,7 +197,9 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
|
|||||||
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
|
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
// The advertised target went back to 3.3 (see "restore target GL version to 3.3"); Voxy only ever
|
||||||
|
// needed the extensions, which stay advertised, so assert what the backend really reports.
|
||||||
|
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||||
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||||
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
||||||
const auto& extensions = rendererInfo.Extensions;
|
const auto& extensions = rendererInfo.Extensions;
|
||||||
@@ -397,7 +399,8 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
|
|||||||
MobileGL::IntVec2(512, 512));
|
MobileGL::IntVec2(512, 512));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
|
// See the DirectGLES twin above: the target version is 3.3 again, the extensions are what matter.
|
||||||
|
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
||||||
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||||
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
|
||||||
const auto& extensions = rendererInfo.Extensions;
|
const auto& extensions = rendererInfo.Extensions;
|
||||||
@@ -516,6 +519,50 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
|||||||
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
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) {
|
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -638,6 +685,48 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
|
|||||||
MG_State::pGLContext.reset();
|
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) {
|
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
|||||||
@@ -1867,11 +1867,13 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
|
|||||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray));
|
||||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D));
|
||||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D));
|
||||||
// 1D and 1D-array are emulated as 2D / 2D-array (MapToBackendTextureTarget), matching
|
// Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and
|
||||||
// SPIRV-Cross's ES 1D-as-2D shader emission; only rectangle textures stay unsupported.
|
// 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and
|
||||||
|
// rectangle as a plain 2D - it is single-level and already clamps, so only the
|
||||||
|
// non-normalized coordinates differ and LowerRectImagesForEssl handles those.
|
||||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
|
||||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
|
||||||
EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
|
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9);
|
// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Loader.h"
|
#include "Loader.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <cmath>
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
#ifndef WIN32_LEAN_AND_MEAN
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
#define WIN32_LEAN_AND_MEAN 1
|
#define WIN32_LEAN_AND_MEAN 1
|
||||||
@@ -823,6 +824,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
|
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
|
||||||
caps.SupportsNorm16Texture = true;
|
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) {
|
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
|
||||||
caps.SupportsTextureFilterAnisotropy = true;
|
caps.SupportsTextureFilterAnisotropy = true;
|
||||||
}
|
}
|
||||||
@@ -838,8 +845,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||||
caps.SupportsNoperspectiveInterpolation = true;
|
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
|
// 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.
|
// 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
|
||||||
@@ -900,6 +913,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLint maxColorAttachments = 8;
|
GLint maxColorAttachments = 8;
|
||||||
GLint maxClipDistances = 8;
|
GLint maxClipDistances = 8;
|
||||||
GLint maxViewports = 16;
|
GLint maxViewports = 16;
|
||||||
|
GLfloat minFragmentInterpolationOffset = -0.5f;
|
||||||
|
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
||||||
|
GLint fragmentInterpolationOffsetBits = 4;
|
||||||
|
// Core minimums of both APIs (GL 4.6 table 23.53, ES 3.1 table 20.40); the probe
|
||||||
|
// below only ever widens them.
|
||||||
|
GLint minProgramTextureGatherOffset = -8;
|
||||||
|
GLint maxProgramTextureGatherOffset = 7;
|
||||||
|
GLint maxPatchVertices = 32;
|
||||||
|
GLint maxTessGenLevel = 64;
|
||||||
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
||||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
||||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
||||||
@@ -919,6 +941,23 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
|
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
|
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_PATCH_VERTICES, &maxPatchVertices);
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
|
glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
|
||||||
|
// A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
|
||||||
|
// drag the advertised range below what GL 4.0 requires of us.
|
||||||
|
minProgramTextureGatherOffset = std::min(minProgramTextureGatherOffset, -8);
|
||||||
|
maxProgramTextureGatherOffset = std::max(maxProgramTextureGatherOffset, 7);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
|
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
|
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
|
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
|
||||||
@@ -950,6 +989,29 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
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
|
// 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.
|
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
|
||||||
if (caps.SupportsTextureFilterAnisotropy) {
|
if (caps.SupportsTextureFilterAnisotropy) {
|
||||||
@@ -979,6 +1041,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxIntegerSamples = maxIntegerSamples;
|
caps.MaxIntegerSamples = maxIntegerSamples;
|
||||||
caps.MaxSamples = maxSamples;
|
caps.MaxSamples = maxSamples;
|
||||||
caps.MaxSampleMaskWords = maxSampleMaskWords;
|
caps.MaxSampleMaskWords = maxSampleMaskWords;
|
||||||
|
caps.MaxPatchVertices = maxPatchVertices;
|
||||||
|
caps.MaxTessGenLevel = maxTessGenLevel;
|
||||||
|
caps.MinProgramTextureGatherOffset = minProgramTextureGatherOffset;
|
||||||
|
caps.MaxProgramTextureGatherOffset = maxProgramTextureGatherOffset;
|
||||||
caps.MaxTextureImageUnits = maxTextureImageUnits;
|
caps.MaxTextureImageUnits = maxTextureImageUnits;
|
||||||
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
|
caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits;
|
||||||
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
|
caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits;
|
||||||
@@ -1007,6 +1073,19 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
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,
|
MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
|
||||||
caps.AliasedLineWidthRangeMax);
|
caps.AliasedLineWidthRangeMax);
|
||||||
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
|
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
|
||||||
|
|||||||
@@ -1031,6 +1031,13 @@ namespace MobileGL {
|
|||||||
String GLESShadingLanguageVersionString;
|
String GLESShadingLanguageVersionString;
|
||||||
Bool SupportsPersistentMapping = false;
|
Bool SupportsPersistentMapping = false;
|
||||||
Bool SupportsNorm16Texture = 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
|
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
|
||||||
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
|
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
|
||||||
Bool SupportsTextureFilterAnisotropy = false;
|
Bool SupportsTextureFilterAnisotropy = false;
|
||||||
@@ -1055,6 +1062,9 @@ namespace MobileGL {
|
|||||||
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||||
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||||
Bool SupportsNoperspectiveInterpolation = false;
|
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".
|
// GL_RENDERER contains "ANGLE".
|
||||||
Bool IsAngleRenderer = false;
|
Bool IsAngleRenderer = false;
|
||||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
@@ -1092,6 +1102,10 @@ namespace MobileGL {
|
|||||||
Int MaxIntegerSamples = 1;
|
Int MaxIntegerSamples = 1;
|
||||||
Int MaxSamples = 1;
|
Int MaxSamples = 1;
|
||||||
Int MaxSampleMaskWords = 1;
|
Int MaxSampleMaskWords = 1;
|
||||||
|
Int MaxPatchVertices = 32;
|
||||||
|
Int MaxTessGenLevel = 64;
|
||||||
|
Int MinProgramTextureGatherOffset = -8;
|
||||||
|
Int MaxProgramTextureGatherOffset = 7;
|
||||||
Int MaxTextureImageUnits = 32;
|
Int MaxTextureImageUnits = 32;
|
||||||
Int MaxVertexTextureImageUnits = 32;
|
Int MaxVertexTextureImageUnits = 32;
|
||||||
Int MaxComputeTextureImageUnits = 32;
|
Int MaxComputeTextureImageUnits = 32;
|
||||||
@@ -1120,6 +1134,9 @@ namespace MobileGL {
|
|||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
Float ViewportBoundsRangeMax = 0.0f;
|
Float ViewportBoundsRangeMax = 0.0f;
|
||||||
Int ViewportSubpixelBits = 0;
|
Int ViewportSubpixelBits = 0;
|
||||||
|
Float MinFragmentInterpolationOffset = -0.5f;
|
||||||
|
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||||
|
Int FragmentInterpolationOffsetBits = 4;
|
||||||
};
|
};
|
||||||
} // namespace MG_External
|
} // namespace MG_External
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "Loader.h"
|
#include "Loader.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::BackendLoader {
|
namespace MobileGL::MG_Util::BackendLoader {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -40,6 +41,25 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
return MaxSampleCountFromFlags(commonFlags);
|
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 LoadVulkanDynamicFunctions(VkInstance instance) {
|
||||||
VulkanDynamicFunctions loaded{};
|
VulkanDynamicFunctions loaded{};
|
||||||
if (instance == VK_NULL_HANDLE) {
|
if (instance == VK_NULL_HANDLE) {
|
||||||
@@ -171,6 +191,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
||||||
|
FillFragmentInterpolationLimits(caps, p.limits);
|
||||||
|
|
||||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||||
@@ -261,6 +282,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||||
|
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||||
caps.SupportsWideLines = false;
|
caps.SupportsWideLines = false;
|
||||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||||
// stage writes disabled rather than inferring them from descriptor limits alone.
|
// stage writes disabled rather than inferring them from descriptor limits alone.
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ namespace MobileGL {
|
|||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
Float ViewportBoundsRangeMax = 0.0f;
|
Float ViewportBoundsRangeMax = 0.0f;
|
||||||
Int ViewportSubpixelBits = 0;
|
Int ViewportSubpixelBits = 0;
|
||||||
|
Float MinFragmentInterpolationOffset = -0.5f;
|
||||||
|
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||||
|
Int FragmentInterpolationOffsetBits = 4;
|
||||||
Bool SupportsWideLines = false;
|
Bool SupportsWideLines = false;
|
||||||
// Storage-image descriptors are limited per stage by
|
// Storage-image descriptors are limited per stage by
|
||||||
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
|
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace MobileGL {
|
|||||||
|
|
||||||
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
|
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
|
||||||
switch (internalformat) {
|
switch (internalformat) {
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
case TextureInternalFormat::Depth24Stencil8:
|
case TextureInternalFormat::Depth24Stencil8:
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
case TextureInternalFormat::Depth32FStencil8:
|
||||||
case TextureInternalFormat::DepthStencil:
|
case TextureInternalFormat::DepthStencil:
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ namespace MobileGL {
|
|||||||
return TextureInternalFormat::Depth24Stencil8;
|
return TextureInternalFormat::Depth24Stencil8;
|
||||||
case GL_DEPTH32F_STENCIL8:
|
case GL_DEPTH32F_STENCIL8:
|
||||||
return TextureInternalFormat::Depth32FStencil8;
|
return TextureInternalFormat::Depth32FStencil8;
|
||||||
|
case GL_STENCIL_INDEX8:
|
||||||
|
return TextureInternalFormat::StencilIndex8;
|
||||||
case GL_DEPTH_COMPONENT:
|
case GL_DEPTH_COMPONENT:
|
||||||
return TextureInternalFormat::DepthComponent;
|
return TextureInternalFormat::DepthComponent;
|
||||||
case GL_DEPTH_STENCIL:
|
case GL_DEPTH_STENCIL:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLbitfield ConvertBufferMappingAccessToGLEnum(BufferMappingAccessBit access) {
|
GLbitfield ConvertBufferMappingAccessToGLEnum(Flags<BufferMappingAccessBit> access) {
|
||||||
GLbitfield result = 0;
|
GLbitfield result = 0;
|
||||||
if (access & BufferMappingAccessBit::Read) result |= GL_MAP_READ_BIT;
|
if (access & BufferMappingAccessBit::Read) result |= GL_MAP_READ_BIT;
|
||||||
if (access & BufferMappingAccessBit::Write) result |= GL_MAP_WRITE_BIT;
|
if (access & BufferMappingAccessBit::Write) result |= GL_MAP_WRITE_BIT;
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ namespace MobileGL {
|
|||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
GLenum ConvertBufferTargetToGLEnum(BufferTarget bufferTarget);
|
GLenum ConvertBufferTargetToGLEnum(BufferTarget bufferTarget);
|
||||||
GLenum ConvertBufferUsageToGLEnum(BufferUsage usage);
|
GLenum ConvertBufferUsageToGLEnum(BufferUsage usage);
|
||||||
GLbitfield ConvertBufferMappingAccessToGLEnum(BufferMappingAccessBit access);
|
GLbitfield ConvertBufferMappingAccessToGLEnum(Flags<BufferMappingAccessBit> access);
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
@@ -233,6 +233,8 @@ namespace MobileGL {
|
|||||||
return GL_DEPTH24_STENCIL8;
|
return GL_DEPTH24_STENCIL8;
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
case TextureInternalFormat::Depth32FStencil8:
|
||||||
return GL_DEPTH32F_STENCIL8;
|
return GL_DEPTH32F_STENCIL8;
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
|
return GL_STENCIL_INDEX8;
|
||||||
case TextureInternalFormat::DepthComponent32:
|
case TextureInternalFormat::DepthComponent32:
|
||||||
return GL_DEPTH_COMPONENT32;
|
return GL_DEPTH_COMPONENT32;
|
||||||
case TextureInternalFormat::DepthStencil:
|
case TextureInternalFormat::DepthStencil:
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ namespace MobileGL {
|
|||||||
return "Depth24Stencil8";
|
return "Depth24Stencil8";
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
case TextureInternalFormat::Depth32FStencil8:
|
||||||
return "Depth32FStencil8";
|
return "Depth32FStencil8";
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
|
return "StencilIndex8";
|
||||||
case TextureInternalFormat::Red:
|
case TextureInternalFormat::Red:
|
||||||
return "Red";
|
return "Red";
|
||||||
case TextureInternalFormat::RG:
|
case TextureInternalFormat::RG:
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ namespace MobileGL {
|
|||||||
case GL_TRIANGLE_FAN:
|
case GL_TRIANGLE_FAN:
|
||||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
|
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
|
||||||
case GL_LINE_LOOP:
|
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:
|
default:
|
||||||
MGLOG_W("Unrecognized primitive topology");
|
MGLOG_W("Unrecognized primitive topology");
|
||||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ namespace MobileGL {
|
|||||||
return VK_FORMAT_D24_UNORM_S8_UINT;
|
return VK_FORMAT_D24_UNORM_S8_UINT;
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
case TextureInternalFormat::Depth32FStencil8:
|
||||||
return VK_FORMAT_D32_SFLOAT_S8_UINT;
|
return VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
|
return VK_FORMAT_S8_UINT;
|
||||||
case TextureInternalFormat::DepthComponent32:
|
case TextureInternalFormat::DepthComponent32:
|
||||||
return VK_FORMAT_D32_SFLOAT;
|
return VK_FORMAT_D32_SFLOAT;
|
||||||
case TextureInternalFormat::DepthStencil:
|
case TextureInternalFormat::DepthStencil:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::Red: // UNorm8 shadow layout
|
case TextureInternalFormat::Red: // UNorm8 shadow layout
|
||||||
case TextureInternalFormat::R8Snorm:
|
case TextureInternalFormat::R8Snorm:
|
||||||
case TextureInternalFormat::R8I:
|
case TextureInternalFormat::R8I:
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
case TextureInternalFormat::R8UI:
|
case TextureInternalFormat::R8UI:
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
@@ -43,8 +44,11 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::SRGB8:
|
case TextureInternalFormat::SRGB8:
|
||||||
case TextureInternalFormat::RGB8I:
|
case TextureInternalFormat::RGB8I:
|
||||||
case TextureInternalFormat::RGB8UI:
|
case TextureInternalFormat::RGB8UI:
|
||||||
case TextureInternalFormat::DepthComponent24:
|
|
||||||
return 3;
|
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::RGBA2:
|
||||||
case TextureInternalFormat::RGBA4:
|
case TextureInternalFormat::RGBA4:
|
||||||
@@ -91,6 +95,9 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::RG32F:
|
case TextureInternalFormat::RG32F:
|
||||||
case TextureInternalFormat::RG32I:
|
case TextureInternalFormat::RG32I:
|
||||||
case TextureInternalFormat::RG32UI:
|
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;
|
return 8;
|
||||||
|
|
||||||
case TextureInternalFormat::RGB32F:
|
case TextureInternalFormat::RGB32F:
|
||||||
@@ -101,7 +108,6 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::RGBA32F:
|
case TextureInternalFormat::RGBA32F:
|
||||||
case TextureInternalFormat::RGBA32I:
|
case TextureInternalFormat::RGBA32I:
|
||||||
case TextureInternalFormat::RGBA32UI:
|
case TextureInternalFormat::RGBA32UI:
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
|
||||||
return 16;
|
return 16;
|
||||||
|
|
||||||
case TextureInternalFormat::R11FG11FB10F:
|
case TextureInternalFormat::R11FG11FB10F:
|
||||||
@@ -253,8 +259,10 @@ namespace MobileGL {
|
|||||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||||
case TexturePixelDataType::UnsignedInt248:
|
case TexturePixelDataType::UnsignedInt248:
|
||||||
case TexturePixelDataType::Float32UnsignedInt248Rev:
|
|
||||||
return 4;
|
return 4;
|
||||||
|
case TexturePixelDataType::Float32UnsignedInt248Rev:
|
||||||
|
// A 32-bit float depth word followed by a 32-bit word holding stencil.
|
||||||
|
return 8;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -501,9 +509,16 @@ namespace MobileGL {
|
|||||||
s.Depth = 32;
|
s.Depth = 32;
|
||||||
s.Stencil = 8;
|
s.Stencil = 8;
|
||||||
break;
|
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:
|
default:
|
||||||
MOBILEGL_ASSERT(false, "Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
MGLOG_W("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
|
||||||
static_cast<Int>(internal));
|
static_cast<Int>(internal));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,11 +220,14 @@ namespace MobileGL {
|
|||||||
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
|
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
|
||||||
if (result) return result;
|
if (result) return result;
|
||||||
|
|
||||||
// Legacy desktop sources are normalized to "#version 330 core", which parses under
|
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
|
||||||
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
|
// directive), which parses under stricter rules than the 460 they used to be forced
|
||||||
// using e.g. layout(binding=...) without the matching #extension line compiles on real
|
// to: a shader declaring 110-150 while using e.g. layout(binding=...) without the
|
||||||
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
|
// matching #extension line compiles on real drivers but is rejected here. Retry once
|
||||||
// broken shader fails both attempts and keeps its original diagnostics.
|
// 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;
|
String retrySource = source;
|
||||||
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
|
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
|
||||||
return result;
|
return result;
|
||||||
@@ -360,6 +363,92 @@ namespace MobileGL {
|
|||||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
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) {
|
||||||
|
// Normalized-coordinate lookups whose ESSL form the backend's
|
||||||
|
// NormalizeRectSamplerCoordinates post-pass cannot repair: the
|
||||||
|
// coordinate is either fused with something else in a single argument
|
||||||
|
// (the Dref sample forms carry the compare value in coord.z) or the
|
||||||
|
// divide would have to happen after a projective divide. 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.
|
||||||
|
//
|
||||||
|
// OpImageSampleImplicitLod, OpImageGather and OpImageDrefGather are
|
||||||
|
// absent because all three become an ESSL call whose argument 1 is the
|
||||||
|
// bare texel-space coordinate, which the post-pass divides by the
|
||||||
|
// texture size.
|
||||||
|
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::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,
|
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ namespace MobileGL {
|
|||||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
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
|
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||||
|
|||||||
@@ -688,6 +688,10 @@ namespace {
|
|||||||
return info;
|
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) {
|
MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) {
|
||||||
if (info.profile == MobileGL::ShaderProfile::ES) {
|
if (info.profile == MobileGL::ShaderProfile::ES) {
|
||||||
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
|
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
|
||||||
@@ -702,9 +706,23 @@ namespace {
|
|||||||
return "#version 460 compatibility\n";
|
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 =
|
const bool useLegacyDesktopVersion =
|
||||||
info.version < 400 && !info.enablesGpuShader5;
|
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) {
|
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
|
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
||||||
// compatibility shaders keep whatever they declared.
|
// compatibility shaders keep whatever they declared.
|
||||||
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
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,
|
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
||||||
"#version 460 core\n");
|
"#version 460 core\n");
|
||||||
return true;
|
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 ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ namespace MobileGL {
|
|||||||
// uses 420-era syntax without the matching #extension line, which real drivers tend to
|
// uses 420-era syntax without the matching #extension line, which real drivers tend to
|
||||||
// accept - can be retried instead of failing to compile.
|
// accept - can be retried instead of failing to compile.
|
||||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
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 ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
Int32,
|
Int32,
|
||||||
Half,
|
Half,
|
||||||
Float32,
|
Float32,
|
||||||
|
UNorm32, // 32-bit fixed-point depth shadow
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InternalShadowLayout {
|
struct InternalShadowLayout {
|
||||||
@@ -123,6 +124,21 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
|
|
||||||
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
|
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
|
||||||
switch (internal) {
|
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::R8:
|
||||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||||
case TextureInternalFormat::RG8:
|
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::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true;
|
||||||
case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; 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;
|
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:
|
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;
|
out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16;
|
||||||
return true;
|
return true;
|
||||||
case TexturePixelDataType::UnsignedInt:
|
case TexturePixelDataType::UnsignedInt:
|
||||||
if (!isInteger) return false; // no 32-bit normalized shadow layout
|
out = isInteger ? ShadowComponent::UInt32 : ShadowComponent::UNorm32;
|
||||||
out = ShadowComponent::UInt32;
|
|
||||||
return true;
|
return true;
|
||||||
case TexturePixelDataType::Int:
|
case TexturePixelDataType::Int:
|
||||||
if (!isInteger) return false;
|
if (!isInteger) return false;
|
||||||
@@ -617,6 +634,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
case ShadowComponent::Float32:
|
case ShadowComponent::Float32:
|
||||||
Memcpy(dst, &v, sizeof(v));
|
Memcpy(dst, &v, sizeof(v));
|
||||||
break;
|
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:
|
default:
|
||||||
break; // integer components never reach the float encoder
|
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 Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height;
|
||||||
const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment);
|
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{};
|
UnpackConversionSpec conversion{};
|
||||||
const Bool needConversion =
|
const Bool needConversion =
|
||||||
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
|
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
|
||||||
@@ -972,6 +1053,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
Memcpy(&v, p, sizeof(v));
|
Memcpy(&v, p, sizeof(v));
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
case ShadowComponent::UNorm32: {
|
||||||
|
Uint32 v;
|
||||||
|
Memcpy(&v, p, sizeof(v));
|
||||||
|
return static_cast<Float>(static_cast<double>(v) / 4294967295.0);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,16 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat)
|
case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat)
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
break;
|
break;
|
||||||
case GL_RGB16_SNORM:
|
case GL_RGB16_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case GL_RGBA16_SNORM:
|
case GL_RGBA16_SNORM:
|
||||||
case GL_RG16_SNORM:
|
case GL_RG16_SNORM:
|
||||||
@@ -46,6 +51,9 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
||||||
break;
|
break;
|
||||||
case GL_RGB8_SNORM:
|
case GL_RGB8_SNORM:
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
|
break;
|
||||||
case GL_RG8_SNORM:
|
case GL_RG8_SNORM:
|
||||||
case GL_R8_SNORM:
|
case GL_R8_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
@@ -66,7 +74,15 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
switch (internalFormat) {
|
switch (internalFormat) {
|
||||||
case GL_DEPTH_COMPONENT32:
|
case GL_DEPTH_COMPONENT32:
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
@@ -79,6 +95,13 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RGB16:
|
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) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRgb16)) {
|
(options & PixelFormatNormalizeOptionBit::NoRgb16)) {
|
||||||
*outInternalFormat = GL_RGB32F;
|
*outInternalFormat = GL_RGB32F;
|
||||||
@@ -109,6 +132,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RGB16_SNORM:
|
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) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||||
@@ -142,6 +173,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RGB8_SNORM:
|
case GL_RGB8_SNORM:
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
|
*outInternalFormat = GL_RGBA16F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||||
*outInternalFormat = GL_RGB16F;
|
*outInternalFormat = GL_RGB16F;
|
||||||
break;
|
break;
|
||||||
@@ -552,6 +587,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outType = GL_UNSIGNED_INT;
|
*outType = GL_UNSIGNED_INT;
|
||||||
break;
|
break;
|
||||||
case GL_DEPTH_COMPONENT32:
|
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;
|
*outType = GL_UNSIGNED_INT;
|
||||||
break;
|
break;
|
||||||
case GL_DEPTH_COMPONENT32F:
|
case GL_DEPTH_COMPONENT32F:
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ namespace MobileGL {
|
|||||||
NoDepthComponent32 = 1 << 4,
|
NoDepthComponent32 = 1 << 4,
|
||||||
NoRGBA8Snorm = 1 << 5,
|
NoRGBA8Snorm = 1 << 5,
|
||||||
NoRGB16Snorm = 1 << 6,
|
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,
|
None = 0,
|
||||||
};
|
};
|
||||||
namespace MG_Util::TextureFormatProcessor {
|
namespace MG_Util::TextureFormatProcessor {
|
||||||
|
|||||||
@@ -181,7 +181,19 @@ namespace MobileGL {
|
|||||||
explicit BindingSlotRange1D(TargetEnum target, const Range1D& range = Range1D())
|
explicit BindingSlotRange1D(TargetEnum target, const Range1D& range = Range1D())
|
||||||
: BindingSlot<ObjectType>(target), m_range(range) {}
|
: 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; }
|
Bool HasExplicitRange() const { return m_hasExplicitRange; }
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ val releaseSigningReady = signingStoreFile.exists()
|
|||||||
val debuggableRelease = (findProperty("mobilegl.debuggableRelease") ?: "false").toString().toBoolean()
|
val debuggableRelease = (findProperty("mobilegl.debuggableRelease") ?: "false").toString().toBoolean()
|
||||||
|
|
||||||
val mobileGlVersionMajor = 26
|
val mobileGlVersionMajor = 26
|
||||||
val mobileGlVersionMinor = 7
|
val mobileGlVersionMinor = 8
|
||||||
val mobileGlGitShortHash = runGit("rev-parse", "--short=7", "HEAD") ?: "nogit"
|
val mobileGlGitShortHash = runGit("rev-parse", "--short=7", "HEAD") ?: "nogit"
|
||||||
val mobileGlMonthlyRevision = runGit(
|
val mobileGlMonthlyRevision = runGit(
|
||||||
"rev-list",
|
"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
|
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
|
||||||
passes, separately for each backend (`DirectGLES`, `DirectVulkan`).
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
* - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with
|
||||||
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
|
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
|
||||||
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
|
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
|
||||||
* - Window surfaces are backed by an AImageReader rather than an Activity,
|
* - On Android, window surfaces are backed by an AImageReader rather than an
|
||||||
* which is what lets the suite run as a plain adb-shell binary. DirectVulkan
|
* Activity, which is what lets the suite run as a plain adb-shell binary.
|
||||||
* needs this: its pbuffer path requires VK_EXT_headless_surface, which
|
* DirectVulkan needs this: its pbuffer path requires VK_EXT_headless_surface,
|
||||||
* Adreno's Android driver does not expose.
|
* 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:
|
* Environment:
|
||||||
* MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so)
|
* 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
|
* MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching
|
||||||
*//*--------------------------------------------------------------------*/
|
*//*--------------------------------------------------------------------*/
|
||||||
|
|
||||||
@@ -58,9 +61,11 @@
|
|||||||
#include "tcuPlatform.hpp"
|
#include "tcuPlatform.hpp"
|
||||||
#include "tcuRenderTarget.hpp"
|
#include "tcuRenderTarget.hpp"
|
||||||
|
|
||||||
|
#if defined(__ANDROID__)
|
||||||
#include <android/hardware_buffer.h>
|
#include <android/hardware_buffer.h>
|
||||||
#include <android/native_window.h>
|
#include <android/native_window.h>
|
||||||
#include <media/NdkImageReader.h>
|
#include <media/NdkImageReader.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
@@ -88,13 +93,24 @@ static string getLibraryName(void)
|
|||||||
return (env && env[0]) ? string(env) : string("libMobileGL.so");
|
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)
|
static bool useWindowSurface(void)
|
||||||
{
|
{
|
||||||
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
|
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
|
||||||
return !(env && string(env) == "pbuffer");
|
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.
|
* \brief A real ANativeWindow with no Activity behind it.
|
||||||
*
|
*
|
||||||
@@ -160,6 +176,12 @@ private:
|
|||||||
AImageReader *m_reader;
|
AImageReader *m_reader;
|
||||||
ANativeWindow *m_window;
|
ANativeWindow *m_window;
|
||||||
};
|
};
|
||||||
|
#else
|
||||||
|
//! Never instantiated on desktop; keeps EglRenderContext's member deletable.
|
||||||
|
class ImageReaderWindow
|
||||||
|
{
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
class GetProcFuncLoader : public glw::FunctionLoader
|
class GetProcFuncLoader : public glw::FunctionLoader
|
||||||
{
|
{
|
||||||
@@ -352,6 +374,7 @@ EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::C
|
|||||||
|
|
||||||
if (wantWindow)
|
if (wantWindow)
|
||||||
{
|
{
|
||||||
|
#if defined(__ANDROID__)
|
||||||
m_window = new ImageReaderWindow(width, height);
|
m_window = new ImageReaderWindow(width, height);
|
||||||
|
|
||||||
eglw::EGLint visualId = 0;
|
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,
|
m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig,
|
||||||
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
|
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
|
||||||
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
|
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
|
||||||
|
#else
|
||||||
|
throw tcu::NotSupportedError("Window surfaces are not supported by the desktop MobileGL platform");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else
|
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 = [
|
COPIES = [
|
||||||
(os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None),
|
(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", ["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()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user