mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3d52faad4 | ||
|
|
ba81ee114e | ||
|
|
c8c7b19579 | ||
|
|
0e7692251d | ||
|
|
34f09291da | ||
|
|
3b65e646e1 | ||
|
|
25b9370815 | ||
|
|
4ce808b9f2 | ||
|
|
5545d31c37 |
@@ -190,6 +190,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
|
||||
@@ -358,7 +358,35 @@ namespace MobileGL {
|
||||
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
|
||||
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
|
||||
// so a backend that never sets it gets the conservative answer.
|
||||
Bool SupportsPerLayerFramebufferAttachment = false;
|
||||
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
|
||||
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
|
||||
// set for each supported target. Deliberately per target rather than one flag: the three
|
||||
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
|
||||
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
|
||||
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
|
||||
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
|
||||
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
|
||||
// a backend that never sets it gets the conservative answer.
|
||||
Uint32 PerLayerFramebufferAttachmentTargets = 0;
|
||||
|
||||
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
|
||||
return (static_cast<Int>(target) >= 0 &&
|
||||
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
|
||||
? (1u << static_cast<Uint32>(target))
|
||||
: 0u;
|
||||
}
|
||||
|
||||
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
|
||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||
}
|
||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -1112,8 +1112,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
|
||||
// attachment's layer passed through, so this backend really does render to the layer it was
|
||||
// given - provided the driver resolved the entry point at all.
|
||||
m_dynamicParameters.SupportsPerLayerFramebufferAttachment =
|
||||
DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr;
|
||||
// SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
|
||||
// five upload targets to glFramebufferTextureLayer with the attachment's layer passed
|
||||
// through, so this backend really does render to the layer it was given - provided the driver
|
||||
// resolved the entry point at all. The cube map array is the one target that also needs
|
||||
// ES-level support before it has any storage to attach.
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
|
||||
if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
|
||||
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||
if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
||||
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
||||
// land on this backend regardless of what the driver underneath happens to support.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
|
||||
@@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!program) return;
|
||||
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
@@ -1377,7 +1377,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_backendProgramObjects.CollectGarbageIfNeeded();
|
||||
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
||||
|
||||
auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
g_lastUsedBackendProgramId = 0;
|
||||
@@ -1546,7 +1546,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Frontend target the current program samples at a given unit; resolves an
|
||||
// aliased native binding when two real textures compete for it (see below).
|
||||
// Only consulted on a conflict, so the ordinary unit costs nothing.
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const auto sampledTargetForUnit = [¤tProgram](Int unit) {
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
return TextureTarget::Unknown;
|
||||
@@ -1679,7 +1679,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// this as much as draws do — e.g. Flywheel's cull shader reads the
|
||||
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
||||
static void BindCurrentProgramWithResources() {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (currentProgram && currentProgram->GetLinkStatus()) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -1865,7 +1865,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static SharedPtr<PrgramImpl::BackendProgramObjectImpl> GetCurrentBackendProgram() {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -2012,7 +2012,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureImpl::SyncImageTextureBindings();
|
||||
PrgramImpl::SyncCurrentProgram();
|
||||
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||
@@ -2511,9 +2511,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static GLenum QueryReadColorAttachmentInternalFormat() {
|
||||
GLint attachmentType = 0;
|
||||
GLint attachmentName = 0;
|
||||
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
// Ask the point the backend read buffer actually names, not COLOR_ATTACHMENT0. The colour map
|
||||
// in BackendFramebufferObject is a permutation, so the read attachment's image only sits at
|
||||
// CA0 when that map is identity; querying CA0 unconditionally would size the resolve
|
||||
// renderbuffer from a different attachment's format and either convert wrongly or fail the
|
||||
// blit outright.
|
||||
GLint readBuffer = GL_COLOR_ATTACHMENT0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
|
||||
if (readBuffer < GL_COLOR_ATTACHMENT0 || readBuffer > GL_COLOR_ATTACHMENT31) {
|
||||
readBuffer = GL_COLOR_ATTACHMENT0;
|
||||
}
|
||||
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &attachmentType);
|
||||
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, static_cast<GLenum>(readBuffer),
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &attachmentName);
|
||||
if (attachmentName == 0) {
|
||||
return 0;
|
||||
|
||||
@@ -1305,6 +1305,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
||||
|
||||
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
|
||||
// backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex
|
||||
// format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever
|
||||
// did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on
|
||||
// the real driver. Disabling rather than merely skipping matters: becoming long bumps
|
||||
// FormatVersion, not SwitchVersion, so the enable/disable block above will not run
|
||||
// again and an already-enabled array would stay enabled with no pointer and no
|
||||
// ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw.
|
||||
if (attrib.IsLong) {
|
||||
MGLOG_E("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
|
||||
"backend cannot feed - disabling the array",
|
||||
attribIndex);
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!BindAttributeBuffer(attrib)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1366,6 +1382,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same reason as SyncToBackend: there is no ES vertex format for a 64-bit array, and
|
||||
// this path only ever reaches glVertexAttribPointer/IPointer.
|
||||
if (attrib.IsLong) {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* clientData = reinterpret_cast<const Uint8*>(attrib.Offset);
|
||||
const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra);
|
||||
if (!clientData || elementSize == 0 || attrib.Size <= 0) {
|
||||
@@ -1888,6 +1911,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
|
||||
// like a 2D array whose depth is 6 * the cube count.
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
g_GLESFuncs.glTexImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
|
||||
@@ -1965,6 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
|
||||
static_cast<GLsizei>(storageSize.x()),
|
||||
static_cast<GLsizei>(storageSize.y()),
|
||||
@@ -2017,6 +2044,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
g_GLESFuncs.glTexSubImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
@@ -2080,7 +2108,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
}
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray: {
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray: {
|
||||
g_GLESFuncs.glTexImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
@@ -2180,6 +2209,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
|
||||
// like a 2D array whose depth is 6 * the cube count.
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
@@ -2523,6 +2555,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Identity until a non-identity draw-buffer array forces a relocation. A framebuffer
|
||||
// that is never draw-bound never runs the recompute, so the table has to start out
|
||||
// matching what the attachment loop will physically do.
|
||||
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
||||
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||
}
|
||||
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
||||
if (m_backendFBOId == 0) {
|
||||
MGLOG_E("Failed to generate framebuffer object.");
|
||||
@@ -2596,6 +2634,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::fill(std::begin(m_frontendDrawBuffers), std::end(m_frontendDrawBuffers),
|
||||
FramebufferAttachmentType::Unknown);
|
||||
std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE);
|
||||
// NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every
|
||||
// attachment it had, possibly under a non-identity permutation. Declaring the table
|
||||
// identity here is safe only because every attachment version below is invalidated too,
|
||||
// so the next sync re-attaches all non-empty attachments at their identity points AND
|
||||
// (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner
|
||||
// is empty. Without that detach a stale image would survive under a point the table now
|
||||
// claims for a different, empty attachment.
|
||||
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
||||
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||
}
|
||||
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
|
||||
m_backendReadBuffer = GL_NONE;
|
||||
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
|
||||
@@ -2630,6 +2678,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
|
||||
uploadTarget == TextureUploadTarget::Texture3D ||
|
||||
uploadTarget == TextureUploadTarget::Texture2DArray ||
|
||||
uploadTarget == TextureUploadTarget::Texture1DArray ||
|
||||
uploadTarget == TextureUploadTarget::CubeMapArray ||
|
||||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
|
||||
// Single slice/layer of a 3D or array texture: ES has no
|
||||
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
|
||||
@@ -2785,6 +2835,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Bool BackendFramebufferObject::RecomputeBackendColorSlots(
|
||||
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) {
|
||||
// Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own
|
||||
// limit (ValidateColorAttachmentInRange, which reads the clamped
|
||||
// GetDynamicParameters().MaxColorAttachments) is never larger than this raw ES cap, so an
|
||||
// index the frontend accepted is always < slotCount. Indices at or above it can never own
|
||||
// an image and stay on their identity point - never touched, never a GL error.
|
||||
const Uint slotCount =
|
||||
std::min<Uint>(MAX_COLOR_ATTACHMENT_SLOTS,
|
||||
static_cast<Uint>(std::max<Int>(g_GLESCapabilities.MaxColorAttachments, 1)));
|
||||
|
||||
GLenum newSlots[MAX_COLOR_ATTACHMENT_SLOTS];
|
||||
for (Uint i = 0; i < MAX_COLOR_ATTACHMENT_SLOTS; ++i) {
|
||||
newSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||
}
|
||||
Bool assigned[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
|
||||
Bool slotTaken[MAX_COLOR_ATTACHMENT_SLOTS] = {false};
|
||||
|
||||
// 1. ES pins draw-buffer slot s to GL_COLOR_ATTACHMENTs, so an attachment named by draw
|
||||
// buffer slot s has no choice: its image must sit at backend point s. This has to
|
||||
// agree with the compaction the caller just pushed through glDrawBuffers.
|
||||
for (Uint s = 0; s < FramebufferObject::MAX_DRAW_BUFFERS && s < slotCount; ++s) {
|
||||
const auto frontendBuf = stateDrawBuffers[s];
|
||||
if (frontendBuf < FramebufferAttachmentType::Color0 ||
|
||||
frontendBuf > FramebufferAttachmentType::Color31) {
|
||||
continue; // GL_NONE, or a default-framebuffer FRONT/BACK token: never relocated.
|
||||
}
|
||||
const Uint a =
|
||||
static_cast<Uint>(frontendBuf) - static_cast<Uint>(FramebufferAttachmentType::Color0);
|
||||
// Neither guard may ever fire: a duplicate draw buffer is already INVALID_OPERATION
|
||||
// and an out-of-range one is rejected by ValidateColorAttachmentInRange. If one did
|
||||
// fire the table would disagree with the glDrawBuffers the caller already issued,
|
||||
// which is the exact non-injectivity this table exists to remove.
|
||||
MOBILEGL_ASSERT(a < slotCount && !assigned[a],
|
||||
"Draw buffer %u names colour attachment %u which is out of range or duplicated.", s,
|
||||
a);
|
||||
if (a >= slotCount || assigned[a]) {
|
||||
continue;
|
||||
}
|
||||
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
|
||||
assigned[a] = true;
|
||||
slotTaken[s] = true;
|
||||
}
|
||||
|
||||
// 2. Everything else keeps its identity point when that point survived step 1. This is
|
||||
// what makes the ordinary drawBuffers[s] == COLOR_ATTACHMENTs case a strict no-op:
|
||||
// the table stays identity, nothing moves, no attachment is re-issued.
|
||||
for (Uint a = 0; a < slotCount; ++a) {
|
||||
if (assigned[a] || slotTaken[a]) {
|
||||
continue;
|
||||
}
|
||||
newSlots[a] = GL_COLOR_ATTACHMENT0 + a;
|
||||
assigned[a] = true;
|
||||
slotTaken[a] = true;
|
||||
}
|
||||
|
||||
// 3. What is left are attachments whose identity point step 1 took away. Park them on the
|
||||
// lowest free point. They are not draw buffers, so nothing is rendered through them;
|
||||
// they only have to stay addressable for glReadBuffer and blits, and the map has to
|
||||
// stay injective so reading one of them cannot land on another's image.
|
||||
for (Uint a = 0; a < slotCount; ++a) {
|
||||
if (assigned[a]) {
|
||||
continue;
|
||||
}
|
||||
for (Uint s = 0; s < slotCount; ++s) {
|
||||
if (!slotTaken[s]) {
|
||||
newSlots[a] = GL_COLOR_ATTACHMENT0 + s;
|
||||
assigned[a] = true;
|
||||
slotTaken[s] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool moved = false;
|
||||
for (Uint a = 0; a < MAX_COLOR_ATTACHMENT_SLOTS; ++a) {
|
||||
if (m_backendColorSlots[a] == newSlots[a]) {
|
||||
continue;
|
||||
}
|
||||
m_backendColorSlots[a] = newSlots[a];
|
||||
moved = true;
|
||||
// This attachment's image now belongs at a different backend point. Its frontend
|
||||
// version has not changed, so the attachment loop would skip it; force it.
|
||||
m_syncedFrontendAttachmentVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0) + a] =
|
||||
static_cast<Uint16>(~0u);
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
void BackendFramebufferObject::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -2840,6 +2979,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
nEffectiveBuffers = i + 1;
|
||||
}
|
||||
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
|
||||
// The line above pinned backend point s to draw-buffer slot s, so the images have to
|
||||
// be moved under those points. Rebuild the whole colour map and, when anything moved,
|
||||
// also drop the read-buffer memo: SyncReadBufferToBackend keys it on the frontend
|
||||
// enum alone, which does not change when the point under it does.
|
||||
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
|
||||
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
|
||||
}
|
||||
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
|
||||
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
|
||||
}
|
||||
@@ -2885,6 +3031,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// relevant FRONTEND!!! version should be checked and updated
|
||||
if (m_syncedFrontendAttachmentVersions[i] != attachmentVersions[i]) {
|
||||
// SyncAttachmentObject only ever attaches: for an empty frontend attachment it
|
||||
// returns true and issues nothing, so the point keeps whatever was there. That is
|
||||
// what makes m_backendColorSlots a permutation of the PHYSICAL layout rather than
|
||||
// a claim about one - a point handed to an attachment with no image would
|
||||
// otherwise still hold the previous owner's image and glReadBuffer would return
|
||||
// it. Bounded by GL_MAX_COLOR_ATTACHMENTS because GL_COLOR_ATTACHMENTn above the
|
||||
// driver's limit is INVALID_ENUM, and restricted to colour points because
|
||||
// FRONT_LEFT/BACK_LEFT and co. are not ES attachment points at all.
|
||||
const Bool isColorPoint =
|
||||
frontendType >= FramebufferAttachmentType::Color0 &&
|
||||
frontendType <= FramebufferAttachmentType::Color31 &&
|
||||
(static_cast<Int>(frontendType) - static_cast<Int>(FramebufferAttachmentType::Color0)) <
|
||||
g_GLESCapabilities.MaxColorAttachments;
|
||||
if (isColorPoint && attachmentObject.IsEmpty() && glBackendAttachment != GL_NONE) {
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
|
||||
}
|
||||
if (SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment)) {
|
||||
m_syncedFrontendAttachmentVersions[i] = attachmentVersions[i];
|
||||
}
|
||||
@@ -2946,21 +3108,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
|
||||
GLenum glBackendReadBuffer = GL_NONE;
|
||||
auto it = std::find(m_frontendDrawBuffers, m_frontendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS,
|
||||
frontendAtt);
|
||||
Bool notFound = (it == m_frontendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
if (notFound) {
|
||||
MGLOG_D(
|
||||
"%s: frontendAtt not found in draw buffer (probably not remapped), just use the same as frontend",
|
||||
__func__);
|
||||
glBackendReadBuffer = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
|
||||
} else {
|
||||
MGLOG_D("%s: frontendAtt found in draw buffer, keep it consistent as in read buffers", __func__);
|
||||
auto index = std::distance(m_frontendDrawBuffers, it);
|
||||
glBackendReadBuffer = m_backendDrawBuffers[index];
|
||||
// Only colour attachments are ever relocated; depth/stencil, the default framebuffer's
|
||||
// FRONT/BACK names and None map straight through.
|
||||
if (frontendAtt < FramebufferAttachmentType::Color0 || frontendAtt > FramebufferAttachmentType::Color31) {
|
||||
return MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendAtt);
|
||||
}
|
||||
return glBackendReadBuffer;
|
||||
// The table is a permutation of the backend colour points, so this is the one point that
|
||||
// owns this attachment. Searching the draw-buffer array instead returned the identity
|
||||
// point for every attachment that was not a draw buffer - which is exactly the point a
|
||||
// relocated draw buffer had just taken over, so COLOR_ATTACHMENT0 read back the image of
|
||||
// whatever attachment was last made the draw buffer.
|
||||
const Uint index = static_cast<Uint>(frontendAtt) - static_cast<Uint>(FramebufferAttachmentType::Color0);
|
||||
return m_backendColorSlots[index];
|
||||
}
|
||||
|
||||
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
|
||||
@@ -433,6 +433,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
this array could be provided as data directly to ES `glDrawBuffers` function
|
||||
*/
|
||||
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||
|
||||
static constexpr Uint MAX_COLOR_ATTACHMENT_SLOTS =
|
||||
static_cast<Uint>(FramebufferAttachmentType::Color31) -
|
||||
static_cast<Uint>(FramebufferAttachmentType::Color0) + 1;
|
||||
/* Where each frontend GL_COLOR_ATTACHMENTn image physically lives in the backend ES
|
||||
framebuffer, as a GL_COLOR_ATTACHMENTm enum. ES only accepts glDrawBuffers bufs[s] ==
|
||||
GL_COLOR_ATTACHMENTs, so a GL draw-buffer slot s naming attachment a forces a's image
|
||||
under backend slot s. This table is the single owner of that decision and is kept a
|
||||
PERMUTATION of the backend colour slots: every other attachment keeps its identity
|
||||
slot when that slot survived, and is parked on the lowest free slot when it did not.
|
||||
Deriving the point per-query from the draw-buffer array instead handed the identity
|
||||
point to any attachment that was not a draw buffer - i.e. exactly the point a
|
||||
relocated draw buffer had just taken over. The permutation is only true of the
|
||||
PHYSICAL framebuffer because the attachment loop detaches a point whose frontend
|
||||
owner is empty; do not remove that detach. */
|
||||
GLenum m_backendColorSlots[MAX_COLOR_ATTACHMENT_SLOTS] = {GL_NONE};
|
||||
/* Rebuild m_backendColorSlots from the frontend draw-buffer array. Returns true when any
|
||||
attachment moved, i.e. when the physical attachments and the memoised read buffer have
|
||||
to be re-applied. */
|
||||
Bool RecomputeBackendColorSlots(
|
||||
const MG_State::GLState::FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers);
|
||||
|
||||
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
||||
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
||||
|
||||
|
||||
@@ -810,6 +810,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
|
||||
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
|
||||
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
|
||||
// layered targets are declared separately as their own machinery lands.
|
||||
{
|
||||
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
|
||||
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
|
||||
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
|
||||
// image can name. Optimistic: a format that refuses the flag is caught at image creation
|
||||
// and declines the slice view there, which the clear path handles as a soft miss.
|
||||
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
|
||||
}
|
||||
if (m_vulkanCaps.SupportsImageCubeArray) {
|
||||
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
|
||||
@@ -209,6 +209,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
@@ -397,6 +399,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
||||
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
||||
raster.lineWidth = 1.0f;
|
||||
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
|
||||
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
|
||||
// selector already collapses to FIRST in exactly that case - so a device without the
|
||||
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
|
||||
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
|
||||
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
|
||||
provokingVertexState.pNext = raster.pNext;
|
||||
raster.pNext = &provokingVertexState;
|
||||
}
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
|
||||
@@ -35,6 +35,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
|
||||
// which vertex a flat varying takes AND the vertex order transform feedback records for
|
||||
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
|
||||
// Vulkan's own convention, which is what a device without the extension gets.
|
||||
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
Bool depthTestEnable = false;
|
||||
Bool depthWriteEnable = false;
|
||||
Bool depthBiasEnable = false;
|
||||
|
||||
@@ -2458,6 +2458,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||
// ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input.
|
||||
//
|
||||
// Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already
|
||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||
// double input - garbage with no diagnostic anywhere.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
moduleSpirvs[i], packedSpirv);
|
||||
MOBILEGL_ASSERT(packOk,
|
||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||
"vertex-input format and the shader input type now disagree",
|
||||
program.GetExternalIndex());
|
||||
if (packOk) {
|
||||
moduleSpirvs[i] = std::move(packedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; "
|
||||
"double-typed vertex attributes will be fetched as uint32 words and not "
|
||||
"reinterpreted",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// When Vulkan can legally access storage images without a statically declared
|
||||
// format, let GL's glBindImageTexture format select the runtime image view. This
|
||||
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
@@ -97,7 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
@@ -273,7 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra) {
|
||||
Bool isBgra, Bool isLong) {
|
||||
if (isBgra) {
|
||||
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
||||
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
||||
@@ -298,6 +299,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case DataType::Int2101010Rev:
|
||||
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
||||
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
||||
case DataType::Float64:
|
||||
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
|
||||
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
|
||||
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
|
||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||
// so they always agree without extra plumbing.
|
||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||
switch (size) {
|
||||
case 1: return VK_FORMAT_R32G32_UINT;
|
||||
case 2: return VK_FORMAT_R32G32B32A32_UINT;
|
||||
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
|
||||
// over two attribute locations, which the location-per-VAO-index model here does not
|
||||
// express. Declined rather than fetched wrong.
|
||||
default: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
case DataType::Float32:
|
||||
switch (size) {
|
||||
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||
|
||||
@@ -93,7 +93,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||
Bool isLong = false);
|
||||
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
@@ -87,9 +87,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkImageViewType ResolveAttachmentViewType(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const VkTextureManager::TextureResource& resource) {
|
||||
return !attachment.IsLayered() && IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ?
|
||||
VK_IMAGE_VIEW_TYPE_2D :
|
||||
resource.viewType;
|
||||
if (attachment.IsLayered()) {
|
||||
return resource.viewType;
|
||||
}
|
||||
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
|
||||
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||
// symmetry with CUBE_ARRAY.
|
||||
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return resource.viewType;
|
||||
}
|
||||
|
||||
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <unordered_map>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -314,7 +315,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
|
||||
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
|
||||
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
|
||||
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
|
||||
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
|
||||
// relocation the cached pointer names freed storage still holding the pre-clear
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
|
||||
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
|
||||
// instead of the clear colour on exactly the iterations that grew the table.
|
||||
//
|
||||
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||
// ordering rule. m_textureResources is node-based for the same reason. This buys stability
|
||||
// across rehash and insert only - erase still invalidates the erased element, which is safe
|
||||
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
|
||||
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
|
||||
@@ -564,6 +564,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outShape.depth = 1;
|
||||
outShape.arrayLayers = 6;
|
||||
return true;
|
||||
case TextureUploadTarget::CubeMapArray:
|
||||
case TextureUploadTarget::ProxyCubeMapArray:
|
||||
// GL_TEXTURE_CUBE_MAP_ARRAY is an array texture whose layers happen to be cube faces:
|
||||
// one 2D image with arrayLayers = 6 * cubeCount, CUBE_COMPATIBLE so the whole thing can
|
||||
// be sampled as a samplerCubeArray. glTexStorage3D hands the 6*n through as the GL depth
|
||||
// and the upload path's depthSelectsArrayLayer already lists VK_IMAGE_VIEW_TYPE_CUBE_ARRAY,
|
||||
// so the copies address layers correctly.
|
||||
//
|
||||
// A depth that is not a whole number of cubes, or a non-square level, has no Vulkan shape
|
||||
// - declined the way every other unrepresentable target is. This function's Bool return
|
||||
// exists for exactly that; asserting here would abort the process on ordinary application
|
||||
// input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all.
|
||||
if (texelSize.z() <= 0 || (texelSize.z() % 6) != 0 || texelSize.x() != texelSize.y()) {
|
||||
return false;
|
||||
}
|
||||
outShape.imageType = VK_IMAGE_TYPE_2D;
|
||||
outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
outShape.depth = 1;
|
||||
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -822,8 +843,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
|
||||
baseArrayLayer + layerCount > resource->arrayLayers) {
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
|
||||
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
|
||||
// SyncTextureResource asks for and may have had refused per format.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
|
||||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
|
||||
"2D-array-compatible=%d)",
|
||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||
mipLevel, sliceCount,
|
||||
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
} else if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
|
||||
baseArrayLayer + layerCount > resource->arrayLayers) {
|
||||
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
|
||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||
resource->arrayLayers);
|
||||
@@ -1492,11 +1528,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// preserve-copy path below carries the pixels over), so sequentially-
|
||||
// defined atlas mips do not recreate per level, and glGenerateMipmap -
|
||||
// which defines every level before syncing - works unchanged.
|
||||
const Uint32 backingMipLevels =
|
||||
isMultisampleTexture ? 1u
|
||||
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
|
||||
TextureShapeInfo shapeInfo{};
|
||||
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
||||
// ComputeFullMipLevelCount takes max(x, y, z), and for every ARRAY shape z is the layer
|
||||
// count, not a mip-able axis: a 4x4 array with 192 layers asked for 6 levels on an image
|
||||
// whose legal maximum is 3 (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own
|
||||
// extent - width, height and shapeInfo.depth, which is 1 for every array - can bound it.
|
||||
// lavapipe has been letting this through unvalidated; a strict driver would not.
|
||||
const IntVec3 mipExtent{texelSize.x(), texelSize.y(), static_cast<Int>(shapeInfo.depth)};
|
||||
const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent);
|
||||
const Uint32 backingMipLevels =
|
||||
isMultisampleTexture ? 1u : (mipLevels > 1 ? std::min(std::max(mipLevels, fullMipLevels), fullMipLevels) : 1u);
|
||||
if (!supportedShape) {
|
||||
// A gap in this backend's coverage, not a broken invariant: the GL front end accepts
|
||||
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||
@@ -1554,6 +1596,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||
// One z slice of a 3D texture can only be attached to a framebuffer through a 2D view over
|
||||
// it, which needs the image to be 2D-array-compatible (Vulkan 1.1 core, promoted from
|
||||
// VK_KHR_maintenance1). Asked for optimistically and withdrawn per format below if the
|
||||
// driver refuses - losing it only costs per-slice attachment, while failing creation would
|
||||
// lose the texture entirely.
|
||||
if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisampleTexture &&
|
||||
m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
}
|
||||
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
@@ -1711,7 +1762,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.pNext = &formatListInfo;
|
||||
}
|
||||
|
||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
if (isMultisampleTexture || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT |
|
||||
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
@@ -1734,6 +1786,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
imageInfo.flags, &imageFormatProperties);
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
m_2dArrayCompatibleUnsupported.insert(format);
|
||||
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
imageCreateFlags = imageInfo.flags;
|
||||
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
imageInfo.flags, &imageFormatProperties);
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS ||
|
||||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||
|
||||
@@ -487,6 +487,10 @@ private:
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
|
||||
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
||||
// remembered rather than probed once globally.
|
||||
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
|
||||
@@ -3037,17 +3037,6 @@ void main() {
|
||||
vkBuffers.assign(bindingCount, VK_NULL_HANDLE);
|
||||
vkOffsets.assign(bindingCount, 0);
|
||||
|
||||
auto findBufferByKey = [&](SizeT bufferKey) -> const SharedPtr<MG_State::GLState::BufferObject>* {
|
||||
const auto& attrs = vao.GetAllAttributes();
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
const auto& attr = attrs[location];
|
||||
if (attr.Buffer && reinterpret_cast<SizeT>(attr.Buffer.get()) == bufferKey) {
|
||||
return &attr.Buffer;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion,
|
||||
const MG_State::GLState::VertexAttribute& attribute,
|
||||
const Uint8* sourceData, SizeT sourceStride,
|
||||
@@ -3150,14 +3139,14 @@ void main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const SizeT bufferKey = vertexInputState.bindingBufferKeys[binding];
|
||||
// The VAO attribute already holds the buffer's SharedPtr; use it by reference directly
|
||||
// instead of re-resolving it from the GL context by external index (a map lookup +
|
||||
// atomic refcount every binding every draw).
|
||||
const SharedPtr<MG_State::GLState::BufferObject>* sourceBufferSharedPtr = findBufferByKey(bufferKey);
|
||||
MOBILEGL_ASSERT(sourceBufferSharedPtr != nullptr && *sourceBufferSharedPtr != nullptr,
|
||||
// VertexInputStateFactory fills bindingBufferKeys[b] and bindingAttributeLocations[b]
|
||||
// from the SAME loop iteration, one binding per enabled attribute with no merging, so
|
||||
// this attribute's Buffer IS the SharedPtr by construction - no need to search the VAO's
|
||||
// 32 slots for it. The client-memory branch above has already returned, so the location
|
||||
// is in range here.
|
||||
const auto& sourceBufferShared = vao.GetAttribute(bindingLocation).Buffer;
|
||||
MOBILEGL_ASSERT(sourceBufferShared != nullptr,
|
||||
"UploadAndBindVertexStreams failed to resolve source buffer");
|
||||
const auto& sourceBufferShared = *sourceBufferSharedPtr;
|
||||
BufferSlice slice{};
|
||||
const SizeT sourceSize = sourceBufferShared->GetSize();
|
||||
const SizeT baseOffset =
|
||||
@@ -3401,8 +3390,12 @@ void main() {
|
||||
substituteRestartIndex = restartIndex;
|
||||
}
|
||||
|
||||
const auto* indexBuffer =
|
||||
pIndexBufferView->forceClientMemory ? nullptr : vao.GetIndexBufferBindingSlot().GetBoundObject().get();
|
||||
// Bound by reference so the SharedPtr below is the one already in hand rather than a fresh
|
||||
// GL-name map lookup plus an atomic refcount pair on every indexed draw - the vertex path
|
||||
// above documents the same cost.
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& indexBufferShared =
|
||||
vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
const auto* indexBuffer = pIndexBufferView->forceClientMemory ? nullptr : indexBufferShared.get();
|
||||
if (indexBuffer == nullptr) {
|
||||
// No element-array buffer: the view's byte offset is a raw client pointer
|
||||
// (desktop drivers accept client-memory indices and the GL CTS relies on
|
||||
@@ -3441,7 +3434,6 @@ void main() {
|
||||
"DrawElements index range out of bounds");
|
||||
|
||||
BufferSlice slice{};
|
||||
auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex());
|
||||
MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO");
|
||||
if (substituteRestart) {
|
||||
// The whole buffer is rewritten, not just this draw's range, so that every element
|
||||
@@ -3694,6 +3686,13 @@ void main() {
|
||||
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
|
||||
.cullMode = VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_CLOCKWISE,
|
||||
// Functionally irrelevant to the blit (no flat varying, no capture), but on a device with
|
||||
// provokingVertexModePerPipeline == VK_FALSE a blit pipeline left on FIRST inside a render
|
||||
// pass whose draw pipelines are LAST is an illegal mix. Note this does NOT cover
|
||||
// GenerateDepthMipmapWithShader, which builds its pipeline directly and keeps Vulkan's
|
||||
// FIRST - legal only because it creates and begins its own render pass. Anything that ever
|
||||
// records that pipeline inside an outer render pass must route through this selector too.
|
||||
.provokingVertexMode = SelectProvokingVertexMode(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false),
|
||||
.depthTestEnable = false,
|
||||
.depthWriteEnable = false,
|
||||
.depthCompareOp = VK_COMPARE_OP_ALWAYS,
|
||||
@@ -4024,6 +4023,16 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A program that runs a geometry shader AND captures transform feedback. Both halves are
|
||||
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
||||
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
|
||||
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
|
||||
for (const auto& shader : program.GetAttachedShaders()) {
|
||||
if (shader && shader->GetShaderStage() == ShaderStage::Geometry) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
VkPipeline VulkanRenderer::GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
@@ -4051,7 +4060,10 @@ void main() {
|
||||
auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
||||
const Uint64 vertexLayoutHash = vis.layoutHash;
|
||||
const Uint64 renderPassHash = renderPassEntry.hash;
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
// The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask
|
||||
// and friends are dynamic state or not pipeline state at all, and keying the memo on the
|
||||
// all-state counter made any of them evict a perfectly good VkPipeline.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
||||
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||
@@ -4242,6 +4254,16 @@ void main() {
|
||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||
: VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_CLOCKWISE,
|
||||
// Read the geometry stage off the program's own shader list rather than
|
||||
// programObj.rasterizationProducerStage: that field is filled by the clip-fixup analysis,
|
||||
// which does not run for every program, so it reads Unknown for exactly the
|
||||
// geometry-plus-capture programs this guard exists to catch. Both inputs are link-time
|
||||
// facts folded into programObj.hash, which is what the pipeline memo and the
|
||||
// SetupDrawSnapshot fast path key on - so no memo can hand back a pipeline built for the
|
||||
// other mode. IsTransformFeedbackActive() would be a live bug here: neither memo key
|
||||
// moves on glBeginTransformFeedback.
|
||||
.provokingVertexMode = SelectProvokingVertexMode(
|
||||
vkTopology, ProgramCapturesXfbFromGeometryStage(program)),
|
||||
.depthTestEnable = depthTestEnabled,
|
||||
.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),
|
||||
.depthBiasEnable = polygonOffsetFillEnabled,
|
||||
@@ -4666,7 +4688,7 @@ void main() {
|
||||
snap.imageIndex != m_imageIndexAcquired) {
|
||||
return false;
|
||||
}
|
||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
if (program.GetLifetimeId() != snap.programLifetimeId ||
|
||||
program.GetBackendStateVersion() != snap.programVersion) {
|
||||
return false;
|
||||
@@ -4681,7 +4703,7 @@ void main() {
|
||||
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
||||
return false;
|
||||
}
|
||||
if (MG_State::pGLContext->GetRenderStateParametersVersion() != snap.renderStateVersion ||
|
||||
if (MG_State::pGLContext->GetPipelineStateVersion() != snap.renderStateVersion ||
|
||||
MG_State::pGLContext->GetTextureBindGeneration() != snap.bindGeneration) {
|
||||
return false;
|
||||
}
|
||||
@@ -4792,7 +4814,7 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
|
||||
// Captured draws take the xfb-decorated program variant.
|
||||
if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||
@@ -5145,7 +5167,7 @@ void main() {
|
||||
snap.drawFbo = drawFbo.get();
|
||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||
snap.drawFboIsDefault = drawFbo->IsDefaultFramebuffer();
|
||||
snap.renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
|
||||
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
snap.baseTransformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()).GetRaw();
|
||||
snap.resolvedTransformFlags = transformFlags.GetRaw();
|
||||
@@ -5176,7 +5198,7 @@ void main() {
|
||||
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
ProgramFactory::CompileOptionFlags transformFlags = 0;
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
|
||||
@@ -5216,7 +5238,7 @@ void main() {
|
||||
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
ProgramFactory::CompileOptionFlags transformFlags = 0;
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
|
||||
@@ -5894,6 +5916,83 @@ void main() {
|
||||
QueueClearBufferPayload(buffer, drawbuffer, payload);
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue) {
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
|
||||
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
|
||||
|
||||
// A 2D view over one z slice. Returns VK_NULL_HANDLE when the image is not
|
||||
// 2D-array-compatible, which is the whole reason this can fail.
|
||||
const VkImageView sliceView = m_textureManager->GetOrCreateAttachmentViewAtMipLevel(
|
||||
texture, mipLevel, depthSlice, 1, VK_IMAGE_VIEW_TYPE_2D);
|
||||
if (sliceView == VK_NULL_HANDLE) return false;
|
||||
|
||||
VkAttachmentDescription colorAttachment{};
|
||||
colorAttachment.format = resource->format;
|
||||
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
// Hand the slice back in the layout the caller already tracks for the whole image, so its
|
||||
// closing barrier stays truthful and resource->layout is never touched from in here.
|
||||
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
|
||||
VkAttachmentReference colorRef{};
|
||||
colorRef.attachment = 0;
|
||||
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &colorRef;
|
||||
|
||||
VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
renderPassInfo.attachmentCount = 1;
|
||||
renderPassInfo.pAttachments = &colorAttachment;
|
||||
renderPassInfo.subpassCount = 1;
|
||||
renderPassInfo.pSubpasses = &subpass;
|
||||
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
if (vkCreateRenderPass(m_device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) return false;
|
||||
|
||||
const Uint32 levelWidth = std::max(resource->extent.width >> mipLevel, 1u);
|
||||
const Uint32 levelHeight = std::max(resource->extent.height >> mipLevel, 1u);
|
||||
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &sliceView;
|
||||
framebufferInfo.width = levelWidth;
|
||||
framebufferInfo.height = levelHeight;
|
||||
framebufferInfo.layers = 1;
|
||||
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
if (vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) {
|
||||
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
VkRenderPassBeginInfo beginInfo{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
beginInfo.renderPass = renderPass;
|
||||
beginInfo.framebuffer = framebuffer;
|
||||
beginInfo.renderArea.extent = {levelWidth, levelHeight};
|
||||
beginInfo.clearValueCount = 1;
|
||||
beginInfo.pClearValues = &clearValue;
|
||||
// The load op is the whole operation: begin and end with nothing in between.
|
||||
vkCmdBeginRenderPass(commandBuffer, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
|
||||
// The image view is owned and memoised by the texture resource; only these two are throwaway.
|
||||
auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()];
|
||||
deferredCleanup.renderPasses.push_back(renderPass);
|
||||
deferredCleanup.framebuffers.push_back(framebuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
Vector<PendingClearEntry> pendingClears;
|
||||
@@ -5930,16 +6029,79 @@ void main() {
|
||||
MOBILEGL_ASSERT(pendingClear.key.mipLevel < resource->mipLevels,
|
||||
"MaterializePendingClearForTexture: textureId=%d pending clear mip=%u out of range %u",
|
||||
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= resource->arrayLayers,
|
||||
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds arrayLayers=%u",
|
||||
// FIXME: a layered clear of a GL_TEXTURE_3D texture still reads back wrong.
|
||||
// KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support fails on
|
||||
// DirectVulkan: it attaches a 4-deep 3D texture with glFramebufferTexture (layered),
|
||||
// clears with glClearBufferiv, then reads each slice back through
|
||||
// glFramebufferTextureLayer and gets zeros. Those cases exist only in the GL44+ lists,
|
||||
// above the 4.0 this backend reports, so they are outside the current conformance
|
||||
// claim - but the feature (layered attachment, GL 3.2) is not, so an application can
|
||||
// reach this.
|
||||
//
|
||||
// Already ruled out by bisecting with temporary bypasses, so do not re-test these:
|
||||
// - the per-slice render-pass clear below (disabling it changes nothing)
|
||||
// - the per-target gate in FramebufferTextureLayer_State (it already permits
|
||||
// Texture3D here; bypassing it changes nothing)
|
||||
// - VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on the 3D image (not requesting it
|
||||
// changes nothing)
|
||||
// What IS fixed here is the subresource range below: a layered GL clear queues
|
||||
// layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image, and the old code
|
||||
// passed it straight through - running the case standalone against the previous build
|
||||
// trips MOBILEGL_ASSERT(baseArrayLayer + layerCount <= arrayLayers) as 0 + 4 <= 1.
|
||||
//
|
||||
// Note when picking this up: the case does not reproduce standalone the way it behaves
|
||||
// in a batch run (batch passed before this change, standalone asserted), so it depends
|
||||
// on state left by earlier cases. Reproduce it inside a chunk, not on its own.
|
||||
//
|
||||
// A 3D image keeps its GL layers on the z axis (arrayLayers == 1), so the pending
|
||||
// clear's "layer" is a slice index bounded by the mip level's depth.
|
||||
const Bool clearAddressesDepthSlices = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
||||
const Uint32 clearableLayers = clearAddressesDepthSlices
|
||||
? std::max(resource->depth >> pendingClear.key.mipLevel, 1u)
|
||||
: resource->arrayLayers;
|
||||
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= clearableLayers,
|
||||
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds %u",
|
||||
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer,
|
||||
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, resource->arrayLayers);
|
||||
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, clearableLayers);
|
||||
// Whether this clear names a strict SUBSET of the level. A layered attachment
|
||||
// (glFramebufferTexture) queues layerCount = the whole depth, a single-slice one
|
||||
// (glFramebufferTextureLayer) queues 1 - so the key already distinguishes them, and it is
|
||||
// the clear's span that decides, not the image's slice count. Reading the latter sent a
|
||||
// layered clear of a 3D texture down the per-slice path, where it cleared slice zero and
|
||||
// left the rest stale (geometry_shader.layered_framebuffer.clear_call_support).
|
||||
const Bool clearsWholeLevel =
|
||||
pendingClear.key.baseArrayLayer == 0 && pendingClear.key.layerCount >= clearableLayers;
|
||||
if (clearAddressesDepthSlices && clearableLayers > 1 && !clearsWholeLevel) {
|
||||
// vkCmdClearColorImage cannot clear a subset of a 3D image's slices:
|
||||
// VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins baseArrayLayer to 0 and
|
||||
// layerCount to 1 for VK_IMAGE_TYPE_3D, i.e. the whole mip level. A render pass whose
|
||||
// only content is its LOAD_OP_CLEAR does address exactly one slice, because its
|
||||
// attachment is a 2D view over that slice.
|
||||
auto clearPayload3D = pendingClear.payload;
|
||||
PreCompensateSrgbClearColor(clearPayload3D, resource->format);
|
||||
VkClearValue sliceClearValue{};
|
||||
sliceClearValue.color = MakeVkClearColorValue(clearPayload3D, ColorFormatLacksAlpha(&texture));
|
||||
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel,
|
||||
pendingClear.key.baseArrayLayer, sliceClearValue)) {
|
||||
// The device or the format refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, so
|
||||
// there is no way to name this slice. Leaving it uncleared is wrong pixels;
|
||||
// asserting would abort a process that glFramebufferTextureLayer can reach at will.
|
||||
MGLOG_W("MaterializePendingClearForTexture: textureId=%d slice %u could not be cleared "
|
||||
"(no 2D-array-compatible view)",
|
||||
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
VkImageSubresourceRange subresourceRange{};
|
||||
subresourceRange.baseMipLevel = pendingClear.key.mipLevel;
|
||||
subresourceRange.levelCount = 1;
|
||||
subresourceRange.baseArrayLayer = pendingClear.key.baseArrayLayer;
|
||||
subresourceRange.layerCount = pendingClear.key.layerCount;
|
||||
// VUID-vkCmdClearColorImage-baseArrayLayer-01472: for a VK_IMAGE_TYPE_3D image the range
|
||||
// must name baseArrayLayer 0 and layerCount 1, which Vulkan reads as "the whole mip
|
||||
// level" - the z extent is not an array dimension. A layered GL clear queues
|
||||
// layerCount = depth, which is the right GL answer and an illegal Vulkan one.
|
||||
subresourceRange.baseArrayLayer = clearAddressesDepthSlices ? 0u : pendingClear.key.baseArrayLayer;
|
||||
subresourceRange.layerCount = clearAddressesDepthSlices ? 1u : pendingClear.key.layerCount;
|
||||
|
||||
auto clearPayload = pendingClear.payload;
|
||||
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
||||
@@ -9279,6 +9441,41 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
VkProvokingVertexModeEXT VulkanRenderer::SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||
Bool capturesXfbFromGeometryStage) const {
|
||||
if (!m_provokingVertexLastEnabled) {
|
||||
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
}
|
||||
// Measured, and identical on lavapipe and on the NVIDIA Vulkan driver: a geometry shader's
|
||||
// emitted triangle strip is already recorded in GL's provoking-last vertex order, so asking
|
||||
// for LAST rotates it a second time. The input-assembler path has the opposite problem, and
|
||||
// the mode is a single pipeline bit, so the two cannot be satisfied at once: a program that
|
||||
// both runs a geometry shader and captures transform feedback keeps Vulkan's own convention,
|
||||
// and pays for it with a GL-wrong flat vertex in that one case. Deliberately a link-time
|
||||
// program property, not IsTransformFeedbackActive() - see the memo note in the header.
|
||||
if (capturesXfbFromGeometryStage) {
|
||||
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
}
|
||||
// VUID-VkGraphicsPipelineCreateInfo-topology-04884 only bites when
|
||||
// transformFeedbackPreservesProvokingVertex is enabled; when it is not, a fan may take LAST.
|
||||
if (m_provokingVertexXfbPreserveEnabled && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN &&
|
||||
!m_provokingVertexFanPreserved) {
|
||||
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
}
|
||||
// Only provokingVertexModePerPipeline lets modes differ inside one render pass instance;
|
||||
// elsewhere every pipeline takes GL's default so the render pass stays self-consistent, and
|
||||
// glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) goes unhonoured. Honouring it there would
|
||||
// mean ending the render pass on every glProvokingVertex change; not worth it until a target
|
||||
// device actually lacks the property.
|
||||
if (!m_provokingVertexModePerPipeline) {
|
||||
return VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
|
||||
}
|
||||
return (MG_State::pGLContext != nullptr &&
|
||||
MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
|
||||
? VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT
|
||||
: VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::IsTimerQuerySupported() const {
|
||||
return m_timerQuerySupported && m_timerQueryManager != nullptr;
|
||||
}
|
||||
@@ -9925,6 +10122,16 @@ void main() {
|
||||
deviceFeatures.wideLines = supportedDeviceFeatures.wideLines;
|
||||
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
|
||||
deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64;
|
||||
// Required for any module that declares OpCapability Float64 - which is every shader with a
|
||||
// double in it, including the 64-bit vertex attribute path (the attribute itself arrives as
|
||||
// uint32 words, but the bitcast result and everything computed from it is Float64). Without
|
||||
// it vkCreateShaderModule is invalid usage (VUID-VkShaderModuleCreateInfo-pCode-08740),
|
||||
// which is why SupportsFloat64VertexAttributes gates the entry point on the same feature.
|
||||
deviceFeatures.shaderFloat64 = supportedDeviceFeatures.shaderFloat64;
|
||||
// Required before a VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created
|
||||
// (VUID-VkImageViewCreateInfo-viewType-01004). Without it a cube map array texture cannot
|
||||
// get its sampled or full view, so SyncTextureResource fails and the texture stays unbacked.
|
||||
deviceFeatures.imageCubeArray = supportedDeviceFeatures.imageCubeArray;
|
||||
// Required for desktop GL image load/store semantics. iterationRP writes storage
|
||||
// images from vertex and fragment stages and uses formats outside Vulkan's small
|
||||
// mandatory storage-image set.
|
||||
@@ -10105,6 +10312,79 @@ void main() {
|
||||
MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME);
|
||||
}
|
||||
}
|
||||
// VK_EXT_provoking_vertex. Two independent features live behind one extension:
|
||||
// provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and
|
||||
// the input-assembler capture order.
|
||||
// transformFeedbackPreservesProvokingVertex -> spec-level guarantee for the capture order;
|
||||
// only legal when the transformFeedback
|
||||
// feature is also enabled, which is why this
|
||||
// block sits after the one above.
|
||||
// They are enabled independently on purpose: gating the first on the second would leave flat
|
||||
// shading GL-wrong on any device without VK_EXT_transform_feedback, for no legality reason.
|
||||
m_provokingVertexLastEnabled = false;
|
||||
m_provokingVertexXfbPreserveEnabled = false;
|
||||
m_provokingVertexModePerPipeline = false;
|
||||
m_provokingVertexFanPreserved = false;
|
||||
VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{};
|
||||
provokingVertexFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT;
|
||||
if (IsExtensionSupported(availableExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME) &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &provokingVertexFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
|
||||
VkPhysicalDeviceProvokingVertexPropertiesEXT provokingVertexProperties{};
|
||||
provokingVertexProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT;
|
||||
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
|
||||
if (getPhysicalDeviceProperties2 == nullptr) {
|
||||
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
|
||||
}
|
||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
||||
VkPhysicalDeviceProperties2 propertyQuery{};
|
||||
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
propertyQuery.pNext = &provokingVertexProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
|
||||
}
|
||||
m_provokingVertexModePerPipeline = provokingVertexProperties.provokingVertexModePerPipeline == VK_TRUE;
|
||||
m_provokingVertexFanPreserved =
|
||||
provokingVertexProperties.transformFeedbackPreservesTriangleFanProvokingVertex == VK_TRUE;
|
||||
|
||||
if (provokingVertexFeatures.provokingVertexLast == VK_TRUE) {
|
||||
// transformFeedbackPreservesProvokingVertex is deliberately NOT requested. Measured:
|
||||
// asking for it regresses transform_feedback.geometry on GL33 through GL45. A
|
||||
// geometry shader emits its triangles already in GL's vertex order, and the pipeline
|
||||
// that captures them runs on FIRST (see SelectProvokingVertexMode); without the
|
||||
// guarantee the driver leaves that stream alone, but with it the capture is forced to
|
||||
// follow the pipeline's FIRST convention and comes back rotated. The guarantee buys
|
||||
// nothing here either - the input-assembler capture order that
|
||||
// direct_state_access.queries_functional needs comes from provokingVertexLast alone,
|
||||
// which was confirmed by measurement. Leaving it off also keeps VU 04884 disarmed, so
|
||||
// a TRIANGLE_FAN pipeline may take LAST on any device.
|
||||
const Bool wantXfbPreserve = false;
|
||||
|
||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME);
|
||||
}
|
||||
provokingVertexFeatures.provokingVertexLast = VK_TRUE;
|
||||
provokingVertexFeatures.transformFeedbackPreservesProvokingVertex =
|
||||
wantXfbPreserve ? VK_TRUE : VK_FALSE;
|
||||
provokingVertexFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &provokingVertexFeatures;
|
||||
m_provokingVertexLastEnabled = true;
|
||||
m_provokingVertexXfbPreserveEnabled = wantXfbPreserve;
|
||||
MGLOG_I("Enabled optional device extension: %s (transformFeedbackPreservesProvokingVertex=%s)",
|
||||
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, wantXfbPreserve ? "true" : "false");
|
||||
}
|
||||
}
|
||||
if (!m_provokingVertexLastEnabled) {
|
||||
MGLOG_W("VK_EXT_provoking_vertex is unavailable; flat-shaded varyings take a primitive's first "
|
||||
"vertex instead of GL's last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN "
|
||||
"triangles rotated (0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
|
||||
}
|
||||
|
||||
if (!m_transformFeedbackFeatureEnabled) {
|
||||
MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work");
|
||||
}
|
||||
|
||||
@@ -498,6 +498,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
// VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
|
||||
// default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
|
||||
// feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
|
||||
// input-assembler capture order and has no dependency on transform feedback; only
|
||||
// transformFeedbackPreservesProvokingVertex does.
|
||||
Bool m_provokingVertexLastEnabled = false;
|
||||
// transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
|
||||
// separate because it is the only thing that arms
|
||||
// VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
|
||||
// pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
|
||||
Bool m_provokingVertexXfbPreserveEnabled = false;
|
||||
// provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
|
||||
// must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
|
||||
// per draw and every pipeline takes GL's default (LAST) instead.
|
||||
Bool m_provokingVertexModePerPipeline = false;
|
||||
// transformFeedbackPreservesTriangleFanProvokingVertex.
|
||||
Bool m_provokingVertexFanPreserved = false;
|
||||
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
||||
// property of the program, never the dynamic "is transform feedback active" flag: the
|
||||
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
||||
// GetRenderStateParametersVersion(), neither of which moves when glBeginTransformFeedback is
|
||||
// called, so a dynamic input here would hand back a stale VkPipeline.
|
||||
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||
Bool capturesXfbFromGeometryStage) const;
|
||||
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
||||
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
||||
Bool m_vertexAttributeDivisorEnabled = false;
|
||||
@@ -791,6 +815,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLenum filter);
|
||||
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
|
||||
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
|
||||
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue);
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
Bool MaterializePendingClearForRenderbuffer(
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!currentProgram) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -37,7 +37,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return false;
|
||||
|
||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -173,7 +173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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 auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||
Bool compatible = false;
|
||||
@@ -707,7 +707,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||
return;
|
||||
}
|
||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
||||
const auto& program = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
|
||||
@@ -320,7 +320,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum prog
|
||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_STUB_END(GLuint, CreateShaderProgramv, type, count, strings)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_END(GLuint, CreateShaderProgramv, type, count, strings)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
|
||||
|
||||
@@ -1069,11 +1069,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
|
||||
break;
|
||||
case TextureTarget::Texture1DArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture1DArray;
|
||||
break;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
textureUploadTarget = TextureUploadTarget::CubeMapArray;
|
||||
break;
|
||||
default:
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"FramebufferTextureLayer requires a 3D, array, 2D multisample "
|
||||
"array, or cube map array texture."));
|
||||
return;
|
||||
}
|
||||
// The same backend question the DSA twin asks. GL 4.6 core 9.2.8 makes the two entry points
|
||||
// equivalent, so they have to decline in the same places - leaving this one ungated is what
|
||||
// let an unrepresentable attachment reach the renderer, and it also refused cube map arrays
|
||||
// that GL requires it to accept.
|
||||
{
|
||||
const auto& layerLimits = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||
: MG_Backend::DynamicBackendParameters{};
|
||||
const TextureTarget layeredTarget = textureObject->GetTarget();
|
||||
if ((layer != 0 || layeredTarget == TextureTarget::TextureCubeMapArray) &&
|
||||
!layerLimits.SupportsPerLayerFramebufferAttachment(layeredTarget)) {
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
|
||||
}
|
||||
|
||||
@@ -1433,11 +1458,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// so a slice lands outside the image and the renderer asserts on the clear. Letting it
|
||||
// through there would only move the failure downstream, so it is declined instead - layer
|
||||
// zero always works, being the plain first-slice attachment.
|
||||
const Bool backsLayeredAttachment = limits.SupportsPerLayerFramebufferAttachment;
|
||||
// A cube map array additionally has no image shape at all in VkTextureManager, so on that
|
||||
// backend it cannot be an attachment whatever the layer is.
|
||||
const Bool isCubeMapArray = textureObject->GetTarget() == TextureTarget::TextureCubeMapArray;
|
||||
if ((layer != 0 && !backsLayeredAttachment) || (isCubeMapArray && !backsLayeredAttachment)) {
|
||||
// ...and it is a DIFFERENT question per target: a 2D/2D-multisample array layer is a Vulkan
|
||||
// array layer, a 3D layer is a z slice, and a cube map array needs a cube-compatible image
|
||||
// before it has any layer to name. Ask the backend about this texture's target rather than
|
||||
// guessing from one blanket flag.
|
||||
const TextureTarget layeredTextureTarget = textureObject->GetTarget();
|
||||
const Bool backsThisTargetsLayers = limits.SupportsPerLayerFramebufferAttachment(layeredTextureTarget);
|
||||
// Layer zero of a 3D or array texture is the plain first-slice attachment every backend can
|
||||
// already express, so it stays legal even where per-layer selection is not backed. A cube map
|
||||
// array has no such fallback: layer zero is still one face of one cube inside a
|
||||
// cube-compatible image, so it needs the same support layer 5 does.
|
||||
const Bool needsPerLayerSupport =
|
||||
layer != 0 || layeredTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||
if (needsPerLayerSupport && !backsThisTargetsLayers) {
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
|
||||
return;
|
||||
|
||||
@@ -701,6 +701,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PROGRAM_BINARY_RETRIEVABLE_HINT:
|
||||
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_PROGRAM_SEPARABLE:
|
||||
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
@@ -1094,7 +1097,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Uniformv_State(GLint location, GLsizei count, T* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1286,7 +1289,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// If transpose is GL_TRUE, we need to transpose the matrix data
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1321,7 +1324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// If transpose is GL_TRUE, we need to transpose the matrix data
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1361,7 +1364,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// If transpose is GL_TRUE, we need to transpose the matrix data
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1394,7 +1397,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2055,7 +2058,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2081,7 +2084,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2107,7 +2110,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2133,7 +2136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2159,7 +2162,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2185,7 +2188,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2211,7 +2214,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2237,7 +2240,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2263,7 +2266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = MG_State::pGLContext->GetCurrentProgram();
|
||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
if (programObject == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -2746,7 +2749,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ProgramParameteri(GLuint program, GLenum pname, GLint value) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT) {
|
||||
if (pname != GL_PROGRAM_BINARY_RETRIEVABLE_HINT && pname != GL_PROGRAM_SEPARABLE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname is not an accepted value."));
|
||||
@@ -2758,9 +2761,48 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "value must be GL_TRUE or GL_FALSE."));
|
||||
return;
|
||||
}
|
||||
if (pname == GL_PROGRAM_SEPARABLE) {
|
||||
programObject->SetSeparable(value == GL_TRUE);
|
||||
return;
|
||||
}
|
||||
programObject->SetBinaryRetrievableHint(value == GL_TRUE);
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.3: glCreateShaderProgramv is defined as the exact sequence below, so it
|
||||
// is written as that sequence rather than as a private shortcut - every error it can
|
||||
// raise is one of theirs, raised at the point they would raise it.
|
||||
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) {
|
||||
const GLuint shader = CreateShader_State(type);
|
||||
if (shader == 0) return 0;
|
||||
|
||||
ShaderSource_State(shader, count, strings, nullptr);
|
||||
CompileShader_State(shader);
|
||||
|
||||
const GLuint program = CreateProgram_State();
|
||||
if (program != 0) {
|
||||
const auto& shaderObject = MG_State::pGLContext->GetShaderObject(shader);
|
||||
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
// The program is separable whether or not the shader compiled: a failed
|
||||
// compile leaves an unlinked but otherwise well-formed separable program.
|
||||
if (programObject) programObject->SetSeparable(true);
|
||||
if (shaderObject && programObject && shaderObject->GetCompileStatus()) {
|
||||
AttachShader_State(program, shader);
|
||||
// Not LinkProgram_State: that injects a default fragment shader into a
|
||||
// program that has none, which is exactly wrong for a separable
|
||||
// vertex-stage program - the pipeline supplies the real one.
|
||||
programObject->Link(false);
|
||||
// glDetachShader defers the removal to the next link, so the program keeps
|
||||
// the shader object it was built from while no longer reporting it attached.
|
||||
DetachShader_State(program, shader);
|
||||
}
|
||||
if (shaderObject && programObject && !shaderObject->GetInfoLog().empty()) {
|
||||
programObject->AppendInfoLog(shaderObject->GetInfoLog());
|
||||
}
|
||||
}
|
||||
DeleteShader_State(shader);
|
||||
return program;
|
||||
}
|
||||
|
||||
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) {
|
||||
(void)binaryFormat;
|
||||
(void)binary;
|
||||
|
||||
@@ -174,6 +174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
|
||||
void ValidateProgram(GLuint program);
|
||||
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
|
||||
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings);
|
||||
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);
|
||||
|
||||
@@ -216,8 +216,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// compressed format up front, so no texture image MobileGL holds can be compressed. Written
|
||||
// as a predicate rather than a literal false so both level-parameter getters stay in step
|
||||
// once compressed formats do land.
|
||||
Bool IsCompressedTextureFormat(TextureInternalFormat) {
|
||||
return false;
|
||||
// GL 4.6 core 8.11 asks "is *this level* stored compressed", not "is the texture's internal
|
||||
// format a compressed one", and here the two genuinely differ: a compressed internalformat
|
||||
// handed to glTexImage2D resolves to the uncompressed storage that backs it (see
|
||||
// ConvertGLEnumToTextureInternalFormat), so the texture's format enum can never answer yes.
|
||||
// The only levels stored compressed are the ones glCompressedTexImage* shadowed verbatim,
|
||||
// which is exactly what the per-level compressed format records.
|
||||
//
|
||||
// No level-count guard on purpose: TextureObject2DCube::GetMipmapLevelCount() reports face
|
||||
// zero's chain only, so a count check would answer GL_NONE for a compressed image on any
|
||||
// other face - precisely the per-face independence the storage layer provides. MipmapStorage's
|
||||
// own getters already bounds-check per target and return GL_NONE for an unallocated level.
|
||||
GLenum GetCompressedLevelFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
if (!textureObject || level < 0) return GL_NONE;
|
||||
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!textureMipmapObject) return GL_NONE;
|
||||
return textureMipmapObject->GetMipmapCompressedFormat(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
GLint GetTextureLevelComponentParameter(TextureInternalFormat textureInternalFormat, GLenum pname) {
|
||||
@@ -538,10 +553,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
}
|
||||
|
||||
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
|
||||
// reports 0, so every compressed internalformat is by definition unsupported and
|
||||
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
|
||||
// a C++ exception through the C GL ABI and takes the process down.
|
||||
// The compressed internalformat is not one this stack can store (see
|
||||
// MG_Util::GetCompressedFormatInfo for the accepted set: the RGTC/BPTC/ETC2-EAC formats core
|
||||
// GL requires). GL_INVALID_ENUM is the specified error for an unsupported compressed format -
|
||||
// unlike THROW_UNIMPL_EXCEPTION, which unwinds a C++ exception through the C GL ABI and takes
|
||||
// the process down. Still the only outcome for the 1D/3D and sub-image entry points, which
|
||||
// have no compressed upload path yet.
|
||||
void RecordUnsupportedCompressedFormat(const char* caller) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -2877,7 +2894,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
*params = (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (compressedFormat != GL_NONE)
|
||||
? (GLint)compressedFormat
|
||||
: (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_SAMPLES:
|
||||
@@ -2907,14 +2931,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_TEXTURE_COMPRESSED:
|
||||
if (params) {
|
||||
*params = IsCompressedTextureFormat(textureObject->GetFormat()) ? GL_TRUE : GL_FALSE;
|
||||
*params =
|
||||
(GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? GL_TRUE
|
||||
: GL_FALSE;
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE:
|
||||
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
|
||||
// GL 4.6 core 8.11: there is no compressed size to report for an image whose internal
|
||||
// format is uncompressed, nor for a proxy target, and the query is INVALID_OPERATION
|
||||
// rather than a zero.
|
||||
if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) {
|
||||
if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -2923,9 +2949,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (params) {
|
||||
*params = 0;
|
||||
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
*params = static_cast<GLint>(
|
||||
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
||||
@@ -3000,7 +3029,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_TEXTURE_INTERNAL_FORMAT:
|
||||
if (params) {
|
||||
*params = (GLfloat)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
|
||||
// A level stored compressed must report the token it was given, not the
|
||||
// uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets
|
||||
// that tag, so every level created by glTexImage*D - including one given a compressed
|
||||
// internalformat - still answers with its resolved storage format.
|
||||
const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
|
||||
*params = (GLfloat)((compressedFormat != GL_NONE)
|
||||
? compressedFormat
|
||||
: MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()));
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_SAMPLES:
|
||||
@@ -3030,13 +3066,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_TEXTURE_COMPRESSED:
|
||||
if (params) {
|
||||
*params = IsCompressedTextureFormat(textureObject->GetFormat()) ? 1.0f : 0.0f;
|
||||
*params =
|
||||
(GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? 1.0f : 0.0f;
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE:
|
||||
case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
|
||||
// See GetTexLevelParameteriv_State: uncompressed images and proxy targets have no
|
||||
// compressed size to report, so GL 4.6 core 8.11 makes the query an error.
|
||||
if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) {
|
||||
if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
@@ -3045,9 +3082,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (params) {
|
||||
*params = 0.0f;
|
||||
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
*params = static_cast<GLfloat>(
|
||||
textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
||||
@@ -3056,14 +3096,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the
|
||||
// same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from
|
||||
// the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target
|
||||
// form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE),
|
||||
// so only the DSA form passes a real bound.
|
||||
void CopyCompressedTextureImageToClientOrPBO(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize,
|
||||
void* pixels, const char* caller) {
|
||||
if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture level is not stored in a compressed format."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
const SizeT imageSize =
|
||||
textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast<Uint>(level));
|
||||
const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast<Uint>(level));
|
||||
if (!src || imageSize == 0) return;
|
||||
|
||||
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < imageSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
if (pixelPackBufferObject) {
|
||||
if (pixelPackBufferObject->IsMapped()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel pack buffer is currently mapped."));
|
||||
return;
|
||||
}
|
||||
const SizeT offset = reinterpret_cast<SizeT>(pixels);
|
||||
const SizeT bufferSize = pixelPackBufferObject->GetSize();
|
||||
if (offset > bufferSize || imageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Packing would write past the end of the pixel pack buffer."));
|
||||
return;
|
||||
}
|
||||
pixelPackBufferObject->UploadSubData({const_cast<void*>(src), imageSize}, offset);
|
||||
return;
|
||||
}
|
||||
|
||||
// No pixel-store packing here on purpose: GL 4.6 core 8.11 says the pixel storage modes are
|
||||
// ignored for a compressed image, which is also the only way the round trip stays byte-exact.
|
||||
if (pixels) Memcpy(pixels, src, imageSize);
|
||||
}
|
||||
|
||||
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
|
||||
// TODO: implement compressed readback. Reporting success while writing nothing hands
|
||||
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
|
||||
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Texture level is not stored in a compressed format."));
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
// ValidateTextureUploadTarget records InvalidEnum itself; wrapping it in a second RecordError
|
||||
// would report one failure twice.
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
CopyCompressedTextureImageToClientOrPBO(textureObject, textureUploadTarget, level, -1, img, __func__);
|
||||
}
|
||||
|
||||
void GenTextures_State(GLsizei n, GLuint* textures) {
|
||||
@@ -3314,16 +3412,102 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
GLint border, GLsizei imageSize, const void* data) {
|
||||
// ======================= Converting ================================
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
// Zero block width doubles as "internalformat is not a specific compressed format", which is
|
||||
// the INVALID_ENUM case - one lookup answers both questions.
|
||||
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
|
||||
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
|
||||
if (compressedInfo.blockWidth == 0) {
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
|
||||
// otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
|
||||
const SizeT expectedImageSize =
|
||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
|
||||
if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"imageSize does not match the compressed image size."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Object resolution copied from TexImage2D_State rather than routed through
|
||||
// GetTextureObjectByTarget: GL 4.6 core 8.7 lets a proxy target reach glCompressedTexImage2D,
|
||||
// and only CreateOrReplaceProxyTextureObject gives the proxy a fresh object to answer the
|
||||
// level queries from.
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement compressed upload. Until then report the spec error for an
|
||||
// unsupported compressed format rather than throwing - a C++ exception unwinding
|
||||
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
|
||||
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
// ======================= Processing ================================
|
||||
// Texel storage stays uncompressed, exactly the deviation the RGTC/BPTC/ETC2 arms of
|
||||
// ConvertGLEnumToTextureInternalFormat already document: neither backend has a BC/ETC codec
|
||||
// and TextureInternalFormat has no compressed enumerator, so the shadow keeps the "one
|
||||
// format, N bytes per texel" layout the backend upload sizing, glGenerateMipmap's
|
||||
// bytes-per-texel division and the pixel-store packer all rely on. The image therefore
|
||||
// samples as zeros. The application's bytes are kept beside it so glGetCompressedTexImage can
|
||||
// return the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could
|
||||
// satisfy byte for byte.
|
||||
const TextureInternalFormat textureInternalFormat =
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
textureObject->SetInternalFormat(textureInternalFormat);
|
||||
|
||||
// A proxy records the format and nothing else - it must never take storage, and it must never
|
||||
// be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
|
||||
// INVALID_OPERATION.
|
||||
if (isProxy) return;
|
||||
|
||||
const SizeT internalBpp =
|
||||
MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
|
||||
const SizeT internalBytes = static_cast<SizeT>(width) * static_cast<SizeT>(height) * internalBpp;
|
||||
|
||||
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
// AllocateStorage clears any compressed image the level used to hold, so this must run before
|
||||
// SetMipmapCompressedImage re-arms it.
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});
|
||||
|
||||
const void* compressedBytes = data;
|
||||
const auto& pixelUnpackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
|
||||
if (pixelUnpackBufferObject) {
|
||||
if (pixelUnpackBufferObject->IsMapped()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Pixel unpack buffer is currently mapped."));
|
||||
return;
|
||||
}
|
||||
const SizeT offset = reinterpret_cast<SizeT>(data);
|
||||
const SizeT bufferSize = pixelUnpackBufferObject->GetSize();
|
||||
if (offset > bufferSize || expectedImageSize > bufferSize - offset) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Unpacking would read past the end of the pixel unpack buffer."));
|
||||
return;
|
||||
}
|
||||
compressedBytes = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) + offset;
|
||||
}
|
||||
textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
|
||||
expectedImageSize);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
}
|
||||
|
||||
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||
@@ -4273,13 +4457,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// texture would also fail the compressed check below.
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
|
||||
// No texture MobileGL holds is compressed (see IsCompressedTextureFormat), so this is the
|
||||
// only outcome today. Reporting success while writing nothing would hand the caller stale
|
||||
// memory with GL_NO_ERROR - the same reasoning as GetCompressedTexImage_State.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Texture level is not stored in a compressed format."));
|
||||
// Unlike glGetTextureImage this never asks a backend: the compressed image only ever exists
|
||||
// in the CPU shadow (no backend was handed the compressed bytes at all), so the shadow is
|
||||
// authoritative rather than potentially stale.
|
||||
CopyCompressedTextureImageToClientOrPBO(textureObject, GetPrimaryUploadTarget(textureObject), level, bufSize,
|
||||
pixels, __func__);
|
||||
}
|
||||
|
||||
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "GL_VertexArray.h"
|
||||
#include "Validators.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Buffer/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
@@ -173,6 +174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_CURRENT_VERTEX_ATTRIB:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||
// Core since GL 4.1 (ARB_vertex_attrib_64bit). It was rejected while no attribute could
|
||||
// ever be long; now that IsLong is real state the pname has to be accepted.
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
||||
return true;
|
||||
@@ -460,19 +464,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
relativeoffset, isBgra);
|
||||
}
|
||||
|
||||
// The long (64-bit) attribute format. MobileGL has no 64-bit vertex attributes, so nothing is
|
||||
// recorded; what the entry point owes the application is the parameter validation, which is
|
||||
// observable through glGetError regardless of whether the format could be used in a draw.
|
||||
static void VertexAttribLFormatSeparate_State(GLuint attribindex, GLint size, GLenum type,
|
||||
// The long (64-bit) attribute format: the values reach the shader as doubles, unconverted
|
||||
// (GL 4.6 core 10.3.2). ValidateVertexAttribLFormat has already pinned type to GL_DOUBLE, so the
|
||||
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
||||
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
||||
//
|
||||
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
||||
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
||||
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
||||
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
||||
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||
GLuint attribindex, GLint size, GLenum type,
|
||||
GLuint relativeoffset) {
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
||||
"64-bit vertex attributes are not supported."));
|
||||
if (!MG_Backend::pActiveBackendObject ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
"backend has no double-precision vertex attribute support - see the "
|
||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||
attribindex);
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
||||
"64-bit vertex attributes are not supported by this backend."));
|
||||
return;
|
||||
}
|
||||
|
||||
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
||||
/*normalized: */ false, /*isInteger: */ false, relativeoffset,
|
||||
/*isBgra: */ false, /*isLong: */ true);
|
||||
}
|
||||
|
||||
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
||||
@@ -915,6 +938,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||
params[0] = attr->IsInteger ? 1.0f : 0.0f;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
params[0] = attr->IsLong ? 1.0f : 0.0f;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLfloat>(attr->Divisor);
|
||||
return;
|
||||
@@ -975,6 +1001,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||
params[0] = attr->IsInteger ? 1.0 : 0.0;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
params[0] = attr->IsLong ? 1.0 : 0.0;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLdouble>(attr->Divisor);
|
||||
return;
|
||||
@@ -1031,6 +1060,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
params[0] = attr->IsLong ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
params[0] = static_cast<GLint>(attr->Divisor);
|
||||
return;
|
||||
@@ -1164,8 +1196,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
// 64-bit attributes are not supported, so no attribute is ever a long one.
|
||||
*param = GL_FALSE;
|
||||
*param = attr.IsLong ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
*param = static_cast<GLint>(attr.Divisor);
|
||||
@@ -1259,13 +1290,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
|
||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||
}
|
||||
|
||||
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
|
||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||
}
|
||||
|
||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||
|
||||
@@ -343,7 +343,60 @@ namespace MobileGL::MG_State {
|
||||
return m_programState.GetCurrentProgram();
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
||||
if (currentProgram) return currentProgram;
|
||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||
const auto& pipeline = GetBoundProgramPipeline();
|
||||
if (!pipeline) return nullProgram;
|
||||
|
||||
const auto signature = pipeline->ComputeDrawProgramSignature();
|
||||
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
|
||||
|
||||
// Everything downstream of here - the backends, the uniform plumbing, the draw
|
||||
// validation - is written against a single linked program, so the pipeline is
|
||||
// flattened into one. Each stage contributes only the shaders that serve it, so a
|
||||
// program bound to two stages is not pulled in twice and a program bound to a
|
||||
// stage it does not implement contributes nothing.
|
||||
// Deliberately not a named program: it is reachable only through the pipeline, it
|
||||
// must not answer glIsProgram, and it must not consume a name the application
|
||||
// could otherwise be handed. Backend registries key on the object, not the name.
|
||||
auto composite = MakeShared<ProgramObject>(0u);
|
||||
|
||||
Bool anyStage = false;
|
||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
for (const auto& shader : stageProgram->GetAttachedShaders()) {
|
||||
if (!shader || static_cast<SizeT>(shader->GetShaderStage()) != stage) continue;
|
||||
composite->AttachShader(shader);
|
||||
anyStage = true;
|
||||
}
|
||||
}
|
||||
if (!anyStage) return nullProgram;
|
||||
// A pipeline with no fragment stage still rasterises, so the default fragment
|
||||
// shader is wanted here even though the separable stage programs never get one.
|
||||
composite->Link(true);
|
||||
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
||||
return pipeline->GetCachedDrawProgram(signature);
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
|
||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
||||
if (currentProgram) return currentProgram;
|
||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||
const auto& pipeline = GetBoundProgramPipeline();
|
||||
if (!pipeline) return nullProgram;
|
||||
return pipeline->GetActiveProgram();
|
||||
}
|
||||
|
||||
// RenderState
|
||||
Uint GLContext::GetPipelineStateVersion() const {
|
||||
return m_renderState.GetPipelineStateVersion();
|
||||
}
|
||||
|
||||
Uint GLContext::GetRenderStateParametersVersion() const {
|
||||
return m_renderState.GetVersion();
|
||||
}
|
||||
|
||||
@@ -137,6 +137,12 @@ namespace MobileGL {
|
||||
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
|
||||
void UseProgram(Uint program);
|
||||
const SharedPtr<ProgramObject>& GetCurrentProgram();
|
||||
// What a draw or dispatch actually executes: the program in use, or - when
|
||||
// there is none - the bound pipeline's stages composited into one program.
|
||||
const SharedPtr<ProgramObject>& GetProgramForDraw();
|
||||
// What glUniform* addresses: the program in use, or the bound pipeline's
|
||||
// active program (GL 4.6 core 7.6.1).
|
||||
const SharedPtr<ProgramObject>& GetProgramForUniform();
|
||||
|
||||
// Program pipeline (GL_ARB_separate_shader_objects, GL 4.6 core 7.4). Like queries
|
||||
// and transform feedbacks, glGenProgramPipelines only RESERVES a name - the object
|
||||
@@ -153,6 +159,8 @@ namespace MobileGL {
|
||||
|
||||
// RenderState
|
||||
Uint GetRenderStateParametersVersion() const;
|
||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetRenderStateParameters() const;
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
|
||||
@@ -45,6 +45,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
|
||||
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
|
||||
const String& GetInfoLog() const { return m_infoLog; }
|
||||
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
|
||||
// is the only place a caller can read it from once the shader name is gone.
|
||||
void AppendInfoLog(const String& text) {
|
||||
if (text.empty()) return;
|
||||
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n';
|
||||
m_infoLog += text;
|
||||
}
|
||||
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; }
|
||||
Uint GetUniformCount() const { return m_activeUniformCount; }
|
||||
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
|
||||
@@ -382,6 +389,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// ARB_get_program_binary requires of it.
|
||||
Bool GetBinaryRetrievableHint() const { return m_binaryRetrievableHint; }
|
||||
void SetBinaryRetrievableHint(Bool hint) { m_binaryRetrievableHint = hint; }
|
||||
// GL_PROGRAM_SEPARABLE (GL_ARB_separate_shader_objects): the program may supply a
|
||||
// subset of the stages of a program pipeline. Only takes effect on the next link,
|
||||
// which is why it is plain state here rather than something Link() consults.
|
||||
Bool GetSeparable() const { return m_separable; }
|
||||
void SetSeparable(Bool separable) { m_separable = separable; }
|
||||
// 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() {
|
||||
@@ -605,6 +617,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool m_deleteStatus = false;
|
||||
Bool m_linkStatus = false;
|
||||
Bool m_binaryRetrievableHint = false;
|
||||
Bool m_separable = false;
|
||||
Bool m_validateStatus = true;
|
||||
Uint32 m_backendStateVersion = 0;
|
||||
|
||||
|
||||
@@ -40,9 +40,40 @@ namespace MobileGL {
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
|
||||
// A draw sees one program, but a pipeline holds one program per stage. The
|
||||
// stages are composited into a single hidden program object, rebuilt whenever
|
||||
// the stage set - or any stage program's own link - changes. The signature is
|
||||
// what that "changes" means: a stage program's lifetime id pins the object and
|
||||
// its backend state version pins the link generation.
|
||||
using DrawProgramSignature =
|
||||
Array<Uint64, static_cast<SizeT>(ShaderStage::ShaderStageCount) * 2>;
|
||||
|
||||
DrawProgramSignature ComputeDrawProgramSignature() const {
|
||||
DrawProgramSignature signature{};
|
||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||
const auto& program = m_stagePrograms[stage];
|
||||
if (!program) continue;
|
||||
signature[stage * 2] = program->GetLifetimeId();
|
||||
signature[stage * 2 + 1] = program->GetBackendStateVersion();
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GetCachedDrawProgram(const DrawProgramSignature& signature) const {
|
||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||
if (!m_drawProgram || m_drawProgramSignature != signature) return nullProgram;
|
||||
return m_drawProgram;
|
||||
}
|
||||
void SetCachedDrawProgram(const DrawProgramSignature& signature, SharedPtr<ProgramObject> program) {
|
||||
m_drawProgramSignature = signature;
|
||||
m_drawProgram = Move(program);
|
||||
}
|
||||
|
||||
private:
|
||||
Array<SharedPtr<ProgramObject>, static_cast<SizeT>(ShaderStage::ShaderStageCount)> m_stagePrograms{};
|
||||
SharedPtr<ProgramObject> m_activeProgram;
|
||||
SharedPtr<ProgramObject> m_drawProgram;
|
||||
DrawProgramSignature m_drawProgramSignature{};
|
||||
String m_infoLog;
|
||||
const Uint m_externalIndex = 0;
|
||||
Bool m_validateStatus = false;
|
||||
|
||||
@@ -37,6 +37,10 @@ namespace MobileGL {
|
||||
return m_version;
|
||||
}
|
||||
|
||||
Uint RenderState::GetPipelineStateVersion() const {
|
||||
return m_pipelineStateVersion;
|
||||
}
|
||||
|
||||
const RenderStateParameters& RenderState::GetAllParameters() const {
|
||||
return m_parameters;
|
||||
}
|
||||
@@ -122,7 +126,7 @@ namespace MobileGL {
|
||||
if (m_parameters.PolygonModeFront == front && m_parameters.PolygonModeBack == back) return;
|
||||
m_parameters.PolygonModeFront = front;
|
||||
m_parameters.PolygonModeBack = back;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
GLenum RenderState::GetPolygonModeFront() const {
|
||||
@@ -158,7 +162,7 @@ namespace MobileGL {
|
||||
if (m_parameters.PatchVertices == vertices) return;
|
||||
|
||||
m_parameters.PatchVertices = vertices;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Uint RenderState::GetPatchVertices() const {
|
||||
@@ -187,7 +191,7 @@ namespace MobileGL {
|
||||
case CapabilityInput::capability: \
|
||||
if (m_parameters.capability##Enabled == (flag)) break; \
|
||||
m_parameters.capability##Enabled = (flag); \
|
||||
++m_version; \
|
||||
BumpVersions(); \
|
||||
break;
|
||||
|
||||
switch (cap) {
|
||||
@@ -220,7 +224,7 @@ namespace MobileGL {
|
||||
blendState.Enabled = enabled;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
if (stateChanged) BumpVersions();
|
||||
break;
|
||||
}
|
||||
default: // not supported currently
|
||||
@@ -276,7 +280,7 @@ namespace MobileGL {
|
||||
if (m_parameters.BlendStates[index].Enabled == enabled) return;
|
||||
|
||||
m_parameters.BlendStates[index].Enabled = enabled;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
@@ -308,7 +312,7 @@ namespace MobileGL {
|
||||
stateChanged = true;
|
||||
}
|
||||
if (!stateChanged) return;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
void RenderState::GetBlendFunc(BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
|
||||
@@ -334,7 +338,7 @@ namespace MobileGL {
|
||||
blendState.DstFactorRGB = dstRGB;
|
||||
blendState.SrcFactorAlpha = srcAlpha;
|
||||
blendState.DstFactorAlpha = dstAlpha;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
void RenderState::GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB,
|
||||
@@ -360,7 +364,7 @@ namespace MobileGL {
|
||||
stateChanged = true;
|
||||
}
|
||||
if (!stateChanged) return;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
void RenderState::GetBlendEquation(BlendEquation& color, BlendEquation& alpha) const {
|
||||
@@ -379,7 +383,7 @@ namespace MobileGL {
|
||||
}
|
||||
blendState.ColorEquation = color;
|
||||
blendState.AlphaEquation = alpha;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
void RenderState::GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
|
||||
@@ -395,7 +399,7 @@ namespace MobileGL {
|
||||
if (m_parameters.LogicOp == logicOp) return;
|
||||
|
||||
m_parameters.LogicOp = logicOp;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
LogicOperation RenderState::GetLogicOp() const {
|
||||
@@ -407,7 +411,7 @@ namespace MobileGL {
|
||||
if (m_parameters.DepthFunc == func) return;
|
||||
|
||||
m_parameters.DepthFunc = func;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
DepthTestFunc RenderState::GetDepthFunc() const {
|
||||
@@ -418,7 +422,7 @@ namespace MobileGL {
|
||||
if (m_parameters.DepthMask == flag) return;
|
||||
|
||||
m_parameters.DepthMask = flag;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Bool RenderState::GetDepthMask() const {
|
||||
@@ -429,10 +433,15 @@ namespace MobileGL {
|
||||
StencilFaceState& state = m_parameters.StencilStates[GetStencilFaceIndex(face)];
|
||||
if (state.Func == func && state.Ref == ref && state.ValueMask == mask) return;
|
||||
|
||||
// Only Func is baked into the pipeline; Ref and ValueMask are dynamic state
|
||||
// (VK_DYNAMIC_STATE_STENCIL_REFERENCE / _COMPARE_MASK), so glStencilFunc changing
|
||||
// only the reference must not evict a cached pipeline.
|
||||
const Bool pipelineRelevantChange = state.Func != func;
|
||||
state.Func = func;
|
||||
state.Ref = ref;
|
||||
state.ValueMask = mask;
|
||||
++m_version;
|
||||
if (pipelineRelevantChange) ++m_pipelineStateVersion;
|
||||
}
|
||||
|
||||
void RenderState::SetStencilMask(StencilFace face, Uint32 mask) {
|
||||
@@ -454,7 +463,7 @@ namespace MobileGL {
|
||||
state.FailOp = fail;
|
||||
state.PassDepthFailOp = depthFail;
|
||||
state.PassDepthPassOp = depthPass;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
const StencilFaceState& RenderState::GetStencilState(StencilFace face) const {
|
||||
@@ -471,7 +480,7 @@ namespace MobileGL {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) ++m_version;
|
||||
if (changed) BumpVersions();
|
||||
}
|
||||
|
||||
BoolVec4 RenderState::GetColorMask() const {
|
||||
@@ -482,7 +491,7 @@ namespace MobileGL {
|
||||
void RenderState::SetColorMaskIndexed(Uint index, BoolVec4 mask) {
|
||||
if (m_parameters.ColorMasks[index] == mask) return;
|
||||
m_parameters.ColorMasks[index] = mask;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
BoolVec4 RenderState::GetColorMaskIndexed(Uint index) const {
|
||||
@@ -550,7 +559,7 @@ namespace MobileGL {
|
||||
|
||||
m_parameters.SampleCoverageValue = value;
|
||||
m_parameters.SampleCoverageInvert = invert;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Float RenderState::GetSampleCoverageValue() const {
|
||||
@@ -565,7 +574,7 @@ namespace MobileGL {
|
||||
if (m_parameters.SampleMaskValue == mask) return;
|
||||
|
||||
m_parameters.SampleMaskValue = mask;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Uint32 RenderState::GetSampleMaskValue() const {
|
||||
@@ -639,7 +648,7 @@ namespace MobileGL {
|
||||
if (m_parameters.CullFaceModeSetting == mode) return;
|
||||
|
||||
m_parameters.CullFaceModeSetting = mode;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
CullFaceMode RenderState::GetCullFaceMode() const {
|
||||
@@ -650,7 +659,7 @@ namespace MobileGL {
|
||||
if (m_parameters.FrontFaceModeSetting == mode) return;
|
||||
|
||||
m_parameters.FrontFaceModeSetting = mode;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
FrontFaceMode RenderState::GetFrontFaceMode() const {
|
||||
@@ -661,7 +670,7 @@ namespace MobileGL {
|
||||
if (m_parameters.ProvokingVertexModeSetting == mode) return;
|
||||
|
||||
m_parameters.ProvokingVertexModeSetting = mode;
|
||||
++m_version;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
ProvokingVertexMode RenderState::GetProvokingVertexMode() const {
|
||||
|
||||
@@ -312,6 +312,8 @@ namespace MobileGL {
|
||||
RenderState();
|
||||
|
||||
Uint GetVersion() const;
|
||||
// Version of the pipeline-relevant subset only - see m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetAllParameters() const;
|
||||
|
||||
// Rasterization
|
||||
@@ -418,7 +420,21 @@ namespace MobileGL {
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
|
||||
private:
|
||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||
// changes the VkPipeline (or its DirectGLES equivalent).
|
||||
void BumpVersions() {
|
||||
++m_version;
|
||||
++m_pipelineStateVersion;
|
||||
}
|
||||
|
||||
Uint16 m_version = 0;
|
||||
// Only the subset of render state that a backend bakes INTO a pipeline object.
|
||||
// Viewport, scissor, depth range, blend colour, line width, polygon offset, stencil
|
||||
// write mask, the clear values, hints and the point-size family are all either
|
||||
// dynamic pipeline state or not pipeline state at all, so changing one of them must
|
||||
// not evict a cached pipeline. Keeping one counter for both made a glViewport call
|
||||
// knock the next draw off the pipeline memo AND the draw fast path.
|
||||
Uint16 m_pipelineStateVersion = 0;
|
||||
RenderStateParameters m_parameters;
|
||||
|
||||
// Pixel Store
|
||||
|
||||
@@ -27,11 +27,52 @@ namespace MobileGL {
|
||||
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
|
||||
// Respecifying a level drops whatever compressed image it used to hold. Without this,
|
||||
// a glTexImage2D or glTexStorage2D over a level a previous glCompressedTexImage2D had
|
||||
// shadowed would leave GL_TEXTURE_COMPRESSED answering true and glGetCompressedTexImage
|
||||
// handing back the stale blob. Every allocation path funnels through here, so clearing
|
||||
// once covers all of them; the compressed path re-arms the tag immediately afterwards
|
||||
// via SetCompressedImage.
|
||||
m_compressedFormats[level] = GL_NONE;
|
||||
m_compressedData[level].clear();
|
||||
m_compressedData[level].shrink_to_fit();
|
||||
}
|
||||
|
||||
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
|
||||
MOBILEGL_ASSERT(level < m_compressedData.size(), "SetCompressedImage: level out of range");
|
||||
|
||||
m_compressedFormats[level] = internalFormat;
|
||||
auto& blob = m_compressedData[level];
|
||||
// Zero-filled when data is null: glCompressedTexImage* with a null pointer defines the
|
||||
// level's size and format but leaves its contents undefined, and zeros are the one
|
||||
// reproducible answer a later glGetCompressedTexImage can give.
|
||||
blob.assign(size, 0);
|
||||
if (data != nullptr && size > 0) {
|
||||
Memcpy(blob.data(), data, size);
|
||||
}
|
||||
}
|
||||
|
||||
GLenum MipmapStorage::GetCompressedFormat(Uint level) const {
|
||||
if (level >= m_compressedFormats.size()) return GL_NONE;
|
||||
return m_compressedFormats[level];
|
||||
}
|
||||
|
||||
SizeT MipmapStorage::GetCompressedByteSize(Uint level) const {
|
||||
if (level >= m_compressedData.size()) return 0;
|
||||
return m_compressedData[level].size();
|
||||
}
|
||||
|
||||
const void* MipmapStorage::MapCompressedData(Uint level) const {
|
||||
if (level >= m_compressedData.size()) return nullptr;
|
||||
return m_compressedData[level].data();
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
@@ -40,6 +81,8 @@ namespace MobileGL {
|
||||
m_data.resize(levelCount);
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
|
||||
@@ -30,10 +30,27 @@ namespace MobileGL {
|
||||
void MarkDirty(Uint level, bool dirty);
|
||||
bool IsDirty(Uint level) const;
|
||||
|
||||
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
||||
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
||||
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
|
||||
// BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has
|
||||
// to keep the "width * height * bytes-per-texel" layout that the backend upload
|
||||
// sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all
|
||||
// divide by. Two parallel vectors, one invariant preserved. Call order is
|
||||
// AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain
|
||||
// glTexImage2D over the level un-compresses it.
|
||||
void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size);
|
||||
// GL_NONE when the level is not stored compressed.
|
||||
GLenum GetCompressedFormat(Uint level) const;
|
||||
SizeT GetCompressedByteSize(Uint level) const;
|
||||
const void* MapCompressedData(Uint level) const;
|
||||
|
||||
protected:
|
||||
Vector<IntVec3> m_texelSizes;
|
||||
Vector<Vector<Uint8>> m_data;
|
||||
Vector<bool> m_isDirty;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
};
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -74,6 +74,27 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].IsDirty(level);
|
||||
}
|
||||
|
||||
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
||||
SizeT size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
||||
m_storage[targetIndex].SetCompressedImage(level, internalFormat, data, size);
|
||||
}
|
||||
|
||||
GLenum GetCompressedFormat(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedFormat: target invalid");
|
||||
return m_storage[targetIndex].GetCompressedFormat(level);
|
||||
}
|
||||
|
||||
SizeT GetCompressedByteSize(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedByteSize: target invalid");
|
||||
return m_storage[targetIndex].GetCompressedByteSize(level);
|
||||
}
|
||||
|
||||
const void* MapCompressedData(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "MapCompressedData: target invalid");
|
||||
return m_storage[targetIndex].MapCompressedData(level);
|
||||
}
|
||||
|
||||
protected:
|
||||
Array<MipmapStorage, TargetCount> m_storage;
|
||||
};
|
||||
|
||||
@@ -301,6 +301,27 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat, data, size);
|
||||
}
|
||||
|
||||
GLenum TextureObjectWithOneMipmap::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
SizeT TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
const void* TextureObjectWithOneMipmap::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
|
||||
if (m_textureStorage.GetLevelCount() == 0) {
|
||||
return {0, 0, 0};
|
||||
|
||||
@@ -150,6 +150,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
|
||||
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
||||
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
||||
// stays uncompressed and correctly sized, so nothing in the backend upload path has to know
|
||||
// these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the
|
||||
// GL_TEXTURE_COMPRESSED* level queries read them.
|
||||
virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) = 0;
|
||||
// GL_NONE when this level is not stored compressed.
|
||||
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
};
|
||||
|
||||
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
|
||||
@@ -206,6 +218,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||
const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
@@ -55,6 +55,27 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
internalFormat, data, size);
|
||||
}
|
||||
|
||||
GLenum TextureObject2DCube::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
SizeT TextureObject2DCube::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
const void* TextureObject2DCube::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
|
||||
target <= TextureUploadTarget::CubeMapNegativeZ,
|
||||
|
||||
@@ -27,6 +27,12 @@ namespace MobileGL {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
|
||||
IntVec3 GetBaseSize() const override;
|
||||
Bool IsComplete() const override;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
||||
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
||||
m_attributes[index].IsBgra == isBgra) {
|
||||
m_attributes[index].IsBgra == isBgra && !m_attributes[index].IsLong) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
attr.Offset = offset;
|
||||
attr.IsInteger = isInteger;
|
||||
attr.IsBgra = isBgra;
|
||||
// glVertexAttribPointer / glVertexAttribIPointer are never the long form, so they always
|
||||
// take the attribute back out of it - and "only IsLong changed" is a real change that has to
|
||||
// reach the backends, which is why the early-out above tests it too. Cleared inside the
|
||||
// mutation block so the clear and the version bump stay atomic.
|
||||
attr.IsLong = false;
|
||||
|
||||
BumpAttributeFormatVersion(index);
|
||||
}
|
||||
@@ -212,18 +217,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra) {
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra,
|
||||
Bool isLong) {
|
||||
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
||||
if (size < 1 || size > 4) return;
|
||||
|
||||
auto& attr = m_attributes[attribIndex];
|
||||
if (attr.Size != size || attr.Type != type || attr.Normalized != normalized || attr.IsInteger != isInteger ||
|
||||
attr.IsBgra != isBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) {
|
||||
attr.IsBgra != isBgra || attr.IsLong != isLong ||
|
||||
m_attributeRelativeOffset[attribIndex] != relativeOffset) {
|
||||
attr.Size = size;
|
||||
attr.Type = type;
|
||||
attr.Normalized = normalized;
|
||||
attr.IsInteger = isInteger;
|
||||
attr.IsBgra = isBgra;
|
||||
attr.IsLong = isLong;
|
||||
m_attributeRelativeOffset[attribIndex] = relativeOffset;
|
||||
BumpAttributeFormatVersion(attribIndex);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ namespace MobileGL {
|
||||
SizeT Offset = 0;
|
||||
Bool IsInteger = false;
|
||||
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
|
||||
// Set only by the long (L) format entry points. It is NOT implied by
|
||||
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
|
||||
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
|
||||
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
|
||||
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
|
||||
Bool IsLong = false;
|
||||
Bool IsBgra = false;
|
||||
Uint Divisor = 0;
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
@@ -88,7 +94,8 @@ namespace MobileGL {
|
||||
void SetBindingDivisor(Uint bindingIndex, Uint divisor);
|
||||
void SetAttributeBinding(Uint attribIndex, Uint bindingIndex);
|
||||
void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra = false);
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra = false,
|
||||
Bool isLong = false);
|
||||
|
||||
// The binding-point view the attributes were resolved from. Kept queryable
|
||||
// because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved
|
||||
|
||||
@@ -837,6 +837,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
std::strcmp(extension, "GL_OES_texture_border_clamp") == 0) {
|
||||
caps.SupportsTextureBorderClamp = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_texture_cube_map_array") == 0 ||
|
||||
std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) {
|
||||
caps.SupportsTextureCubeMapArray = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
|
||||
caps.SupportsBaseInstance = true;
|
||||
}
|
||||
@@ -1021,6 +1025,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// Core from ES 3.2 on, whatever the extension string says.
|
||||
if (caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
||||
caps.SupportsTextureBorderClamp = true;
|
||||
caps.SupportsTextureCubeMapArray = true;
|
||||
}
|
||||
if (caps.SupportsTextureFilterAnisotropy) {
|
||||
GLfloat maxTextureMaxAnisotropy = 1.0f;
|
||||
|
||||
@@ -1045,6 +1045,8 @@ namespace MobileGL {
|
||||
// EXT/OES_texture_border_clamp before that. Without it every border-colour parameter
|
||||
// raises INVALID_ENUM on the driver, so the syncs have to be gated on it.
|
||||
Bool SupportsTextureBorderClamp = false;
|
||||
// GL_TEXTURE_CUBE_MAP_ARRAY: ES 3.2 core, or EXT/OES_texture_cube_map_array before it.
|
||||
Bool SupportsTextureCubeMapArray = false;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the
|
||||
// extension above is present, and left at 1.0 (no anisotropy) otherwise.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
|
||||
@@ -198,6 +198,30 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
||||
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
|
||||
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
|
||||
{
|
||||
// Probe the formats a colour render target actually uses. A driver that refuses the flag
|
||||
// for one of them refuses per-slice attachment for that format only, which
|
||||
// VkTextureManager detects and records at image creation; this field just says whether
|
||||
// the capability is worth offering at all.
|
||||
static constexpr VkFormat k3DSliceProbeFormats[] = {VK_FORMAT_R8G8B8A8_UNORM,
|
||||
VK_FORMAT_R8G8B8A8_SRGB};
|
||||
Bool all2DArrayCompatible = true;
|
||||
for (const VkFormat probeFormat : k3DSliceProbeFormats) {
|
||||
VkImageFormatProperties probeProperties{};
|
||||
const VkResult probeResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
physicalDevice, probeFormat, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &probeProperties);
|
||||
if (probeResult != VK_SUCCESS) {
|
||||
all2DArrayCompatible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
caps.Supports2DArrayCompatible3DImages = all2DArrayCompatible;
|
||||
}
|
||||
caps.SupportsVertexPipelineStoresAndAtomics =
|
||||
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
|
||||
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
||||
@@ -288,6 +312,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||
caps.SupportsWideLines = false;
|
||||
caps.SupportsShaderFloat64 = false;
|
||||
caps.SupportsImageCubeArray = false;
|
||||
caps.Supports2DArrayCompatible3DImages = false;
|
||||
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||
// stage writes disabled rather than inferring them from descriptor limits alone.
|
||||
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
||||
|
||||
@@ -74,6 +74,22 @@ namespace MobileGL {
|
||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||
Int FragmentInterpolationOffsetBits = 4;
|
||||
Bool SupportsWideLines = false;
|
||||
// VkPhysicalDeviceFeatures::shaderFloat64. Any module declaring OpCapability Float64
|
||||
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
|
||||
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
|
||||
Bool SupportsShaderFloat64 = false;
|
||||
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
|
||||
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
|
||||
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
|
||||
// both its sampled view and its full view.
|
||||
Bool SupportsImageCubeArray = false;
|
||||
// VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on a 3D colour image, i.e. whether one z slice
|
||||
// of a GL_TEXTURE_3D texture can be named by a 2D view and attached to a framebuffer.
|
||||
// Vulkan 1.1 core, but per format+usage - this is an OPTIMISTIC summary probed over the
|
||||
// common colour attachment formats. The authoritative answer is taken per format at
|
||||
// image creation in VkTextureManager, which withdraws the flag and remembers the verdict
|
||||
// when a driver refuses it.
|
||||
Bool Supports2DArrayCompatible3DImages = false;
|
||||
// Storage-image descriptors are limited per stage by
|
||||
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
|
||||
// require these core Vulkan features to be enabled on the logical device.
|
||||
|
||||
@@ -525,5 +525,50 @@ namespace MobileGL {
|
||||
return s;
|
||||
}
|
||||
|
||||
CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat) {
|
||||
// Every format here is 4x4-blocked; only the bytes per block differ (8 for the one- and
|
||||
// two-channel RGTC/EAC and the 1-bit-alpha ETC2 forms, 16 for BPTC and the full-alpha
|
||||
// ETC2/EAC and two-channel EAC forms). The generic compressed formats (GL_COMPRESSED_RGBA
|
||||
// and friends) are deliberately absent: GL lets the implementation pick, MobileGL picks
|
||||
// uncompressed, and glCompressedTexImage* must reject them because there is no defined
|
||||
// block layout to hand it. The accepted set is exactly the set
|
||||
// ConvertGLEnumToTextureInternalFormat can back with uncompressed storage, so this call
|
||||
// can never accept a format whose texel shadow cannot be allocated.
|
||||
switch (internalFormat) {
|
||||
case GL_COMPRESSED_RED_RGTC1:
|
||||
case GL_COMPRESSED_SIGNED_RED_RGTC1:
|
||||
case GL_COMPRESSED_RGB8_ETC2:
|
||||
case GL_COMPRESSED_SRGB8_ETC2:
|
||||
case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2:
|
||||
case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2:
|
||||
case GL_COMPRESSED_R11_EAC:
|
||||
case GL_COMPRESSED_SIGNED_R11_EAC:
|
||||
return {4, 4, 8};
|
||||
case GL_COMPRESSED_RG_RGTC2:
|
||||
case GL_COMPRESSED_SIGNED_RG_RGTC2:
|
||||
case GL_COMPRESSED_RGBA_BPTC_UNORM:
|
||||
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
|
||||
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
|
||||
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
|
||||
case GL_COMPRESSED_RGBA8_ETC2_EAC:
|
||||
case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:
|
||||
case GL_COMPRESSED_RG11_EAC:
|
||||
case GL_COMPRESSED_SIGNED_RG11_EAC:
|
||||
return {4, 4, 16};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size) {
|
||||
if (info.blockWidth == 0 || info.blockHeight == 0) return 0;
|
||||
const SizeT width = static_cast<SizeT>(std::max<Int>(size.x(), 0));
|
||||
const SizeT height = static_cast<SizeT>(std::max<Int>(size.y(), 0));
|
||||
const SizeT depth = static_cast<SizeT>(std::max<Int>(size.z(), 1));
|
||||
const SizeT blocksX = (width + info.blockWidth - 1) / info.blockWidth;
|
||||
const SizeT blocksY = (height + info.blockHeight - 1) / info.blockHeight;
|
||||
return blocksX * blocksY * depth * info.blockByteSize;
|
||||
}
|
||||
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -24,5 +24,23 @@ namespace MobileGL {
|
||||
SizeT CalculateInputTextureImageSize(TextureInputFormat inputFormat, TexturePixelDataType pixelDataType,
|
||||
IntVec3 size);
|
||||
ComponentSizes GetComponentSizesForInternalFormat(TextureInternalFormat internal);
|
||||
|
||||
// A specific compressed internal format described the only two ways the CPU shadow needs it:
|
||||
// the texel footprint of one block and the bytes that block occupies. Kept as a GLenum query
|
||||
// rather than a TextureInternalFormat one on purpose - TextureInternalFormat has no
|
||||
// compressed enumerator (a compressed format resolves to the uncompressed storage backing
|
||||
// it), so by the time the enum has been converted the block geometry is gone.
|
||||
struct CompressedFormatInfo {
|
||||
// Zero means "internalformat is not one of the specific compressed formats core GL
|
||||
// requires" - which is exactly the glCompressedTexImage* INVALID_ENUM case, so callers
|
||||
// get the format test and the block geometry from one lookup.
|
||||
Uint blockWidth = 0;
|
||||
Uint blockHeight = 0;
|
||||
SizeT blockByteSize = 0;
|
||||
};
|
||||
CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat);
|
||||
// Blocks are counted rounded up, exactly as GL 4.6 core 8.7 sizes a compressed image, so a
|
||||
// 4x4 BPTC image is one 16-byte block and a 1x1 one still is.
|
||||
SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size);
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -309,6 +309,24 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
|
||||
"border instead of the requested colour");
|
||||
}
|
||||
if (caps.SupportsTextureCubeMapArray) {
|
||||
builder.Pass("Texture cube map array",
|
||||
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
|
||||
"attached to a framebuffer)");
|
||||
} else {
|
||||
builder.Warn("Texture cube map array",
|
||||
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
|
||||
"map array texture gets no driver storage at all, so sampling one reads nothing "
|
||||
"and rendering to one does not reach the screen");
|
||||
}
|
||||
// Reported rather than probed: this one cannot come out any other way. OpenGL ES has no
|
||||
// double-precision vertex format and ESSL has no fp64 type, so there is no driver and no
|
||||
// extension that could make it work - the row exists so the loss is named at startup
|
||||
// instead of discovered as an unexplained GL_INVALID_OPERATION at draw setup.
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported on any GLES driver (ES has no GL_DOUBLE vertex format and ESSL has "
|
||||
"no fp64 type); glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
||||
"GL_INVALID_OPERATION - use the Vulkan backend if the application needs them");
|
||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
@@ -1401,6 +1419,44 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
} else {
|
||||
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
|
||||
}
|
||||
{
|
||||
VkImageFormatProperties sliceProbe{};
|
||||
const Bool sliceCapable =
|
||||
vkGetPhysicalDeviceImageFormatProperties(
|
||||
physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &sliceProbe) == VK_SUCCESS;
|
||||
if (sliceCapable) {
|
||||
builder.Pass("2D-array-compatible 3D images",
|
||||
"supported for the common colour attachment formats (one z slice of a "
|
||||
"GL_TEXTURE_3D texture can be attached to a framebuffer and cleared and read "
|
||||
"back on its own; a format that refuses the flag is detected at image "
|
||||
"creation and declines per-slice attachment)");
|
||||
} else {
|
||||
builder.Warn("2D-array-compatible 3D images",
|
||||
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
|
||||
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
|
||||
"slice past the first");
|
||||
}
|
||||
}
|
||||
if (features.imageCubeArray == VK_TRUE) {
|
||||
builder.Pass("imageCubeArray",
|
||||
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
|
||||
"attached to a framebuffer per layer");
|
||||
} else {
|
||||
builder.Warn("imageCubeArray",
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
||||
}
|
||||
if (features.shaderFloat64 == VK_TRUE) {
|
||||
builder.Pass("shaderFloat64",
|
||||
"GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported");
|
||||
} else {
|
||||
builder.Warn("shaderFloat64",
|
||||
"unsupported; any shader declaring a double fails to create a shader module, and "
|
||||
"glVertexAttribLFormat reports GL_INVALID_OPERATION instead of feeding the attribute");
|
||||
}
|
||||
|
||||
Bool shaderDrawParameters = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
@@ -1422,6 +1478,56 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
||||
}
|
||||
|
||||
Bool provokingVertexLast = false;
|
||||
Bool transformFeedbackPreservesProvokingVertex = false;
|
||||
Bool provokingVertexModePerPipeline = false;
|
||||
Bool transformFeedbackPreservesTriangleFanProvokingVertex = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
|
||||
HasVkExtension(deviceExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{};
|
||||
provokingVertexFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT;
|
||||
VkPhysicalDeviceFeatures2 features2{};
|
||||
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
features2.pNext = &provokingVertexFeatures;
|
||||
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
|
||||
provokingVertexLast = provokingVertexFeatures.provokingVertexLast == VK_TRUE;
|
||||
transformFeedbackPreservesProvokingVertex =
|
||||
provokingVertexFeatures.transformFeedbackPreservesProvokingVertex == VK_TRUE;
|
||||
if (vkGetPhysicalDeviceProperties2Fn != nullptr) {
|
||||
VkPhysicalDeviceProvokingVertexPropertiesEXT provokingVertexProperties{};
|
||||
provokingVertexProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT;
|
||||
VkPhysicalDeviceProperties2 properties2{};
|
||||
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
properties2.pNext = &provokingVertexProperties;
|
||||
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
|
||||
provokingVertexModePerPipeline =
|
||||
provokingVertexProperties.provokingVertexModePerPipeline == VK_TRUE;
|
||||
transformFeedbackPreservesTriangleFanProvokingVertex =
|
||||
provokingVertexProperties.transformFeedbackPreservesTriangleFanProvokingVertex == VK_TRUE;
|
||||
}
|
||||
}
|
||||
if (provokingVertexLast) {
|
||||
builder.Pass("provokingVertexLast",
|
||||
"supported; flat varyings take GL's last vertex and transform feedback records "
|
||||
"strip/fan triangles in GL's vertex order");
|
||||
} else {
|
||||
builder.Warn("provokingVertexLast",
|
||||
"unsupported; flat-shaded varyings take a primitive's first vertex instead of GL's "
|
||||
"last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated "
|
||||
"(e.g. 0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
|
||||
}
|
||||
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
|
||||
builder.Warn("transformFeedbackPreservesProvokingVertex",
|
||||
"unsupported; the captured vertex order for strips/fans is not guaranteed by the "
|
||||
"spec even though the flat-shading convention is correct");
|
||||
}
|
||||
if (provokingVertexLast && transformFeedbackPreservesProvokingVertex &&
|
||||
!transformFeedbackPreservesTriangleFanProvokingVertex && !provokingVertexModePerPipeline) {
|
||||
builder.Warn("transformFeedbackPreservesTriangleFanProvokingVertex",
|
||||
"unsupported and per-pipeline modes unavailable; the transform-feedback "
|
||||
"provoking-vertex guarantee is left off so GL_TRIANGLE_FAN pipelines stay legal");
|
||||
}
|
||||
|
||||
Bool primitiveTopologyListRestart = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
|
||||
HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
@@ -327,6 +328,18 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -54,6 +54,13 @@ namespace MobileGL {
|
||||
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||
// buffers. Vertex stage, DirectVulkan only; pairs with the Float64 case in
|
||||
// VertexInputStateFactory::ToVkVertexFormat.
|
||||
static bool PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Adds the Invariant decoration to every Position builtin output. GL apps
|
||||
// routinely rely on cross-program position invariance for multi-pass
|
||||
// equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "PackDoubleVertexInputsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// Component count of a 64-bit float input, or 0 if the type is not one.
|
||||
Uint32 DoubleComponentCount(const analysis::Type* type) {
|
||||
if (type == nullptr) return 0;
|
||||
if (const auto* scalar = type->AsFloat()) {
|
||||
return scalar->width() == 64 ? 1u : 0u;
|
||||
}
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
const auto* element = vector->element_type()->AsFloat();
|
||||
if (element == nullptr || element->width() != 64) return 0;
|
||||
return vector->element_count();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status PackDoubleVertexInputsPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto entryPoints = irContext->module()->entry_points();
|
||||
if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange;
|
||||
|
||||
Instruction* entryPoint = &*entryPoints.begin();
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Vertex) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
|
||||
struct Target {
|
||||
Instruction* variable = nullptr;
|
||||
Uint32 doubleTypeId = 0;
|
||||
Uint32 componentCount = 0;
|
||||
Uint32 packedTypeId = 0;
|
||||
Uint32 packedPointerTypeId = 0;
|
||||
Uint32 privatePointerTypeId = 0;
|
||||
};
|
||||
std::vector<Target> targets;
|
||||
|
||||
for (Instruction& inst : irContext->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr) continue;
|
||||
const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
const Uint32 components = DoubleComponentCount(typeMgr->GetType(pointeeTypeId));
|
||||
if (components == 0) continue;
|
||||
if (components > 2) {
|
||||
// 6 or 8 uint32 components has no single vertex format, and GL spreads such
|
||||
// an input over two attribute locations. Left alone; the vertex-input
|
||||
// factory declines the matching attribute for the same reason.
|
||||
MGLOG_E("PackDoubleVertexInputsPass: vertex input %%%u is a %u-component 64-bit "
|
||||
"float; only double and dvec2 inputs can be packed",
|
||||
inst.result_id(), components);
|
||||
continue;
|
||||
}
|
||||
targets.push_back({&inst, pointeeTypeId, components});
|
||||
}
|
||||
|
||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Entry block insertion point: after the block's leading OpVariable run, which
|
||||
// SPIR-V requires to stay at the top of a function's first block.
|
||||
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
spvtools::opt::Function* entryFunction = nullptr;
|
||||
for (auto& function : *irContext->module()) {
|
||||
if (function.result_id() == entryFunctionId) {
|
||||
entryFunction = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
auto& entryBlock = *entryFunction->begin();
|
||||
auto insertPoint = entryBlock.begin();
|
||||
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
||||
++insertPoint;
|
||||
}
|
||||
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
|
||||
|
||||
const Uint32 uintTypeId = typeMgr->GetUIntTypeId();
|
||||
const analysis::Integer* uintType = typeMgr->GetType(uintTypeId)->AsInteger();
|
||||
|
||||
// Every type instruction has to exist before any variable that names it: the
|
||||
// types-and-variables section is walked in order and a forward reference to a type is
|
||||
// invalid SPIR-V. GetTypeInstruction/FindPointerToType append, and the variables are
|
||||
// appended (or re-appended) below, so all three lookups run first for every target.
|
||||
for (auto& target : targets) {
|
||||
analysis::Vector packedVectorType(uintType, target.componentCount * 2u);
|
||||
target.packedTypeId = typeMgr->GetTypeInstruction(&packedVectorType);
|
||||
target.packedPointerTypeId =
|
||||
typeMgr->FindPointerToType(target.packedTypeId, spv::StorageClass::Input);
|
||||
target.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(target.doubleTypeId, spv::StorageClass::Private);
|
||||
}
|
||||
|
||||
for (const auto& target : targets) {
|
||||
Instruction* variable = target.variable;
|
||||
const Uint32 oldVariableId = variable->result_id();
|
||||
const Uint32 packedTypeId = target.packedTypeId;
|
||||
const Uint32 packedPointerTypeId = target.packedPointerTypeId;
|
||||
|
||||
const Uint32 packedVariableId = irContext->TakeNextId();
|
||||
irContext->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVariable, packedPointerTypeId, packedVariableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<Uint32>(spv::StorageClass::Input)}}}));
|
||||
|
||||
// The interface decorations belong to whatever is actually the Input now.
|
||||
std::vector<Instruction*> deadDecorations;
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (annotation.GetSingleWordInOperand(0) != oldVariableId) continue;
|
||||
const auto decoration =
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1));
|
||||
if (decoration == spv::Decoration::Location ||
|
||||
decoration == spv::Decoration::Component ||
|
||||
decoration == spv::Decoration::RelaxedPrecision) {
|
||||
annotation.SetInOperand(0, {packedVariableId});
|
||||
} else {
|
||||
deadDecorations.push_back(&annotation);
|
||||
}
|
||||
}
|
||||
for (auto* annotation : deadDecorations) {
|
||||
irContext->KillInst(annotation);
|
||||
}
|
||||
|
||||
// Demote the original to a Private global: every existing OpLoad /
|
||||
// OpAccessChain on it stays valid and keeps its double type. It is also moved to
|
||||
// the end of the section, because the pointer-to-Private type it now names was
|
||||
// appended above and a variable may not forward-reference its own type.
|
||||
variable->SetResultType(target.privatePointerTypeId);
|
||||
variable->SetInOperand(0, {static_cast<Uint32>(spv::StorageClass::Private)});
|
||||
variable->RemoveFromList();
|
||||
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
|
||||
|
||||
// SPIR-V 1.3 lists only Input/Output in the entry-point interface.
|
||||
std::vector<Operand> interfaceOperands;
|
||||
for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) {
|
||||
const Operand& operand = entryPoint->GetInOperand(i);
|
||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
|
||||
entryPoint->GetSingleWordInOperand(i) == oldVariableId) {
|
||||
interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {packedVariableId}});
|
||||
continue;
|
||||
}
|
||||
interfaceOperands.push_back(operand);
|
||||
}
|
||||
entryPoint->SetInOperands(std::move(interfaceOperands));
|
||||
|
||||
const Uint32 loadedId = irContext->TakeNextId();
|
||||
const Uint32 bitcastId = irContext->TakeNextId();
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, packedTypeId, loadedId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {packedVariableId}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpBitcast, target.doubleTypeId, bitcastId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {loadedId}}}));
|
||||
++insertPoint;
|
||||
insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {oldVariableId}},
|
||||
{SPV_OPERAND_TYPE_ID, {bitcastId}}}));
|
||||
++insertPoint;
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<PackDoubleVertexInputsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,43 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Re-declares every 64-bit floating-point *vertex input* as the 32-bit unsigned word
|
||||
// pair that holds the same bytes (double -> uvec2, dvec2 -> uvec4), demotes the
|
||||
// original variable to a Private global, and seeds it once at the top of the entry
|
||||
// point with an OpBitcast of the new input. Everything downstream keeps loading the
|
||||
// same id and the same double type, so no other instruction is rewritten.
|
||||
//
|
||||
// Why not just use VK_FORMAT_R64*_SFLOAT: those formats are optional, and lavapipe
|
||||
// reports zero bufferFeatures for all four of them, so a 64-bit vertex fetch is
|
||||
// impossible there even though shaderFloat64 is supported. The word-pair form needs no
|
||||
// format capability at all and is bit-exact, so it is applied unconditionally rather
|
||||
// than as a fallback - which also keeps it in lockstep with
|
||||
// VertexInputStateFactory::ToVkVertexFormat, since both branch on nothing but "is this
|
||||
// a 64-bit vertex input".
|
||||
//
|
||||
// DirectVulkan only. The 64-bit *arithmetic* still needs the Float64 capability, i.e.
|
||||
// an enabled VkPhysicalDeviceFeatures::shaderFloat64.
|
||||
class PackDoubleVertexInputsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-pack-double-vertex-inputs"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreatePackDoubleVertexInputsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user