mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
46
Commits
itrp
...
5e676b338b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e676b338b | ||
|
|
db01bfa3e8 | ||
|
|
cc3dcfd80e | ||
|
|
d9556ff041 | ||
|
|
39f21e52ea | ||
|
|
0e933b8f2f | ||
|
|
199164c2e0 | ||
|
|
e87063e90c | ||
|
|
122da27249 | ||
|
|
bc2d698b3e | ||
|
|
3049c4b82b | ||
|
|
2b3850b76b | ||
|
|
2a0ae743a0 | ||
|
|
6839219c10 | ||
|
|
52ddb440ca | ||
|
|
79feeffd25 | ||
|
|
202037b5a3 | ||
|
|
bce9c48c8e | ||
|
|
b6a7807a3a | ||
|
|
48ba622387 | ||
|
|
05260d1262 | ||
|
|
6eb5ff51c5 | ||
|
|
e526f8e8ac | ||
|
|
e724e88eec | ||
|
|
3b175fb88a | ||
|
|
b5a4e7075a | ||
|
|
520c2b6750 | ||
|
|
57cc652b1d | ||
|
|
65ea54da9e | ||
|
|
c81dd04f08 | ||
|
|
c158bfa584 | ||
|
|
f9f455144c | ||
|
|
64e4840de2 | ||
|
|
bf7b5755cc | ||
|
|
293f64b3c2 | ||
|
|
f0cc07c937 | ||
|
|
4658536652 | ||
|
|
04b4627c65 | ||
|
|
68e13705c8 | ||
|
|
e5388c0e7e | ||
|
|
8bc4808b1a | ||
|
|
5d8a5387e2 | ||
|
|
aa2184e47a | ||
|
|
9152a4a4bc | ||
|
|
1963b427db | ||
|
|
f3def150e7 |
@@ -9,7 +9,23 @@ fi
|
|||||||
case_name="$1"
|
case_name="$1"
|
||||||
fixture_dir="${2:-tools/trace_replay/fixtures}"
|
fixture_dir="${2:-tools/trace_replay/fixtures}"
|
||||||
python_bin="${PYTHON:-python3}"
|
python_bin="${PYTHON:-python3}"
|
||||||
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
|
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
|
||||||
|
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
|
||||||
|
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
|
||||||
|
default_mirror_bases=(
|
||||||
|
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
|
||||||
|
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
|
||||||
|
)
|
||||||
|
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
|
||||||
|
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
|
||||||
|
else
|
||||||
|
mirror_bases=("${default_mirror_bases[@]}")
|
||||||
|
fi
|
||||||
|
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
|
||||||
|
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
|
||||||
|
fi
|
||||||
|
# Optional bearer token for mirrors that require authentication (private Gitea).
|
||||||
|
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
|
||||||
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
|
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
|
||||||
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
|
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
|
||||||
|
|
||||||
@@ -30,7 +46,8 @@ fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
|
|||||||
--format fixture-files \
|
--format fixture-files \
|
||||||
--case "${case_name}" \
|
--case "${case_name}" \
|
||||||
--fixture-root "${fixture_dir}")"
|
--fixture-root "${fixture_dir}")"
|
||||||
mapfile -t files <<< "${fixture_list}"
|
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
|
||||||
|
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
|
||||||
|
|
||||||
include="$(IFS=,; echo "${files[*]}")"
|
include="$(IFS=,; echo "${files[*]}")"
|
||||||
if [ "${case_name}" = "OpenRA" ]; then
|
if [ "${case_name}" = "OpenRA" ]; then
|
||||||
@@ -106,6 +123,7 @@ fetch_file_from_mirror() {
|
|||||||
local attempt
|
local attempt
|
||||||
local partial_size
|
local partial_size
|
||||||
local curl_status
|
local curl_status
|
||||||
|
local curl_auth
|
||||||
|
|
||||||
metadata="$(get_lfs_metadata "${file}")" || return 1
|
metadata="$(get_lfs_metadata "${file}")" || return 1
|
||||||
read -r expected_oid expected_size <<< "${metadata}"
|
read -r expected_oid expected_size <<< "${metadata}"
|
||||||
@@ -136,7 +154,11 @@ fetch_file_from_mirror() {
|
|||||||
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
|
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
|
curl_auth=()
|
||||||
|
if [ -n "${mirror_token}" ]; then
|
||||||
|
curl_auth=(--header "Authorization: token ${mirror_token}")
|
||||||
|
fi
|
||||||
|
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
|
||||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||||
mv "${tmp_file}" "${file}"
|
mv "${tmp_file}" "${file}"
|
||||||
return 0
|
return 0
|
||||||
@@ -184,10 +206,19 @@ fetch_from_mirror() {
|
|||||||
for file in "${files[@]}"; do
|
for file in "${files[@]}"; do
|
||||||
local name
|
local name
|
||||||
local url
|
local url
|
||||||
|
local base
|
||||||
|
local fetched=0
|
||||||
name="$(basename "${file}")"
|
name="$(basename "${file}")"
|
||||||
url="${mirror_base%/}/${name}"
|
for base in "${mirror_bases[@]}"; do
|
||||||
echo "Fetching trace fixture from mirror: ${url}"
|
url="${base%/}/${name}"
|
||||||
if ! fetch_file_from_mirror "${file}" "${url}"; then
|
echo "Fetching trace fixture from mirror: ${url}"
|
||||||
|
if fetch_file_from_mirror "${file}" "${url}"; then
|
||||||
|
fetched=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Mirror did not serve ${name}; trying the next mirror" >&2
|
||||||
|
done
|
||||||
|
if [ "${fetched}" -ne 1 ]; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -196,7 +227,7 @@ fetch_from_mirror() {
|
|||||||
if fetch_from_mirror; then
|
if fetch_from_mirror; then
|
||||||
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
||||||
else
|
else
|
||||||
echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
|
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
|
||||||
git lfs install --local
|
git lfs install --local
|
||||||
git lfs pull --include="${include}" --exclude=""
|
git lfs pull --include="${include}" --exclude=""
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -455,6 +455,15 @@ jobs:
|
|||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||||
fi
|
fi
|
||||||
|
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
||||||
|
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||||
|
# bypasses only the vendor gate, so this exercises the real strip on
|
||||||
|
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
|
||||||
|
# unstripped path covered for the same trace.
|
||||||
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
||||||
|
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
|
||||||
|
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
|
||||||
|
fi
|
||||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||||
|
|
||||||
- name: Upload actual image
|
- name: Upload actual image
|
||||||
|
|||||||
@@ -188,9 +188,12 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
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/LowerDrawParametersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ namespace MobileGL::MG_Config {
|
|||||||
|
|
||||||
extern BackendType ActiveBackendType;
|
extern BackendType ActiveBackendType;
|
||||||
|
|
||||||
|
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
|
||||||
|
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
|
||||||
|
// device gate - each quirk keeps its structural safety checks.
|
||||||
|
enum class QuirkOverride : Uint8 {
|
||||||
|
Auto = 0,
|
||||||
|
ForceOn,
|
||||||
|
ForceOff,
|
||||||
|
};
|
||||||
|
|
||||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||||
@@ -67,6 +76,21 @@ namespace MobileGL::MG_Config {
|
|||||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||||
// version request.
|
// version request.
|
||||||
Bool RelaxedSemantics = false;
|
Bool RelaxedSemantics = false;
|
||||||
|
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||||
|
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||||
|
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||||
|
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||||
|
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||||
|
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||||
|
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
|
||||||
|
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||||
|
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||||
|
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||||
|
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||||
|
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||||
|
Bool DisableRobustBufferAccess = false;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
|
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
|
||||||
|
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
|
||||||
|
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
|
||||||
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
|
return MG_Config::QuirkOverride::Auto;
|
||||||
|
}
|
||||||
|
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
|
||||||
|
: MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
|
||||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||||
auto it = acceptedEnvVariablesMap->find(key);
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
if (it == acceptedEnvVariablesMap->end()) {
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
@@ -123,6 +134,10 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
|
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||||
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||||
|
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -230,6 +230,21 @@ namespace MobileGL {
|
|||||||
void (*SetSwapInterval)(Int interval);
|
void (*SetSwapInterval)(Int interval);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
|
||||||
|
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
|
||||||
|
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
|
||||||
|
enum class GpuVendorKind : Uint8 {
|
||||||
|
Unknown = 0,
|
||||||
|
Qualcomm,
|
||||||
|
Arm,
|
||||||
|
Nvidia,
|
||||||
|
Amd,
|
||||||
|
Intel,
|
||||||
|
ImgTec,
|
||||||
|
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
|
||||||
|
Software,
|
||||||
|
};
|
||||||
|
|
||||||
struct DynamicBackendParameters {
|
struct DynamicBackendParameters {
|
||||||
SizeT UniformBufferOffsetAlignment = 256;
|
SizeT UniformBufferOffsetAlignment = 256;
|
||||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||||
@@ -272,6 +287,9 @@ namespace MobileGL {
|
|||||||
Int MaxUniformBlockSize = 16384;
|
Int MaxUniformBlockSize = 16384;
|
||||||
Int MaxImageUnits = 8;
|
Int MaxImageUnits = 8;
|
||||||
Int MaxCombinedImageUniforms = 8;
|
Int MaxCombinedImageUniforms = 8;
|
||||||
|
Int MaxVertexImageUniforms = 0;
|
||||||
|
Int MaxGeometryImageUniforms = 0;
|
||||||
|
Int MaxFragmentImageUniforms = 8;
|
||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
@@ -288,6 +306,7 @@ namespace MobileGL {
|
|||||||
Uint32 SubgroupSupportedStages = 0;
|
Uint32 SubgroupSupportedStages = 0;
|
||||||
Uint32 SubgroupSupportedFeatures = 0;
|
Uint32 SubgroupSupportedFeatures = 0;
|
||||||
Bool SubgroupQuadOperationsInAllStages = false;
|
Bool SubgroupQuadOperationsInAllStages = false;
|
||||||
|
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class WindowBackend {
|
enum class WindowBackend {
|
||||||
|
|||||||
@@ -826,7 +826,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
||||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||||
E_GL_ARB_direct_state_access,
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||||
@@ -947,6 +947,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return m_dynamicParameters;
|
return m_dynamicParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
|
||||||
|
const MG_External::GLESCapabilities& capabilities) {
|
||||||
|
m_GLESCapabilities = capabilities;
|
||||||
|
UpdateDynamicBackendParameters();
|
||||||
|
}
|
||||||
|
|
||||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||||
@@ -1003,9 +1009,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
||||||
const Int maxSupportedTextureUnits =
|
const Int maxSupportedTextureUnits =
|
||||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits);
|
m_dynamicParameters.MaxImageUnits =
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms;
|
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms;
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
|
||||||
|
const auto clampStageImageUniforms = [this](Int stageLimit) {
|
||||||
|
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
|
||||||
|
m_dynamicParameters.MaxCombinedImageUniforms});
|
||||||
|
};
|
||||||
|
m_dynamicParameters.MaxVertexImageUniforms =
|
||||||
|
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
|
||||||
|
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||||
|
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
|
||||||
|
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||||
|
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||||
|
m_dynamicParameters.MaxComputeImageUniforms =
|
||||||
|
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
@@ -1017,6 +1035,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
|
||||||
m_dynamicParameters.SupportsWideLines =
|
m_dynamicParameters.SupportsWideLines =
|
||||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
||||||
|
|
||||||
|
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
|
||||||
|
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
|
||||||
|
return haystack.find(needle) != String::npos;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const String vendorAndRenderer =
|
||||||
|
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
|
||||||
|
if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) {
|
||||||
|
// Check software rasterizers first: ANGLE-on-llvmpipe reports both.
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"NVIDIA"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"Intel"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
|
||||||
|
} else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
||||||
|
} else {
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
|
const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
|
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
|
||||||
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
|
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
|
||||||
|
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void UpdateDynamicBackendParameters();
|
void UpdateDynamicBackendParameters();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -267,16 +267,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint g_boundArrayBufferId = 0;
|
Uint g_boundArrayBufferId = 0;
|
||||||
Bool g_boundArrayBufferKnown = false;
|
Bool g_boundArrayBufferKnown = false;
|
||||||
|
|
||||||
|
// Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see
|
||||||
|
// Managers.h). Resting state between operations is 0; scopes in the
|
||||||
|
// readback/upload paths bind what they need through the cache and
|
||||||
|
// return to 0, so a stale user PBO can never capture a later
|
||||||
|
// readback that meant to target client memory.
|
||||||
|
Uint g_boundPixelPackBufferId = 0;
|
||||||
|
Bool g_boundPixelPackBufferKnown = false;
|
||||||
|
Uint g_boundPixelUnpackBufferId = 0;
|
||||||
|
Bool g_boundPixelUnpackBufferKnown = false;
|
||||||
|
|
||||||
// Bumped whenever the backend ES context is destroyed; resources with
|
// Bumped whenever the backend ES context is destroyed; resources with
|
||||||
// an older generation hold ids from a dead context.
|
// an older generation hold ids from a dead context.
|
||||||
Uint g_bufferContextGeneration = 1;
|
Uint g_bufferContextGeneration = 1;
|
||||||
|
|
||||||
// Defined next to the indexed-binding shadow below; forward-declared so
|
// Defined next to the indexed-binding shadow below; forward-declared so
|
||||||
// every glDeleteBuffers site in this namespace can scrub stale shadow
|
// every glDeleteBuffers site in this namespace can scrub stale shadow
|
||||||
// entries (GL resets a deleted buffer's indexed bindings to 0, and a
|
// entries (GL resets a deleted buffer's bindings - indexed and pixel
|
||||||
// recycled name matching a stale shadow entry would otherwise
|
// pack/unpack alike - to 0, and a recycled name matching a stale shadow
|
||||||
// false-skip the rebind).
|
// entry would otherwise false-skip the rebind).
|
||||||
void ScrubIndexedBufferBindingShadowForId(Uint id);
|
void ScrubBufferBindingShadowsForId(Uint id);
|
||||||
|
|
||||||
// Resources whose owning BufferObject died; ids deleted at the next
|
// Resources whose owning BufferObject died; ids deleted at the next
|
||||||
// sync point with a current ES context.
|
// sync point with a current ES context.
|
||||||
@@ -318,10 +328,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
}
|
}
|
||||||
|
// Pooling keeps the id alive (and thus any driver binding of it);
|
||||||
|
// drop to unknown rather than claiming the post-delete 0 state.
|
||||||
|
if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) ||
|
||||||
|
(g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) {
|
||||||
|
InvalidatePixelBufferBindingCaches();
|
||||||
|
}
|
||||||
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
const std::lock_guard<std::mutex> lock(g_poolMutex);
|
||||||
auto& bucket = g_bufferPool[r.storageSize];
|
auto& bucket = g_bufferPool[r.storageSize];
|
||||||
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
|
||||||
ScrubIndexedBufferBindingShadowForId(r.id);
|
ScrubBufferBindingShadowsForId(r.id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
|
||||||
r.id = 0;
|
r.id = 0;
|
||||||
return;
|
return;
|
||||||
@@ -502,7 +518,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Need a fresh id: glBufferStorage fails on a buffer that already has
|
// Need a fresh id: glBufferStorage fails on a buffer that already has
|
||||||
// immutable storage, and any prior mutable store is replaced anyway.
|
// immutable storage, and any prior mutable store is replaced anyway.
|
||||||
if (resource->id != 0) {
|
if (resource->id != 0) {
|
||||||
ScrubIndexedBufferBindingShadowForId(resource->id);
|
ScrubBufferBindingShadowsForId(resource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
|
||||||
resource->id = 0;
|
resource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -630,7 +646,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
}
|
}
|
||||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
ScrubBufferBindingShadowsForId(glesResource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||||
glesResource->id = 0;
|
glesResource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -668,6 +684,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void OnBackendContextDestroyed() {
|
void OnBackendContextDestroyed() {
|
||||||
UnregisterBufferBackendOps();
|
UnregisterBufferBackendOps();
|
||||||
++g_bufferContextGeneration;
|
++g_bufferContextGeneration;
|
||||||
|
InvalidateArrayBufferBindingCache();
|
||||||
|
InvalidateIndexedBufferBindingCache();
|
||||||
|
InvalidatePixelBufferBindingCaches();
|
||||||
// The global-UBO ring's id and persistent map died with the context;
|
// The global-UBO ring's id and persistent map died with the context;
|
||||||
// drop the handles (no GL) and let the next draw recreate the ring.
|
// drop the handles (no GL) and let the next draw recreate the ring.
|
||||||
ResetUboRingForNewContext();
|
ResetUboRingForNewContext();
|
||||||
@@ -694,7 +713,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
|
||||||
InvalidateArrayBufferBindingCache();
|
InvalidateArrayBufferBindingCache();
|
||||||
}
|
}
|
||||||
ScrubIndexedBufferBindingShadowForId(glesResource->id);
|
ScrubBufferBindingShadowsForId(glesResource->id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
|
||||||
glesResource->id = 0;
|
glesResource->id = 0;
|
||||||
}
|
}
|
||||||
@@ -819,6 +838,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_boundArrayBufferKnown = false;
|
g_boundArrayBufferKnown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BindPixelPackBufferId(Uint id) {
|
||||||
|
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id);
|
||||||
|
g_boundPixelPackBufferId = id;
|
||||||
|
g_boundPixelPackBufferKnown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindPixelUnpackBufferId(Uint id) {
|
||||||
|
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id);
|
||||||
|
g_boundPixelUnpackBufferId = id;
|
||||||
|
g_boundPixelUnpackBufferKnown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvalidatePixelBufferBindingCaches() {
|
||||||
|
g_boundPixelPackBufferId = 0;
|
||||||
|
g_boundPixelPackBufferKnown = false;
|
||||||
|
g_boundPixelUnpackBufferId = 0;
|
||||||
|
g_boundPixelUnpackBufferKnown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoteBufferIdDeleted(Uint id) {
|
||||||
|
if (id == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
|
||||||
|
InvalidateArrayBufferBindingCache();
|
||||||
|
}
|
||||||
|
ScrubBufferBindingShadowsForId(id);
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
|
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
|
||||||
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
|
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
|
||||||
@@ -840,12 +894,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// glDeleteBuffers resets the deleted buffer's bindings (indexed ones
|
// glDeleteBuffers resets the deleted buffer's bindings (indexed and
|
||||||
// included) to 0 in the current context; mirror that in the shadow, or a
|
// pixel pack/unpack ones included) to 0 in the current context; mirror
|
||||||
// later buffer recycling the same name with a matching range would
|
// that in the shadows, or a later buffer recycling the same name with a
|
||||||
// false-skip its rebind. Default IndexedBufferBinding{} == base(0) ==
|
// matching shadow entry would false-skip its rebind. Default
|
||||||
// the post-delete GL state.
|
// IndexedBufferBinding{} == base(0) == the post-delete GL state.
|
||||||
void ScrubIndexedBufferBindingShadowForId(Uint id) {
|
void ScrubBufferBindingShadowsForId(Uint id) {
|
||||||
if (id == 0) return;
|
if (id == 0) return;
|
||||||
for (auto& binding : g_indexedUBOBindings) {
|
for (auto& binding : g_indexedUBOBindings) {
|
||||||
if (binding.id == id) binding = {};
|
if (binding.id == id) binding = {};
|
||||||
@@ -853,6 +907,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (auto& binding : g_indexedSSBOBindings) {
|
for (auto& binding : g_indexedSSBOBindings) {
|
||||||
if (binding.id == id) binding = {};
|
if (binding.id == id) binding = {};
|
||||||
}
|
}
|
||||||
|
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
|
||||||
|
g_boundPixelPackBufferId = 0;
|
||||||
|
}
|
||||||
|
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
|
||||||
|
g_boundPixelUnpackBufferId = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -897,7 +957,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
auto& bucket = g_bufferPool[oldestKey];
|
auto& bucket = g_bufferPool[oldestKey];
|
||||||
PooledBuffer& e = bucket[oldestIdx];
|
PooledBuffer& e = bucket[oldestIdx];
|
||||||
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
|
||||||
ScrubIndexedBufferBindingShadowForId(e.id);
|
ScrubBufferBindingShadowsForId(e.id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
g_GLESFuncs.glDeleteBuffers(1, &e.id);
|
||||||
}
|
}
|
||||||
g_pooledBytes -= e.size;
|
g_pooledBytes -= e.size;
|
||||||
@@ -1064,7 +1124,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
|
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
|
||||||
if (!staleContext && entry.retireSerial > completed) continue;
|
if (!staleContext && entry.retireSerial > completed) continue;
|
||||||
if (!staleContext && entry.id != 0) {
|
if (!staleContext && entry.id != 0) {
|
||||||
ScrubIndexedBufferBindingShadowForId(entry.id);
|
ScrubBufferBindingShadowsForId(entry.id);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
|
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
|
||||||
}
|
}
|
||||||
g_retiredUboRings[i] = g_retiredUboRings.back();
|
g_retiredUboRings[i] = g_retiredUboRings.back();
|
||||||
@@ -1152,6 +1212,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
for (auto& bufferId : m_clientAttributeBufferIds) {
|
for (auto& bufferId : m_clientAttributeBufferIds) {
|
||||||
if (bufferId != 0) {
|
if (bufferId != 0) {
|
||||||
|
BufferImpl::NoteBufferIdDeleted(bufferId);
|
||||||
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
||||||
bufferId = 0;
|
bufferId = 0;
|
||||||
}
|
}
|
||||||
@@ -1324,6 +1385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
|
m_contextGeneration = g_textureContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to generate texture object.");
|
MGLOG_E("Failed to generate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -1332,6 +1394,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackendTextureObject::~BackendTextureObject() {
|
||||||
|
if (m_backendTextureId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Scrub every driver-state shadow that could false-skip when the name
|
||||||
|
// or this heap address is recycled - regardless of whether the id can
|
||||||
|
// still be deleted.
|
||||||
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||||
|
for (auto& unitCache : g_boundTexturesCache) {
|
||||||
|
for (auto& boundTexture : unitCache) {
|
||||||
|
if (boundTexture == this) {
|
||||||
|
boundTexture = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||||
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
|
}
|
||||||
|
m_backendTextureId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void BackendTextureObject::Bind(GLenum target, Uint unit) {
|
void BackendTextureObject::Bind(GLenum target, Uint unit) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -1364,7 +1447,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
void BackendTextureObject::RecreateBackendTexture() {
|
void BackendTextureObject::RecreateBackendTexture() {
|
||||||
if (m_backendTextureId != 0) {
|
if (m_backendTextureId != 0) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||||
|
if (m_contextGeneration == g_textureContextGeneration) {
|
||||||
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
|
}
|
||||||
for (auto& unitCache : g_boundTexturesCache) {
|
for (auto& unitCache : g_boundTexturesCache) {
|
||||||
for (auto& boundTexture : unitCache) {
|
for (auto& boundTexture : unitCache) {
|
||||||
if (boundTexture == this) {
|
if (boundTexture == this) {
|
||||||
@@ -1375,6 +1461,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
|
m_contextGeneration = g_textureContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to regenerate texture object.");
|
MGLOG_E("Failed to regenerate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -1391,7 +1478,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
|
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
|
||||||
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
|
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
|
||||||
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
|
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
|
||||||
// helpers (this class, TempPixelStoreParameterSync, the R32F copy path), all of
|
// helpers (this class and, historically, the R32F copy path), all of
|
||||||
// which restore to the resting default, so the shadow stays accurate; a one-time
|
// which restore to the resting default, so the shadow stays accurate; a one-time
|
||||||
// forced sync pins the backend to that known default up front. Apply() is
|
// forced sync pins the backend to that known default up front. Apply() is
|
||||||
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
|
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
|
||||||
@@ -1563,6 +1650,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return convertedData.data();
|
return convertedData.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
|
||||||
|
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
|
||||||
|
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
|
||||||
|
// drifting mid-range texels one 5-bit step down and failing the KHR-GL3x
|
||||||
|
// pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into
|
||||||
|
// the packed 16-bit client type with round-to-nearest instead - that recovers the original
|
||||||
|
// 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the
|
||||||
|
// driver stores them verbatim with no requantization left to its discretion. 4-bit formats
|
||||||
|
// (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding.
|
||||||
|
// Always retargets *inOutType for these formats (even for null data) so every upload of a
|
||||||
|
// level uses the same client type.
|
||||||
|
static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize,
|
||||||
|
const void* data, SizeT byteSize, GLenum* inOutType,
|
||||||
|
Vector<Uint8>& packedData) {
|
||||||
|
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
|
||||||
|
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
|
||||||
|
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
|
||||||
|
// the already-retargeted packed type and must still be converted.
|
||||||
|
if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
*inOutType = packedType;
|
||||||
|
if (data == nullptr || byteSize == 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const SizeT srcPixelBytes = hasAlpha ? 4 : 3;
|
||||||
|
const SizeT texelCount = std::min(static_cast<SizeT>(std::max(texelSize.x(), 0)) *
|
||||||
|
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
|
||||||
|
static_cast<SizeT>(std::max(texelSize.z(), 1)),
|
||||||
|
byteSize / srcPixelBytes);
|
||||||
|
packedData.resize(texelCount * sizeof(Uint16));
|
||||||
|
const Uint8* src = static_cast<const Uint8*>(data);
|
||||||
|
auto* dst = reinterpret_cast<Uint16*>(packedData.data());
|
||||||
|
for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) {
|
||||||
|
const Uint32 r = (static_cast<Uint32>(src[0]) * 31u + 127u) / 255u;
|
||||||
|
const Uint32 b = (static_cast<Uint32>(src[2]) * 31u + 127u) / 255u;
|
||||||
|
if (hasAlpha) {
|
||||||
|
const Uint32 g = (static_cast<Uint32>(src[1]) * 31u + 127u) / 255u;
|
||||||
|
dst[i] = static_cast<Uint16>((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u));
|
||||||
|
} else {
|
||||||
|
const Uint32 g = (static_cast<Uint32>(src[1]) * 63u + 127u) / 255u;
|
||||||
|
dst[i] = static_cast<Uint16>((r << 11) | (g << 5) | b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packedData.data();
|
||||||
|
}
|
||||||
|
|
||||||
void BackendTextureObject::SyncMipmapsToBackend(
|
void BackendTextureObject::SyncMipmapsToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||||
if (!stateTextureObject) {
|
if (!stateTextureObject) {
|
||||||
@@ -1706,9 +1843,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||||
convertedUploadData);
|
convertedUploadData);
|
||||||
|
Vector<Uint8> packedUploadData;
|
||||||
|
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||||
|
uploadData, levelByteSize, &glType, packedUploadData);
|
||||||
|
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
const IntVec3 uploadSize =
|
const IntVec3 uploadSize =
|
||||||
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
|
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
|
||||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||||
@@ -1760,7 +1900,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
|
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
|
||||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
switch (targetInternal) {
|
switch (targetInternal) {
|
||||||
case TextureTarget::Texture2DMultisample:
|
case TextureTarget::Texture2DMultisample:
|
||||||
g_GLESFuncs.glTexStorage2DMultisample(
|
g_GLESFuncs.glTexStorage2DMultisample(
|
||||||
@@ -1787,7 +1927,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
|
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
|
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
|
||||||
switch (MapToBackendTextureTarget(targetInternal)) {
|
switch (MapToBackendTextureTarget(targetInternal)) {
|
||||||
case TextureTarget::Texture2D:
|
case TextureTarget::Texture2D:
|
||||||
@@ -1831,9 +1971,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||||
convertedUploadData);
|
convertedUploadData);
|
||||||
|
Vector<Uint8> packedUploadData;
|
||||||
|
uploadData =
|
||||||
|
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||||
|
uploadData, levelByteSize, &glType, packedUploadData);
|
||||||
|
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
const IntVec3 uploadSize =
|
const IntVec3 uploadSize =
|
||||||
GetBackendUploadSize(targetInternal, levelTexelSize);
|
GetBackendUploadSize(targetInternal, levelTexelSize);
|
||||||
switch (MapToBackendTextureTarget(targetInternal)) {
|
switch (MapToBackendTextureTarget(targetInternal)) {
|
||||||
@@ -1885,6 +2029,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||||
convertedUploadData);
|
convertedUploadData);
|
||||||
|
Vector<Uint8> packedUploadData;
|
||||||
|
uploadData =
|
||||||
|
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||||
|
uploadData, levelByteSize, &glType, packedUploadData);
|
||||||
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
|
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
|
||||||
"levelDirty = %s",
|
"levelDirty = %s",
|
||||||
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
@@ -1892,7 +2040,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
levelByteSize, pData, levelDirty ? "true" : "false");
|
levelByteSize, pData, levelDirty ? "true" : "false");
|
||||||
|
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
auto textureTarget = stateTextureObject->GetTarget();
|
auto textureTarget = stateTextureObject->GetTarget();
|
||||||
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
|
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
|
||||||
switch (MapToBackendTextureTarget(textureTarget)) {
|
switch (MapToBackendTextureTarget(textureTarget)) {
|
||||||
@@ -1978,7 +2126,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
|
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
|
||||||
|
|
||||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
DebugImpl::ErrorLopper::Loop(
|
DebugImpl::ErrorLopper::Loop(
|
||||||
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||||
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
|
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
|
||||||
@@ -1990,6 +2138,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||||
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
|
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
|
||||||
convertedUploadData);
|
convertedUploadData);
|
||||||
|
Vector<Uint8> packedUploadData;
|
||||||
|
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
|
||||||
|
uploadData, byteSize, &glType, packedUploadData);
|
||||||
const IntVec3 uploadSize =
|
const IntVec3 uploadSize =
|
||||||
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
||||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||||
@@ -2216,11 +2367,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
// Multisample targets reject the *sampler* parameters (LOD range, border color) but
|
||||||
|
// GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them.
|
||||||
|
// Bailing out entirely used to drop every swizzle write on the floor, which is what the
|
||||||
|
// frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which
|
||||||
|
// deliberately omits the swizzle enums). Note the caches for the skipped parameters are
|
||||||
|
// still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the
|
||||||
|
// change detection below would swallow the very writes we came here to emit.
|
||||||
|
const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal);
|
||||||
|
if (isMultisampleTarget) {
|
||||||
m_cacheLodRange = stateTextureObject->GetLevelRange();
|
m_cacheLodRange = stateTextureObject->GetLevelRange();
|
||||||
m_cacheSwizzleParams = stateTextureObject->GetAllSwizzleParams();
|
|
||||||
m_cacheBorderColor = stateTextureObject->GetBorderColor();
|
m_cacheBorderColor = stateTextureObject->GetBorderColor();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bind(target);
|
Bind(target);
|
||||||
@@ -2233,14 +2390,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
const auto& levelRange = stateTextureObject->GetLevelRange();
|
const auto& levelRange = stateTextureObject->GetLevelRange();
|
||||||
|
|
||||||
if (m_cacheLodRange.x() != levelRange.x()) {
|
if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) {
|
||||||
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
|
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
|
||||||
m_cacheLodRange.x() = levelRange.x();
|
m_cacheLodRange.x() = levelRange.x();
|
||||||
}
|
}
|
||||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||||
});
|
});
|
||||||
if (m_cacheLodRange.y() != levelRange.y()) {
|
if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) {
|
||||||
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
|
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
|
||||||
m_cacheLodRange.y() = levelRange.y();
|
m_cacheLodRange.y() = levelRange.y();
|
||||||
}
|
}
|
||||||
@@ -2266,7 +2423,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
|
if (!isMultisampleTarget && m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
|
||||||
const auto& borderColor = stateTextureObject->GetBorderColor();
|
const auto& borderColor = stateTextureObject->GetBorderColor();
|
||||||
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
|
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
|
||||||
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||||
@@ -2295,6 +2452,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint g_activeTextureUnit = 0;
|
Uint g_activeTextureUnit = 0;
|
||||||
|
Uint g_textureContextGeneration = 1;
|
||||||
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
@@ -2320,9 +2478,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (target == FramebufferTarget::Read)
|
if (target == FramebufferTarget::Read)
|
||||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_backendFBOId);
|
BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId);
|
||||||
else
|
else
|
||||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Driver-level framebuffer-binding shadow (see Managers.h). Indexed by
|
||||||
|
// FramebufferTarget {Draw, Read}.
|
||||||
|
Array<Uint, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindings = {0, 0};
|
||||||
|
Array<Bool, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindingKnown = {false, false};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void BindFramebufferId(GLenum fbTarget, Uint id) {
|
||||||
|
const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
||||||
|
const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
|
||||||
|
const SizeT drawIdx = SizeT(FramebufferTarget::Draw);
|
||||||
|
const SizeT readIdx = SizeT(FramebufferTarget::Read);
|
||||||
|
const Bool drawMatches =
|
||||||
|
!bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id);
|
||||||
|
const Bool readMatches =
|
||||||
|
!bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id);
|
||||||
|
if (drawMatches && readMatches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glBindFramebuffer(fbTarget, id);
|
||||||
|
if (bindsDraw) {
|
||||||
|
g_driverFBOBindings[drawIdx] = id;
|
||||||
|
g_driverFBOBindingKnown[drawIdx] = true;
|
||||||
|
}
|
||||||
|
if (bindsRead) {
|
||||||
|
g_driverFBOBindings[readIdx] = id;
|
||||||
|
g_driverFBOBindingKnown[readIdx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint CurrentFramebufferBinding(FramebufferTarget target) {
|
||||||
|
const SizeT idx = SizeT(target);
|
||||||
|
if (!g_driverFBOBindingKnown[idx]) {
|
||||||
|
// Cold path: pin the shadow from the driver once (init probes and
|
||||||
|
// pre-shadow code bind raw but restore what they found).
|
||||||
|
GLint binding = 0;
|
||||||
|
g_GLESFuncs.glGetIntegerv(
|
||||||
|
target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING,
|
||||||
|
&binding);
|
||||||
|
g_driverFBOBindings[idx] = static_cast<Uint>(binding);
|
||||||
|
g_driverFBOBindingKnown[idx] = true;
|
||||||
|
}
|
||||||
|
return g_driverFBOBindings[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvalidateFramebufferBindingCache() {
|
||||||
|
g_driverFBOBindings = {0, 0};
|
||||||
|
g_driverFBOBindingKnown = {false, false};
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendFramebufferObject::InvalidateSyncedState() {
|
void BackendFramebufferObject::InvalidateSyncedState() {
|
||||||
@@ -2376,7 +2584,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (glTextureTarget == GL_UNKNOWN_MGL) {
|
if (glTextureTarget == GL_UNKNOWN_MGL) {
|
||||||
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
||||||
}
|
}
|
||||||
backendTextureObject->Bind(glTextureTarget);
|
// glBindTexture rejects cube-face enums (INVALID_ENUM with no
|
||||||
|
// bind, while Bind() would still record the cube-map cache slot
|
||||||
|
// as bound): bind via the owning cube target; the attach below
|
||||||
|
// keeps the face target.
|
||||||
|
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||||
|
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||||
|
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||||
backendTextureObject->GetBackendTextureId(),
|
backendTextureObject->GetBackendTextureId(),
|
||||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||||
@@ -2465,6 +2679,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BackendFramebufferObject::SyncReadBufferToBackend(
|
||||||
|
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
|
||||||
|
if (!stateFBOObject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
|
||||||
|
if (frontendReadBuf == m_frontendReadBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_frontendReadBuffer = frontendReadBuf;
|
||||||
|
|
||||||
|
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
|
||||||
|
if (m_backendReadBuffer != glBackendReadBuffer) {
|
||||||
|
m_backendReadBuffer = glBackendReadBuffer;
|
||||||
|
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER. When this is
|
||||||
|
// reached from SyncCurrentFBO's "same FBO as draw" skip path the backend FBO was
|
||||||
|
// only bound as DRAW, so bind it as READ first to route the read buffer correctly.
|
||||||
|
Bind(FramebufferTarget::Read);
|
||||||
|
g_GLESFuncs.glReadBuffer(glBackendReadBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BackendFramebufferObject::SyncToBackend(
|
void BackendFramebufferObject::SyncToBackend(
|
||||||
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
|
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -2546,16 +2782,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
|
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
|
||||||
// only apply (and stamp the memo) when this object is bound as READ.
|
// only apply (and stamp the memo) when this object is bound as READ.
|
||||||
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
|
if (asTarget == FramebufferTarget::Read) {
|
||||||
if (frontendReadBuf != m_frontendReadBuffer && asTarget == FramebufferTarget::Read) {
|
SyncReadBufferToBackend(stateFBOObject);
|
||||||
m_frontendReadBuffer = frontendReadBuf;
|
|
||||||
|
|
||||||
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
|
|
||||||
|
|
||||||
if (m_backendReadBuffer != glBackendReadBuffer) {
|
|
||||||
m_backendReadBuffer = glBackendReadBuffer;
|
|
||||||
g_GLESFuncs.glReadBuffer(glBackendReadBuffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Attach texture to backend FBO -----------------------
|
// -------------------- Attach texture to backend FBO -----------------------
|
||||||
@@ -2662,6 +2890,295 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fboSyncedObjects = {};
|
g_fboSyncedObjects = {};
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
|
namespace ScratchFBOImpl {
|
||||||
|
namespace {
|
||||||
|
ScratchFramebuffer g_tempFramebuffer;
|
||||||
|
ScratchFramebuffer g_blitReadFramebuffer;
|
||||||
|
ScratchFramebuffer g_blitDrawFramebuffer;
|
||||||
|
Uint g_completeTinyFBOId = 0;
|
||||||
|
Uint g_completeTinyRBOId = 0;
|
||||||
|
|
||||||
|
// Detach every point the shadow no longer vouches for. Used when the
|
||||||
|
// shadow is unknown (context reset, texture id deleted while attached).
|
||||||
|
void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
fb.colorTex = 0;
|
||||||
|
fb.colorTarget = 0;
|
||||||
|
fb.colorLevel = 0;
|
||||||
|
fb.colorLayer = -1;
|
||||||
|
fb.depthTex = 0;
|
||||||
|
fb.depthTarget = 0;
|
||||||
|
fb.depthLevel = 0;
|
||||||
|
fb.depthHasStencil = false;
|
||||||
|
fb.attachmentsKnown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||||
|
if (!fb.attachmentsKnown) {
|
||||||
|
ScrubAllAttachments(fb, fbTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The post-attach glGetError probe below must not misread an error some
|
||||||
|
// earlier operation left queued; drain before attaching (rare path -
|
||||||
|
// only runs when the attachment actually changes).
|
||||||
|
void DrainPendingGLErrors() {
|
||||||
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the color point as detached when the shadow said something was
|
||||||
|
// there; the actual detach call is the caller's (it may be replaced by
|
||||||
|
// the new attach directly when the point is being overwritten).
|
||||||
|
void RecordNoColor(ScratchFramebuffer& fb) {
|
||||||
|
fb.colorTex = 0;
|
||||||
|
fb.colorTarget = 0;
|
||||||
|
fb.colorLevel = 0;
|
||||||
|
fb.colorLayer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecordNoDepth(ScratchFramebuffer& fb) {
|
||||||
|
fb.depthTex = 0;
|
||||||
|
fb.depthTarget = 0;
|
||||||
|
fb.depthLevel = 0;
|
||||||
|
fb.depthHasStencil = false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScratchFramebuffer& TempFramebuffer() {
|
||||||
|
return g_tempFramebuffer;
|
||||||
|
}
|
||||||
|
ScratchFramebuffer& BlitReadFramebuffer() {
|
||||||
|
return g_blitReadFramebuffer;
|
||||||
|
}
|
||||||
|
ScratchFramebuffer& BlitDrawFramebuffer() {
|
||||||
|
return g_blitDrawFramebuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint EnsureId(ScratchFramebuffer& fb) {
|
||||||
|
if (fb.id == 0) {
|
||||||
|
g_GLESFuncs.glGenFramebuffers(1, &fb.id);
|
||||||
|
// A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw
|
||||||
|
// buffers (the ES defaults for a non-default framebuffer).
|
||||||
|
fb.attachmentsKnown = true;
|
||||||
|
RecordNoColor(fb);
|
||||||
|
RecordNoDepth(fb);
|
||||||
|
fb.readBuffer = GL_COLOR_ATTACHMENT0;
|
||||||
|
fb.drawBuffer = GL_COLOR_ATTACHMENT0;
|
||||||
|
}
|
||||||
|
return fb.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget,
|
||||||
|
GLint level) {
|
||||||
|
PrepareForUse(fb, fbTarget);
|
||||||
|
if (fb.depthTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
RecordNoDepth(fb);
|
||||||
|
}
|
||||||
|
if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fb.colorTex != 0) {
|
||||||
|
// Detach first: if the new attach fails, the point must read as
|
||||||
|
// missing (incomplete FBO), not silently keep the old texture.
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
}
|
||||||
|
DrainPendingGLErrors();
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level);
|
||||||
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
RecordNoColor(fb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fb.colorTex = tex;
|
||||||
|
fb.colorTarget = texTarget;
|
||||||
|
fb.colorLevel = level;
|
||||||
|
fb.colorLayer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
|
||||||
|
PrepareForUse(fb, fbTarget);
|
||||||
|
if (fb.depthTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
RecordNoDepth(fb);
|
||||||
|
}
|
||||||
|
if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fb.colorTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
}
|
||||||
|
DrainPendingGLErrors();
|
||||||
|
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer);
|
||||||
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
RecordNoColor(fb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fb.colorTex = tex;
|
||||||
|
fb.colorTarget = 0;
|
||||||
|
fb.colorLevel = level;
|
||||||
|
fb.colorLayer = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
||||||
|
Bool withStencil) {
|
||||||
|
PrepareForUse(fb, fbTarget);
|
||||||
|
if (fb.colorTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
RecordNoColor(fb);
|
||||||
|
}
|
||||||
|
if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level &&
|
||||||
|
fb.depthHasStencil == withStencil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fb.depthTex != 0) {
|
||||||
|
// One call clears both depth and stencil points regardless of how
|
||||||
|
// the previous attachment was made.
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
}
|
||||||
|
DrainPendingGLErrors();
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget,
|
||||||
|
withStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT,
|
||||||
|
texTarget, tex, level);
|
||||||
|
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
RecordNoDepth(fb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fb.depthTex = tex;
|
||||||
|
fb.depthTarget = texTarget;
|
||||||
|
fb.depthLevel = level;
|
||||||
|
fb.depthHasStencil = withStencil;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||||
|
PrepareForUse(fb, fbTarget);
|
||||||
|
if (fb.colorTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
RecordNoColor(fb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
|
||||||
|
PrepareForUse(fb, fbTarget);
|
||||||
|
if (fb.depthTex != 0) {
|
||||||
|
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
RecordNoDepth(fb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) {
|
||||||
|
if (fb.readBuffer == readBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glReadBuffer(readBuffer);
|
||||||
|
fb.readBuffer = readBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) {
|
||||||
|
if (fb.drawBuffer == drawBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
||||||
|
fb.drawBuffer = drawBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint EnsureCompleteTinyFramebufferId() {
|
||||||
|
if (g_completeTinyFBOId != 0) {
|
||||||
|
return g_completeTinyFBOId;
|
||||||
|
}
|
||||||
|
// One-time creation: the renderbuffer binding is context state with no
|
||||||
|
// shadow, so save/restore it by query here (cold path only).
|
||||||
|
GLint prevRenderbuffer = 0;
|
||||||
|
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||||
|
g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId);
|
||||||
|
g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId);
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId);
|
||||||
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId);
|
||||||
|
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
|
||||||
|
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
|
||||||
|
g_completeTinyRBOId);
|
||||||
|
const GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
|
||||||
|
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
|
||||||
|
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
|
||||||
|
"Scratch 1x1 framebuffer is incomplete.");
|
||||||
|
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<Uint>(prevRenderbuffer));
|
||||||
|
return g_completeTinyFBOId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoteTextureIdDeleted(Uint textureId) {
|
||||||
|
if (textureId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) {
|
||||||
|
if (fb->colorTex == textureId || fb->depthTex == textureId) {
|
||||||
|
fb->attachmentsKnown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnBackendContextDestroyed() {
|
||||||
|
g_tempFramebuffer = {};
|
||||||
|
g_blitReadFramebuffer = {};
|
||||||
|
g_blitDrawFramebuffer = {};
|
||||||
|
g_completeTinyFBOId = 0;
|
||||||
|
g_completeTinyRBOId = 0;
|
||||||
|
}
|
||||||
|
} // namespace ScratchFBOImpl
|
||||||
|
|
||||||
|
namespace PixelStoreImpl {
|
||||||
|
namespace {
|
||||||
|
PackState g_packState;
|
||||||
|
Bool g_packStateKnown = false;
|
||||||
|
|
||||||
|
void PinPackState(const PackState& value) {
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment);
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength);
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows);
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels);
|
||||||
|
g_packState = value;
|
||||||
|
g_packStateKnown = true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ApplyPackState(const PackState& desired) {
|
||||||
|
if (!g_packStateKnown) {
|
||||||
|
PinPackState(desired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (desired.Alignment != g_packState.Alignment) {
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment);
|
||||||
|
g_packState.Alignment = desired.Alignment;
|
||||||
|
}
|
||||||
|
if (desired.RowLength != g_packState.RowLength) {
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength);
|
||||||
|
g_packState.RowLength = desired.RowLength;
|
||||||
|
}
|
||||||
|
if (desired.SkipRows != g_packState.SkipRows) {
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows);
|
||||||
|
g_packState.SkipRows = desired.SkipRows;
|
||||||
|
}
|
||||||
|
if (desired.SkipPixels != g_packState.SkipPixels) {
|
||||||
|
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels);
|
||||||
|
g_packState.SkipPixels = desired.SkipPixels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PackState CurrentPackState() {
|
||||||
|
if (!g_packStateKnown) {
|
||||||
|
// Fresh/unknown context: pin to the GL defaults (what a new context
|
||||||
|
// starts with; writing them makes the shadow authoritative either way).
|
||||||
|
PinPackState(PackState{});
|
||||||
|
}
|
||||||
|
return g_packState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvalidatePackStateCache() {
|
||||||
|
g_packStateKnown = false;
|
||||||
|
}
|
||||||
|
} // namespace PixelStoreImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
Uint32 g_snormFallbackClampOutputMask = 0;
|
Uint32 g_snormFallbackClampOutputMask = 0;
|
||||||
Uint32 g_unormFallbackClampOutputMask = 0;
|
Uint32 g_unormFallbackClampOutputMask = 0;
|
||||||
@@ -2784,6 +3301,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &uboPrecisionSpirv;
|
effectiveSpirv = &uboPrecisionSpirv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
||||||
|
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
||||||
|
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
||||||
|
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
||||||
|
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
||||||
|
// the decoration - exact, extension-free. Devices that have the extension keep the
|
||||||
|
// decoration and let the hardware do it natively.
|
||||||
|
Vector<unsigned int> noperspectiveSpirv;
|
||||||
|
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||||
|
*effectiveSpirv, noperspectiveSpirv) &&
|
||||||
|
!noperspectiveSpirv.empty()) {
|
||||||
|
effectiveSpirv = &noperspectiveSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
|
||||||
void BindBufferId(GLenum target, Uint id);
|
void BindBufferId(GLenum target, Uint id);
|
||||||
void InvalidateArrayBufferBindingCache();
|
void InvalidateArrayBufferBindingCache();
|
||||||
|
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
|
||||||
|
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
|
||||||
|
// upload site routes its binding through these so the shadow always matches
|
||||||
|
// the driver; the resting state between operations is 0, which keeps any
|
||||||
|
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
|
||||||
|
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
|
||||||
|
// and a recycled name matching the shadow would false-skip the rebind) and
|
||||||
|
// invalidated on MakeCurrent (context may reset).
|
||||||
|
void BindPixelPackBufferId(Uint id);
|
||||||
|
void BindPixelUnpackBufferId(Uint id);
|
||||||
|
void InvalidatePixelBufferBindingCaches();
|
||||||
|
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
|
||||||
|
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||||
|
// could false-skip when the name is recycled.
|
||||||
|
void NoteBufferIdDeleted(Uint id);
|
||||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||||
@@ -332,6 +347,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendTextureObject {
|
class BackendTextureObject {
|
||||||
public:
|
public:
|
||||||
BackendTextureObject();
|
BackendTextureObject();
|
||||||
|
// Deletes the GL texture (frontend glDeleteTextures used to leak every
|
||||||
|
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
|
||||||
|
// shadows so a recycled name or heap address cannot false-skip a rebind.
|
||||||
|
~BackendTextureObject();
|
||||||
|
BackendTextureObject(const BackendTextureObject&) = delete;
|
||||||
|
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
|
||||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
@@ -343,6 +364,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void RecreateBackendTexture();
|
void RecreateBackendTexture();
|
||||||
|
|
||||||
Uint m_backendTextureId = 0;
|
Uint m_backendTextureId = 0;
|
||||||
|
// ES context generation the id was created under; a dtor running after
|
||||||
|
// that context died must not delete a foreign (recycled) name.
|
||||||
|
Uint m_contextGeneration = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Bool m_imageBindableStorageRequired = false;
|
Bool m_imageBindableStorageRequired = false;
|
||||||
Bool m_backendStorageImmutable = false;
|
Bool m_backendStorageImmutable = false;
|
||||||
@@ -367,6 +391,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
extern Uint g_activeTextureUnit;
|
extern Uint g_activeTextureUnit;
|
||||||
|
// Bumped when the backend ES context is destroyed; texture ids stamped with
|
||||||
|
// an older generation belong to a dead context and must not be deleted.
|
||||||
|
extern Uint g_textureContextGeneration;
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
@@ -375,6 +402,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BackendFramebufferObject();
|
BackendFramebufferObject();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||||
FramebufferTarget asTarget);
|
FramebufferTarget asTarget);
|
||||||
|
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||||
|
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
|
||||||
|
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
|
||||||
|
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
|
||||||
void InvalidateSyncedState();
|
void InvalidateSyncedState();
|
||||||
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
|
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
|
||||||
void Bind(FramebufferTarget target) const;
|
void Bind(FramebufferTarget target) const;
|
||||||
@@ -414,8 +445,99 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||||
g_fboSyncedObjects;
|
g_fboSyncedObjects;
|
||||||
|
|
||||||
|
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||||
|
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||||
|
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||||
|
// query forces a driver pipeline sync) and so redundant rebinds no-op.
|
||||||
|
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
|
||||||
|
// the driver once. Invalidated on MakeCurrent (context may reset).
|
||||||
|
// GL_FRAMEBUFFER binds both targets.
|
||||||
|
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||||
|
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||||
|
void InvalidateFramebufferBindingCache();
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
|
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||||
|
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
|
||||||
|
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
|
||||||
|
// texture) is detached exactly when a later use of another aspect would
|
||||||
|
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
|
||||||
|
// incomplete and silently degraded later readbacks).
|
||||||
|
namespace ScratchFBOImpl {
|
||||||
|
struct ScratchFramebuffer {
|
||||||
|
Uint id = 0;
|
||||||
|
// false => attachment state unknown; scrub every point on next use.
|
||||||
|
// A fresh FBO starts with nothing attached, so creation sets it true.
|
||||||
|
Bool attachmentsKnown = false;
|
||||||
|
Uint colorTex = 0;
|
||||||
|
GLenum colorTarget = 0;
|
||||||
|
GLint colorLevel = 0;
|
||||||
|
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
|
||||||
|
Uint depthTex = 0;
|
||||||
|
GLenum depthTarget = 0;
|
||||||
|
GLint depthLevel = 0;
|
||||||
|
Bool depthHasStencil = false;
|
||||||
|
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
|
||||||
|
GLenum readBuffer = 0;
|
||||||
|
GLenum drawBuffer = 0;
|
||||||
|
};
|
||||||
|
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
|
||||||
|
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
|
||||||
|
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
|
||||||
|
// Returns the GL id, generating it if needed (requires a current ES context).
|
||||||
|
Uint EnsureId(ScratchFramebuffer& fb);
|
||||||
|
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
|
||||||
|
// target the READ/DRAW binding respectively). Each Ensure* performs the
|
||||||
|
// minimal detach/attach set and keeps the shadow in sync; a failed attach
|
||||||
|
// records the point as detached so the completeness check fails instead of
|
||||||
|
// silently reading a stale attachment.
|
||||||
|
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
|
||||||
|
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
|
||||||
|
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
|
||||||
|
Bool withStencil);
|
||||||
|
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||||
|
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
|
||||||
|
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
|
||||||
|
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
|
||||||
|
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
|
||||||
|
// binding while respecifying texture storage). Attachment is set once at
|
||||||
|
// creation and never changes.
|
||||||
|
Uint EnsureCompleteTinyFramebufferId();
|
||||||
|
// A backend texture id is being deleted or respecified: a scratch FBO still
|
||||||
|
// referencing it would hold a dangling attachment (ES only auto-detaches
|
||||||
|
// from the *bound* framebuffer), and a recycled name could false-skip a
|
||||||
|
// re-attach; force a full scrub on next use.
|
||||||
|
void NoteTextureIdDeleted(Uint textureId);
|
||||||
|
// The ES context (and the scratch FBO ids with it) is going away.
|
||||||
|
void OnBackendContextDestroyed();
|
||||||
|
} // namespace ScratchFBOImpl
|
||||||
|
|
||||||
|
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
|
||||||
|
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
|
||||||
|
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
|
||||||
|
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
|
||||||
|
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
|
||||||
|
// pins the driver to the shadow by writing all fields once. Invalidated on
|
||||||
|
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
|
||||||
|
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
|
||||||
|
// frontend context state instead.
|
||||||
|
namespace PixelStoreImpl {
|
||||||
|
struct PackState {
|
||||||
|
GLint Alignment = 4;
|
||||||
|
GLint RowLength = 0;
|
||||||
|
GLint SkipRows = 0;
|
||||||
|
GLint SkipPixels = 0;
|
||||||
|
Bool operator==(const PackState& o) const {
|
||||||
|
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
|
||||||
|
SkipPixels == o.SkipPixels;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void ApplyPackState(const PackState& desired);
|
||||||
|
PackState CurrentPackState();
|
||||||
|
void InvalidatePackStateCache();
|
||||||
|
} // namespace PixelStoreImpl
|
||||||
|
|
||||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||||
// assigned through glUniform1i.
|
// assigned through glUniform1i.
|
||||||
|
|||||||
@@ -764,5 +764,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
|
||||||
|
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
|
||||||
|
return (rowBytes + align - 1) / align * align;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||||
|
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||||
|
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||||
|
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||||
|
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||||
|
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||||
|
// Per the GL addressing rules, slice k row j lands at
|
||||||
|
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||||
|
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||||
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
|
void* pixels, Bool applyPackImageParams) {
|
||||||
|
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||||
|
if (dstPixelBytes == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PackedReadbackLayout packedLayout{};
|
||||||
|
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||||
|
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||||
|
|
||||||
|
const auto& pixelPackBufferObject =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||||
|
|
||||||
|
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||||
|
// rows are written so skip regions of the destination stay untouched.
|
||||||
|
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||||
|
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||||
|
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||||
|
const SizeT imageRows =
|
||||||
|
applyPackImageParams && packParams.ImageHeight > 0
|
||||||
|
? static_cast<SizeT>(packParams.ImageHeight)
|
||||||
|
: static_cast<SizeT>(sliceHeight);
|
||||||
|
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||||
|
const SizeT skipImages =
|
||||||
|
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||||
|
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||||
|
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||||
|
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||||
|
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||||
|
|
||||||
|
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||||
|
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||||
|
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||||
|
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||||
|
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||||
|
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||||
|
Vector<Uint8> convertedRow(dstRowBytes);
|
||||||
|
|
||||||
|
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||||
|
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||||
|
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||||
|
static_cast<SizeT>(row);
|
||||||
|
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||||
|
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||||
|
mapping, type);
|
||||||
|
|
||||||
|
if (packParams.SwapBytes) {
|
||||||
|
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||||
|
if (groupSize > 1) {
|
||||||
|
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||||
|
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||||
|
static_cast<SizeT>(row) * dstRowStride;
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||||
|
pboBaseOffset + dstOffset);
|
||||||
|
} else {
|
||||||
|
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
||||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||||
const ReadbackChannelMapping& mapping, GLenum type);
|
const ReadbackChannelMapping& mapping, GLenum type);
|
||||||
|
|
||||||
|
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
|
||||||
|
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
|
||||||
|
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
|
||||||
|
// DirectGLES and DirectVulkan readback conversion paths.
|
||||||
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
|
void* pixels, Bool applyPackImageParams);
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
|
|||||||
@@ -140,6 +140,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case TextureInternalFormat::RGB:
|
case TextureInternalFormat::RGB:
|
||||||
case TextureInternalFormat::RGB8:
|
case TextureInternalFormat::RGB8:
|
||||||
return TextureInternalFormat::RGBA8;
|
return TextureInternalFormat::RGBA8;
|
||||||
|
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
|
||||||
|
// encoding; a wider normalized fallback keeps at least the required precision.
|
||||||
|
case TextureInternalFormat::R3G3B2:
|
||||||
|
case TextureInternalFormat::RGB4:
|
||||||
|
case TextureInternalFormat::RGB5:
|
||||||
|
case TextureInternalFormat::RGBA2:
|
||||||
|
case TextureInternalFormat::RGBA4:
|
||||||
|
case TextureInternalFormat::RGB5A1:
|
||||||
|
return TextureInternalFormat::RGBA8;
|
||||||
|
case TextureInternalFormat::RGB10:
|
||||||
|
return TextureInternalFormat::RGB10A2;
|
||||||
|
case TextureInternalFormat::RGB12:
|
||||||
|
case TextureInternalFormat::RGBA12:
|
||||||
|
return TextureInternalFormat::RGBA16;
|
||||||
case TextureInternalFormat::SRGB8:
|
case TextureInternalFormat::SRGB8:
|
||||||
return TextureInternalFormat::SRGB8Alpha8;
|
return TextureInternalFormat::SRGB8Alpha8;
|
||||||
case TextureInternalFormat::RGB8Snorm:
|
case TextureInternalFormat::RGB8Snorm:
|
||||||
@@ -504,7 +518,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||||
E_GL_ARB_direct_state_access,
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
|
||||||
@@ -746,9 +760,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||||
m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
|
m_dynamicParameters.MaxImageUnits =
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
|
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||||
|
const Int maxPerStageImageUniforms =
|
||||||
|
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
||||||
|
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
|
||||||
|
// optional device features. VulkanRenderer enables each feature whenever the physical
|
||||||
|
// device reports it, so these are the exact limits the logical device can compile and run.
|
||||||
|
m_dynamicParameters.MaxVertexImageUniforms =
|
||||||
|
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||||
|
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||||
|
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
|
||||||
|
? maxPerStageImageUniforms
|
||||||
|
: 0;
|
||||||
|
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||||
|
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||||
|
m_dynamicParameters.MaxComputeImageUniforms =
|
||||||
|
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
||||||
const Int maxSupportedDrawBuffers =
|
const Int maxSupportedDrawBuffers =
|
||||||
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||||
@@ -779,5 +808,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_vulkanCaps.MaxShaderStorageBlockSize,
|
m_vulkanCaps.MaxShaderStorageBlockSize,
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize);
|
m_dynamicParameters.MaxShaderStorageBlockSize);
|
||||||
}
|
}
|
||||||
|
switch (m_vulkanCaps.VendorId) {
|
||||||
|
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
|
||||||
|
break;
|
||||||
|
case 0x13B5u: // ARM
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
|
||||||
|
break;
|
||||||
|
case 0x10DEu: // NVIDIA
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
|
||||||
|
break;
|
||||||
|
case 0x1002u: // AMD
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
|
||||||
|
break;
|
||||||
|
case 0x8086u: // Intel
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
|
||||||
|
break;
|
||||||
|
case 0x1010u: // Imagination
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
||||||
|
break;
|
||||||
|
case 0x10005u: // Mesa software (lavapipe)
|
||||||
|
case 0x1AE0u: // Google (SwiftShader)
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "PipelineFactory.h"
|
#include "PipelineFactory.h"
|
||||||
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||||
switch (topology) {
|
switch (topology) {
|
||||||
@@ -108,6 +109,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"vkCreatePipelineCache");
|
"vkCreatePipelineCache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must be called once, before any pipeline is created: the flag is not part of the
|
||||||
|
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
|
||||||
|
// old value.
|
||||||
|
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
|
||||||
|
s_suppressBlendedDepthWrite = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
|
||||||
|
Uint32 vendorId) {
|
||||||
|
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||||
|
switch (quirkOverride) {
|
||||||
|
case MG_Config::QuirkOverride::ForceOn:
|
||||||
|
return true;
|
||||||
|
case MG_Config::QuirkOverride::ForceOff:
|
||||||
|
return false;
|
||||||
|
case MG_Config::QuirkOverride::Auto:
|
||||||
|
default:
|
||||||
|
return vendorId == kVendorIdQualcomm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
|
||||||
|
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
|
||||||
|
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
|
||||||
|
//
|
||||||
|
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
|
||||||
|
// content as possible, and a trace sweep of every fixture showed the wider
|
||||||
|
// alternatives all cost more than they fix:
|
||||||
|
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
|
||||||
|
// (its transmittance/accumulate passes disable depth writes themselves) - the
|
||||||
|
// only real content it caught was harmless additive glow effects (Create);
|
||||||
|
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
|
||||||
|
// drawn once per surface, and rely on their depth writes for occlusion;
|
||||||
|
// - separate-alpha accumulation over an over-blending color channel has no
|
||||||
|
// known pairing with a depth-equality chain (color channel only, see tests).
|
||||||
|
// If a future workload pairs another blend shape with an equality chain, widen
|
||||||
|
// this with that evidence in hand rather than pre-emptively.
|
||||||
|
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
|
||||||
|
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
|
||||||
|
attachment.colorBlendOp == VK_BLEND_OP_MAX;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
|
||||||
|
if (!payload.depthWriteEnable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
|
||||||
|
// pipeline's interpolated Z, so a driver that varies the vertex position math
|
||||||
|
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
|
||||||
|
// passthrough is the exception that stays exposed; no known content pairs one with
|
||||||
|
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
|
||||||
|
if (payload.fragmentReplacesDepth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
|
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
|
||||||
|
if (attachment.blendEnable != VK_TRUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// All color writes masked: blending is moot (depth-prepass pattern that left
|
||||||
|
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
|
||||||
|
if (attachment.colorWriteMask == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
|
||||||
|
// accumulates into a 2-target MRT and must stay stripped.
|
||||||
|
if (IsAccumulationBlend(attachment)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PipelineFactory::~PipelineFactory() {
|
PipelineFactory::~PipelineFactory() {
|
||||||
DestroyAll();
|
DestroyAll();
|
||||||
if (m_pipelineCache != VK_NULL_HANDLE) {
|
if (m_pipelineCache != VK_NULL_HANDLE) {
|
||||||
@@ -152,6 +228,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||||
|
XXHASH_VERIFY(
|
||||||
|
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||||
if (payload.colorAttachmentCount > 0) {
|
if (payload.colorAttachmentCount > 0) {
|
||||||
XXHASH_VERIFY(XXH64_update(
|
XXHASH_VERIFY(XXH64_update(
|
||||||
m_hashState,
|
m_hashState,
|
||||||
@@ -258,6 +336,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
colorAttachments[i] = payload.colorBlendAttachments[i];
|
colorAttachments[i] = payload.colorBlendAttachments[i];
|
||||||
}
|
}
|
||||||
|
// Suppress depth writes on accumulation-blended pipelines when the active driver
|
||||||
|
// cannot keep vertex positions invariant across the pipelines of a multi-pass
|
||||||
|
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
|
||||||
|
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
|
||||||
|
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
|
||||||
|
// This bakes the decision into the pipeline, which only works because depth write is
|
||||||
|
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
|
||||||
|
// would let the record-time value override it and silently disable the quirk.
|
||||||
|
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
|
||||||
|
depthStencil.depthWriteEnable = VK_FALSE;
|
||||||
|
}
|
||||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||||
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
||||||
blend.logicOp = payload.logicOp;
|
blend.logicOp = payload.logicOp;
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
|
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
|
||||||
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
|
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
|
||||||
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
|
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||||
|
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
|
||||||
|
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
|
||||||
|
Bool fragmentReplacesDepth = false;
|
||||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||||
@@ -62,6 +65,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||||
void DestroyAll();
|
void DestroyAll();
|
||||||
|
|
||||||
|
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||||
|
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||||
|
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||||
|
// cross-pipeline position invariance that some mobile compilers do not provide, even
|
||||||
|
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
|
||||||
|
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
|
||||||
|
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
|
||||||
|
// only depth-writing shape the chain actually uses - so every other blend
|
||||||
|
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
|
||||||
|
// its depth writes. Set at renderer initialization based on the active driver.
|
||||||
|
static void SetSuppressBlendedDepthWrite(Bool enabled);
|
||||||
|
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
|
||||||
|
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
|
||||||
|
// the known-affected vendor (Qualcomm).
|
||||||
|
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
|
||||||
|
Uint32 vendorId);
|
||||||
|
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
|
||||||
|
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
|
||||||
|
// and payload-only so tests can pin the contract without a VkDevice.
|
||||||
|
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||||
|
|
||||||
@@ -70,5 +94,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
|
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return targetEnv;
|
return targetEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cheap raw-word scan for an `OpDecorate <id> BuiltIn InstanceIndex` decoration. Used
|
|
||||||
// only to decide whether to warn when shaderDrawParameters is unavailable; a false
|
|
||||||
// negative merely suppresses a diagnostic.
|
|
||||||
Bool SpirvDeclaresInstanceIndexBuiltin(const Vector<Uint>& spirv) {
|
|
||||||
constexpr Uint32 kSpirvMagicNumber = 0x07230203u;
|
|
||||||
constexpr SizeT kHeaderWordCount = 5;
|
|
||||||
if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SizeT wordIndex = kHeaderWordCount;
|
|
||||||
while (wordIndex < spirv.size()) {
|
|
||||||
const Uint32 firstWord = spirv[wordIndex];
|
|
||||||
const Uint32 wordCount = firstWord >> 16;
|
|
||||||
const auto opcode = static_cast<spv::Op>(firstWord & 0xffffu);
|
|
||||||
if (wordCount == 0 || wordIndex + wordCount > spirv.size()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (opcode == spv::Op::OpDecorate && wordCount >= 4 &&
|
|
||||||
static_cast<spv::Decoration>(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn &&
|
|
||||||
static_cast<spv::BuiltIn>(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
wordIndex += wordCount;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
|
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
|
||||||
if (spirv.empty() || spirvId == 0) {
|
if (spirv.empty() || spirvId == 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1122,9 +1094,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto* binding : bindings) {
|
for (auto* binding : bindings) {
|
||||||
MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record");
|
MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record");
|
||||||
const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type);
|
const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type);
|
||||||
MOBILEGL_ASSERT(binding->count == 1,
|
// UBO instance arrays (uniform Block {...} b[N];) occupy one binding with
|
||||||
"ProgramFactory: descriptor arrays are unsupported (name='%s' count=%u)",
|
// descriptorCount = N; other descriptor arrays stay unsupported and must
|
||||||
binding->name ? binding->name : "<null>", binding->count);
|
// fail program creation cleanly rather than continue with corrupt state.
|
||||||
|
if (binding->count != 1 && kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
|
||||||
|
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||||
|
"kind (name='%s' count=%u type=%d)",
|
||||||
|
binding->name ? binding->name : "<null>", binding->count,
|
||||||
|
static_cast<Int>(binding->descriptor_type));
|
||||||
|
destroyReflectModules();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
DescriptorKey key{};
|
DescriptorKey key{};
|
||||||
key.kind = kind;
|
key.kind = kind;
|
||||||
@@ -1197,8 +1177,62 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return TextureTarget::Unknown;
|
return TextureTarget::Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
|
||||||
|
switch (uniformType) {
|
||||||
|
case GL_IMAGE_1D:
|
||||||
|
case GL_IMAGE_2D:
|
||||||
|
case GL_IMAGE_3D:
|
||||||
|
case GL_IMAGE_2D_RECT:
|
||||||
|
case GL_IMAGE_CUBE:
|
||||||
|
case GL_IMAGE_BUFFER:
|
||||||
|
case GL_IMAGE_1D_ARRAY:
|
||||||
|
case GL_IMAGE_2D_ARRAY:
|
||||||
|
case GL_IMAGE_CUBE_MAP_ARRAY:
|
||||||
|
case GL_IMAGE_2D_MULTISAMPLE:
|
||||||
|
case GL_IMAGE_2D_MULTISAMPLE_ARRAY:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
|
||||||
|
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
|
||||||
|
// position math between pipelines cannot desynchronize it; the blended depth-write
|
||||||
|
// quirk therefore leaves it alone (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
|
Bool ProgramFactory::ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule) {
|
||||||
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
|
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||||
|
for (Uint32 modeIndex = 0; modeIndex < entryPoint.execution_mode_count; ++modeIndex) {
|
||||||
|
if (entryPoint.execution_modes[modeIndex] == SpvExecutionModeDepthReplacing) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glslang's relaxed-Vulkan mode maps GL's gl_InstanceID onto the InstanceIndex builtin.
|
||||||
|
// Without shaderDrawParameters there is no gl_BaseInstance to subtract, so such a shader
|
||||||
|
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
|
||||||
|
// detects the case so the user gets one warning instead of silent corruption.
|
||||||
|
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||||
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
|
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||||
|
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
||||||
|
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
||||||
|
if (variable != nullptr &&
|
||||||
|
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||||
|
variable->built_in == SpvBuiltInInstanceIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case ShaderStage::Vertex:
|
case ShaderStage::Vertex:
|
||||||
@@ -1218,6 +1252,105 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED;
|
||||||
|
case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||||
|
case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT;
|
||||||
|
case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM;
|
||||||
|
case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT;
|
||||||
|
case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT;
|
||||||
|
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||||
|
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||||
|
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||||
|
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||||
|
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||||
|
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||||
|
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||||
|
case SpvImageFormatR8: return VK_FORMAT_R8_UNORM;
|
||||||
|
case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM;
|
||||||
|
case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM;
|
||||||
|
case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM;
|
||||||
|
case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM;
|
||||||
|
case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM;
|
||||||
|
case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT;
|
||||||
|
case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT;
|
||||||
|
case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT;
|
||||||
|
case SpvImageFormatR32i: return VK_FORMAT_R32_SINT;
|
||||||
|
case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT;
|
||||||
|
case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT;
|
||||||
|
case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT;
|
||||||
|
case SpvImageFormatR16i: return VK_FORMAT_R16_SINT;
|
||||||
|
case SpvImageFormatR8i: return VK_FORMAT_R8_SINT;
|
||||||
|
case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT;
|
||||||
|
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||||
|
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||||
|
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||||
|
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||||
|
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||||
|
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||||
|
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||||
|
case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT;
|
||||||
|
case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT;
|
||||||
|
case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT;
|
||||||
|
case SpvImageFormatR64i: return VK_FORMAT_R64_SINT;
|
||||||
|
case SpvImageFormatMax: return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) {
|
||||||
|
switch (glType) {
|
||||||
|
case GL_INT_SAMPLER_1D:
|
||||||
|
case GL_INT_SAMPLER_2D:
|
||||||
|
case GL_INT_SAMPLER_3D:
|
||||||
|
case GL_INT_SAMPLER_CUBE:
|
||||||
|
case GL_INT_SAMPLER_2D_RECT:
|
||||||
|
case GL_INT_SAMPLER_1D_ARRAY:
|
||||||
|
case GL_INT_SAMPLER_2D_ARRAY:
|
||||||
|
case GL_INT_SAMPLER_BUFFER:
|
||||||
|
case GL_INT_SAMPLER_2D_MULTISAMPLE:
|
||||||
|
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||||
|
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||||
|
return SamplerNumericDomain::SignedInteger;
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_1D:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_3D:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_CUBE:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||||
|
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
|
||||||
|
return SamplerNumericDomain::UnsignedInteger;
|
||||||
|
case GL_SAMPLER_1D:
|
||||||
|
case GL_SAMPLER_2D:
|
||||||
|
case GL_SAMPLER_3D:
|
||||||
|
case GL_SAMPLER_CUBE:
|
||||||
|
case GL_SAMPLER_2D_RECT:
|
||||||
|
case GL_SAMPLER_1D_ARRAY:
|
||||||
|
case GL_SAMPLER_2D_ARRAY:
|
||||||
|
case GL_SAMPLER_BUFFER:
|
||||||
|
case GL_SAMPLER_2D_MULTISAMPLE:
|
||||||
|
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
|
||||||
|
case GL_SAMPLER_CUBE_MAP_ARRAY:
|
||||||
|
case GL_SAMPLER_1D_SHADOW:
|
||||||
|
case GL_SAMPLER_2D_SHADOW:
|
||||||
|
case GL_SAMPLER_CUBE_SHADOW:
|
||||||
|
case GL_SAMPLER_2D_RECT_SHADOW:
|
||||||
|
case GL_SAMPLER_1D_ARRAY_SHADOW:
|
||||||
|
case GL_SAMPLER_2D_ARRAY_SHADOW:
|
||||||
|
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
|
||||||
|
return SamplerNumericDomain::Float;
|
||||||
|
default:
|
||||||
|
return SamplerNumericDomain::Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||||
CompileOptionFlags flags) const {
|
CompileOptionFlags flags) const {
|
||||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||||
@@ -1347,6 +1480,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
||||||
|
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||||
|
if (!s_warnedInstanceIndexUnsupported) {
|
||||||
|
s_warnedInstanceIndexUnsupported = true;
|
||||||
|
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
|
||||||
|
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t inputCount = 0;
|
uint32_t inputCount = 0;
|
||||||
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
||||||
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
||||||
@@ -1394,6 +1536,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeFragmentOutputLocationMask = 0;
|
entry.activeFragmentOutputLocationMask = 0;
|
||||||
entry.fragmentOutputTypes.fill(0);
|
entry.fragmentOutputTypes.fill(0);
|
||||||
|
entry.fragmentReplacesDepth = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
||||||
@@ -1412,9 +1555,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
|
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
|
||||||
static_cast<Int>(createResult));
|
static_cast<Int>(createResult));
|
||||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||||
|
// Fail toward the exemption: stripping a genuine gl_FragDepth writer would
|
||||||
|
// corrupt its depth output outright, while wrongly exempting an accumulation
|
||||||
|
// pass merely reverts that one program to the pre-quirk behavior.
|
||||||
|
entry.fragmentReplacesDepth = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule);
|
||||||
|
|
||||||
uint32_t outputCount = 0;
|
uint32_t outputCount = 0;
|
||||||
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
|
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
|
||||||
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
||||||
@@ -1465,10 +1614,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.samplerNameByBinding.assign(m_maxBindings, String());
|
entry.samplerNameByBinding.assign(m_maxBindings, String());
|
||||||
entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
|
entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
|
||||||
entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
|
entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
|
||||||
|
entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown);
|
||||||
|
entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED);
|
||||||
|
entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false);
|
||||||
entry.storageBlockNameByBinding.assign(m_maxBindings, String());
|
entry.storageBlockNameByBinding.assign(m_maxBindings, String());
|
||||||
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
|
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
|
||||||
entry.globalUboBinding = -1;
|
entry.globalUboBinding = -1;
|
||||||
entry.dynamicBindings.clear();
|
entry.dynamicBindings.clear();
|
||||||
|
entry.bindingDescriptorCounts.assign(m_maxBindings, 1);
|
||||||
|
entry.arrayedUniformBlockIndicesByBinding.clear();
|
||||||
|
|
||||||
// Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module
|
// Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module
|
||||||
for (const auto& module : spirv) {
|
for (const auto& module : spirv) {
|
||||||
@@ -1484,6 +1638,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)",
|
"ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)",
|
||||||
static_cast<Int>(createReflectResult));
|
static_cast<Int>(createReflectResult));
|
||||||
|
|
||||||
|
// Descriptor counts per binding (UBO instance arrays reflect count > 1).
|
||||||
|
UnorderedMap<Uint32, Uint32> descriptorCountByBinding;
|
||||||
|
{
|
||||||
|
uint32_t countProbe = 0;
|
||||||
|
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) ==
|
||||||
|
SPV_REFLECT_RESULT_SUCCESS &&
|
||||||
|
countProbe > 0) {
|
||||||
|
Vector<SpvReflectDescriptorBinding*> probeBindings(countProbe);
|
||||||
|
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe,
|
||||||
|
probeBindings.data()) ==
|
||||||
|
SPV_REFLECT_RESULT_SUCCESS) {
|
||||||
|
for (const auto* probeBinding : probeBindings) {
|
||||||
|
if (probeBinding != nullptr) {
|
||||||
|
descriptorCountByBinding[probeBinding->binding] =
|
||||||
|
std::max<Uint32>(1, probeBinding->count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reflect uniform buffers
|
// Reflect uniform buffers
|
||||||
auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER);
|
auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER);
|
||||||
for (const auto& ubo : ubos) {
|
for (const auto& ubo : ubos) {
|
||||||
@@ -1509,9 +1684,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
const auto countIt = descriptorCountByBinding.find(binding);
|
||||||
if (blockIndex == 0xFFFFFFFFu) {
|
const Uint32 descriptorCount =
|
||||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
|
countIt != descriptorCountByBinding.end() ? countIt->second : 1u;
|
||||||
|
|
||||||
|
if (descriptorCount <= 1) {
|
||||||
|
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
||||||
|
if (blockIndex == 0xFFFFFFFFu) {
|
||||||
|
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
|
||||||
|
ubo.name.c_str(), binding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None ||
|
||||||
|
entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic,
|
||||||
|
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
|
||||||
|
binding, ubo.name.c_str());
|
||||||
|
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
|
||||||
|
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
|
||||||
|
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
|
||||||
|
ubo.name.c_str(), binding);
|
||||||
|
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
|
||||||
|
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
|
||||||
|
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
|
||||||
|
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
|
||||||
|
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UBO instance array: one binding, descriptorCount elements. GL exposes each
|
||||||
|
// element as its own active block named "Name[i]"; map every element to its
|
||||||
|
// GL block index so the descriptor write can gather per-element buffer ranges.
|
||||||
|
if (descriptorCount > m_maxBindings) {
|
||||||
|
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; "
|
||||||
|
"leaving binding %u unmapped",
|
||||||
|
ubo.name.c_str(), descriptorCount, m_maxBindings, binding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Vector<Int> elementBlockIndices;
|
||||||
|
elementBlockIndices.reserve(descriptorCount);
|
||||||
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
|
String elementName = ubo.name + "[" + std::to_string(element) + "]";
|
||||||
|
Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str());
|
||||||
|
if (elementBlockIndex == 0xFFFFFFFFu && element == 0) {
|
||||||
|
// Some frontends report the first element under the bare block name.
|
||||||
|
elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
|
||||||
|
}
|
||||||
|
if (elementBlockIndex == 0xFFFFFFFFu) {
|
||||||
|
// Degrade rather than corrupt: reuse element 0's block if we have one,
|
||||||
|
// otherwise give up on the binding (same observable behavior as an
|
||||||
|
// inactive block: wrong values, but no crash).
|
||||||
|
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active "
|
||||||
|
"GL uniform block",
|
||||||
|
ubo.name.c_str(), element);
|
||||||
|
if (!elementBlockIndices.empty()) {
|
||||||
|
elementBlockIndex = static_cast<Uint>(elementBlockIndices.front());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elementBlockIndices.push_back(static_cast<Int>(elementBlockIndex));
|
||||||
|
}
|
||||||
|
if (elementBlockIndices.size() != descriptorCount) {
|
||||||
|
MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u",
|
||||||
ubo.name.c_str(), binding);
|
ubo.name.c_str(), binding);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1521,14 +1756,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
|
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
|
||||||
binding, ubo.name.c_str());
|
binding, ubo.name.c_str());
|
||||||
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
|
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
|
||||||
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
|
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(descriptorCount);
|
||||||
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
|
entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0];
|
||||||
ubo.name.c_str(), binding);
|
entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices);
|
||||||
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
|
|
||||||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
|
|
||||||
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
|
|
||||||
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
|
|
||||||
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs.
|
// Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs.
|
||||||
@@ -1592,10 +1822,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextureTarget target = UniformTypeToTextureTarget(program.GetUniformType(static_cast<Uint>(location)));
|
const GLenum uniformType = program.GetUniformType(static_cast<Uint>(location));
|
||||||
|
|
||||||
|
if (descriptorKind == DescriptorBindingKind::StorageImage) {
|
||||||
|
const VkFormat reflectedFormat =
|
||||||
|
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
|
||||||
|
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
|
||||||
|
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
reflectedFormat == VK_FORMAT_UNDEFINED ||
|
||||||
|
existingFormat == reflectedFormat,
|
||||||
|
"ProgramFactory::ReflectLayout: storage image binding %u ('%s') has "
|
||||||
|
"conflicting reflected formats (%d vs %d)",
|
||||||
|
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
|
||||||
|
static_cast<Int>(reflectedFormat));
|
||||||
|
if (existingFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
existingFormat = reflectedFormat;
|
||||||
|
}
|
||||||
|
if (m_unformattedFloatStorageImagesEnabled &&
|
||||||
|
existingFormat == VK_FORMAT_UNDEFINED &&
|
||||||
|
IsFloatStorageImageUniformType(uniformType)) {
|
||||||
|
entry.storageImageUsesBindingFormatByBinding[binding] = true;
|
||||||
|
} else if (reflectedFormat != VK_FORMAT_UNDEFINED) {
|
||||||
|
// A typed declaration in any stage wins for the entire binding. This is
|
||||||
|
// required when another stage reaches the same image through an atomic
|
||||||
|
// path and therefore could not be made formatless.
|
||||||
|
entry.storageImageUsesBindingFormatByBinding[binding] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
|
||||||
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
MOBILEGL_ASSERT(target != TextureTarget::Unknown,
|
||||||
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
||||||
uniformName.c_str());
|
uniformName.c_str());
|
||||||
|
if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) {
|
||||||
|
const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType);
|
||||||
|
MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown,
|
||||||
|
"ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain "
|
||||||
|
"for '%s' (uniformType=0x%x)",
|
||||||
|
uniformName.c_str(), uniformType);
|
||||||
|
MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] ==
|
||||||
|
SamplerNumericDomain::Unknown ||
|
||||||
|
entry.samplerNumericDomainByBinding[binding] == numericDomain,
|
||||||
|
"ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting "
|
||||||
|
"numeric domains (%d vs %d)",
|
||||||
|
binding, uniformName.c_str(),
|
||||||
|
static_cast<Int>(entry.samplerNumericDomainByBinding[binding]),
|
||||||
|
static_cast<Int>(numericDomain));
|
||||||
|
entry.samplerNumericDomainByBinding[binding] = numericDomain;
|
||||||
|
}
|
||||||
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
|
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
|
||||||
entry.samplerUniformLocationByBinding[binding] == location,
|
entry.samplerUniformLocationByBinding[binding] == location,
|
||||||
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
|
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
|
||||||
@@ -1631,7 +1905,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkDescriptorSetLayoutBinding layoutBinding{};
|
VkDescriptorSetLayoutBinding layoutBinding{};
|
||||||
layoutBinding.binding = binding;
|
layoutBinding.binding = binding;
|
||||||
layoutBinding.descriptorCount = 1;
|
layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding];
|
||||||
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL;
|
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL;
|
||||||
layoutBinding.pImmutableSamplers = nullptr;
|
layoutBinding.pImmutableSamplers = nullptr;
|
||||||
if (kind == DescriptorBindingKind::UniformBufferDynamic) {
|
if (kind == DescriptorBindingKind::UniformBufferDynamic) {
|
||||||
@@ -1643,6 +1917,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
} else if (kind == DescriptorBindingKind::StorageImage) {
|
} else if (kind == DescriptorBindingKind::StorageImage) {
|
||||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||||
|
entry.hasStorageImages = true;
|
||||||
} else {
|
} else {
|
||||||
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
}
|
}
|
||||||
@@ -1697,28 +1972,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
moduleSpirvs[i] = spv;
|
moduleSpirvs[i] = spv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||||
|
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||||
|
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||||
|
// per-pipeline compilers cannot vary the position math between passes.
|
||||||
|
{
|
||||||
|
Vector<Uint> invariantSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||||
|
moduleSpirvs[i], invariantSpirv)) {
|
||||||
|
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||||
|
} else {
|
||||||
|
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
||||||
|
// fails open and keeps the undecorated words - which silently reinstates
|
||||||
|
// the multi-pass invariance bug rather than breaking anything loudly.
|
||||||
|
MGLOG_E("ProgramFactory: position-invariant decoration failed for program %u; "
|
||||||
|
"keeping the original module - multi-pass depth-equality chains "
|
||||||
|
"(e.g. MC 26.3 OIT clouds) may drop primitives on this device",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
|
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
|
||||||
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
|
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
|
||||||
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
|
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
|
||||||
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
|
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
|
||||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
||||||
if (m_shaderDrawParametersEnabled) {
|
// the builtin but shaderDrawParameters is missing) rides along with
|
||||||
Vector<Uint> rebasedSpirv;
|
// ReflectVertexInputs, which already reflects this stage.
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||||
rebasedSpirv)) {
|
m_shaderDrawParametersEnabled) {
|
||||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
Vector<Uint> rebasedSpirv;
|
||||||
} else {
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
rebasedSpirv)) {
|
||||||
"instanced draws with a non-zero baseInstance may render incorrectly",
|
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||||
program.GetExternalIndex());
|
} else {
|
||||||
}
|
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||||
} else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) {
|
"instanced draws with a non-zero baseInstance may render incorrectly",
|
||||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
program.GetExternalIndex());
|
||||||
if (!s_warnedInstanceIndexUnsupported) {
|
}
|
||||||
s_warnedInstanceIndexUnsupported = true;
|
}
|
||||||
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
|
|
||||||
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
|
// 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
|
||||||
|
// float image qualifier can disagree with the bound render-target format. Integer
|
||||||
|
// storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the
|
||||||
|
// exact descriptor format required by their shader operations.
|
||||||
|
if (m_unformattedFloatStorageImagesEnabled) {
|
||||||
|
Vector<Uint> unformattedSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||||
|
moduleSpirvs[i], unformattedSpirv)) {
|
||||||
|
moduleSpirvs[i] = std::move(unformattedSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
||||||
|
program.GetExternalIndex());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,16 @@
|
|||||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
enum class SamplerNumericDomain : Uint8 {
|
||||||
|
Unknown = 0,
|
||||||
|
Float,
|
||||||
|
SignedInteger,
|
||||||
|
UnsignedInteger,
|
||||||
|
};
|
||||||
|
|
||||||
class ProgramFactory {
|
class ProgramFactory {
|
||||||
public:
|
public:
|
||||||
enum class DescriptorBindingKind : Uint8 {
|
enum class DescriptorBindingKind : Uint8 {
|
||||||
@@ -51,11 +59,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<DescriptorBindingKind> bindingKinds;
|
Vector<DescriptorBindingKind> bindingKinds;
|
||||||
Vector<Uint32> dynamicBindings;
|
Vector<Uint32> dynamicBindings;
|
||||||
Vector<Int> uniformBlockIndexByBinding;
|
Vector<Int> uniformBlockIndexByBinding;
|
||||||
|
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||||
|
// binding with descriptorCount = N).
|
||||||
|
Vector<Uint16> bindingDescriptorCounts;
|
||||||
|
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
||||||
|
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
||||||
|
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
|
||||||
Vector<String> samplerNameByBinding;
|
Vector<String> samplerNameByBinding;
|
||||||
Vector<Int> samplerUniformLocationByBinding;
|
Vector<Int> samplerUniformLocationByBinding;
|
||||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||||
|
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||||
|
Vector<VkFormat> storageImageFormatByBinding;
|
||||||
|
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||||
Vector<String> storageBlockNameByBinding;
|
Vector<String> storageBlockNameByBinding;
|
||||||
Vector<Int> storageBlockIndexByBinding;
|
Vector<Int> storageBlockIndexByBinding;
|
||||||
|
// Set once during ReflectLayout so the per-draw path can skip the whole
|
||||||
|
// storage-image preparation for the overwhelming majority of programs.
|
||||||
|
Bool hasStorageImages = false;
|
||||||
Int globalUboBinding = -1;
|
Int globalUboBinding = -1;
|
||||||
Uint32 activeVertexInputLocationMask = 0;
|
Uint32 activeVertexInputLocationMask = 0;
|
||||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||||
@@ -64,6 +84,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
Uint32 producerOutputComponentCount = 0;
|
Uint32 producerOutputComponentCount = 0;
|
||||||
Uint32 fragmentInputComponentCount = 0;
|
Uint32 fragmentInputComponentCount = 0;
|
||||||
|
// The fragment module declares the DepthReplacing execution mode (writes
|
||||||
|
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||||
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
|
Bool fragmentReplacesDepth = false;
|
||||||
|
|
||||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -79,11 +103,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
|
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
|
||||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||||
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
||||||
|
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
|
||||||
|
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
|
||||||
|
storageImageUsesBindingFormatByBinding =
|
||||||
|
std::move(other.storageImageUsesBindingFormatByBinding);
|
||||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||||
|
hasStorageImages = other.hasStorageImages;
|
||||||
globalUboBinding = other.globalUboBinding;
|
globalUboBinding = other.globalUboBinding;
|
||||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||||
vertexInputTypes = other.vertexInputTypes;
|
vertexInputTypes = other.vertexInputTypes;
|
||||||
@@ -92,15 +123,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
|
other.hasStorageImages = false;
|
||||||
other.globalUboBinding = -1;
|
other.globalUboBinding = -1;
|
||||||
other.activeVertexInputLocationMask = 0;
|
other.activeVertexInputLocationMask = 0;
|
||||||
other.activeFragmentOutputLocationMask = 0;
|
other.activeFragmentOutputLocationMask = 0;
|
||||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
|
other.fragmentReplacesDepth = false;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
if (this == &other) {
|
if (this == &other) {
|
||||||
@@ -115,11 +149,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
|
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
|
||||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||||
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
|
||||||
|
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
|
||||||
|
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
|
||||||
|
storageImageUsesBindingFormatByBinding =
|
||||||
|
std::move(other.storageImageUsesBindingFormatByBinding);
|
||||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||||
|
hasStorageImages = other.hasStorageImages;
|
||||||
globalUboBinding = other.globalUboBinding;
|
globalUboBinding = other.globalUboBinding;
|
||||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||||
vertexInputTypes = other.vertexInputTypes;
|
vertexInputTypes = other.vertexInputTypes;
|
||||||
@@ -128,15 +169,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
|
other.hasStorageImages = false;
|
||||||
other.globalUboBinding = -1;
|
other.globalUboBinding = -1;
|
||||||
other.activeVertexInputLocationMask = 0;
|
other.activeVertexInputLocationMask = 0;
|
||||||
other.activeFragmentOutputLocationMask = 0;
|
other.activeFragmentOutputLocationMask = 0;
|
||||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
|
other.fragmentReplacesDepth = false;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +211,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||||
Bool shaderDrawParametersEnabled = false)
|
Bool shaderDrawParametersEnabled = false,
|
||||||
|
Bool unformattedFloatStorageImagesEnabled = false)
|
||||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
|
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||||
|
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||||
VkProgramObject::s_device = device;
|
VkProgramObject::s_device = device;
|
||||||
}
|
}
|
||||||
~ProgramFactory() = default;
|
~ProgramFactory() = default;
|
||||||
@@ -180,6 +226,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||||
|
|
||||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
|
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||||
|
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||||
|
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||||
|
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||||
|
// stripped conservatively and forfeits its depth write.
|
||||||
|
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
|
||||||
|
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||||
|
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||||
|
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
@@ -206,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
|
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
|
||||||
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
|
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
|
||||||
Bool m_shaderDrawParametersEnabled = false;
|
Bool m_shaderDrawParametersEnabled = false;
|
||||||
|
// True only when the logical device enabled both
|
||||||
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
mutable ProgramLookupCache m_lastLookup;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
|
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
|
||||||
|
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
@@ -78,6 +79,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return uniformUnit >= 0 ? uniformUnit : 0;
|
return uniformUnit >= 0 ? uniformUnit : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||||
|
VkFormat resourceFormat, Bool useBindingFormat) {
|
||||||
|
if (useBindingFormat) {
|
||||||
|
const TextureInternalFormat bindingInternalFormat =
|
||||||
|
MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat);
|
||||||
|
return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat);
|
||||||
|
}
|
||||||
|
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||||
ProgramFactory* programFactory,
|
ProgramFactory* programFactory,
|
||||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||||
@@ -276,6 +287,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
|
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
|
||||||
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
|
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||||
|
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
|
||||||
|
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
|
||||||
|
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
|
||||||
|
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
|
||||||
|
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
|
||||||
|
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
|
||||||
|
// descriptor valid.
|
||||||
|
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
|
||||||
|
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
||||||
|
SamplerResolveMemo* viewFormatMemo =
|
||||||
|
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
|
||||||
|
VkFormat sampledViewFormat;
|
||||||
|
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
|
||||||
|
viewFormatMemo->viewFormatSource == resource->format &&
|
||||||
|
viewFormatMemo->viewFormatDomain == numericDomain) {
|
||||||
|
sampledViewFormat = viewFormatMemo->viewFormat;
|
||||||
|
} else {
|
||||||
|
sampledViewFormat =
|
||||||
|
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
|
||||||
|
if (viewFormatMemo != nullptr) {
|
||||||
|
viewFormatMemo->viewFormatSource = resource->format;
|
||||||
|
viewFormatMemo->viewFormatDomain = numericDomain;
|
||||||
|
viewFormatMemo->viewFormat = sampledViewFormat;
|
||||||
|
viewFormatMemo->viewFormatValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||||
|
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||||
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
|
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
|
||||||
|
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
|
||||||
|
const VkImageView sampledImageView =
|
||||||
|
sampledViewFormat == resource->format
|
||||||
|
? resource->sampledView
|
||||||
|
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||||
|
if (sampledImageView == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||||
|
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||||
|
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||||
|
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
||||||
|
static_cast<Int>(numericDomain));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
|
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
|
||||||
// sampler object and texture (both by lifetime id + version) are unchanged from the
|
// sampler object and texture (both by lifetime id + version) are unchanged from the
|
||||||
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler
|
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler
|
||||||
@@ -291,23 +351,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||||
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
||||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) {
|
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
||||||
|
memo.forceNearestFiltering == forceNearestFiltering) {
|
||||||
resolvedSampler = memo.sampler;
|
resolvedSampler = memo.sampler;
|
||||||
} else {
|
} else {
|
||||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
resolvedSampler =
|
||||||
|
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||||
memo.samplerLifetimeId = samplerLifetimeId;
|
memo.samplerLifetimeId = samplerLifetimeId;
|
||||||
memo.samplerVersion = samplerVersion;
|
memo.samplerVersion = samplerVersion;
|
||||||
memo.textureLifetimeId = textureLifetimeId;
|
memo.textureLifetimeId = textureLifetimeId;
|
||||||
memo.textureParamsVersion = textureParamsVersion;
|
memo.textureParamsVersion = textureParamsVersion;
|
||||||
|
memo.forceNearestFiltering = forceNearestFiltering;
|
||||||
memo.sampler = resolvedSampler;
|
memo.sampler = resolvedSampler;
|
||||||
memo.valid = true;
|
memo.valid = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
resolvedSampler =
|
||||||
|
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||||
}
|
}
|
||||||
outImageInfo = {
|
outImageInfo = {
|
||||||
.sampler = resolvedSampler,
|
.sampler = resolvedSampler,
|
||||||
.imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
|
.imageView = sampledImageView,
|
||||||
.imageLayout = resource->layout,
|
.imageLayout = resource->layout,
|
||||||
};
|
};
|
||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||||
@@ -572,9 +636,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
|
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
|
||||||
VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel);
|
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
|
||||||
|
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
|
||||||
|
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
|
||||||
|
binding);
|
||||||
|
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
|
||||||
|
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
|
||||||
|
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||||
|
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||||
|
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
|
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||||
|
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||||
|
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
||||||
|
useBindingFormat ? "true" : "false");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||||
|
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||||
if (view == VK_NULL_HANDLE) {
|
if (view == VK_NULL_HANDLE) {
|
||||||
view = resource->fullView;
|
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||||
|
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||||
|
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
||||||
|
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
||||||
|
static_cast<Int>(viewFormat),
|
||||||
|
useBindingFormat ? "true" : "false");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
outImageInfo.sampler = VK_NULL_HANDLE;
|
outImageInfo.sampler = VK_NULL_HANDLE;
|
||||||
outImageInfo.imageView = view;
|
outImageInfo.imageView = view;
|
||||||
@@ -632,9 +719,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::CollectStorageImageTextures(
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
|
||||||
|
outTextures.clear();
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||||
|
"CollectStorageImageTextures: GL context is null");
|
||||||
|
|
||||||
|
const Uint32 bindingCount =
|
||||||
|
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||||
|
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
||||||
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
||||||
|
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||||
|
if (location < 0) {
|
||||||
|
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||||
|
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||||
|
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u",
|
||||||
|
imageUnit, binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||||
|
if (texture == nullptr) {
|
||||||
|
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u",
|
||||||
|
imageUnit, binding);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
|
||||||
|
outTextures.push_back(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
UboBindResult& out) const {
|
Uint32 arrayElement, UboBindResult& out) const {
|
||||||
const void* outData = nullptr;
|
const void* outData = nullptr;
|
||||||
VkDeviceSize outSize = 0;
|
VkDeviceSize outSize = 0;
|
||||||
|
|
||||||
@@ -660,7 +791,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
|
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
|
||||||
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
|
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
|
||||||
const Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
|
Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
|
||||||
|
if (arrayElement > 0) {
|
||||||
|
const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding);
|
||||||
|
const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() &&
|
||||||
|
arrayElement < arrayIt->second.size();
|
||||||
|
MOBILEGL_ASSERT(elementValid,
|
||||||
|
"ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding,
|
||||||
|
arrayElement);
|
||||||
|
if (!elementValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
blockIndex = arrayIt->second[arrayElement];
|
||||||
|
}
|
||||||
MOBILEGL_ASSERT(blockIndex >= 0,
|
MOBILEGL_ASSERT(blockIndex >= 0,
|
||||||
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
|
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
|
||||||
|
|
||||||
@@ -907,11 +1050,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfos.clear();
|
imageInfos.clear();
|
||||||
texelBufferViews.clear();
|
texelBufferViews.clear();
|
||||||
dynamicOffsets.clear();
|
dynamicOffsets.clear();
|
||||||
|
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
||||||
|
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
|
||||||
|
Uint32 uboArrayExtra = 0;
|
||||||
|
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
||||||
|
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
||||||
|
}
|
||||||
writes.reserve(m_maxBindings);
|
writes.reserve(m_maxBindings);
|
||||||
bufferInfos.reserve(m_maxBindings);
|
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
|
||||||
imageInfos.reserve(m_maxBindings);
|
imageInfos.reserve(m_maxBindings);
|
||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size());
|
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||||
|
|
||||||
const Uint32 bindingCount =
|
const Uint32 bindingCount =
|
||||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||||
@@ -929,40 +1078,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
write.descriptorCount = 1;
|
write.descriptorCount = 1;
|
||||||
|
|
||||||
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
|
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
|
||||||
UboBindResult ubo{};
|
const Uint32 descriptorCount =
|
||||||
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo);
|
binding < programObj.bindingDescriptorCounts.size()
|
||||||
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
|
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u",
|
: 1u;
|
||||||
binding);
|
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
||||||
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
|
UboBindResult ubo{};
|
||||||
|
const Bool hasPayload =
|
||||||
|
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
|
||||||
|
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
|
||||||
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
|
||||||
|
binding, element);
|
||||||
|
|
||||||
VkDescriptorBufferInfo bufferInfo{};
|
VkDescriptorBufferInfo bufferInfo{};
|
||||||
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
||||||
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
||||||
bufferInfo.offset = 0;
|
bufferInfo.offset = 0;
|
||||||
Uint32 dynOffset;
|
Uint32 dynOffset;
|
||||||
if (ubo.directBindable) {
|
if (ubo.directBindable) {
|
||||||
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
||||||
bufferInfo.buffer = ubo.buffer;
|
bufferInfo.buffer = ubo.buffer;
|
||||||
bufferInfo.range = ubo.range;
|
bufferInfo.range = ubo.range;
|
||||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||||
} else {
|
} else {
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
||||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
||||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
|
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
||||||
binding);
|
binding, element);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
bufferInfo.buffer = slice.buffer;
|
||||||
|
bufferInfo.range = ubo.payloadSize;
|
||||||
|
dynOffset = static_cast<Uint32>(slice.offset);
|
||||||
}
|
}
|
||||||
bufferInfo.buffer = slice.buffer;
|
bufferInfos.push_back(bufferInfo);
|
||||||
bufferInfo.range = ubo.payloadSize;
|
// Dynamic offsets are consumed in binding order, then array element order,
|
||||||
dynOffset = static_cast<Uint32>(slice.offset);
|
// matching Vulkan's dynamic-offset consumption rules.
|
||||||
|
dynamicOffsets.push_back(dynOffset);
|
||||||
}
|
}
|
||||||
bufferInfos.push_back(bufferInfo);
|
|
||||||
|
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
|
||||||
write.pBufferInfo = &bufferInfos.back();
|
write.descriptorCount = descriptorCount;
|
||||||
|
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
dynamicOffsets.push_back(dynOffset);
|
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
|
||||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||||
|
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -49,6 +52,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
const SamplerBindingOverride* samplerBindingOverride = nullptr);
|
const SamplerBindingOverride* samplerBindingOverride = nullptr);
|
||||||
|
|
||||||
|
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||||
|
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||||
|
// format and never silently fall back to the backing image format.
|
||||||
|
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||||
|
VkFormat resourceFormat, Bool useBindingFormat);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct DescriptorPoolBucket {
|
struct DescriptorPoolBucket {
|
||||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||||
@@ -107,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
UboBindResult& out) const;
|
Uint32 arrayElement, UboBindResult& out) const;
|
||||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||||
VkResult AllocateDescriptorSetsFromActivePool(
|
VkResult AllocateDescriptorSetsFromActivePool(
|
||||||
@@ -165,9 +174,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkSampler sampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
Uint16 samplerVersion = 0;
|
Uint16 samplerVersion = 0;
|
||||||
Uint16 textureParamsVersion = 0;
|
Uint16 textureParamsVersion = 0;
|
||||||
|
Bool forceNearestFiltering = false;
|
||||||
Bool valid = false;
|
Bool valid = false;
|
||||||
|
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
|
||||||
|
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
|
||||||
|
// so a reinterpreted sampler pays that scan once, not once per draw.
|
||||||
|
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
|
||||||
|
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||||
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||||
|
Bool viewFormatValid = false;
|
||||||
};
|
};
|
||||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<SizeT> bindingBaseOffsets;
|
Vector<SizeT> bindingBaseOffsets;
|
||||||
Vector<Uint32> bindingAttributeLocations;
|
Vector<Uint32> bindingAttributeLocations;
|
||||||
Vector<Bool> bindingUsesClientMemory;
|
Vector<Bool> bindingUsesClientMemory;
|
||||||
|
Vector<VertexStreamConversion> bindingConversions;
|
||||||
Uint32 unsupportedAttribMask = 0;
|
Uint32 unsupportedAttribMask = 0;
|
||||||
|
|
||||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||||
@@ -74,8 +75,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
const VkFormat sourceVkFormat =
|
||||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||||
|
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||||
"enabled but cannot be mapped to a VkFormat",
|
"enabled but cannot be mapped to a VkFormat",
|
||||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||||
@@ -83,6 +85,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkFormat vkFormat = sourceVkFormat;
|
||||||
|
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||||
|
if (!SupportsVertexBufferFormat(vkFormat)) {
|
||||||
|
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||||
|
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||||
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||||
|
vkFormat = fallbackFormat;
|
||||||
|
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||||
|
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||||
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||||
|
"(type=%s size=%d normalized=%s integer=%s)",
|
||||||
|
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||||
|
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
|
||||||
|
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion == VertexStreamConversion::None) {
|
||||||
|
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||||
|
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||||
|
location, static_cast<Int>(sourceVkFormat),
|
||||||
|
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||||
|
unsupportedAttribMask |= (1u << location);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||||
if (attribByteSize == 0) {
|
if (attribByteSize == 0) {
|
||||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||||
@@ -92,8 +121,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint32 stride =
|
const Uint32 sourceStride =
|
||||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||||
|
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||||
|
attr.Type == DataType::Uint2101010Rev;
|
||||||
|
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||||
|
// For a client-memory array attr.Offset holds the raw client pointer, and the
|
||||||
|
// draw path re-uploads the data to a 16-aligned transient slice with attribute
|
||||||
|
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
|
||||||
|
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
|
||||||
|
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
|
||||||
|
((sourceStride % requiredAlignment) != 0 ||
|
||||||
|
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
|
||||||
|
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
|
||||||
|
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||||
|
// attribute into a tightly packed transient stream without changing its format.
|
||||||
|
conversion = VertexStreamConversion::Repack;
|
||||||
|
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||||
|
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||||
|
location, attr.Offset, sourceStride, requiredAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 stride = sourceStride;
|
||||||
|
if (conversion == VertexStreamConversion::Repack) {
|
||||||
|
stride = static_cast<Uint32>(attribByteSize);
|
||||||
|
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||||
|
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||||
|
}
|
||||||
const VkVertexInputRate inputRate =
|
const VkVertexInputRate inputRate =
|
||||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||||
|
|
||||||
@@ -103,6 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
|
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
|
||||||
bindingAttributeLocations.push_back(location);
|
bindingAttributeLocations.push_back(location);
|
||||||
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
||||||
|
bindingConversions.push_back(conversion);
|
||||||
builder.AddBinding(binding, stride, inputRate);
|
builder.AddBinding(binding, stride, inputRate);
|
||||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||||
}
|
}
|
||||||
@@ -117,6 +172,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||||
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
||||||
|
entry.bindingConversions = std::move(bindingConversions);
|
||||||
entry.unsupportedAttribMask = unsupportedAttribMask;
|
entry.unsupportedAttribMask = unsupportedAttribMask;
|
||||||
entry.state = state;
|
entry.state = state;
|
||||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||||
@@ -282,4 +338,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const SizeT componentSize = GetComponentSize(type);
|
const SizeT componentSize = GetComponentSize(type);
|
||||||
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
|
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case VK_FORMAT_R8_USCALED:
|
||||||
|
case VK_FORMAT_R8_SSCALED:
|
||||||
|
case VK_FORMAT_R8G8_USCALED:
|
||||||
|
case VK_FORMAT_R8G8_SSCALED:
|
||||||
|
case VK_FORMAT_R8G8B8_USCALED:
|
||||||
|
case VK_FORMAT_R8G8B8_SSCALED:
|
||||||
|
case VK_FORMAT_R8G8B8A8_USCALED:
|
||||||
|
case VK_FORMAT_R8G8B8A8_SSCALED:
|
||||||
|
case VK_FORMAT_R16_USCALED:
|
||||||
|
case VK_FORMAT_R16_SSCALED:
|
||||||
|
case VK_FORMAT_R16G16_USCALED:
|
||||||
|
case VK_FORMAT_R16G16_SSCALED:
|
||||||
|
case VK_FORMAT_R16G16B16_USCALED:
|
||||||
|
case VK_FORMAT_R16G16B16_SSCALED:
|
||||||
|
case VK_FORMAT_R16G16B16A16_USCALED:
|
||||||
|
case VK_FORMAT_R16G16B16A16_SSCALED:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) {
|
||||||
|
switch (componentCount) {
|
||||||
|
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||||
|
case 2: return VK_FORMAT_R32G32_SFLOAT;
|
||||||
|
case 3: return VK_FORMAT_R32G32B32_SFLOAT;
|
||||||
|
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
default: return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const {
|
||||||
|
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
VkFormatProperties properties{};
|
||||||
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||||
|
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
|
|
||||||
|
enum class VertexStreamConversion : Uint8 {
|
||||||
|
None = 0,
|
||||||
|
Repack,
|
||||||
|
ScaledIntegerToFloat32,
|
||||||
|
};
|
||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
Vector<VkVertexInputBindingDescription> bindings;
|
Vector<VkVertexInputBindingDescription> bindings;
|
||||||
@@ -27,6 +33,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<SizeT> bindingBaseOffsets;
|
Vector<SizeT> bindingBaseOffsets;
|
||||||
Vector<Uint32> bindingAttributeLocations;
|
Vector<Uint32> bindingAttributeLocations;
|
||||||
Vector<Bool> bindingUsesClientMemory;
|
Vector<Bool> bindingUsesClientMemory;
|
||||||
|
Vector<VertexStreamConversion> bindingConversions;
|
||||||
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
|
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
|
||||||
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
||||||
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
||||||
@@ -36,8 +43,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit VertexInputStateFactory(const VulkanRendererConfig& config):
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||||
m_config(config) {}
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||||
~VertexInputStateFactory() = default;
|
~VertexInputStateFactory() = default;
|
||||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||||
|
|
||||||
@@ -56,8 +63,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
private:
|
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);
|
||||||
|
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||||
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||||
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||||
|
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,6 +157,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
|
||||||
|
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
|
||||||
|
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
|
||||||
|
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
|
||||||
|
|
||||||
|
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
|
||||||
|
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
|
||||||
|
if (resolvedSize == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||||
|
if (result != VK_SUCCESS) {
|
||||||
|
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
||||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void* Map();
|
void* Map();
|
||||||
void Unmap();
|
void Unmap();
|
||||||
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
|
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
|
||||||
|
Bool Invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
|
||||||
|
|
||||||
VkBuffer GetHandle() const { return m_buffer; }
|
VkBuffer GetHandle() const { return m_buffer; }
|
||||||
VkDeviceSize GetSize() const { return m_size; }
|
VkDeviceSize GetSize() const { return m_size; }
|
||||||
|
|||||||
@@ -251,11 +251,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||||
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||||
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
|
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||||
MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet",
|
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||||
renderbuffer->GetExternalIndex());
|
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization.
|
||||||
return nullptr;
|
const VkImageUsageFlags imageUsage =
|
||||||
}
|
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
|
||||||
|
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
|
||||||
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||||
|
|
||||||
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
auto& resource = m_renderbufferResources[renderbuffer.get()];
|
||||||
const Bool needsCreate =
|
const Bool needsCreate =
|
||||||
@@ -285,7 +287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.format = format;
|
imageInfo.format = format;
|
||||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
imageInfo.usage = imageUsage;
|
||||||
imageInfo.samples = sampleCount;
|
imageInfo.samples = sampleCount;
|
||||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
|
||||||
@@ -386,6 +388,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkRenderPassManager::QueueRenderbufferClear(
|
void VkRenderPassManager::QueueRenderbufferClear(
|
||||||
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||||
const MG_State::GLState::FramebufferObject& drawFbo) {
|
const MG_State::GLState::FramebufferObject& drawFbo) {
|
||||||
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
|
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
|
||||||
|
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
|
||||||
|
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
|
||||||
|
if (attachmentType == FramebufferAttachmentType::None) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QueueRenderbufferClear(
|
||||||
|
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
|
||||||
|
drawFbo.GetAttachment(attachmentType));
|
||||||
|
}
|
||||||
|
}
|
||||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
QueueRenderbufferClear(
|
QueueRenderbufferClear(
|
||||||
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
|
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
|
||||||
@@ -682,6 +696,83 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// assuming default FBO has the right param
|
// assuming default FBO has the right param
|
||||||
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
|
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
|
||||||
auto drawbuf = drawbufs[i];
|
auto drawbuf = drawbufs[i];
|
||||||
|
|
||||||
|
// Renderbuffer color attachments mirror the texture path below, with the
|
||||||
|
// resource (image/view/format/layout) coming from the render-pass manager's
|
||||||
|
// renderbuffer store instead of the texture manager.
|
||||||
|
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
|
||||||
|
const auto& rbAtt = fbo.GetAttachment(drawbuf);
|
||||||
|
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
|
||||||
|
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||||
|
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||||
|
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||||
|
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||||
|
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||||
|
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||||
|
attachmentDescriptions.emplace_back();
|
||||||
|
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
|
||||||
|
|
||||||
|
ClearAttachmentPayload rbClearPayload{};
|
||||||
|
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
|
||||||
|
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
|
||||||
|
if (rbHasClear &&
|
||||||
|
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
|
||||||
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||||
|
rbClearPayload.color =
|
||||||
|
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
|
||||||
|
rbClearPayload.color.z(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||||
|
rbDesc.flags = 0;
|
||||||
|
rbDesc.format = rbResource->format;
|
||||||
|
rbDesc.samples = rbResource->sampleCount;
|
||||||
|
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
|
||||||
|
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
|
||||||
|
: VK_ATTACHMENT_LOAD_OP_LOAD);
|
||||||
|
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||||
|
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||||
|
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||||
|
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
|
||||||
|
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
|
||||||
|
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||||
|
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
|
||||||
|
static_cast<Int>(renderbuffer->GetExternalIndex()));
|
||||||
|
|
||||||
|
if (rbHasClear) {
|
||||||
|
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
||||||
|
.attachmentIndex = rbAttachmentIndex,
|
||||||
|
.colorAttachmentSlot = i,
|
||||||
|
.renderbuffer = renderbuffer.get(),
|
||||||
|
.hasInlinePayload = true,
|
||||||
|
.inlinePayload = rbClearPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width == 0)
|
||||||
|
width = static_cast<Int>(rbResource->extent.width);
|
||||||
|
if (height == 0)
|
||||||
|
height = static_cast<Int>(rbResource->extent.height);
|
||||||
|
|
||||||
|
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||||
|
.target = TrackedAttachmentTarget::Renderbuffer,
|
||||||
|
.renderbuffer = renderbuffer,
|
||||||
|
.finalLayout = rbDesc.finalLayout,
|
||||||
|
});
|
||||||
|
textureResources.emplace_back(nullptr);
|
||||||
|
attachmentViews.emplace_back(rbResource->view);
|
||||||
|
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||||
|
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||||
|
|
||||||
|
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
|
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
|
||||||
if (texture == nullptr)
|
if (texture == nullptr)
|
||||||
continue;
|
continue;
|
||||||
@@ -700,6 +791,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case TextureTarget::Texture2D:
|
case TextureTarget::Texture2D:
|
||||||
case TextureTarget::Texture2DArray:
|
case TextureTarget::Texture2DArray:
|
||||||
case TextureTarget::Texture2DMultisample:
|
case TextureTarget::Texture2DMultisample:
|
||||||
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
|
case TextureTarget::Texture3D:
|
||||||
|
case TextureTarget::TextureCubeMap:
|
||||||
|
case TextureTarget::TextureCubeMapArray:
|
||||||
case TextureTarget::TextureRectangle: {
|
case TextureTarget::TextureRectangle: {
|
||||||
desc.flags = 0;
|
desc.flags = 0;
|
||||||
desc.format = isDefaultFbo ?
|
desc.format = isDefaultFbo ?
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 m_rpFastRbEpoch = 0;
|
Uint64 m_rpFastRbEpoch = 0;
|
||||||
Uint64 m_rpFastRenderPassHash = 0;
|
Uint64 m_rpFastRenderPassHash = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
struct RenderbufferResource {
|
struct RenderbufferResource {
|
||||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
@@ -227,6 +228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
|
||||||
|
// attachments the same way texture attachments go through the texture manager.
|
||||||
|
RenderbufferResource* GetOrCreateRenderbufferResource(
|
||||||
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||||
|
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
|
||||||
|
ClearAttachmentPayload& outPayload) const;
|
||||||
|
|
||||||
|
private:
|
||||||
struct PendingRenderbufferClear {
|
struct PendingRenderbufferClear {
|
||||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
ClearAttachmentPayload payload{};
|
ClearAttachmentPayload payload{};
|
||||||
@@ -235,10 +244,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
|
|
||||||
RenderbufferResource* GetOrCreateRenderbufferResource(
|
|
||||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
|
||||||
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
|
|
||||||
ClearAttachmentPayload& outPayload) const;
|
|
||||||
Bool HasPendingRenderbufferClear(
|
Bool HasPendingRenderbufferClear(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||||
void CollectRenderbufferGarbage();
|
void CollectRenderbufferGarbage();
|
||||||
|
|||||||
@@ -65,8 +65,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const {
|
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||||
|
Bool forceNearestFiltering) const {
|
||||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||||
|
if (forceNearestFiltering) return 1.0f;
|
||||||
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
|
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
|
||||||
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
|
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
|
||||||
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
|
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
|
||||||
@@ -90,10 +92,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture) const {
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
|
Bool forceNearestFiltering) const {
|
||||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||||
|
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||||
|
|
||||||
const auto minFilter = sampler.GetMinFilter();
|
const auto minFilter = sampler.GetMinFilter();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||||
const auto magFilter = sampler.GetMagFilter();
|
const auto magFilter = sampler.GetMagFilter();
|
||||||
@@ -115,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
|
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||||
const auto compareMode = sampler.GetCompareMode();
|
const auto compareMode = sampler.GetCompareMode();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||||
@@ -127,8 +132,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture) {
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
const Uint64 key = BuildSamplerKey(sampler, texture);
|
Bool forceNearestFiltering) {
|
||||||
|
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||||
auto it = m_samplers.find(key);
|
auto it = m_samplers.find(key);
|
||||||
if (it != m_samplers.end()) {
|
if (it != m_samplers.end()) {
|
||||||
return it->second.handle;
|
return it->second.handle;
|
||||||
@@ -136,16 +142,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkSamplerCreateInfo samplerInfo{};
|
VkSamplerCreateInfo samplerInfo{};
|
||||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
|
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||||
samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
|
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||||
samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
|
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||||
|
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||||
samplerInfo.mipLodBias = sampler.GetLodBias();
|
samplerInfo.mipLodBias = sampler.GetLodBias();
|
||||||
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
|
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
|
||||||
// different samplers or silently create duplicates.
|
// different samplers or silently create duplicates.
|
||||||
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
|
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||||
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
||||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ public:
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture);
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
|
Bool forceNearestFiltering = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SamplerCacheEntry {
|
struct SamplerCacheEntry {
|
||||||
@@ -44,7 +45,8 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture) const;
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
|
Bool forceNearestFiltering) const;
|
||||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||||
@@ -57,7 +59,8 @@ private:
|
|||||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||||
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
|
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
|
||||||
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const;
|
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||||
|
Bool forceNearestFiltering) const;
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
const VulkanRendererConfig* m_config = nullptr;
|
const VulkanRendererConfig* m_config = nullptr;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "VkTextureManager.h"
|
#include "VkTextureManager.h"
|
||||||
|
|
||||||
|
#include "ProgramFactory.h"
|
||||||
|
|
||||||
#include "MG_State/GLState/Core.h"
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vulkan/utility/vk_format_utils.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
||||||
@@ -63,6 +66,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
|
target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Bool IsMutableStorageImageFormat(VkFormat format) {
|
||||||
|
if (!vkuFormatIsColor(format) || vkuFormatIsCompressed(format)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are the uncompressed color compatibility classes covered by the core GLSL/SPIR-V
|
||||||
|
// storage-image formats. OpenGL mutable texture storage uses image-format compatibility by
|
||||||
|
// size, so a shader may legally reinterpret (for example) RGBA16_UNORM storage as rgba16f. Vulkan
|
||||||
|
// requires the image to be mutable and the view formats to share this exact compatibility
|
||||||
|
// class for the equivalent operation.
|
||||||
|
switch (vkuFormatCompatibilityClass(format)) {
|
||||||
|
case VKU_FORMAT_COMPATIBILITY_CLASS_8BIT:
|
||||||
|
case VKU_FORMAT_COMPATIBILITY_CLASS_16BIT:
|
||||||
|
case VKU_FORMAT_COMPATIBILITY_CLASS_32BIT:
|
||||||
|
case VKU_FORMAT_COMPATIBILITY_CLASS_64BIT:
|
||||||
|
case VKU_FORMAT_COMPATIBILITY_CLASS_128BIT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bool HasMatchingColorComponentLayout(VkFormat lhs, VkFormat rhs) {
|
||||||
|
const VKU_FORMAT_INFO lhsInfo = vkuGetFormatInfo(lhs);
|
||||||
|
const VKU_FORMAT_INFO rhsInfo = vkuGetFormatInfo(rhs);
|
||||||
|
if (lhsInfo.component_count == 0 || lhsInfo.component_count != rhsInfo.component_count ||
|
||||||
|
lhsInfo.texel_block_size != rhsInfo.texel_block_size ||
|
||||||
|
lhsInfo.texels_per_block != 1 || rhsInfo.texels_per_block != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Uint32 component = 0; component < lhsInfo.component_count; ++component) {
|
||||||
|
if (lhsInfo.components[component].type != rhsInfo.components[component].type ||
|
||||||
|
lhsInfo.components[component].size != rhsInfo.components[component].size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bool FormatMatchesSamplerNumericDomain(VkFormat format, SamplerNumericDomain numericDomain) {
|
||||||
|
switch (numericDomain) {
|
||||||
|
case SamplerNumericDomain::Float:
|
||||||
|
return vkuFormatIsSampledFloat(format);
|
||||||
|
case SamplerNumericDomain::SignedInteger:
|
||||||
|
return vkuFormatIsSINT(format);
|
||||||
|
case SamplerNumericDomain::UnsignedInteger:
|
||||||
|
return vkuFormatIsUINT(format);
|
||||||
|
case SamplerNumericDomain::Unknown:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||||
switch (requestedSamples) {
|
switch (requestedSamples) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -319,7 +375,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
switch (format) {
|
switch (format) {
|
||||||
case TextureInternalFormat::RGB:
|
case TextureInternalFormat::RGB:
|
||||||
case TextureInternalFormat::RGB8:
|
case TextureInternalFormat::RGB8:
|
||||||
|
// Legacy low-bit RGB formats share the UNorm8 canonical shadow layout (see
|
||||||
|
// TextureFormatProcessor), so they upload exactly like RGB8 with an alpha expand.
|
||||||
|
case TextureInternalFormat::R3G3B2:
|
||||||
|
case TextureInternalFormat::RGB4:
|
||||||
|
case TextureInternalFormat::RGB5:
|
||||||
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
||||||
|
// Low-bit RGBA formats: UNorm8x4 canonical shadow, no expansion needed.
|
||||||
|
case TextureInternalFormat::RGBA2:
|
||||||
|
case TextureInternalFormat::RGBA4:
|
||||||
|
case TextureInternalFormat::RGB5A1:
|
||||||
|
return {VK_FORMAT_R8G8B8A8_UNORM, false, 0, {0, 0, 0, 0}};
|
||||||
|
// 10/12-bit RGB(A): UNorm16 canonical shadow.
|
||||||
|
case TextureInternalFormat::RGB10:
|
||||||
|
case TextureInternalFormat::RGB12:
|
||||||
|
return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}};
|
||||||
|
case TextureInternalFormat::RGBA12:
|
||||||
|
return {VK_FORMAT_R16G16B16A16_UNORM, false, 0, {0, 0, 0, 0}};
|
||||||
case TextureInternalFormat::SRGB8:
|
case TextureInternalFormat::SRGB8:
|
||||||
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
|
||||||
case TextureInternalFormat::RGB8Snorm:
|
case TextureInternalFormat::RGB8Snorm:
|
||||||
@@ -767,6 +839,180 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return perMipSampledView;
|
return perMipSampledView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
|
||||||
|
VkFormat format) {
|
||||||
|
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||||
|
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
|
||||||
|
resource->sampledView == VK_NULL_HANDLE) {
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
|
||||||
|
return resource->sampledView;
|
||||||
|
}
|
||||||
|
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||||
|
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||||
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
|
static_cast<Int>(resource->format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
|
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||||
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextureResource::SampledImageViewKey key{
|
||||||
|
.baseMipLevel = resource->sampledBaseMipLevel,
|
||||||
|
.levelCount = resource->sampledLevelCount,
|
||||||
|
.viewType = resource->viewType,
|
||||||
|
.format = format,
|
||||||
|
};
|
||||||
|
const auto existing = resource->alternateSampledViews.find(key);
|
||||||
|
if (existing != resource->alternateSampledViews.end()) {
|
||||||
|
return existing->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFormatProperties formatProperties{};
|
||||||
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
|
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||||
|
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||||
|
"for textureId=%d (available=0x%x)",
|
||||||
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
|
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
|
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||||
|
const VkImageView view = CreateImageView(
|
||||||
|
resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType,
|
||||||
|
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||||
|
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||||
|
if (view == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||||
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
resource->alternateSampledViews.emplace(key, view);
|
||||||
|
MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)",
|
||||||
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format), resource->sampledBaseMipLevel,
|
||||||
|
resource->sampledBaseMipLevel + resource->sampledLevelCount);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
|
||||||
|
Uint32 mipLevel, VkFormat format,
|
||||||
|
Bool layered, Int32 layer) {
|
||||||
|
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
|
||||||
|
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
|
||||||
|
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
||||||
|
(resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == VK_FORMAT_UNDEFINED) {
|
||||||
|
format = resource->format;
|
||||||
|
}
|
||||||
|
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||||
|
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||||
|
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||||
|
static_cast<Int>(resource->format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (format != resource->format &&
|
||||||
|
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||||
|
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||||
|
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 baseArrayLayer = 0;
|
||||||
|
Uint32 layerCount = resource->arrayLayers;
|
||||||
|
VkImageViewType viewType = resource->viewType;
|
||||||
|
if (!layered) {
|
||||||
|
switch (resource->viewType) {
|
||||||
|
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
||||||
|
viewType = VK_IMAGE_VIEW_TYPE_1D;
|
||||||
|
break;
|
||||||
|
case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
|
||||||
|
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||||
|
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||||
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
break;
|
||||||
|
case VK_IMAGE_VIEW_TYPE_3D:
|
||||||
|
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||||
|
__func__, texture.GetExternalIndex());
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewType != resource->viewType) {
|
||||||
|
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||||
|
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||||
|
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
baseArrayLayer = static_cast<Uint32>(layer);
|
||||||
|
layerCount = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
|
||||||
|
viewType == resource->viewType;
|
||||||
|
if (format == resource->format && isFullResourceView) {
|
||||||
|
return GetOrCreateViewAtMipLevel(texture, mipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextureResource::StorageImageViewKey key{
|
||||||
|
.mipLevel = mipLevel,
|
||||||
|
.baseArrayLayer = baseArrayLayer,
|
||||||
|
.layerCount = layerCount,
|
||||||
|
.viewType = viewType,
|
||||||
|
.format = format,
|
||||||
|
};
|
||||||
|
auto it = resource->storageImageViews.find(key);
|
||||||
|
if (it != resource->storageImageViews.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
|
||||||
|
if (format != resource->format &&
|
||||||
|
(format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) {
|
||||||
|
requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT;
|
||||||
|
}
|
||||||
|
VkFormatProperties formatProperties{};
|
||||||
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
|
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||||
|
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||||
|
"(available=0x%x)",
|
||||||
|
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||||
|
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType,
|
||||||
|
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||||
|
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||||
|
if (view == VK_NULL_HANDLE) {
|
||||||
|
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||||
|
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
resource->storageImageViews.emplace(key, view);
|
||||||
|
MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||||
|
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||||
|
static_cast<Int>(format));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||||
@@ -910,6 +1156,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||||
|
const auto it = m_textureResources.find(MakeTextureIdentity(&texture));
|
||||||
|
if (it == m_textureResources.end()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const TextureResource& resource = it->second;
|
||||||
|
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||||
|
// path may upload or rebuild, both of which need the render pass ended first.
|
||||||
|
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
|
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||||
|
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||||
|
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||||
|
resource.syncedMipLevelCount != mipLevelCount;
|
||||||
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
|
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
|
||||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||||
@@ -1085,6 +1349,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||||
|
VkFormatProperties formatProperties{};
|
||||||
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
|
const Bool supportsStorageImage =
|
||||||
|
!isMultisampleTexture &&
|
||||||
|
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||||
|
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||||
|
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||||
|
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
||||||
|
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||||
|
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||||
@@ -1092,6 +1369,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||||
resource.viewType == shapeInfo.viewType &&
|
resource.viewType == shapeInfo.viewType &&
|
||||||
resource.sampleCount == resolvedSampleCount &&
|
resource.sampleCount == resolvedSampleCount &&
|
||||||
|
resource.imageCreateFlags == imageCreateFlags &&
|
||||||
resource.mipLevels == backingMipLevels;
|
resource.mipLevels == backingMipLevels;
|
||||||
if (compatible) {
|
if (compatible) {
|
||||||
if (resource.perMipViews.size() != backingMipLevels) {
|
if (resource.perMipViews.size() != backingMipLevels) {
|
||||||
@@ -1112,6 +1390,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||||
resource.viewType == shapeInfo.viewType &&
|
resource.viewType == shapeInfo.viewType &&
|
||||||
resource.sampleCount == resolvedSampleCount &&
|
resource.sampleCount == resolvedSampleCount &&
|
||||||
|
resource.imageCreateFlags == imageCreateFlags &&
|
||||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||||
resource.mipLevels < backingMipLevels &&
|
resource.mipLevels < backingMipLevels &&
|
||||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
@@ -1123,26 +1402,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DeferResourceRelease(Move(resource));
|
DeferResourceRelease(Move(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto aspect = GetAspectMaskForFormat(format);
|
|
||||||
|
|
||||||
VkImageCreateInfo imageInfo{};
|
VkImageCreateInfo imageInfo{};
|
||||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
imageInfo.flags = shapeInfo.imageFlags;
|
imageInfo.flags = imageCreateFlags;
|
||||||
imageInfo.imageType = shapeInfo.imageType;
|
imageInfo.imageType = shapeInfo.imageType;
|
||||||
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
|
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
|
||||||
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
|
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
|
||||||
imageInfo.extent.depth = shapeInfo.depth;
|
imageInfo.extent.depth = shapeInfo.depth;
|
||||||
imageInfo.mipLevels = backingMipLevels;
|
imageInfo.mipLevels = backingMipLevels;
|
||||||
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
||||||
imageInfo.format = format;
|
imageInfo.format = format;
|
||||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
VkFormatProperties formatProperties{};
|
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
|
||||||
const Bool supportsStorageImage =
|
|
||||||
!isMultisampleTexture &&
|
|
||||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
|
||||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
|
||||||
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||||
@@ -1153,15 +1424,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||||
}
|
}
|
||||||
imageInfo.samples = resolvedSampleCount;
|
imageInfo.samples = resolvedSampleCount;
|
||||||
if (isMultisampleTexture) {
|
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
VkImageFormatProperties imageFormatProperties{};
|
VkImageFormatProperties imageFormatProperties{};
|
||||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||||
imageInfo.flags, &imageFormatProperties);
|
imageInfo.flags, &imageFormatProperties);
|
||||||
|
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||||
|
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
|
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||||
|
// this texture; failing creation would lose the texture entirely, so retry
|
||||||
|
// as a plain immutable-format image.
|
||||||
|
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||||
|
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||||
|
"will be unavailable for it)",
|
||||||
|
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||||
|
// Remember the verdict so later syncs of same-format textures neither retry
|
||||||
|
// the probe nor flag-mismatch against this image and recreate it.
|
||||||
|
m_mutableFormatUnsupported.insert(format);
|
||||||
|
imageInfo.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
|
imageCreateFlags = imageInfo.flags;
|
||||||
|
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||||
|
imageInfo.flags, &imageFormatProperties);
|
||||||
|
}
|
||||||
if (imageFormatResult != VK_SUCCESS ||
|
if (imageFormatResult != VK_SUCCESS ||
|
||||||
(imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) {
|
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||||
MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x",
|
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||||
__func__, texture.GetSamples(), texture.GetExternalIndex(),
|
"format=%d usage=0x%x",
|
||||||
|
__func__, static_cast<Uint32>(imageInfo.flags), texture.GetSamples(),
|
||||||
|
texture.GetExternalIndex(),
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||||
return false;
|
return false;
|
||||||
@@ -1188,6 +1479,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.aspect = aspect;
|
resource.aspect = aspect;
|
||||||
resource.viewType = shapeInfo.viewType;
|
resource.viewType = shapeInfo.viewType;
|
||||||
resource.sampleCount = resolvedSampleCount;
|
resource.sampleCount = resolvedSampleCount;
|
||||||
|
resource.imageCreateFlags = imageCreateFlags;
|
||||||
resource.syncedTextureParamsVersion = 0;
|
resource.syncedTextureParamsVersion = 0;
|
||||||
|
|
||||||
if (preservedResource) {
|
if (preservedResource) {
|
||||||
@@ -1203,7 +1495,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
||||||
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
|
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
|
||||||
resource.sampledView == VK_NULL_HANDLE &&
|
resource.sampledView == VK_NULL_HANDLE &&
|
||||||
resource.perMipViews.empty() && resource.perMipSampledViews.empty()) {
|
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
|
||||||
|
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
|
||||||
|
resource.storageImageViews.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1299,6 +1593,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
sampledView = VK_NULL_HANDLE;
|
sampledView = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const auto& [_, sampledView] : resource.alternateSampledViews) {
|
||||||
|
DeferViewRelease(sampledView);
|
||||||
|
}
|
||||||
|
resource.alternateSampledViews.clear();
|
||||||
|
|
||||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||||
@@ -1324,7 +1622,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||||
Uint32 baseArrayLayer,
|
Uint32 baseArrayLayer,
|
||||||
Uint32 layerCount,
|
Uint32 layerCount,
|
||||||
const VkComponentMapping* components) const {
|
const VkComponentMapping* components,
|
||||||
|
VkImageUsageFlags viewUsage) const {
|
||||||
VkImageViewCreateInfo viewInfo{};
|
VkImageViewCreateInfo viewInfo{};
|
||||||
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
viewInfo.image = image;
|
viewInfo.image = image;
|
||||||
@@ -1340,6 +1639,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
|
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
|
||||||
viewInfo.subresourceRange.layerCount = layerCount;
|
viewInfo.subresourceRange.layerCount = layerCount;
|
||||||
|
|
||||||
|
VkImageViewUsageCreateInfo usageInfo{};
|
||||||
|
if (viewUsage != 0) {
|
||||||
|
usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
|
||||||
|
usageInfo.usage = viewUsage;
|
||||||
|
viewInfo.pNext = &usageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
VkImageView view = VK_NULL_HANDLE;
|
||||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
|
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
|
||||||
return view;
|
return view;
|
||||||
@@ -1665,4 +1971,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
return imageAspect;
|
return imageAspect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
|
||||||
|
SamplerNumericDomain numericDomain) {
|
||||||
|
// Depth/stencil images always sample through the existing depth-aspect sampledView.
|
||||||
|
// Combined formats (D24S8, D32FS8) are multi-numeric, so vkuFormatIsSampledFloat is
|
||||||
|
// false for them by design, yet their depth aspect reads as float in every GL depth
|
||||||
|
// texture mode; Vulkan also forbids reinterpreting them through color-class views.
|
||||||
|
// Integer domains keep the same view (pre-reinterpretation behavior for stencil-index
|
||||||
|
// style access) rather than failing the draw.
|
||||||
|
if (vkuFormatIsDepthOrStencil(imageFormat)) {
|
||||||
|
return imageFormat;
|
||||||
|
}
|
||||||
|
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
|
||||||
|
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
|
||||||
|
return imageFormat;
|
||||||
|
}
|
||||||
|
if (!IsMutableStorageImageFormat(imageFormat)) {
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve component ordering and bit widths. This selects R32_UINT for an R32_SFLOAT
|
||||||
|
// texture sampled by a usampler rather than an arbitrary member (such as
|
||||||
|
// R8G8B8A8_UINT) of Vulkan's broad 32-bit compatibility class.
|
||||||
|
for (Int candidateValue = static_cast<Int>(VK_FORMAT_R4G4_UNORM_PACK8);
|
||||||
|
candidateValue <= static_cast<Int>(VK_FORMAT_ASTC_12x12_SRGB_BLOCK);
|
||||||
|
++candidateValue) {
|
||||||
|
const VkFormat candidate = static_cast<VkFormat>(candidateValue);
|
||||||
|
if (!IsMutableStorageImageFormat(candidate) ||
|
||||||
|
!FormatMatchesSamplerNumericDomain(candidate, numericDomain) ||
|
||||||
|
!HasMatchingColorComponentLayout(imageFormat, candidate) ||
|
||||||
|
!AreSampledImageViewFormatsCompatible(imageFormat, candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an integer backing is intentionally bit-read through a float sampler, require
|
||||||
|
// a true floating-point view. Normalized/scaled views satisfy OpTypeFloat but apply
|
||||||
|
// an unrelated numeric conversion to those bits.
|
||||||
|
if (numericDomain == SamplerNumericDomain::Float && !vkuFormatIsSFLOAT(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
|
||||||
|
if (imageFormat == viewFormat) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
|
||||||
|
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
|
||||||
|
if (imageFormat == viewFormat) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
|
||||||
|
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -13,12 +13,15 @@
|
|||||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
class ITextureObject;
|
class ITextureObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
enum class SamplerNumericDomain : Uint8;
|
||||||
|
|
||||||
class VkTextureManager {
|
class VkTextureManager {
|
||||||
public:
|
public:
|
||||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||||
@@ -78,6 +81,61 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StorageImageViewKey {
|
||||||
|
Uint32 mipLevel = 0;
|
||||||
|
Uint32 baseArrayLayer = 0;
|
||||||
|
Uint32 layerCount = 1;
|
||||||
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||||
|
|
||||||
|
Bool operator==(const StorageImageViewKey& other) const {
|
||||||
|
return mipLevel == other.mipLevel &&
|
||||||
|
baseArrayLayer == other.baseArrayLayer &&
|
||||||
|
layerCount == other.layerCount &&
|
||||||
|
viewType == other.viewType &&
|
||||||
|
format == other.format;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SampledImageViewKey {
|
||||||
|
Uint32 baseMipLevel = 0;
|
||||||
|
Uint32 levelCount = 1;
|
||||||
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||||
|
|
||||||
|
Bool operator==(const SampledImageViewKey& other) const {
|
||||||
|
return baseMipLevel == other.baseMipLevel &&
|
||||||
|
levelCount == other.levelCount &&
|
||||||
|
viewType == other.viewType &&
|
||||||
|
format == other.format;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SampledImageViewKeyHash {
|
||||||
|
SizeT operator()(const SampledImageViewKey& key) const {
|
||||||
|
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
|
||||||
|
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||||
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||||
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StorageImageViewKeyHash {
|
||||||
|
SizeT operator()(const StorageImageViewKey& key) const {
|
||||||
|
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
|
||||||
|
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
|
||||||
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
|
||||||
|
0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VmaAllocation allocation = nullptr;
|
VmaAllocation allocation = nullptr;
|
||||||
VkImageView fullView = VK_NULL_HANDLE;
|
VkImageView fullView = VK_NULL_HANDLE;
|
||||||
@@ -85,6 +143,8 @@ public:
|
|||||||
Vector<VkImageView> perMipViews;
|
Vector<VkImageView> perMipViews;
|
||||||
Vector<VkImageView> perMipSampledViews;
|
Vector<VkImageView> perMipSampledViews;
|
||||||
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
|
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
|
||||||
|
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
|
||||||
|
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
|
||||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
VkExtent2D extent = {0, 0};
|
VkExtent2D extent = {0, 0};
|
||||||
Uint32 depth = 1;
|
Uint32 depth = 1;
|
||||||
@@ -96,6 +156,7 @@ public:
|
|||||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
VkImageCreateFlags imageCreateFlags = 0;
|
||||||
Uint16 syncedTextureParamsVersion = 0;
|
Uint16 syncedTextureParamsVersion = 0;
|
||||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||||
@@ -115,6 +176,8 @@ public:
|
|||||||
std::swap(this->perMipViews, that.perMipViews);
|
std::swap(this->perMipViews, that.perMipViews);
|
||||||
std::swap(this->perMipSampledViews, that.perMipSampledViews);
|
std::swap(this->perMipSampledViews, that.perMipSampledViews);
|
||||||
std::swap(this->attachmentViews, that.attachmentViews);
|
std::swap(this->attachmentViews, that.attachmentViews);
|
||||||
|
std::swap(this->alternateSampledViews, that.alternateSampledViews);
|
||||||
|
std::swap(this->storageImageViews, that.storageImageViews);
|
||||||
std::swap(this->layout, that.layout);
|
std::swap(this->layout, that.layout);
|
||||||
std::swap(this->extent, that.extent);
|
std::swap(this->extent, that.extent);
|
||||||
std::swap(this->depth, that.depth);
|
std::swap(this->depth, that.depth);
|
||||||
@@ -126,6 +189,7 @@ public:
|
|||||||
std::swap(this->aspect, that.aspect);
|
std::swap(this->aspect, that.aspect);
|
||||||
std::swap(this->viewType, that.viewType);
|
std::swap(this->viewType, that.viewType);
|
||||||
std::swap(this->sampleCount, that.sampleCount);
|
std::swap(this->sampleCount, that.sampleCount);
|
||||||
|
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
@@ -153,6 +217,16 @@ public:
|
|||||||
vkDestroyImageView(s_device, attachmentView, nullptr);
|
vkDestroyImageView(s_device, attachmentView, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const auto& [_, sampledView] : alternateSampledViews) {
|
||||||
|
if (sampledView != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyImageView(s_device, sampledView, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [_, storageImageView] : storageImageViews) {
|
||||||
|
if (storageImageView != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyImageView(s_device, storageImageView, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||||
vmaDestroyImage(s_allocator, image, allocation);
|
vmaDestroyImage(s_allocator, image, allocation);
|
||||||
}
|
}
|
||||||
@@ -161,6 +235,8 @@ public:
|
|||||||
perMipViews.clear();
|
perMipViews.clear();
|
||||||
perMipSampledViews.clear();
|
perMipSampledViews.clear();
|
||||||
attachmentViews.clear();
|
attachmentViews.clear();
|
||||||
|
alternateSampledViews.clear();
|
||||||
|
storageImageViews.clear();
|
||||||
image = VK_NULL_HANDLE;
|
image = VK_NULL_HANDLE;
|
||||||
allocation = nullptr;
|
allocation = nullptr;
|
||||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
@@ -174,6 +250,7 @@ public:
|
|||||||
aspect = VK_IMAGE_ASPECT_NONE;
|
aspect = VK_IMAGE_ASPECT_NONE;
|
||||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageCreateFlags = 0;
|
||||||
syncedTextureParamsVersion = 0;
|
syncedTextureParamsVersion = 0;
|
||||||
syncedContentVersion = 0;
|
syncedContentVersion = 0;
|
||||||
syncedMipLevelCount = 0;
|
syncedMipLevelCount = 0;
|
||||||
@@ -198,6 +275,9 @@ public:
|
|||||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||||
VkImageViewType viewType);
|
VkImageViewType viewType);
|
||||||
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||||
|
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
|
||||||
|
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||||
|
VkFormat format, Bool layered, Int32 layer);
|
||||||
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
|
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
|
||||||
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
||||||
MG_State::GLState::ITextureObject* texture,
|
MG_State::GLState::ITextureObject* texture,
|
||||||
@@ -205,8 +285,16 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
|
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||||
|
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||||
|
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||||
|
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||||
|
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||||
|
|
||||||
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
|
||||||
|
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||||
|
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
|
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||||
|
|
||||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||||
@@ -255,7 +343,8 @@ private:
|
|||||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||||
Uint32 baseArrayLayer,
|
Uint32 baseArrayLayer,
|
||||||
Uint32 layerCount,
|
Uint32 layerCount,
|
||||||
const VkComponentMapping* components = nullptr) const;
|
const VkComponentMapping* components = nullptr,
|
||||||
|
VkImageUsageFlags viewUsage = 0) const;
|
||||||
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
||||||
TextureUploadTarget uploadTarget,
|
TextureUploadTarget uploadTarget,
|
||||||
TextureResource &outResource);
|
TextureResource &outResource);
|
||||||
@@ -296,6 +385,9 @@ private:
|
|||||||
TextureResource* resource = nullptr;
|
TextureResource* resource = nullptr;
|
||||||
};
|
};
|
||||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||||
|
// 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;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 instanceCount = 1;
|
Uint32 instanceCount = 1;
|
||||||
Uint32 firstVertex = 0;
|
Uint32 firstVertex = 0;
|
||||||
Uint32 firstInstance = 0;
|
Uint32 firstInstance = 0;
|
||||||
|
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
|
||||||
|
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
|
||||||
|
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
|
||||||
|
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
|
||||||
|
Int32 baseVertex = 0;
|
||||||
|
Bool indexRangeIsExactView = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DrawIndexedCmdParam {
|
struct DrawIndexedCmdParam {
|
||||||
@@ -165,6 +171,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
|
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||||
|
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||||
|
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||||
|
GLenum destinationType, SizeT destinationRowStride,
|
||||||
|
Uint8* destinationPixels);
|
||||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||||
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
||||||
@@ -365,6 +376,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||||
// drive a runtime fallback when the device lacks them.
|
// drive a runtime fallback when the device lacks them.
|
||||||
@@ -437,9 +449,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||||
// draw call and must not allocate.
|
// draw call and must not allocate.
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||||
|
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||||
|
Vector<Float> m_vertexConversionScratch;
|
||||||
|
Vector<Uint8> m_vertexRepackScratch;
|
||||||
|
|
||||||
|
struct ConvertedVertexStreamKey {
|
||||||
|
const MG_State::GLState::BufferObject* buffer = nullptr;
|
||||||
|
Uint64 changeSerial = 0;
|
||||||
|
SizeT baseOffset = 0;
|
||||||
|
Uint32 sourceStride = 0;
|
||||||
|
DataType type = DataType::Float32;
|
||||||
|
Int size = 0;
|
||||||
|
Bool normalized = false;
|
||||||
|
Bool isInteger = false;
|
||||||
|
VertexInputStateFactory::VertexStreamConversion conversion =
|
||||||
|
VertexInputStateFactory::VertexStreamConversion::None;
|
||||||
|
|
||||||
|
Bool operator==(const ConvertedVertexStreamKey& other) const {
|
||||||
|
return buffer == other.buffer && changeSerial == other.changeSerial &&
|
||||||
|
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
|
||||||
|
type == other.type && size == other.size && normalized == other.normalized &&
|
||||||
|
isInteger == other.isInteger && conversion == other.conversion;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConvertedVertexStreamKeyHash {
|
||||||
|
SizeT operator()(const ConvertedVertexStreamKey& key) const {
|
||||||
|
SizeT hash = std::hash<const void*>{}(key.buffer);
|
||||||
|
auto combine = [&hash](SizeT value) {
|
||||||
|
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
|
||||||
|
};
|
||||||
|
combine(std::hash<Uint64>{}(key.changeSerial));
|
||||||
|
combine(std::hash<SizeT>{}(key.baseOffset));
|
||||||
|
combine(std::hash<Uint32>{}(key.sourceStride));
|
||||||
|
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
|
||||||
|
combine(std::hash<Int>{}(key.size));
|
||||||
|
combine(std::hash<Bool>{}(key.normalized));
|
||||||
|
combine(std::hash<Bool>{}(key.isInteger));
|
||||||
|
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConvertedVertexStream {
|
||||||
|
BufferSlice slice;
|
||||||
|
// Number of source elements the cached slice covers. A draw needing a prefix of
|
||||||
|
// this range reuses the slice (converted streams are tightly packed); a draw
|
||||||
|
// needing more reconverts and replaces the entry, so per (buffer, layout) a
|
||||||
|
// frame converts at most the largest range any draw asked for.
|
||||||
|
SizeT elementCount = 0;
|
||||||
|
// Pins the source buffer for the frame so its heap address cannot be reused by
|
||||||
|
// a new BufferObject while this pointer-keyed entry is alive.
|
||||||
|
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
|
||||||
|
};
|
||||||
|
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
||||||
|
m_convertedVertexStreams;
|
||||||
|
|
||||||
void CreateInstance();
|
void CreateInstance();
|
||||||
VkResult SetupDebugMessenger();
|
VkResult SetupDebugMessenger();
|
||||||
@@ -462,10 +529,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const RenderPassEntry& renderPassEntry);
|
const RenderPassEntry& renderPassEntry);
|
||||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||||
void DestroyComputePipelines();
|
void DestroyComputePipelines();
|
||||||
|
Bool PrepareStorageImageTextures(
|
||||||
|
VkCommandBuffer commandBuffer,
|
||||||
|
const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
|
||||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
const DrawCmdParam& drawParams);
|
const DrawCmdParam& drawParams,
|
||||||
|
const IndexBufferView* pIndexBufferView);
|
||||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::VertexArrayObject& vao,
|
const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
const IndexBufferView* pIndexBufferView = nullptr);
|
||||||
@@ -483,6 +555,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLenum filter);
|
GLenum filter);
|
||||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||||
MG_State::GLState::ITextureObject& texture);
|
MG_State::GLState::ITextureObject& texture);
|
||||||
|
Bool MaterializePendingClearForRenderbuffer(
|
||||||
|
VkCommandBuffer commandBuffer,
|
||||||
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::ITextureObject& texture,
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
|||||||
@@ -295,7 +295,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
|
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
|
||||||
|
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
|
||||||
|
// is both truthful and what the spec requires for a name that was never generated.
|
||||||
|
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
|
||||||
|
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||||
|
return GL_FALSE;
|
||||||
|
}
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
||||||
@@ -418,7 +424,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
|
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||||
@@ -998,8 +1004,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
|
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
|
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
|
||||||
@@ -1063,7 +1069,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture,
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
|
||||||
@@ -2583,7 +2589,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
|
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||||
|
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||||
|
return GL_FALSE;
|
||||||
|
}
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
|
||||||
|
|||||||
@@ -1175,10 +1175,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxFragmentInputComponents;
|
*params = kFrontendMaxFragmentInputComponents;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||||
// TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap.
|
|
||||||
*params = MG_Backend::pActiveBackendObject
|
*params = MG_Backend::pActiveBackendObject
|
||||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms
|
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms
|
||||||
: MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms;
|
: MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
|
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
|
||||||
*params = kFrontendMaxFragmentUniformComponents;
|
*params = kFrontendMaxFragmentUniformComponents;
|
||||||
@@ -1208,7 +1207,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxGeometryTextureImageUnits;
|
*params = kFrontendMaxGeometryTextureImageUnits;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||||
*params = 0;
|
*params = MG_Backend::pActiveBackendObject
|
||||||
|
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
|
||||||
|
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
|
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||||
@@ -1277,7 +1278,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxVertexAtomicCounters;
|
*params = kFrontendMaxVertexAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||||
*params = 0;
|
*params = MG_Backend::pActiveBackendObject
|
||||||
|
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||||
|
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
*params = 16; // TODO
|
*params = 16; // TODO
|
||||||
@@ -1993,4 +1996,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
|
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GLenum GetGraphicsResetStatus() {
|
||||||
|
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
|
||||||
|
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
|
||||||
|
// device after every case (gl3cTestPackages.cpp:121) and, under the default
|
||||||
|
// --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
|
||||||
|
return GL_NO_ERROR;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
GLenum GetError();
|
GLenum GetError();
|
||||||
|
GLenum GetGraphicsResetStatus();
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -350,6 +350,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
||||||
texture.MarkStorageDirty(uploadTarget, level, false);
|
texture.MarkStorageDirty(uploadTarget, level, false);
|
||||||
}
|
}
|
||||||
|
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
|
||||||
|
// grows, so a previously longer chain (a bigger base image before respecification) would
|
||||||
|
// otherwise keep a tail of stale levels here and read as incomplete.
|
||||||
|
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
|
||||||
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
|
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
|
||||||
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
|
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
|
||||||
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
|
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
|
||||||
@@ -429,8 +433,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
||||||
if (samples > maxSamples) {
|
if (samples > maxSamples) {
|
||||||
|
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
|
||||||
|
// exceeds what the format supports, and the native Adreno driver agrees.
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", caller,
|
"MG_Impl/GLImpl", caller,
|
||||||
std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
|
std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
|
||||||
@@ -454,8 +460,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureObject->SetSamples(samples);
|
textureObject->SetSamples(samples);
|
||||||
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
|
||||||
|
// Multisample textures are single-level by definition, so a name that previously held a
|
||||||
|
// mip chain must not keep its tail now that AllocateStorage only grows.
|
||||||
|
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
|
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redefining level 0 of a texture that already had a base image drops the rest of the chain,
|
||||||
|
// which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
|
||||||
|
// behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
|
||||||
|
// any level-0 respecification leaves the chain in precisely the state it would have had
|
||||||
|
// before, while an upload to level N no longer destroys the levels beneath it.
|
||||||
|
//
|
||||||
|
// Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
|
||||||
|
// Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
|
||||||
|
// level count. A size-only test would leave the old tail in place, and because Mojang
|
||||||
|
// terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
|
||||||
|
// IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
|
||||||
|
// entirely (Managers.cpp) and the atlas samples black.
|
||||||
|
//
|
||||||
|
// The "already has a base image" test is what lets the fix work at all: a level that was
|
||||||
|
// never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
|
||||||
|
// then level 0 - must not discard the levels just uploaded. That ordering is what
|
||||||
|
// KHR-GL33.texture_repeat_mode does.
|
||||||
|
// Scoped to the respecified upload target only, which is what AllocateLevel already did.
|
||||||
|
// Cube maps keep six independent chains while reporting a single level count (face +X), so
|
||||||
|
// respecifying a face other than +X can leave the count longer than that face - but that
|
||||||
|
// asymmetry predates this change and widening the truncation to all six faces would destroy
|
||||||
|
// mip data for faces the application never touched. Left alone deliberately.
|
||||||
|
void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
|
||||||
|
TextureUploadTarget uploadTarget, Uint level) {
|
||||||
|
if (level != 0) return;
|
||||||
|
|
||||||
|
const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
|
||||||
|
const Bool hasExistingBaseImage =
|
||||||
|
existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
|
||||||
|
if (!hasExistingBaseImage) return;
|
||||||
|
|
||||||
|
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.
|
||||||
|
void RecordUnsupportedCompressedFormat(const char* caller) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Compressed texture formats are not supported."));
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||||
@@ -470,6 +524,223 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return textureObject;
|
return textureObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
|
||||||
|
const char* caller) {
|
||||||
|
if (texture == 0) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
"Clear texture operations require a non-zero texture name.");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto textureObject = GetTextureObjectByName(texture, caller);
|
||||||
|
if (!textureObject) return nullptr;
|
||||||
|
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
"Buffer textures cannot be cleared with glClearTexImage.");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
|
||||||
|
if (level < 0) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidValue,
|
||||||
|
std::format("Texture level {} is negative.", level));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// ARB_clear_texture: clearing an image that was never defined by TexImage*/
|
||||||
|
// TexStorage* is INVALID_OPERATION, not INVALID_VALUE.
|
||||||
|
if (static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
std::format("Texture level {} is not defined.", level));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return mipmapTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
|
||||||
|
GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
|
||||||
|
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||||
|
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||||
|
if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
|
||||||
|
!TextureImpl::ValidateTexturePixelDataType(inputType) ||
|
||||||
|
!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
||||||
|
inputFormat, textureObject->GetFormat(), inputType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPixel.clear();
|
||||||
|
if (data == nullptr) {
|
||||||
|
// ARB_clear_texture defines a null clear value as all zeroes. Keeping the
|
||||||
|
// pattern empty lets the region writer use a fast memset path.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PixelStoreParameters clearPixelStore{};
|
||||||
|
clearPixelStore.Alignment = 1;
|
||||||
|
SizeT clearPixelSize = 0;
|
||||||
|
void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
|
||||||
|
data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
|
||||||
|
{1, 1, 1}, false, clearPixelSize);
|
||||||
|
if (!converted || clearPixelSize == 0) {
|
||||||
|
if (converted) free(converted);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPixel.resize(clearPixelSize);
|
||||||
|
Memcpy(clearPixel.data(), converted, clearPixelSize);
|
||||||
|
free(converted);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the clear into the CPU shadow and marks the whole level dirty, exactly like
|
||||||
|
// TexSubImage*_State does. Shared limitation of the level-granular shadow sync: the
|
||||||
|
// shadow does not reflect GPU-side writes (FBO rendering, imageStore), so a PARTIAL
|
||||||
|
// clear of a GPU-written level re-uploads stale shadow bytes outside the region on
|
||||||
|
// the next sync. Full-level clears (glClearTexImage, or a sub-clear covering the
|
||||||
|
// level) rewrite the entire shadow and are always correct.
|
||||||
|
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
|
||||||
|
TextureUploadTarget uploadTarget, GLint level,
|
||||||
|
GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
|
GLsizei width, GLsizei height, GLsizei depth,
|
||||||
|
const Vector<Uint8>& clearPixel, const char* caller) {
|
||||||
|
const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||||
|
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
"The requested texture level has no storage.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
|
||||||
|
width < 0 || height < 0 || depth < 0 ||
|
||||||
|
width > texelSize.x() - xoffset ||
|
||||||
|
height > texelSize.y() - yoffset ||
|
||||||
|
depth > texelSize.z() - zoffset) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidValue,
|
||||||
|
"The clear region lies outside the requested texture level.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (width == 0 || height == 0 || depth == 0) return true;
|
||||||
|
|
||||||
|
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||||
|
static_cast<SizeT>(texelSize.y()) *
|
||||||
|
static_cast<SizeT>(texelSize.z());
|
||||||
|
const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
|
||||||
|
if (byteSize == 0 || byteSize % texelCount != 0) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
"The requested texture storage cannot be cleared.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT bytesPerTexel = byteSize / texelCount;
|
||||||
|
if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
|
||||||
|
RecordClearTextureError(
|
||||||
|
caller, ErrorCode::InvalidOperation,
|
||||||
|
std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
|
||||||
|
clearPixel.size(), bytesPerTexel));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* destination = static_cast<Uint8*>(
|
||||||
|
textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
|
||||||
|
if (!destination) {
|
||||||
|
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
||||||
|
"The requested texture level could not be mapped.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
|
||||||
|
const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
|
||||||
|
const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
|
||||||
|
Uint8* firstClearRow = nullptr;
|
||||||
|
|
||||||
|
for (GLsizei z = 0; z < depth; ++z) {
|
||||||
|
for (GLsizei y = 0; y < height; ++y) {
|
||||||
|
Uint8* row = destination +
|
||||||
|
static_cast<SizeT>(zoffset + z) * fullSliceBytes +
|
||||||
|
static_cast<SizeT>(yoffset + y) * fullRowBytes +
|
||||||
|
static_cast<SizeT>(xoffset) * bytesPerTexel;
|
||||||
|
if (firstClearRow) {
|
||||||
|
Memcpy(row, firstClearRow, clearRowBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstClearRow = row;
|
||||||
|
if (clearPixel.empty()) {
|
||||||
|
Memset(row, 0, clearRowBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Memcpy(row, clearPixel.data(), bytesPerTexel);
|
||||||
|
SizeT filled = bytesPerTexel;
|
||||||
|
while (filled < clearRowBytes) {
|
||||||
|
const SizeT copySize = std::min(filled, clearRowBytes - filled);
|
||||||
|
Memcpy(row + filled, row, copySize);
|
||||||
|
filled += copySize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
|
||||||
|
auto textureObject = GetClearTextureObject(texture, level, __func__);
|
||||||
|
if (!textureObject) return;
|
||||||
|
|
||||||
|
Vector<Uint8> clearPixel;
|
||||||
|
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
|
||||||
|
|
||||||
|
for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
|
||||||
|
const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||||
|
if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
|
||||||
|
size.x(), size.y(), size.z(), clearPixel, __func__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
|
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
|
||||||
|
const void* data) {
|
||||||
|
auto textureObject = GetClearTextureObject(texture, level, __func__);
|
||||||
|
if (!textureObject) return;
|
||||||
|
|
||||||
|
Vector<Uint8> clearPixel;
|
||||||
|
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
|
||||||
|
|
||||||
|
const auto& uploadTargets = textureObject->GetUploadTargets();
|
||||||
|
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
|
||||||
|
if (zoffset < 0 || depth < 0 ||
|
||||||
|
static_cast<SizeT>(zoffset) > uploadTargets.size() ||
|
||||||
|
static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
|
||||||
|
RecordClearTextureError(__func__, ErrorCode::InvalidValue,
|
||||||
|
"The cube-map clear region selects invalid faces.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei face = 0; face < depth; ++face) {
|
||||||
|
if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
|
||||||
|
xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadTargets.empty()) {
|
||||||
|
RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
|
||||||
|
"The requested texture has no upload target.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
|
||||||
|
width, height, depth, clearPixel, __func__);
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
GLenum pname, GLint param, const char* caller) {
|
GLenum pname, GLint param, const char* caller) {
|
||||||
const auto target = textureObject->GetTarget();
|
const auto target = textureObject->GetTarget();
|
||||||
@@ -1453,6 +1724,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RGTC is a 2D-only compression scheme, so a 3D target rejects it. This has to be tested on
|
||||||
|
// the raw enum: the RGTC formats resolve to plain R8/RG8/SNORM storage on the way in (see
|
||||||
|
// GLToMG's TextureEnumConverter), so once the internal format is converted there is nothing
|
||||||
|
// left to distinguish them from an ordinary one- or two-channel upload.
|
||||||
|
if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
|
||||||
|
textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
|
||||||
|
(internalformat == GL_COMPRESSED_RED_RGTC1 || internalformat == GL_COMPRESSED_SIGNED_RED_RGTC1 ||
|
||||||
|
internalformat == GL_COMPRESSED_RG_RGTC2 || internalformat == GL_COMPRESSED_SIGNED_RG_RGTC2)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"RGTC compressed formats are invalid for 3D texture targets"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
|
// TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
|
||||||
// GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
|
// GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
|
||||||
// GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
|
// GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
|
||||||
@@ -1510,6 +1796,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (isProxy) {
|
if (isProxy) {
|
||||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||||
} else {
|
} else {
|
||||||
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1637,6 +1924,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||||
} else {
|
} else {
|
||||||
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
|
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
|
||||||
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||||
{{width, height, 1}, internalBytes});
|
{{width, height, 1}, internalBytes});
|
||||||
}
|
}
|
||||||
@@ -1725,6 +2013,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Texture object here should always be an object with mipmap");
|
"Texture object here should always be an object with mipmap");
|
||||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
if (!isProxy) {
|
if (!isProxy) {
|
||||||
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2377,7 +2666,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
|
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
|
||||||
// TODO: implement
|
// 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."));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenTextures_State(GLsizei n, GLuint* textures) {
|
void GenTextures_State(GLsizei n, GLuint* textures) {
|
||||||
@@ -2564,20 +2859,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
||||||
const void* data) {
|
const void* data) {
|
||||||
// TODO: implement
|
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedCompressedFormat(__func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||||
// TODO: implement
|
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedCompressedFormat(__func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||||
GLsizei imageSize, const void* data) {
|
GLsizei imageSize, const void* data) {
|
||||||
// TODO: implement
|
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedCompressedFormat(__func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
@@ -2587,8 +2882,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||||
|
|
||||||
// TODO: implement
|
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedCompressedFormat(__func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
@@ -2598,8 +2893,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||||
|
|
||||||
// TODO: implement
|
// TODO: implement compressed upload. Until then report the spec error for an
|
||||||
THROW_UNIMPL_EXCEPTION;
|
// 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__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||||
@@ -2609,8 +2907,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||||
|
|
||||||
// TODO: implement
|
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||||
THROW_UNIMPL_EXCEPTION;
|
RecordUnsupportedCompressedFormat(__func__);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindTexture_State(GLenum target, GLuint texture) {
|
void BindTexture_State(GLenum target, GLuint texture) {
|
||||||
@@ -2956,6 +3254,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||||
}
|
}
|
||||||
|
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
||||||
|
// longer pre-existing chain has to be dropped explicitly.
|
||||||
|
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3008,6 +3309,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||||
}
|
}
|
||||||
|
// See TextureStorage1D.
|
||||||
|
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3060,6 +3363,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||||
}
|
}
|
||||||
|
// See TextureStorage1D.
|
||||||
|
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3818,6 +4123,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
|
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height) {
|
||||||
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
|
if (!textureObject) return;
|
||||||
|
// GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
|
||||||
|
// go through CopyTextureSubImage3D with the face as a layer.
|
||||||
|
const auto target = textureObject->GetTarget();
|
||||||
|
if (target != TextureTarget::Texture2D && target != TextureTarget::Texture1DArray &&
|
||||||
|
target != TextureTarget::TextureRectangle) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"CopyTextureSubImage2D requires a 2D, 1D-array, or "
|
||||||
|
"rectangle texture."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) {
|
||||||
|
CopyTexSubImage2D_Backend(glTarget, level, xoffset, yoffset, x, y, width, height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
|
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
|
||||||
CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
|
CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||||
|
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
|
||||||
|
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||||
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
|
||||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||||
GLenum format);
|
GLenum format);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
@@ -95,6 +98,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLsizei width, GLsizei height);
|
GLsizei width, GLsizei height);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height);
|
GLsizei height);
|
||||||
|
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height);
|
||||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
|
|||||||
@@ -16,17 +16,32 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
|
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
|
||||||
m_data.reserve(std::bit_ceil(level + 1));
|
// Grow only. GL respecifies exactly the level it is handed, so allocating level 0
|
||||||
m_data.resize(level + 1);
|
// must not disturb the levels above it - but resize() shrinks as readily as it
|
||||||
m_texelSizes.reserve(std::bit_ceil(level + 1));
|
// grows, so this used to truncate the whole chain to a single level. Callers that
|
||||||
m_texelSizes.resize(level + 1);
|
// genuinely redefine the complete level set say so with TruncateToLevelCount.
|
||||||
m_texelSizes[level] = input.texelSize;
|
const SizeT requiredLevelCount = static_cast<SizeT>(level) + 1;
|
||||||
m_isDirty.resize(level + 1, false);
|
if (m_data.size() < requiredLevelCount) {
|
||||||
|
m_data.reserve(std::bit_ceil(requiredLevelCount));
|
||||||
|
m_data.resize(requiredLevelCount);
|
||||||
|
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||||
|
m_texelSizes.resize(requiredLevelCount);
|
||||||
|
m_isDirty.resize(requiredLevelCount, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_texelSizes[level] = input.texelSize;
|
||||||
auto& data = m_data[level];
|
auto& data = m_data[level];
|
||||||
data.resize(input.byteSize, 0);
|
data.resize(input.byteSize, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||||
|
if (levelCount >= m_data.size()) return;
|
||||||
|
|
||||||
|
m_data.resize(levelCount);
|
||||||
|
m_texelSizes.resize(levelCount);
|
||||||
|
m_isDirty.resize(levelCount);
|
||||||
|
}
|
||||||
|
|
||||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||||
auto& targetData = m_data;
|
auto& targetData = m_data;
|
||||||
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
||||||
@@ -55,6 +70,7 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SizeT MipmapStorage::GetByteSize(Uint level) const {
|
SizeT MipmapStorage::GetByteSize(Uint level) const {
|
||||||
|
if (level >= m_data.size()) return 0;
|
||||||
return m_data[level].size();
|
return m_data[level].size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ namespace MobileGL {
|
|||||||
public:
|
public:
|
||||||
SizeT GetLevelCount() const;
|
SizeT GetLevelCount() const;
|
||||||
void AllocateLevel(Uint level, MipmapInput input);
|
void AllocateLevel(Uint level, MipmapInput input);
|
||||||
|
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
|
||||||
|
// is the only way a chain gets shorter - use it where the caller defines the whole
|
||||||
|
// level set (glTexStorage*, mip regeneration, atlas respecification).
|
||||||
|
void TruncateToLevelCount(SizeT levelCount);
|
||||||
void UpdateSubData(Uint level, DataPtr input);
|
void UpdateSubData(Uint level, DataPtr input);
|
||||||
void* MapData(Uint level);
|
void* MapData(Uint level);
|
||||||
IntVec3 GetTexelSize(Uint level) const;
|
IntVec3 GetTexelSize(Uint level) const;
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ namespace MobileGL {
|
|||||||
m_storage[targetIndex].AllocateLevel(level, input);
|
m_storage[targetIndex].AllocateLevel(level, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-target, like AllocateLevel: cube-map faces are respecified independently, so
|
||||||
|
// truncating one face must not disturb the others.
|
||||||
|
void TruncateToLevelCount(Uint targetIndex, SizeT levelCount) {
|
||||||
|
MOBILEGL_ASSERT(targetIndex < TargetCount, "TruncateToLevelCount: target invalid");
|
||||||
|
|
||||||
|
m_storage[targetIndex].TruncateToLevelCount(levelCount);
|
||||||
|
}
|
||||||
|
|
||||||
void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) {
|
void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) {
|
||||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid");
|
MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid");
|
||||||
m_storage[targetIndex].UpdateSubData(level, input);
|
m_storage[targetIndex].UpdateSubData(level, input);
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ namespace MobileGL {
|
|||||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||||
|
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||||
|
}
|
||||||
|
|
||||||
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
DataPtr input) {
|
DataPtr input) {
|
||||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||||
virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||||
virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
|
virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
|
||||||
|
// AllocateStorage only ever grows the chain. Callers that define the complete level set -
|
||||||
|
// glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the
|
||||||
|
// leftovers explicitly, so a stale tail can never make the texture silently incomplete.
|
||||||
|
virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0;
|
||||||
virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
|
virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
|
||||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||||
@@ -175,6 +179,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||||
|
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ namespace MobileGL {
|
|||||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||||
|
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||||
|
}
|
||||||
|
|
||||||
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
DataPtr input) {
|
DataPtr input) {
|
||||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace MobileGL {
|
|||||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||||
|
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ namespace {
|
|||||||
struct FakeDriverState {
|
struct FakeDriverState {
|
||||||
// Behavior knobs, configured per test before running the probe.
|
// Behavior knobs, configured per test before running the probe.
|
||||||
GLint maxVertexSsboBlocks = 4;
|
GLint maxVertexSsboBlocks = 4;
|
||||||
|
GLint glesMajorVersion = 3;
|
||||||
|
GLint glesMinorVersion = 1;
|
||||||
|
GLint maxVertexImageUniforms = 2;
|
||||||
|
GLint maxGeometryImageUniforms = 3;
|
||||||
|
GLint maxFragmentImageUniforms = 4;
|
||||||
|
GLint maxComputeImageUniforms = 5;
|
||||||
|
bool maxGeometryImageUniformsQueried = false;
|
||||||
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
|
||||||
// baseInstance word and exposes it through gl_InstanceID.
|
// baseInstance word and exposes it through gl_InstanceID.
|
||||||
bool drawLeaksBaseInstanceWord = false;
|
bool drawLeaksBaseInstanceWord = false;
|
||||||
@@ -88,13 +95,26 @@ namespace {
|
|||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
*data = g_fake.maxVertexSsboBlocks;
|
*data = g_fake.maxVertexSsboBlocks;
|
||||||
break;
|
break;
|
||||||
|
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||||
|
*data = g_fake.maxVertexImageUniforms;
|
||||||
|
break;
|
||||||
|
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||||
|
g_fake.maxGeometryImageUniformsQueried = true;
|
||||||
|
*data = g_fake.maxGeometryImageUniforms;
|
||||||
|
break;
|
||||||
|
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||||
|
*data = g_fake.maxFragmentImageUniforms;
|
||||||
|
break;
|
||||||
|
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||||
|
*data = g_fake.maxComputeImageUniforms;
|
||||||
|
break;
|
||||||
// FillInGLESCapabilities reads the context version before running the
|
// FillInGLESCapabilities reads the context version before running the
|
||||||
// baseInstance probe, which requires ES >= 3.1.
|
// baseInstance probe, which requires ES >= 3.1.
|
||||||
case GL_MAJOR_VERSION:
|
case GL_MAJOR_VERSION:
|
||||||
*data = 3;
|
*data = g_fake.glesMajorVersion;
|
||||||
break;
|
break;
|
||||||
case GL_MINOR_VERSION:
|
case GL_MINOR_VERSION:
|
||||||
*data = 1;
|
*data = g_fake.glesMinorVersion;
|
||||||
break;
|
break;
|
||||||
case GL_NUM_EXTENSIONS:
|
case GL_NUM_EXTENSIONS:
|
||||||
*data = static_cast<GLint>(g_fake.extensions.size());
|
*data = static_cast<GLint>(g_fake.extensions.size());
|
||||||
@@ -417,6 +437,31 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
|
|||||||
ExpectProbeReleasedAllObjects();
|
ExpectProbeReleasedAllObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGeometry) {
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
MobileGL::MG_External::GLESCapabilities es31Caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
||||||
|
EXPECT_EQ(es31Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
|
||||||
|
EXPECT_EQ(es31Caps.MaxGeometryImageUniforms, 0);
|
||||||
|
EXPECT_EQ(es31Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
|
||||||
|
EXPECT_EQ(es31Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
|
||||||
|
EXPECT_FALSE(g_fake.maxGeometryImageUniformsQueried);
|
||||||
|
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.glesMinorVersion = 2;
|
||||||
|
MobileGL::MG_External::GLESCapabilities es32Caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
||||||
|
EXPECT_EQ(es32Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
|
||||||
|
EXPECT_EQ(es32Caps.MaxGeometryImageUniforms, g_fake.maxGeometryImageUniforms);
|
||||||
|
EXPECT_EQ(es32Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
|
||||||
|
EXPECT_EQ(es32Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
|
||||||
|
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||||
|
}
|
||||||
|
|
||||||
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
||||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ add_subdirectory(Texture)
|
|||||||
add_subdirectory(VertexArray)
|
add_subdirectory(VertexArray)
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
add_subdirectory(Query)
|
add_subdirectory(Query)
|
||||||
|
add_subdirectory(Pipeline)
|
||||||
|
add_subdirectory(ShaderTranspiler)
|
||||||
if (ENABLE_INTEGRATION_TESTS)
|
if (ENABLE_INTEGRATION_TESTS)
|
||||||
add_subdirectory(Backend/DirectVulkan)
|
add_subdirectory(Backend/DirectVulkan)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
PipelineQuirkTest
|
||||||
|
PipelineQuirkTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(PipelineQuirkTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
PipelineQuirkTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(PipelineQuirkTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(PipelineQuirkTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipeline/PipelineQuirkTest.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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Backend/DirectVulkan/Renderer/PipelineFactory.h>
|
||||||
|
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::PipelineFactory;
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||||
|
using MobileGL::MG_Config::QuirkOverride;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||||
|
constexpr Uint32 kVendorIdArm = 0x13B5;
|
||||||
|
|
||||||
|
constexpr VkColorComponentFlags kFullColorWriteMask =
|
||||||
|
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
||||||
|
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||||
|
|
||||||
|
// Builds non-separate blend state: the alpha channel repeats the color factors/op, which
|
||||||
|
// is what glBlendFunc/glBlendEquation (as opposed to their *Separate forms) produce.
|
||||||
|
// ShouldSuppressDepthWrite deliberately decides on the color channel alone, so these
|
||||||
|
// cases cover its whole input space; SeparateAlphaAccumulationIsNotStripped below pins
|
||||||
|
// the separate-alpha contract.
|
||||||
|
VkPipelineColorBlendAttachmentState MakeBlendAttachment(Bool blendEnable,
|
||||||
|
VkBlendFactor srcColor,
|
||||||
|
VkBlendFactor dstColor,
|
||||||
|
VkBlendOp colorOp,
|
||||||
|
VkColorComponentFlags colorWriteMask) {
|
||||||
|
VkPipelineColorBlendAttachmentState attachment{};
|
||||||
|
attachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE;
|
||||||
|
attachment.srcColorBlendFactor = srcColor;
|
||||||
|
attachment.dstColorBlendFactor = dstColor;
|
||||||
|
attachment.colorBlendOp = colorOp;
|
||||||
|
attachment.srcAlphaBlendFactor = srcColor;
|
||||||
|
attachment.dstAlphaBlendFactor = dstColor;
|
||||||
|
attachment.alphaBlendOp = colorOp;
|
||||||
|
attachment.colorWriteMask = colorWriteMask;
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glslangValidator -V output for:
|
||||||
|
// #version 450
|
||||||
|
// layout(location = 0) out vec4 outColor;
|
||||||
|
// void main() { outColor = vec4(1.0); gl_FragDepth = 0.5; }
|
||||||
|
// Assigning gl_FragDepth makes glslang emit OpExecutionMode ... DepthReplacing.
|
||||||
|
constexpr Uint32 kFragDepthWriterSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000fu, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x0000000du, 0x00030010u,
|
||||||
|
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u,
|
||||||
|
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||||
|
0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00060005u,
|
||||||
|
0x0000000du, 0x465f6c67u, 0x44676172u, 0x68747065u, 0x00000000u, 0x00040047u,
|
||||||
|
0x00000009u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000000du, 0x0000000bu,
|
||||||
|
0x00000016u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
|
||||||
|
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
|
||||||
|
0x00000004u, 0x00040020u, 0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu,
|
||||||
|
0x00000008u, 0x00000009u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au,
|
||||||
|
0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au,
|
||||||
|
0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x00000006u,
|
||||||
|
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x0004002bu, 0x00000006u,
|
||||||
|
0x0000000eu, 0x3f000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||||
|
0x00000003u, 0x000200f8u, 0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu,
|
||||||
|
0x0003003eu, 0x0000000du, 0x0000000eu, 0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same shader without the gl_FragDepth assignment.
|
||||||
|
constexpr Uint32 kPlainFragmentSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000cu, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0006000fu, 0x00000004u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x00030010u, 0x00000004u,
|
||||||
|
0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u,
|
||||||
|
0x6e69616du, 0x00000000u, 0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu,
|
||||||
|
0x00000000u, 0x00040047u, 0x00000009u, 0x0000001eu, 0x00000000u, 0x00020013u,
|
||||||
|
0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u,
|
||||||
|
0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040020u,
|
||||||
|
0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000008u, 0x00000009u,
|
||||||
|
0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x3f800000u, 0x0007002cu,
|
||||||
|
0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au, 0x0000000au, 0x0000000au,
|
||||||
|
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u,
|
||||||
|
0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu, 0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// glslangValidator -V output for a vertex shader reading gl_InstanceIndex:
|
||||||
|
// #version 450
|
||||||
|
// layout(location = 0) in vec4 inPos;
|
||||||
|
// void main() { gl_Position = inPos + vec4(float(gl_InstanceIndex)); }
|
||||||
|
constexpr Uint32 kInstanceIndexVertexSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u,
|
||||||
|
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
|
||||||
|
0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||||
|
0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu,
|
||||||
|
0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu,
|
||||||
|
0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u,
|
||||||
|
0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u,
|
||||||
|
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du,
|
||||||
|
0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00070005u,
|
||||||
|
0x00000014u, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x7865646eu, 0x00000000u,
|
||||||
|
0x00030047u, 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u,
|
||||||
|
0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu,
|
||||||
|
0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u,
|
||||||
|
0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u,
|
||||||
|
0x00000011u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu,
|
||||||
|
0x0000002bu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
|
||||||
|
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
|
||||||
|
0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu,
|
||||||
|
0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u,
|
||||||
|
0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au,
|
||||||
|
0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu,
|
||||||
|
0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u,
|
||||||
|
0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u,
|
||||||
|
0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u,
|
||||||
|
0x00000001u, 0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu,
|
||||||
|
0x00000013u, 0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u,
|
||||||
|
0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
|
||||||
|
0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u,
|
||||||
|
0x0004003du, 0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u,
|
||||||
|
0x00000016u, 0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u,
|
||||||
|
0x00000016u, 0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u,
|
||||||
|
0x00000012u, 0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du,
|
||||||
|
0x0000000fu, 0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Same, but reading gl_VertexIndex instead: a DIFFERENT input builtin. glslang emits
|
||||||
|
// this for GL's gl_VertexID, so nearly every real vertex shader has one - it is what
|
||||||
|
// separates "declares some builtin" from "declares the InstanceIndex builtin".
|
||||||
|
constexpr Uint32 kVertexIndexVertexSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u,
|
||||||
|
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
|
||||||
|
0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||||
|
0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu,
|
||||||
|
0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu,
|
||||||
|
0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u,
|
||||||
|
0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u,
|
||||||
|
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du,
|
||||||
|
0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00060005u,
|
||||||
|
0x00000014u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u, 0x00030047u,
|
||||||
|
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu,
|
||||||
|
0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u,
|
||||||
|
0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u,
|
||||||
|
0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u,
|
||||||
|
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu, 0x0000002au,
|
||||||
|
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u,
|
||||||
|
0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
|
||||||
|
0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
|
||||||
|
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u,
|
||||||
|
0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au,
|
||||||
|
0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu,
|
||||||
|
0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
|
||||||
|
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u,
|
||||||
|
0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u,
|
||||||
|
0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000013u,
|
||||||
|
0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u, 0x00000007u,
|
||||||
|
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u,
|
||||||
|
0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x0004003du,
|
||||||
|
0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u, 0x00000016u,
|
||||||
|
0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u, 0x00000016u,
|
||||||
|
0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u, 0x00000012u,
|
||||||
|
0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du, 0x0000000fu,
|
||||||
|
0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Owns the reflection module so each test case cleans up after itself.
|
||||||
|
class ReflectModule {
|
||||||
|
public:
|
||||||
|
template <SizeT WordCount>
|
||||||
|
explicit ReflectModule(const Uint32 (&spirv)[WordCount]) {
|
||||||
|
m_created = spvReflectCreateShaderModule(sizeof(spirv), spirv, &m_module) ==
|
||||||
|
SPV_REFLECT_RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
~ReflectModule() {
|
||||||
|
if (m_created) {
|
||||||
|
spvReflectDestroyShaderModule(&m_module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReflectModule(const ReflectModule&) = delete;
|
||||||
|
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||||
|
|
||||||
|
Bool Created() const { return m_created; }
|
||||||
|
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SpvReflectShaderModule m_module{};
|
||||||
|
Bool m_created = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
PipelineFactory::PipelineCreatePayload MakeDepthWritingPayload(
|
||||||
|
const VkPipelineColorBlendAttachmentState& attachment0) {
|
||||||
|
PipelineFactory::PipelineCreatePayload payload{};
|
||||||
|
payload.colorAttachmentCount = 1;
|
||||||
|
payload.depthTestEnable = true;
|
||||||
|
payload.depthWriteEnable = true;
|
||||||
|
payload.colorBlendAttachments[0] = attachment0;
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// --- Device gate: MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE tri-state ---
|
||||||
|
|
||||||
|
TEST(PipelineQuirkDeviceGate, ForceOnEnablesOnAnyVendor) {
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn,
|
||||||
|
kVendorIdArm));
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn,
|
||||||
|
kVendorIdQualcomm));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkDeviceGate, ForceOffDisablesEvenOnQualcomm) {
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOff,
|
||||||
|
kVendorIdQualcomm));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkDeviceGate, AutoDetectsQualcommOnly) {
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto,
|
||||||
|
kVendorIdQualcomm));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto,
|
||||||
|
kVendorIdArm));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkDeviceGate, ForceOnRoundTripsThroughTheFactoryFlag) {
|
||||||
|
const Bool previous = PipelineFactory::IsSuppressBlendedDepthWriteEnabled();
|
||||||
|
PipelineFactory::SetSuppressBlendedDepthWrite(
|
||||||
|
PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn, kVendorIdArm));
|
||||||
|
EXPECT_TRUE(PipelineFactory::IsSuppressBlendedDepthWriteEnabled());
|
||||||
|
PipelineFactory::SetSuppressBlendedDepthWrite(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-pipeline strip decision against the pipeline create-info payload ---
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, MaxBlendIsStripped) {
|
||||||
|
// MC 26.3 OIT depth_bounds: GL_MAX accumulation writing depth - the case the quirk fixes.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MAX, kFullColorWriteMask));
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, MinBlendIsStripped) {
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MIN, kFullColorWriteMask));
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsNotStripped) {
|
||||||
|
// ONE+ONE additive with a depth write matched zero draws of the 26.3 chain in the
|
||||||
|
// fixture sweep (transmittance/accumulate disable depth writes themselves); the only
|
||||||
|
// real content with this shape was harmless additive glow effects (Create). A quirk
|
||||||
|
// touches as little unrelated content as possible, so the shape stays exempt.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) {
|
||||||
|
// Vanilla MC translucent layer (water, stained glass): SRC_ALPHA "over" compositing
|
||||||
|
// draws each surface once and depends on its depth writes to occlude particles, rain,
|
||||||
|
// and clouds drawn later - it must keep them.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
|
||||||
|
kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) {
|
||||||
|
// GL_BLEND left enabled with ONE/ZERO+ADD factors is opaque in effect; stripping its
|
||||||
|
// depth write would break occlusion for plainly opaque geometry.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) {
|
||||||
|
// Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is
|
||||||
|
// moot, and stripping would delete the entire prepass. MAX so the exemption, not the
|
||||||
|
// blend-op filter, is what keeps the depth write.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, DisabledBlendIsNotStripped) {
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, NoDepthWriteMeansNoStrip) {
|
||||||
|
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
|
||||||
|
payload.depthWriteEnable = false;
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) {
|
||||||
|
// gl_FragDepth output does not go through per-pipeline vertex position math, so the
|
||||||
|
// cross-pipeline invariance hazard cannot affect it (e.g. the 26.3 OIT composite).
|
||||||
|
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
|
||||||
|
payload.fragmentReplacesDepth = true;
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
|
||||||
|
// The scan is not limited to attachment 0: an extremum accumulation on any live
|
||||||
|
// attachment marks the pipeline.
|
||||||
|
PipelineFactory::PipelineCreatePayload payload{};
|
||||||
|
payload.colorAttachmentCount = 2;
|
||||||
|
payload.depthTestEnable = true;
|
||||||
|
payload.depthWriteEnable = true;
|
||||||
|
payload.colorBlendAttachments[0] = MakeBlendAttachment(
|
||||||
|
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask);
|
||||||
|
payload.colorBlendAttachments[1] = MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask);
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) {
|
||||||
|
// SRC_ALPHA,ONE additive: the classic *sorted* particle/glow blend. Kept exempt like
|
||||||
|
// every other ADD-op shape now that the strip is extremum-only.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
|
||||||
|
// Deliberate narrowing: only the MIN/MAX extremum ops carry the depth-bounds
|
||||||
|
// signature. SUBTRACT-class ops stay outside the quirk until content demands them.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT,
|
||||||
|
kFullColorWriteMask));
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) {
|
||||||
|
// Only a fully masked attachment is exempt; a live alpha channel still accumulates.
|
||||||
|
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, VK_COLOR_COMPONENT_A_BIT));
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, NoColorAttachmentsMeansNoStrip) {
|
||||||
|
// Depth-only FBO: the loop must not read the (stale) attachment array at all.
|
||||||
|
PipelineFactory::PipelineCreatePayload payload{};
|
||||||
|
payload.colorAttachmentCount = 0;
|
||||||
|
payload.depthTestEnable = true;
|
||||||
|
payload.depthWriteEnable = true;
|
||||||
|
payload.colorBlendAttachments[0] = MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask);
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, SeparateAlphaAccumulationIsNotStripped) {
|
||||||
|
// glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX) over an ordinary color over-blend: the
|
||||||
|
// alpha channel accumulates but the color channel does not. Pins that the decision is
|
||||||
|
// color-channel only - widening it to alpha would re-capture sorted transparency.
|
||||||
|
auto attachment = MakeBlendAttachment(true, VK_BLEND_FACTOR_SRC_ALPHA,
|
||||||
|
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
|
||||||
|
kFullColorWriteMask);
|
||||||
|
attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||||
|
attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||||
|
attachment.alphaBlendOp = VK_BLEND_OP_MAX;
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(MakeDepthWritingPayload(attachment)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PipelineQuirkStripDecision, MixedOverAndMaskedAttachmentsAreNotStripped) {
|
||||||
|
PipelineFactory::PipelineCreatePayload payload{};
|
||||||
|
payload.colorAttachmentCount = 2;
|
||||||
|
payload.depthTestEnable = true;
|
||||||
|
payload.depthWriteEnable = true;
|
||||||
|
payload.colorBlendAttachments[0] = MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
|
||||||
|
kFullColorWriteMask);
|
||||||
|
payload.colorBlendAttachments[1] = MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0);
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DepthReplacing reflection feeding the gl_FragDepth exemption ---
|
||||||
|
|
||||||
|
TEST(ReflectedFragmentReplacesDepth, TrueForAShaderThatAssignsFragDepth) {
|
||||||
|
const ReflectModule module(kFragDepthWriterSpirv);
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_TRUE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedFragmentReplacesDepth, FalseForAPlainFragmentShader) {
|
||||||
|
const ReflectModule module(kPlainFragmentSpirv);
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedFragmentReplacesDepth, FalseForAnEmptyModule) {
|
||||||
|
// A default-constructed module has no entry points; the scan must not dereference.
|
||||||
|
SpvReflectShaderModule emptyModule{};
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(emptyModule));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedFragmentReplacesDepth, ReflectedFlagFlipsTheStripDecision) {
|
||||||
|
// The two fixtures differ only by the gl_FragDepth assignment, so they pin that the
|
||||||
|
// reflected flag is what flips the strip decision for an otherwise identical pipeline.
|
||||||
|
const ReflectModule depthWriter(kFragDepthWriterSpirv);
|
||||||
|
const ReflectModule plain(kPlainFragmentSpirv);
|
||||||
|
ASSERT_TRUE(depthWriter.Created());
|
||||||
|
ASSERT_TRUE(plain.Created());
|
||||||
|
|
||||||
|
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||||
|
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
|
||||||
|
|
||||||
|
payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(plain.Get());
|
||||||
|
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
|
||||||
|
payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(depthWriter.Get());
|
||||||
|
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- InstanceIndex reflection feeding the shaderDrawParameters diagnostic ---
|
||||||
|
|
||||||
|
TEST(ReflectedReadsInstanceIndexBuiltin, TrueForAShaderReadingInstanceIndex) {
|
||||||
|
const ReflectModule module(kInstanceIndexVertexSpirv);
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_TRUE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderReadingADifferentBuiltin) {
|
||||||
|
// Discriminates the builtin's identity, not merely its presence: weakening the check to
|
||||||
|
// "has any BuiltIn decoration" would fire the diagnostic on every real vertex shader.
|
||||||
|
const ReflectModule module(kVertexIndexVertexSpirv);
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderWithNoInputBuiltins) {
|
||||||
|
const ReflectModule module(kPlainFragmentSpirv);
|
||||||
|
ASSERT_TRUE(module.Created());
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAnEmptyModule) {
|
||||||
|
SpvReflectShaderModule emptyModule{};
|
||||||
|
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(emptyModule));
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <spirv_reflect.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -1325,6 +1326,88 @@ TEST_F(ProgramTest, CompileAndLinkWithExplicitVertexIn) {
|
|||||||
<< "\")";
|
<< "\")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramTest, InactiveExplicitVertexBindingsDoNotReserveLocations) {
|
||||||
|
const char* vertexSource = R"(#version 430 compatibility
|
||||||
|
|
||||||
|
in vec3 Position;
|
||||||
|
in vec2 UV0;
|
||||||
|
in vec3 vaPosition;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(vaPosition, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* fragmentSource = R"(#version 430 compatibility
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
GLuint vertexShader = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
ShaderSource(vertexShader, 1, &vertexSource, nullptr);
|
||||||
|
CompileShader(vertexShader);
|
||||||
|
GLint compileStatus = GL_FALSE;
|
||||||
|
GetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileStatus);
|
||||||
|
ASSERT_EQ(compileStatus, GL_TRUE);
|
||||||
|
|
||||||
|
GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
ShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
|
||||||
|
CompileShader(fragmentShader);
|
||||||
|
GetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileStatus);
|
||||||
|
ASSERT_EQ(compileStatus, GL_TRUE);
|
||||||
|
|
||||||
|
GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, vertexShader);
|
||||||
|
AttachShader(program, fragmentShader);
|
||||||
|
|
||||||
|
// Iris binds these canonical names before linking every program. Its compatibility
|
||||||
|
// transformer can inject both declarations even when the shader pack instead reads
|
||||||
|
// vaPosition. Inactive API bindings must not consume locations during the link.
|
||||||
|
BindAttribLocation(program, 0, "Position");
|
||||||
|
BindAttribLocation(program, 1, "UV0");
|
||||||
|
LinkProgram(program);
|
||||||
|
|
||||||
|
GLint linkStatus = GL_FALSE;
|
||||||
|
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||||
|
ASSERT_EQ(linkStatus, GL_TRUE);
|
||||||
|
|
||||||
|
EXPECT_EQ(GetAttribLocation(program, "Position"), -1);
|
||||||
|
EXPECT_EQ(GetAttribLocation(program, "UV0"), -1);
|
||||||
|
EXPECT_EQ(GetAttribLocation(program, "vaPosition"), 0);
|
||||||
|
|
||||||
|
auto programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
ASSERT_NE(programObject, nullptr);
|
||||||
|
const Int vertexIndex = programObject->GetShaderIndexByStage(ShaderStage::Vertex);
|
||||||
|
ASSERT_GE(vertexIndex, 0);
|
||||||
|
const auto& spirvs = programObject->GetGeneratedSpirv();
|
||||||
|
ASSERT_LT(static_cast<SizeT>(vertexIndex), spirvs.size());
|
||||||
|
|
||||||
|
const auto& vertexSpirv = spirvs[vertexIndex];
|
||||||
|
spv_reflect::ShaderModule reflection(vertexSpirv.size() * sizeof(Uint), vertexSpirv.data());
|
||||||
|
ASSERT_EQ(reflection.GetResult(), SPV_REFLECT_RESULT_SUCCESS);
|
||||||
|
|
||||||
|
uint32_t inputCount = 0;
|
||||||
|
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, nullptr), SPV_REFLECT_RESULT_SUCCESS);
|
||||||
|
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
|
||||||
|
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
|
||||||
|
|
||||||
|
Uint32 userInputCount = 0;
|
||||||
|
Uint32 locationMask = 0;
|
||||||
|
for (const auto* input : inputs) {
|
||||||
|
if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ASSERT_LT(input->location, 32u);
|
||||||
|
locationMask |= 1u << input->location;
|
||||||
|
++userInputCount;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(userInputCount, 1u);
|
||||||
|
EXPECT_EQ(locationMask, 0x1u);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) {
|
TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) {
|
||||||
char infoLog[1024] = "";
|
char infoLog[1024] = "";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include "Init.h"
|
#include "Init.h"
|
||||||
@@ -92,6 +94,120 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
|||||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
EXPECT_EQ(exactSamplerNameCount, 1u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
layout(rgba16, binding = 0) uniform image2D floatImage;
|
||||||
|
layout(r32ui, binding = 1) uniform uimage2D atomicImage;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 coordinate = ivec2(gl_GlobalInvocationID.xy);
|
||||||
|
imageStore(floatImage, coordinate, imageLoad(floatImage, coordinate));
|
||||||
|
imageAtomicAdd(atomicImage, coordinate, 1u);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
ASSERT_TRUE(programResult) << programResult.error().log;
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
||||||
|
ASSERT_EQ(binaryResult->size(), 1u);
|
||||||
|
const auto& inputBinary = binaryResult->front();
|
||||||
|
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String inputText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
|
||||||
|
EXPECT_NE(inputText.find("2D 0 0 0 2 Rgba16"), String::npos) << inputText;
|
||||||
|
EXPECT_NE(inputText.find("2D 0 0 0 2 R32ui"), String::npos) << inputText;
|
||||||
|
EXPECT_EQ(inputText.find("StorageImageReadWithoutFormat"), String::npos) << inputText;
|
||||||
|
EXPECT_EQ(inputText.find("StorageImageWriteWithoutFormat"), String::npos) << inputText;
|
||||||
|
|
||||||
|
Vector<Uint32> outputBinary;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
|
||||||
|
|
||||||
|
String outputText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||||
|
EXPECT_EQ(outputText.find("2D 0 0 0 2 Rgba16"), String::npos) << outputText;
|
||||||
|
EXPECT_NE(outputText.find("2D 0 0 0 2 Unknown"), String::npos) << outputText;
|
||||||
|
EXPECT_NE(outputText.find("2D 0 0 0 2 R32ui"), String::npos) << outputText;
|
||||||
|
|
||||||
|
const auto countOccurrences = [](const String& text, const String& needle) {
|
||||||
|
SizeT count = 0;
|
||||||
|
for (SizeT offset = 0; (offset = text.find(needle, offset)) != String::npos;
|
||||||
|
offset += needle.size()) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageReadWithoutFormat"), 1u)
|
||||||
|
<< outputText;
|
||||||
|
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageWriteWithoutFormat"), 1u)
|
||||||
|
<< outputText;
|
||||||
|
EXPECT_TRUE(tools.Validate(outputBinary));
|
||||||
|
|
||||||
|
Vector<Uint32> secondOutputBinary;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(outputBinary, secondOutputBinary));
|
||||||
|
EXPECT_EQ(secondOutputBinary, outputBinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepFloatAtomicImageTypesTyped) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String spirvText = R"(
|
||||||
|
OpCapability Shader
|
||||||
|
OpCapability StorageImageExtendedFormats
|
||||||
|
OpMemoryModel Logical GLSL450
|
||||||
|
OpEntryPoint GLCompute %main "main"
|
||||||
|
OpExecutionMode %main LocalSize 1 1 1
|
||||||
|
OpDecorate %target DescriptorSet 0
|
||||||
|
OpDecorate %target Binding 0
|
||||||
|
%void = OpTypeVoid
|
||||||
|
%float = OpTypeFloat 32
|
||||||
|
%int = OpTypeInt 32 1
|
||||||
|
%v2int = OpTypeVector %int 2
|
||||||
|
%image = OpTypeImage %float 2D 0 0 0 2 R32f
|
||||||
|
%imageUniformPtr = OpTypePointer UniformConstant %image
|
||||||
|
%imageTexelPtr = OpTypePointer Image %float
|
||||||
|
%mainType = OpTypeFunction %void
|
||||||
|
%zero = OpConstant %int 0
|
||||||
|
%coordinate = OpConstantComposite %v2int %zero %zero
|
||||||
|
%target = OpVariable %imageUniformPtr UniformConstant
|
||||||
|
%main = OpFunction %void None %mainType
|
||||||
|
%entry = OpLabel
|
||||||
|
%texelPtr = OpImageTexelPointer %imageTexelPtr %target %coordinate %zero
|
||||||
|
OpReturn
|
||||||
|
OpFunctionEnd
|
||||||
|
)";
|
||||||
|
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
Vector<Uint32> inputBinary;
|
||||||
|
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
|
||||||
|
|
||||||
|
Vector<Uint32> outputBinary;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
|
||||||
|
|
||||||
|
String outputText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||||
|
EXPECT_NE(outputText.find("2D 0 0 0 2 R32f"), String::npos) << outputText;
|
||||||
|
EXPECT_EQ(outputText.find("StorageImageReadWithoutFormat"), String::npos) << outputText;
|
||||||
|
EXPECT_EQ(outputText.find("StorageImageWriteWithoutFormat"), String::npos) << outputText;
|
||||||
|
String validationDiagnostics;
|
||||||
|
tools.SetMessageConsumer([&validationDiagnostics](spv_message_level_t, const char*,
|
||||||
|
const spv_position_t&, const char* message) {
|
||||||
|
validationDiagnostics += message;
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(tools.Validate(outputBinary)) << validationDiagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) {
|
TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
@@ -122,6 +238,77 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KHR-GL33.shaders.preprocessor.* — a block comment is one preprocessing token that the C/GLSL
|
||||||
|
// preprocessor replaces with a single space, even when it spans newlines inside a directive. glslang
|
||||||
|
// handles this natively, so MobileGL must not mangle it. These reproduce the CTS cases that failed
|
||||||
|
// because comment blanking preserved the interior newline, truncating multi-line #define bodies.
|
||||||
|
static void ExpectCompiles(MobileGL::ShaderStage stage, GLenum glStage, MobileGL::String source) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
PreprocessShaderSource(stage, source);
|
||||||
|
ShaderAttrib attrib{.shaderType = glStage, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessMultilineCommentInDefineBodyCompiles) {
|
||||||
|
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||||
|
R"(#version 330
|
||||||
|
precision mediump float;
|
||||||
|
out float out0;
|
||||||
|
#define VALUE /* current
|
||||||
|
value */ 4.2
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
out0 = VALUE;
|
||||||
|
})");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessRedefineObjectMultilineCommentCompiles) {
|
||||||
|
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||||
|
R"(#version 330
|
||||||
|
precision mediump float;
|
||||||
|
out float out0;
|
||||||
|
# define VAL1 1.0
|
||||||
|
#define VAL2 2.0
|
||||||
|
|
||||||
|
#define RES2 /* fdsjklfdsjkl
|
||||||
|
dsfjkhfdsjkh
|
||||||
|
fdsjklhfdsjkh */ (RES1 * VAL2)
|
||||||
|
#define RES1 (VAL2 / VAL1)
|
||||||
|
#define RES2 /* ewrlkjhsadf */ (RES1 * VAL2)
|
||||||
|
#define VALUE (RES2 + RES1)
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
out0 = VALUE;
|
||||||
|
})");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessFunctionMacroRedefinitionMultilineCommentCompiles) {
|
||||||
|
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||||
|
R"(#version 330
|
||||||
|
precision mediump float;
|
||||||
|
out float out0;
|
||||||
|
# define FUNC(a,b) (a +b)
|
||||||
|
# define FUNC(a,b)(a /* comment
|
||||||
|
*/ +b)
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
out0 = FUNC(1.0, 2.0);
|
||||||
|
})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: KHR-GL3x.shaders.preprocessor.conditional_inclusion.basic_2 (`#define AAA defined(BBB)` used
|
||||||
|
// in `#if !AAA`) is intentionally NOT handled here. Generating the `defined` operator via macro
|
||||||
|
// expansion is undefined per the C/GLSL preprocessor spec, and glslang deliberately rejects it
|
||||||
|
// ("'defined' : cannot use in preprocessor expression when expanded from macros"). Making it pass
|
||||||
|
// would require MobileGL to run its own macro expansion ahead of glslang, which is exactly the
|
||||||
|
// preprocessing we defer to glslang; the two cases stay failing by design.
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesGlmarkStyleSource) {
|
TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesGlmarkStyleSource) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
@@ -310,6 +497,62 @@ void main() {
|
|||||||
verifyVersion("#version 460 core");
|
verifyVersion("#version 460 core");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KHR-GL33.shaders.preprocessor.directive.version_* (also re-run verbatim under GL40-GL44): the
|
||||||
|
// compiler must REJECT a malformed #version line. MobileGL used to rewrite the whole line to
|
||||||
|
// "#version 330 core" whenever it could scrape a leading integer - or treat an unknown profile token
|
||||||
|
// as core - which silently legalized every form below. CTS compiles the shader's own #version
|
||||||
|
// verbatim, so the rejection has to survive preprocessing (and the 460 retry).
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessRejectsMalformedVersionDirectives) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const char* body = "\nout vec4 fragColor;\nvoid main() { fragColor = vec4(1.0); }\n";
|
||||||
|
const auto rejects = [](const String& fullSource) {
|
||||||
|
String src = fullSource;
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, src);
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = src};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
return res ? false : true; // "rejects" == compile failed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Silently legalized today - the five this fix must flip to rejection:
|
||||||
|
EXPECT_TRUE(rejects(String("#version 329") + body)) << "329 is not a real version";
|
||||||
|
EXPECT_TRUE(rejects(String("#version 331") + body)) << "331 is not a real version";
|
||||||
|
EXPECT_TRUE(rejects(String("#version 330 foo") + body)) << "unknown profile keyword";
|
||||||
|
EXPECT_TRUE(rejects(String("#version 330.0") + body)) << "float literal, not an int token";
|
||||||
|
EXPECT_TRUE(rejects(String("#version 330 foobar") + body)) << "trailing tokens after a valid decl";
|
||||||
|
|
||||||
|
// Already rejected (no leading integer, or #version is not the first token) - pinned so a future
|
||||||
|
// change to the normalizer cannot start legalizing them either:
|
||||||
|
EXPECT_TRUE(rejects(String("#version") + body)) << "missing version number";
|
||||||
|
EXPECT_TRUE(rejects(String("#version foobar") + body)) << "identifier where the int belongs";
|
||||||
|
EXPECT_TRUE(rejects(String("#version AAA") + body)) << "identifier where the int belongs";
|
||||||
|
EXPECT_TRUE(rejects(String("precision mediump float;\n#version 330") + body))
|
||||||
|
<< "#version must be the first statement";
|
||||||
|
EXPECT_TRUE(rejects(String("#define FOO BAR\n#version 330") + body))
|
||||||
|
<< "#version must precede a #define";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PASS half of the same CTS group: a valid decl, and #version preceded only by whitespace or a
|
||||||
|
// comment, must still compile. Guards the fix above from over-rejecting.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessKeepsValidVersionDirectivesCompiling) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const char* body = "\nout vec4 fragColor;\nvoid main() { fragColor = vec4(1.0); }\n";
|
||||||
|
const auto compiles = [](const String& fullSource) {
|
||||||
|
String src = fullSource;
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, src);
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = src};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
return res ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_TRUE(compiles(String("#version 330 core") + body));
|
||||||
|
EXPECT_TRUE(compiles(String("\n#version 330 core") + body))
|
||||||
|
<< "leading whitespace is legal before #version";
|
||||||
|
EXPECT_TRUE(compiles(String("// test\n#version 330 core") + body))
|
||||||
|
<< "a leading comment is legal before #version";
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessUsesRealSpacedVersionDirectiveForInjectedOutput) {
|
TEST_F(ProgramUtilTest, PreprocessUsesRealSpacedVersionDirectiveForInjectedOutput) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
@@ -330,6 +573,9 @@ void main() {
|
|||||||
EXPECT_NE(versionPos, String::npos);
|
EXPECT_NE(versionPos, String::npos);
|
||||||
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
|
EXPECT_EQ(outputPos, versionPos + std::strlen("#version 330 core\n"));
|
||||||
EXPECT_NE(source.find("// #version 460 core"), String::npos);
|
EXPECT_NE(source.find("// #version 460 core"), String::npos);
|
||||||
|
// This #line sits ahead of the version directive, where GLSL would never have honoured it, so
|
||||||
|
// it is still dropped. Directives that follow the version line are kept - see
|
||||||
|
// PreprocessKeepsPlainLineDirectivesAndSparesLookalikeIdentifiers.
|
||||||
EXPECT_EQ(source.find("#line"), String::npos);
|
EXPECT_EQ(source.find("#line"), String::npos);
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
@@ -339,6 +585,105 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A banner line like "//*** NOTE ***" contains "/*" at offset 1 and no "*/" anywhere after it. The
|
||||||
|
// old hand-rolled comment stripper searched for "/*" with no lexical state, found that, failed to
|
||||||
|
// find a terminator, and erased everything from there to the end of the file - deleting the entire
|
||||||
|
// shader. Banner comments in that exact shape are common in Iris and OptiFine packs.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessKeepsShaderBodyAfterAStarredLineComment) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 330 core
|
||||||
|
//*** lighting pass ***
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("void main()"), String::npos) << "shader body was truncated:\n" << source;
|
||||||
|
EXPECT_NE(source.find("fragColor = vec4(1.0);"), String::npos);
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The builtin-shadowing rename only fires when the shader really defines its own round/tanh/etc.
|
||||||
|
// Deciding that from a commented-out definition renames every genuine call to the builtin to a
|
||||||
|
// mg_ name that nothing defines, which fails to link.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessIgnoresCommentedOutBuiltinShadowingDefinition) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 330 core
|
||||||
|
// float round(float x) { return floor(x + 0.5); }
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(round(1.25));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("round(1.25)"), String::npos) << "call was renamed from a comment:\n" << source;
|
||||||
|
EXPECT_EQ(source.find("mg_round"), String::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A block-commented extension directive must not be treated as a real one - the int64 filter turns
|
||||||
|
// unsupported directives into #error, so reading one out of a comment manufactures a compile
|
||||||
|
// failure for a shader that never asked for the extension.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessIgnoresBlockCommentedExtensionDirectives) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 330 core
|
||||||
|
/*
|
||||||
|
#extension GL_ARB_gpu_shader_int64 : require
|
||||||
|
*/
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_EQ(source.find("#error"), String::npos) << "#error synthesized from a comment:\n" << source;
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL33.shaders.preprocessor.builtin.line_* checks that __LINE__ follows #line. That only works
|
||||||
|
// if the directive reaches glslang, so a plain integer form must pass through untouched - while
|
||||||
|
// "#linear" and friends must not be mistaken for it.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessKeepsPlainLineDirectivesAndSparesLookalikeIdentifiers) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 330 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
#line 42
|
||||||
|
float linear(float x) { return x; }
|
||||||
|
void main() {
|
||||||
|
#line 100
|
||||||
|
fragColor = vec4(linear(float(__LINE__)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("#line 42"), String::npos) << source;
|
||||||
|
EXPECT_NE(source.find("#line 100"), String::npos) << source;
|
||||||
|
EXPECT_NE(source.find("float linear(float x)"), String::npos) << "identifier lookalike was eaten:\n" << source;
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) {
|
||||||
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
|
TEST_F(ProgramUtilTest, PreprocessModernSampleQualifierStaysAtVersion460) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
@@ -629,6 +974,16 @@ TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDeskt
|
|||||||
String commented = "// #version 330 core\nvoid main() {}\n";
|
String commented = "// #version 330 core\nvoid main() {}\n";
|
||||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented));
|
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented));
|
||||||
EXPECT_EQ(commented.find("#version 460"), String::npos);
|
EXPECT_EQ(commented.find("#version 460"), String::npos);
|
||||||
|
|
||||||
|
// A malformed directive must NOT be rescued to 460 - that is what silently legalized the CTS
|
||||||
|
// directive.version_* rejection cases. The bad version stays put so glslang keeps rejecting it.
|
||||||
|
String badNumber = "#version 331\nvoid main() {}\n";
|
||||||
|
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(badNumber));
|
||||||
|
EXPECT_EQ(badNumber.find("#version 460"), String::npos);
|
||||||
|
|
||||||
|
String badProfile = "#version 330 foo\nvoid main() {}\n";
|
||||||
|
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(badProfile));
|
||||||
|
EXPECT_EQ(badProfile.find("#version 460"), String::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* fs = R"(#version 150
|
const char* fs = R"(#version 150
|
||||||
@@ -805,6 +1160,353 @@ TEST_F(ProgramUtilTest, CompileFragmentShaderWithDiscard) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noperspective is core desktop GLSL (1.30+) and maps to the SPIR-V NoPerspective decoration. It must
|
||||||
|
// reach glslang (not be stripped as text) so the SPIR-V carries the decoration; SPIRV-Cross then emits
|
||||||
|
// ESSL `noperspective` + the GL_NV_shader_noperspective_interpolation extension. Shader packs
|
||||||
|
// (Iris/Complementary) depend on it, and KHR-GL33.glsl_noperspective fails if the result matches
|
||||||
|
// smooth. This is the DirectGLES path with the NV extension available (SPIRV-Cross's default).
|
||||||
|
TEST_F(ProgramUtilTest, NoperspectiveInterpolationSurvivesToEssl) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String fs = R"(#version 330 core
|
||||||
|
noperspective in vec4 vColor;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vColor; }
|
||||||
|
)";
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||||
|
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||||
|
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||||
|
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||||
|
|
||||||
|
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||||
|
|
||||||
|
EXPECT_NE(essl.value().find("noperspective"), String::npos)
|
||||||
|
<< "noperspective was lost before it reached SPIR-V:\n" << essl.value();
|
||||||
|
EXPECT_NE(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||||
|
<< "SPIRV-Cross must require the NV extension for ES noperspective:\n" << essl.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The old handling was a naked substring erase of "noperspective", so any identifier that merely
|
||||||
|
// contained those characters (a uniform named noperspectiveBlend, say) got mangled. Removing the
|
||||||
|
// strip fixes it - glslang, which is identifier-aware, is the only thing that should see the keyword.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessDoesNotCorruptIdentifiersContainingNoperspective) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 330 core
|
||||||
|
uniform float noperspectiveBlend;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(noperspectiveBlend); }
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
EXPECT_NE(source.find("noperspectiveBlend"), String::npos)
|
||||||
|
<< "identifier was corrupted by substring stripping:\n" << source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DirectGLES fallback for devices without GL_NV_shader_noperspective_interpolation: stripping the
|
||||||
|
// NoPerspective decoration makes SPIRV-Cross emit a plain smooth varying with no `#extension … :
|
||||||
|
// require`, so the shader still compiles (rendering as smooth) instead of being rejected by the driver.
|
||||||
|
TEST_F(ProgramUtilTest, StripNoPerspectiveFallbackProducesPlainEssl) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String fs = R"(#version 330 core
|
||||||
|
noperspective in vec4 vColor;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vColor; }
|
||||||
|
)";
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||||
|
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||||
|
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||||
|
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||||
|
|
||||||
|
// Precondition: with the decoration present the default decompile requires the NV extension.
|
||||||
|
{
|
||||||
|
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
if (!essl) FAIL() << "decompile errc: " << essl.error().errc;
|
||||||
|
ASSERT_NE(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fallback strips the decoration -> plain smooth ESSL, no extension require.
|
||||||
|
Vector<Uint32> stripped;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(bin_res.value()[0], stripped));
|
||||||
|
ASSERT_FALSE(stripped.empty());
|
||||||
|
|
||||||
|
SpvcSession session(stripped, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||||
|
EXPECT_EQ(essl.value().find("noperspective"), String::npos)
|
||||||
|
<< "the decoration should be gone:\n" << essl.value();
|
||||||
|
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||||
|
<< "no extension require without the decoration:\n" << essl.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directly exercises BOTH decoration forms StripNoPerspectivePass handles: a plain-variable
|
||||||
|
// OpDecorate NoPerspective (in-operand 1) and an interface-block-member OpMemberDecorate NoPerspective
|
||||||
|
// (in-operand 2). The ESSL round-trip tests above use only a scalar input, so they never reach the
|
||||||
|
// member-decorate branch, which a block varying like `in Block { noperspective vec4 c; }` (common in
|
||||||
|
// shader packs) produces. Unrelated decorations (Flat, Location) must survive untouched.
|
||||||
|
TEST_F(ProgramUtilTest, StripNoPerspectivePassRemovesBothDecorateForms) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String spirvText = R"(
|
||||||
|
OpCapability Shader
|
||||||
|
OpMemoryModel Logical GLSL450
|
||||||
|
OpEntryPoint Fragment %main "main" %plainVar %blockVar %flatVar
|
||||||
|
OpExecutionMode %main OriginUpperLeft
|
||||||
|
OpName %main "main"
|
||||||
|
OpDecorate %plainVar Location 0
|
||||||
|
OpDecorate %plainVar NoPerspective
|
||||||
|
OpMemberDecorate %Block 0 NoPerspective
|
||||||
|
OpDecorate %blockVar Location 1
|
||||||
|
OpDecorate %flatVar Location 2
|
||||||
|
OpDecorate %flatVar Flat
|
||||||
|
%void = OpTypeVoid
|
||||||
|
%mainFn = OpTypeFunction %void
|
||||||
|
%float = OpTypeFloat 32
|
||||||
|
%v4float = OpTypeVector %float 4
|
||||||
|
%int = OpTypeInt 32 1
|
||||||
|
%inV4Ptr = OpTypePointer Input %v4float
|
||||||
|
%plainVar = OpVariable %inV4Ptr Input
|
||||||
|
%Block = OpTypeStruct %v4float
|
||||||
|
%inBlockPtr = OpTypePointer Input %Block
|
||||||
|
%blockVar = OpVariable %inBlockPtr Input
|
||||||
|
%inIntPtr = OpTypePointer Input %int
|
||||||
|
%flatVar = OpVariable %inIntPtr Input
|
||||||
|
%main = OpFunction %void None %mainFn
|
||||||
|
%mainBody = OpLabel
|
||||||
|
OpReturn
|
||||||
|
OpFunctionEnd
|
||||||
|
)";
|
||||||
|
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
Vector<uint32_t> inputBinary;
|
||||||
|
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
|
||||||
|
|
||||||
|
const auto countNoPerspective = [](const String& text) {
|
||||||
|
SizeT count = 0, offset = 0;
|
||||||
|
while ((offset = text.find("NoPerspective", offset)) != String::npos) {
|
||||||
|
++count;
|
||||||
|
offset += std::strlen("NoPerspective");
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
String inputText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
|
||||||
|
ASSERT_EQ(countNoPerspective(inputText), 2u)
|
||||||
|
<< "fixture must carry both a plain and a member NoPerspective:\n" << inputText;
|
||||||
|
|
||||||
|
Vector<uint32_t> outputBinary;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(inputBinary, outputBinary));
|
||||||
|
ASSERT_FALSE(outputBinary.empty());
|
||||||
|
|
||||||
|
String outputText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||||
|
EXPECT_EQ(countNoPerspective(outputText), 0u)
|
||||||
|
<< "both NoPerspective decorations (OpDecorate and OpMemberDecorate) must be stripped:\n" << outputText;
|
||||||
|
EXPECT_NE(outputText.find("Flat"), String::npos)
|
||||||
|
<< "the unrelated Flat decoration must survive:\n" << outputText;
|
||||||
|
EXPECT_NE(outputText.find("Location"), String::npos)
|
||||||
|
<< "Location decorations must survive:\n" << outputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2 emulation - fragment side. On a device without the NV extension the NoPerspective input is
|
||||||
|
// recovered as `load * gl_FragCoord.w` and the decoration removed; gl_FragCoord is synthesized because
|
||||||
|
// the shader did not otherwise use it. The emulated SPIR-V must validate and decompile without the
|
||||||
|
// extension require.
|
||||||
|
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentRecoversWithFragCoordW) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String fs = R"(#version 330 core
|
||||||
|
noperspective in vec4 vColor;
|
||||||
|
out vec4 f;
|
||||||
|
void main() { f = vColor; }
|
||||||
|
)";
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) FAIL() << "compile: " << res.error().log;
|
||||||
|
ProgramAttrib pa{.shaders = {res.value()}};
|
||||||
|
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||||
|
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||||
|
ProgramBinaryAttrib ba{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *pr.value()};
|
||||||
|
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||||
|
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||||
|
ASSERT_EQ(br.value().size(), 1u);
|
||||||
|
|
||||||
|
Vector<uint32_t> emulated;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||||
|
ASSERT_FALSE(emulated.empty());
|
||||||
|
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String dis;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||||
|
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||||
|
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||||
|
EXPECT_NE(dis.find("FragCoord"), String::npos) << "gl_FragCoord must be synthesized:\n" << dis;
|
||||||
|
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the recovery multiply must be present:\n" << dis;
|
||||||
|
|
||||||
|
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||||
|
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||||
|
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos) << essl.value();
|
||||||
|
EXPECT_NE(essl.value().find("gl_FragCoord"), String::npos) << "recovery must reference gl_FragCoord:\n" << essl.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2 emulation - vertex side. The NoPerspective output is pre-multiplied by gl_Position.w before
|
||||||
|
// return and the decoration removed. Emulated SPIR-V must validate and decompile without the extension.
|
||||||
|
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexPreMultipliesByPositionW) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String vs = R"(#version 330 core
|
||||||
|
in vec4 pos;
|
||||||
|
noperspective out vec4 vColor;
|
||||||
|
void main() { gl_Position = pos; vColor = pos; }
|
||||||
|
)";
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vs};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!res) FAIL() << "compile: " << res.error().log;
|
||||||
|
ProgramAttrib pa{.shaders = {res.value()}};
|
||||||
|
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||||
|
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||||
|
ProgramBinaryAttrib ba{.shaderTypes = {GL_VERTEX_SHADER}, .program = *pr.value()};
|
||||||
|
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||||
|
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||||
|
ASSERT_EQ(br.value().size(), 1u);
|
||||||
|
|
||||||
|
Vector<uint32_t> emulated;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||||
|
ASSERT_FALSE(emulated.empty());
|
||||||
|
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String dis;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||||
|
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||||
|
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||||
|
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the pre-multiply must be present:\n" << dis;
|
||||||
|
|
||||||
|
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||||
|
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||||
|
EXPECT_NE(essl.value().find("gl_Position"), String::npos) << "pre-multiply must reference gl_Position:\n" << essl.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Compiles one shader stage through the full pipeline and returns its SPIR-V, or fails the test.
|
||||||
|
MobileGL::Vector<uint32_t> CompileStageSpirv(GLenum type, const char* src) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
ShaderAttrib attrib{.shaderType = type, .sourceStr = src};
|
||||||
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
|
EXPECT_TRUE(static_cast<bool>(res)) << (res ? "" : res.error().log);
|
||||||
|
if (!res) return {};
|
||||||
|
ProgramAttrib pa{.shaders = {res.value()}};
|
||||||
|
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||||
|
EXPECT_TRUE(static_cast<bool>(pr)) << (pr ? "" : pr.error().log);
|
||||||
|
if (!pr) return {};
|
||||||
|
ProgramBinaryAttrib ba{.shaderTypes = {type}, .program = *pr.value()};
|
||||||
|
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||||
|
EXPECT_TRUE(static_cast<bool>(br)) << (br ? "" : br.error().log);
|
||||||
|
if (!br || br.value().empty()) return {};
|
||||||
|
return br.value()[0];
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Regression: the vertex pre-multiply must be applied exactly once (in main), not once per function.
|
||||||
|
// glslang does not inline, so a helper function survives as its own OpFunction; instrumenting its
|
||||||
|
// return too would scale the varying by gl_Position.w twice (w^2).
|
||||||
|
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexWithHelperScalesExactlyOnce) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
// helper() returns via OpReturnValue and adds (no vector*scalar), so the ONLY OpVectorTimesScalar
|
||||||
|
// in the module is the emulation's pre-multiply. The old all-functions code injected it at both
|
||||||
|
// helper's and main's return -> count 2; restricted to the entry function it is 1.
|
||||||
|
auto spirv = CompileStageSpirv(GL_VERTEX_SHADER, R"(#version 330 core
|
||||||
|
in vec4 pos;
|
||||||
|
noperspective out vec4 vColor;
|
||||||
|
vec4 helper(vec4 x) { return x + vec4(1.0); }
|
||||||
|
void main() { gl_Position = pos; vColor = helper(pos); }
|
||||||
|
)");
|
||||||
|
ASSERT_FALSE(spirv.empty());
|
||||||
|
|
||||||
|
Vector<uint32_t> emulated;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String dis;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||||
|
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||||
|
|
||||||
|
SizeT count = 0, off = 0;
|
||||||
|
while ((off = dis.find("OpVectorTimesScalar", off)) != String::npos) {
|
||||||
|
++count;
|
||||||
|
off += std::strlen("OpVectorTimesScalar");
|
||||||
|
}
|
||||||
|
EXPECT_EQ(count, 1u) << "the gl_Position.w pre-multiply must happen exactly once, not per function:\n" << dis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression: a single-component read (vColor.x), which glslang lowers via OpAccessChain, must still be
|
||||||
|
// recovered with gl_FragCoord.w - not silently left un-scaled.
|
||||||
|
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentComponentReadIsRecovered) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||||
|
noperspective in vec4 vColor;
|
||||||
|
out vec4 f;
|
||||||
|
void main() { f = vec4(vColor.x); }
|
||||||
|
)");
|
||||||
|
ASSERT_FALSE(spirv.empty());
|
||||||
|
|
||||||
|
Vector<uint32_t> emulated;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String dis;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||||
|
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||||
|
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||||
|
EXPECT_NE(dis.find("FragCoord"), String::npos)
|
||||||
|
<< "the component read must still be recovered via gl_FragCoord.w:\n" << dis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage: a scalar float varying exercises the OpFMul path; a vector varying the OpVectorTimesScalar
|
||||||
|
// path; multiple noperspective varyings in one stage are all handled.
|
||||||
|
TEST_F(ProgramUtilTest, EmulateNoperspectiveHandlesScalarAndMultipleVaryings) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||||
|
noperspective in float a;
|
||||||
|
noperspective in vec2 b;
|
||||||
|
out vec4 f;
|
||||||
|
void main() { f = vec4(a, b, 1.0); }
|
||||||
|
)");
|
||||||
|
ASSERT_FALSE(spirv.empty());
|
||||||
|
|
||||||
|
Vector<uint32_t> emulated;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String dis;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||||
|
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||||
|
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||||
|
EXPECT_NE(dis.find("OpFMul"), String::npos) << "the scalar varying must scale with OpFMul:\n" << dis;
|
||||||
|
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos)
|
||||||
|
<< "the vector varying must scale with OpVectorTimesScalar:\n" << dis;
|
||||||
|
}
|
||||||
|
|
||||||
const char* vs_location = R"(#version 460
|
const char* vs_location = R"(#version 460
|
||||||
|
|
||||||
in vec4 Position;
|
in vec4 Position;
|
||||||
@@ -1383,3 +2085,150 @@ void main() {
|
|||||||
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
|
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
|
||||||
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
|
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String MakeLinearSubgroupPrefixScanShader() {
|
||||||
|
return R"(#version 460 core
|
||||||
|
#extension GL_KHR_shader_subgroup_arithmetic : enable
|
||||||
|
layout(local_size_x = 1024) in;
|
||||||
|
shared float prefixSumCache[64];
|
||||||
|
|
||||||
|
layout(std430, binding = 0) writeonly buffer OutputBuffer {
|
||||||
|
float outputValues[];
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float importance = 1.0f;
|
||||||
|
float prefixSum = subgroupInclusiveAdd(importance);
|
||||||
|
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
|
||||||
|
barrier();
|
||||||
|
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
||||||
|
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||||
|
for (uint i = 0; i < loopLength; i++) {
|
||||||
|
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
||||||
|
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
||||||
|
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
|
||||||
|
}
|
||||||
|
barrier();
|
||||||
|
}
|
||||||
|
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
|
||||||
|
barrier();
|
||||||
|
float sum = prefixSumCache[0];
|
||||||
|
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
|
||||||
|
outputValues[gl_GlobalInvocationID.x] = warp;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||||
|
|
||||||
|
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
|
||||||
|
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
|
||||||
|
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
|
||||||
|
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
|
||||||
|
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
|
||||||
|
|
||||||
|
const String onceRewritten = source;
|
||||||
|
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||||
|
EXPECT_EQ(source, onceRewritten);
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
ASSERT_TRUE(programResult) << programResult.error().log;
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
||||||
|
ASSERT_EQ(binaryResult->size(), 1u);
|
||||||
|
|
||||||
|
String validationDiagnostics;
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||||
|
validationDiagnostics += message;
|
||||||
|
validationDiagnostics += '\n';
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
|
||||||
|
|
||||||
|
String spirvText;
|
||||||
|
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
|
||||||
|
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String original = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
for (const auto& [stage, subgroupSize] :
|
||||||
|
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
|
||||||
|
std::pair{ShaderStage::Compute, Uint32{96}}}) {
|
||||||
|
String source = original;
|
||||||
|
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
|
||||||
|
EXPECT_EQ(source, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const auto expectUnchanged = [](String source) {
|
||||||
|
const String original = source;
|
||||||
|
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||||
|
EXPECT_EQ(source, original);
|
||||||
|
};
|
||||||
|
|
||||||
|
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
|
||||||
|
"local_size_x = 512");
|
||||||
|
expectUnchanged(std::move(wrongLocalSize));
|
||||||
|
|
||||||
|
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
|
||||||
|
expectUnchanged(std::move(cacheHasAnotherUse));
|
||||||
|
|
||||||
|
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
|
||||||
|
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
|
||||||
|
expectUnchanged(std::move(extraSubgroupBuiltin));
|
||||||
|
|
||||||
|
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
|
||||||
|
expectUnchanged(std::move(alteredBarrier));
|
||||||
|
|
||||||
|
String nestedScan = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
|
||||||
|
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
|
||||||
|
ASSERT_NE(consumerEnd, String::npos);
|
||||||
|
nestedScan.insert(consumerEnd + 1, "\n }");
|
||||||
|
expectUnchanged(std::move(nestedScan));
|
||||||
|
|
||||||
|
// ARB/NV spellings of lane-width-sensitive builtins must block the rewrite exactly
|
||||||
|
// like their KHR counterparts.
|
||||||
|
String arbSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
arbSubgroupBuiltin.insert(arbSubgroupBuiltin.find("float importance"),
|
||||||
|
"uint arbLane = gl_SubGroupInvocationARB;\n ");
|
||||||
|
expectUnchanged(std::move(arbSubgroupBuiltin));
|
||||||
|
|
||||||
|
String arbBallotCall = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
arbBallotCall.insert(arbBallotCall.find("float importance"),
|
||||||
|
"uint64_t arbMask = ballotARB(true);\n ");
|
||||||
|
expectUnchanged(std::move(arbBallotCall));
|
||||||
|
|
||||||
|
String nvWarpBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
nvWarpBuiltin.insert(nvWarpBuiltin.find("float importance"),
|
||||||
|
"uint warpSize = gl_WarpSizeNV;\n ");
|
||||||
|
expectUnchanged(std::move(nvWarpBuiltin));
|
||||||
|
|
||||||
|
String nvShuffleCall = MakeLinearSubgroupPrefixScanShader();
|
||||||
|
nvShuffleCall.insert(nvShuffleCall.find("float importance"),
|
||||||
|
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
|
||||||
|
expectUnchanged(std::move(nvShuffleCall));
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,9 +24,12 @@
|
|||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||||
|
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||||
|
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
|
||||||
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
|
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
|
||||||
#include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h>
|
#include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h>
|
||||||
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
||||||
|
#include <MG_Util/Math/HalfFloat.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
@@ -460,6 +463,58 @@ TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendSta
|
|||||||
EXPECT_EQ(lowParams.MaxColorAttachments, 6);
|
EXPECT_EQ(lowParams.MaxColorAttachments, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, GatesPerStageImageUniformLimitsOnPhysicalDeviceFeatures) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||||
|
MG_External::VulkanCapabilities caps;
|
||||||
|
caps.MaxImageUnits = 12;
|
||||||
|
caps.MaxCombinedImageUniforms = 10;
|
||||||
|
caps.MaxComputeImageUniforms = 9;
|
||||||
|
caps.SupportsVertexPipelineStoresAndAtomics = true;
|
||||||
|
caps.SupportsFragmentStoresAndAtomics = true;
|
||||||
|
caps.SupportsGeometryShader = false;
|
||||||
|
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||||
|
|
||||||
|
const auto& withoutGeometry = backend.GetDynamicParameters();
|
||||||
|
EXPECT_EQ(withoutGeometry.MaxVertexImageUniforms, 10);
|
||||||
|
EXPECT_EQ(withoutGeometry.MaxGeometryImageUniforms, 0);
|
||||||
|
EXPECT_EQ(withoutGeometry.MaxFragmentImageUniforms, 10);
|
||||||
|
EXPECT_EQ(withoutGeometry.MaxComputeImageUniforms, 9);
|
||||||
|
|
||||||
|
caps.SupportsGeometryShader = true;
|
||||||
|
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 10);
|
||||||
|
|
||||||
|
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
||||||
|
caps.SupportsFragmentStoresAndAtomics = false;
|
||||||
|
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxVertexImageUniforms, 0);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 0);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxFragmentImageUniforms, 0);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxComputeImageUniforms, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||||
|
MG_External::GLESCapabilities caps;
|
||||||
|
caps.MaxImageUnits = 8;
|
||||||
|
caps.MaxCombinedImageUniforms = 16;
|
||||||
|
caps.MaxVertexImageUniforms = 2;
|
||||||
|
caps.MaxGeometryImageUniforms = 3;
|
||||||
|
caps.MaxFragmentImageUniforms = 4;
|
||||||
|
caps.MaxComputeImageUniforms = 5;
|
||||||
|
backend.ApplyGLESCapabilitiesForTesting(caps);
|
||||||
|
|
||||||
|
const auto& params = backend.GetDynamicParameters();
|
||||||
|
EXPECT_EQ(params.MaxVertexImageUniforms, 2);
|
||||||
|
EXPECT_EQ(params.MaxGeometryImageUniforms, 3);
|
||||||
|
EXPECT_EQ(params.MaxFragmentImageUniforms, 4);
|
||||||
|
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -582,6 +637,54 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
|
|||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
MG_Backend::DynamicBackendParameters params;
|
||||||
|
params.MaxImageUnits = 8;
|
||||||
|
params.MaxCombinedImageUniforms = 8;
|
||||||
|
params.MaxVertexImageUniforms = 1;
|
||||||
|
params.MaxGeometryImageUniforms = 2;
|
||||||
|
params.MaxFragmentImageUniforms = 3;
|
||||||
|
params.MaxComputeImageUniforms = 4;
|
||||||
|
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||||
|
|
||||||
|
GLint reported = -1;
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
|
||||||
|
EXPECT_EQ(reported, 1);
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &reported);
|
||||||
|
EXPECT_EQ(reported, 2);
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &reported);
|
||||||
|
EXPECT_EQ(reported, 3);
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &reported);
|
||||||
|
EXPECT_EQ(reported, 4);
|
||||||
|
|
||||||
|
const String vertexImageStore = R"(#version 430 core
|
||||||
|
layout(r32ui, binding = 0) uniform uimage2D targetImages[gl_MaxVertexImageUniforms];
|
||||||
|
void main() {
|
||||||
|
imageStore(targetImages[0], ivec2(0), uvec4(1));
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
auto supported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_VERTEX_SHADER,
|
||||||
|
.sourceStr = vertexImageStore,
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(supported) << (supported ? "" : supported.error().log);
|
||||||
|
|
||||||
|
params.MaxVertexImageUniforms = 0;
|
||||||
|
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
|
||||||
|
EXPECT_EQ(reported, 0);
|
||||||
|
auto unsupported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_VERTEX_SHADER,
|
||||||
|
.sourceStr = vertexImageStore,
|
||||||
|
});
|
||||||
|
EXPECT_FALSE(unsupported);
|
||||||
|
|
||||||
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
|
}
|
||||||
|
|
||||||
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -629,6 +732,95 @@ TEST(DirectVulkanSanity, CommandMemoryBarrierMakesIndirectDrawCommandsVisible) {
|
|||||||
EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
|
EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||||
|
|
||||||
|
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R8G8B8A8_UNORM), 4u);
|
||||||
|
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R16G16B16A16_SFLOAT), 8u);
|
||||||
|
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||||
|
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
||||||
|
|
||||||
|
const MobileGL::Uint8 rgba8[] = {17, 34, 51, 68, 85, 102, 119, 136};
|
||||||
|
MobileGL::Uint8 rgba8Result[sizeof(rgba8)]{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
rgba8, VK_FORMAT_R8G8B8A8_UNORM, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
sizeof(rgba8Result), rgba8Result));
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(rgba8), std::end(rgba8), std::begin(rgba8Result)));
|
||||||
|
|
||||||
|
const MobileGL::Uint8 bgra8[] = {51, 34, 17, 68};
|
||||||
|
MobileGL::Uint8 bgra8Result[4]{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
bgra8, VK_FORMAT_B8G8R8A8_UNORM, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
sizeof(bgra8Result), bgra8Result));
|
||||||
|
const MobileGL::Uint8 expectedBgra8[] = {17, 34, 51, 68};
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(expectedBgra8), std::end(expectedBgra8), std::begin(bgra8Result)));
|
||||||
|
|
||||||
|
const MobileGL::Uint16 rgba16f[] = {
|
||||||
|
EncodeFloatToHalfBits(-0.25f), EncodeFloatToHalfBits(0.5f), EncodeFloatToHalfBits(1.5f),
|
||||||
|
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
|
||||||
|
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.5f),
|
||||||
|
EncodeFloatToHalfBits(0.75f), EncodeFloatToHalfBits(0.125f), EncodeFloatToHalfBits(-1.0f),
|
||||||
|
EncodeFloatToHalfBits(2.0f), EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.75f),
|
||||||
|
EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
|
||||||
|
};
|
||||||
|
constexpr MobileGL::SizeT kDestinationRowStride = 12;
|
||||||
|
MobileGL::Uint8 rgba16fResult[kDestinationRowStride * 2];
|
||||||
|
std::fill(std::begin(rgba16fResult), std::end(rgba16fResult), 0xCD);
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
|
||||||
|
2, 2, GL_RGBA, GL_UNSIGNED_BYTE, kDestinationRowStride, rgba16fResult));
|
||||||
|
const MobileGL::Uint8 expectedRgba16fRow0[] = {0, 128, 255, 255, 64, 0, 255, 128};
|
||||||
|
const MobileGL::Uint8 expectedRgba16fRow1[] = {191, 32, 0, 255, 255, 191, 64, 0};
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow0), std::end(expectedRgba16fRow0),
|
||||||
|
std::begin(rgba16fResult)));
|
||||||
|
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow1), std::end(expectedRgba16fRow1),
|
||||||
|
std::begin(rgba16fResult) + kDestinationRowStride));
|
||||||
|
EXPECT_TRUE(std::all_of(std::begin(rgba16fResult) + 8,
|
||||||
|
std::begin(rgba16fResult) + kDestinationRowStride,
|
||||||
|
[](MobileGL::Uint8 value) { return value == 0xCD; }));
|
||||||
|
|
||||||
|
MobileGL::Float rgba16fFloatResult[16]{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
|
||||||
|
2, 2, GL_RGBA, GL_FLOAT, sizeof(MobileGL::Float) * 8,
|
||||||
|
reinterpret_cast<MobileGL::Uint8*>(rgba16fFloatResult)));
|
||||||
|
EXPECT_FLOAT_EQ(rgba16fFloatResult[0], -0.25f);
|
||||||
|
EXPECT_FLOAT_EQ(rgba16fFloatResult[1], 0.5f);
|
||||||
|
EXPECT_FLOAT_EQ(rgba16fFloatResult[2], 1.5f);
|
||||||
|
EXPECT_FLOAT_EQ(rgba16fFloatResult[3], 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, ReadbackDecodesSingleChannel32BitFormats) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||||
|
|
||||||
|
// The reinterpretation feature makes R32F/R32UI-class images common readback sources
|
||||||
|
// (iterationRP custom images). Missing channels take GL defaults: 0 for GB, 1 for alpha.
|
||||||
|
const MobileGL::Float r32f[] = {0.75f, -2.0f};
|
||||||
|
MobileGL::Float r32fResult[8]{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
reinterpret_cast<const MobileGL::Uint8*>(r32f), VK_FORMAT_R32_SFLOAT,
|
||||||
|
2, 1, GL_RGBA, GL_FLOAT, sizeof(MobileGL::Float) * 8,
|
||||||
|
reinterpret_cast<MobileGL::Uint8*>(r32fResult)));
|
||||||
|
EXPECT_FLOAT_EQ(r32fResult[0], 0.75f);
|
||||||
|
EXPECT_FLOAT_EQ(r32fResult[1], 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(r32fResult[2], 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(r32fResult[3], 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(r32fResult[4], -2.0f);
|
||||||
|
|
||||||
|
const MobileGL::Uint32 r32ui[] = {12345u};
|
||||||
|
MobileGL::Float r32uiResult[4]{};
|
||||||
|
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
|
||||||
|
reinterpret_cast<const MobileGL::Uint8*>(r32ui), VK_FORMAT_R32_UINT,
|
||||||
|
1, 1, GL_RGBA, GL_FLOAT, sizeof(MobileGL::Float) * 4,
|
||||||
|
reinterpret_cast<MobileGL::Uint8*>(r32uiResult)));
|
||||||
|
EXPECT_FLOAT_EQ(r32uiResult[0], 12345.0f);
|
||||||
|
EXPECT_FLOAT_EQ(r32uiResult[3], 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) {
|
TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) {
|
||||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||||
|
|
||||||
@@ -675,6 +867,193 @@ TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) {
|
|||||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||||
|
|
||||||
|
struct FormatCase {
|
||||||
|
SpvImageFormat spirv;
|
||||||
|
VkFormat vulkan;
|
||||||
|
};
|
||||||
|
const FormatCase cases[] = {
|
||||||
|
{SpvImageFormatUnknown, VK_FORMAT_UNDEFINED},
|
||||||
|
{SpvImageFormatRgba32f, VK_FORMAT_R32G32B32A32_SFLOAT},
|
||||||
|
{SpvImageFormatRgba16f, VK_FORMAT_R16G16B16A16_SFLOAT},
|
||||||
|
{SpvImageFormatR32f, VK_FORMAT_R32_SFLOAT},
|
||||||
|
{SpvImageFormatRgba8, VK_FORMAT_R8G8B8A8_UNORM},
|
||||||
|
{SpvImageFormatRgba8Snorm, VK_FORMAT_R8G8B8A8_SNORM},
|
||||||
|
{SpvImageFormatRg32f, VK_FORMAT_R32G32_SFLOAT},
|
||||||
|
{SpvImageFormatRg16f, VK_FORMAT_R16G16_SFLOAT},
|
||||||
|
{SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32},
|
||||||
|
{SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT},
|
||||||
|
{SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM},
|
||||||
|
{SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32},
|
||||||
|
{SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM},
|
||||||
|
{SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM},
|
||||||
|
{SpvImageFormatR16, VK_FORMAT_R16_UNORM},
|
||||||
|
{SpvImageFormatR8, VK_FORMAT_R8_UNORM},
|
||||||
|
{SpvImageFormatRgba16Snorm, VK_FORMAT_R16G16B16A16_SNORM},
|
||||||
|
{SpvImageFormatRg16Snorm, VK_FORMAT_R16G16_SNORM},
|
||||||
|
{SpvImageFormatRg8Snorm, VK_FORMAT_R8G8_SNORM},
|
||||||
|
{SpvImageFormatR16Snorm, VK_FORMAT_R16_SNORM},
|
||||||
|
{SpvImageFormatR8Snorm, VK_FORMAT_R8_SNORM},
|
||||||
|
{SpvImageFormatRgba32i, VK_FORMAT_R32G32B32A32_SINT},
|
||||||
|
{SpvImageFormatRgba16i, VK_FORMAT_R16G16B16A16_SINT},
|
||||||
|
{SpvImageFormatRgba8i, VK_FORMAT_R8G8B8A8_SINT},
|
||||||
|
{SpvImageFormatR32i, VK_FORMAT_R32_SINT},
|
||||||
|
{SpvImageFormatRg32i, VK_FORMAT_R32G32_SINT},
|
||||||
|
{SpvImageFormatRg16i, VK_FORMAT_R16G16_SINT},
|
||||||
|
{SpvImageFormatRg8i, VK_FORMAT_R8G8_SINT},
|
||||||
|
{SpvImageFormatR16i, VK_FORMAT_R16_SINT},
|
||||||
|
{SpvImageFormatR8i, VK_FORMAT_R8_SINT},
|
||||||
|
{SpvImageFormatRgba32ui, VK_FORMAT_R32G32B32A32_UINT},
|
||||||
|
{SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT},
|
||||||
|
{SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT},
|
||||||
|
{SpvImageFormatR32ui, VK_FORMAT_R32_UINT},
|
||||||
|
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32},
|
||||||
|
{SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT},
|
||||||
|
{SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT},
|
||||||
|
{SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT},
|
||||||
|
{SpvImageFormatR16ui, VK_FORMAT_R16_UINT},
|
||||||
|
{SpvImageFormatR8ui, VK_FORMAT_R8_UINT},
|
||||||
|
{SpvImageFormatR64ui, VK_FORMAT_R64_UINT},
|
||||||
|
{SpvImageFormatR64i, VK_FORMAT_R64_SINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& testCase : cases) {
|
||||||
|
EXPECT_EQ(ProgramFactory::ConvertSpirvImageFormatToVkFormat(testCase.spirv), testCase.vulkan)
|
||||||
|
<< "SpvImageFormat=" << static_cast<int>(testCase.spirv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, MutableStorageImageViewsUseVulkanCompatibilityClasses) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::VkTextureManager;
|
||||||
|
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT));
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT));
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R8G8B8A8_UINT));
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_SFLOAT));
|
||||||
|
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT));
|
||||||
|
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_D32_SFLOAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, StorageImageViewFormatUsesBindingOnlyForFormatlessFloatPolicy) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
|
||||||
|
|
||||||
|
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
|
||||||
|
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R16G16B16A16_UNORM, true),
|
||||||
|
VK_FORMAT_R16G16B16A16_SFLOAT);
|
||||||
|
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
|
||||||
|
VK_FORMAT_UNDEFINED, GL_RGBA16, VK_FORMAT_R16G16B16A16_SFLOAT, true),
|
||||||
|
VK_FORMAT_R16G16B16A16_UNORM);
|
||||||
|
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
|
||||||
|
VK_FORMAT_R32_UINT, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
|
||||||
|
VK_FORMAT_R32_UINT);
|
||||||
|
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
|
||||||
|
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
|
||||||
|
VK_FORMAT_R32_SFLOAT);
|
||||||
|
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
|
||||||
|
VK_FORMAT_UNDEFINED, GL_NONE, VK_FORMAT_R16G16B16A16_SFLOAT, true),
|
||||||
|
VK_FORMAT_UNDEFINED);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, ProgramObjectMovePreservesStorageImageFormatPolicy) {
|
||||||
|
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||||
|
|
||||||
|
ProgramFactory::VkProgramObject source;
|
||||||
|
source.storageImageFormatByBinding = {VK_FORMAT_UNDEFINED, VK_FORMAT_R32_UINT};
|
||||||
|
source.storageImageUsesBindingFormatByBinding = {true, false};
|
||||||
|
|
||||||
|
ProgramFactory::VkProgramObject moved(std::move(source));
|
||||||
|
ASSERT_EQ(moved.storageImageFormatByBinding.size(), 2u);
|
||||||
|
ASSERT_EQ(moved.storageImageUsesBindingFormatByBinding.size(), 2u);
|
||||||
|
EXPECT_EQ(moved.storageImageFormatByBinding[0], VK_FORMAT_UNDEFINED);
|
||||||
|
EXPECT_EQ(moved.storageImageFormatByBinding[1], VK_FORMAT_R32_UINT);
|
||||||
|
EXPECT_TRUE(moved.storageImageUsesBindingFormatByBinding[0]);
|
||||||
|
EXPECT_FALSE(moved.storageImageUsesBindingFormatByBinding[1]);
|
||||||
|
|
||||||
|
ProgramFactory::VkProgramObject assigned;
|
||||||
|
assigned = std::move(moved);
|
||||||
|
ASSERT_EQ(assigned.storageImageFormatByBinding.size(), 2u);
|
||||||
|
ASSERT_EQ(assigned.storageImageUsesBindingFormatByBinding.size(), 2u);
|
||||||
|
EXPECT_TRUE(assigned.storageImageUsesBindingFormatByBinding[0]);
|
||||||
|
EXPECT_FALSE(assigned.storageImageUsesBindingFormatByBinding[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||||
|
|
||||||
|
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_2D),
|
||||||
|
SamplerNumericDomain::Float);
|
||||||
|
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW),
|
||||||
|
SamplerNumericDomain::Float);
|
||||||
|
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_INT_SAMPLER_2D_ARRAY),
|
||||||
|
SamplerNumericDomain::SignedInteger);
|
||||||
|
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_UNSIGNED_INT_SAMPLER_2D),
|
||||||
|
SamplerNumericDomain::UnsignedInteger);
|
||||||
|
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_IMAGE_2D),
|
||||||
|
SamplerNumericDomain::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||||
|
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_R32_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::SignedInteger),
|
||||||
|
VK_FORMAT_R32_SINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R32_UINT, SamplerNumericDomain::Float),
|
||||||
|
VK_FORMAT_R32_SFLOAT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R16G16B16A16_SFLOAT, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_R16G16B16A16_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R8G8B8A8_UNORM, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_R8G8B8A8_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_R32_UINT, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_R32_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_UNDEFINED);
|
||||||
|
|
||||||
|
// Depth/stencil formats never resolve through color-class reinterpretation; they pass
|
||||||
|
// through unchanged so the existing depth-aspect sampled view is used. Combined
|
||||||
|
// depth-stencil formats are multi-numeric (vkuFormatIsSampledFloat is false for them),
|
||||||
|
// so without the passthrough a plain sampler2D/sampler2DShadow on GL_DEPTH24_STENCIL8
|
||||||
|
// would resolve to UNDEFINED and the draw would be dropped.
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_D24_UNORM_S8_UINT, SamplerNumericDomain::Float),
|
||||||
|
VK_FORMAT_D24_UNORM_S8_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_D32_SFLOAT_S8_UINT, SamplerNumericDomain::Float),
|
||||||
|
VK_FORMAT_D32_SFLOAT_S8_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::Float),
|
||||||
|
VK_FORMAT_D32_SFLOAT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_D24_UNORM_S8_UINT, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_D24_UNORM_S8_UINT);
|
||||||
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
|
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
|
||||||
|
VK_FORMAT_D32_SFLOAT);
|
||||||
|
|
||||||
|
EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
|
||||||
|
EXPECT_FALSE(VkTextureManager::AreSampledImageViewFormatsCompatible(
|
||||||
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_UINT));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) {
|
TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -1052,3 +1431,308 @@ TEST(RenderStateSanity, PrimitiveRestartIndexStoresAndReadsBack) {
|
|||||||
|
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- DirectGLES readback driver-state shadows ----------------------------------------------------
|
||||||
|
// Regression coverage for the readback-path state-leak overhaul: the pixel-PBO
|
||||||
|
// binding cache, the framebuffer-binding shadow, the PACK pixel-store shadow and
|
||||||
|
// the scratch-FBO attachment shadow must (a) leave the driver in the documented
|
||||||
|
// resting state, (b) skip redundant GL calls, and (c) scrub correctly on
|
||||||
|
// deletion. All drive the real Managers.cpp implementations against a recording
|
||||||
|
// mock GLES table.
|
||||||
|
namespace {
|
||||||
|
struct StateGuardCallLog {
|
||||||
|
MobileGL::Vector<MobileGL::String> calls;
|
||||||
|
|
||||||
|
MobileGL::SizeT Count(const MobileGL::String& prefix) const {
|
||||||
|
MobileGL::SizeT n = 0;
|
||||||
|
for (const auto& c : calls) {
|
||||||
|
if (c.compare(0, prefix.size(), prefix) == 0) ++n;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
StateGuardCallLog* g_stateGuardLog = nullptr;
|
||||||
|
GLuint g_nextStateGuardFBOId = 201;
|
||||||
|
|
||||||
|
void SG_Log(MobileGL::String entry) {
|
||||||
|
if (g_stateGuardLog) g_stateGuardLog->calls.push_back(MobileGL::Move(entry));
|
||||||
|
}
|
||||||
|
void SG_BindBuffer(GLenum target, GLuint buffer) {
|
||||||
|
SG_Log("BindBuffer:" + std::to_string(target) + ":" + std::to_string(buffer));
|
||||||
|
}
|
||||||
|
void SG_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
||||||
|
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
||||||
|
}
|
||||||
|
void SG_GetIntegerv(GLenum pname, GLint* data) {
|
||||||
|
SG_Log("GetIntegerv:" + std::to_string(pname));
|
||||||
|
if (data) *data = 0;
|
||||||
|
}
|
||||||
|
void SG_PixelStorei(GLenum pname, GLint param) {
|
||||||
|
SG_Log("PixelStorei:" + std::to_string(pname) + ":" + std::to_string(param));
|
||||||
|
}
|
||||||
|
void SG_GenFramebuffers(GLsizei count, GLuint* framebuffers) {
|
||||||
|
for (GLsizei i = 0; i < count; ++i) framebuffers[i] = g_nextStateGuardFBOId++;
|
||||||
|
}
|
||||||
|
void SG_FramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
|
||||||
|
SG_Log("FramebufferTexture2D:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" +
|
||||||
|
std::to_string(textarget) + ":" + std::to_string(texture) + ":" + std::to_string(level));
|
||||||
|
}
|
||||||
|
void SG_FramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
|
||||||
|
SG_Log("FramebufferTextureLayer:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" +
|
||||||
|
std::to_string(texture) + ":" + std::to_string(level) + ":" + std::to_string(layer));
|
||||||
|
}
|
||||||
|
void SG_ReadBuffer(GLenum src) {
|
||||||
|
SG_Log("ReadBuffer:" + std::to_string(src));
|
||||||
|
}
|
||||||
|
void SG_DrawBuffers(GLsizei n, const GLenum* bufs) {
|
||||||
|
SG_Log("DrawBuffers:" + std::to_string(n) + ":" + std::to_string(n > 0 && bufs ? bufs[0] : 0));
|
||||||
|
}
|
||||||
|
GLenum SG_NoError() {
|
||||||
|
return GL_NO_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installs the recording table and resets every readback driver-state shadow on
|
||||||
|
// both ends, so these tests cannot bleed into (or inherit from) other tests.
|
||||||
|
struct ScopedStateGuardMocks {
|
||||||
|
ScopedStateGuardMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) {
|
||||||
|
ResetShadows();
|
||||||
|
MobileGL::MG_External::GLESFunctionsTable functions{};
|
||||||
|
functions.glBindBuffer = SG_BindBuffer;
|
||||||
|
functions.glBindFramebuffer = SG_BindFramebuffer;
|
||||||
|
functions.glGetIntegerv = SG_GetIntegerv;
|
||||||
|
functions.glPixelStorei = SG_PixelStorei;
|
||||||
|
functions.glGenFramebuffers = SG_GenFramebuffers;
|
||||||
|
functions.glFramebufferTexture2D = SG_FramebufferTexture2D;
|
||||||
|
functions.glFramebufferTextureLayer = SG_FramebufferTextureLayer;
|
||||||
|
functions.glReadBuffer = SG_ReadBuffer;
|
||||||
|
functions.glDrawBuffers = SG_DrawBuffers;
|
||||||
|
functions.glGetError = SG_NoError;
|
||||||
|
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||||
|
g_stateGuardLog = &log;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedStateGuardMocks() {
|
||||||
|
g_stateGuardLog = nullptr;
|
||||||
|
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
||||||
|
ResetShadows();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedStateGuardMocks(const ScopedStateGuardMocks&) = delete;
|
||||||
|
ScopedStateGuardMocks& operator=(const ScopedStateGuardMocks&) = delete;
|
||||||
|
|
||||||
|
static void ResetShadows() {
|
||||||
|
MobileGL::MG_Backend::DirectGLES::BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||||
|
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||||
|
MobileGL::MG_Backend::DirectGLES::PixelStoreImpl::InvalidatePackStateCache();
|
||||||
|
MobileGL::MG_Backend::DirectGLES::ScratchFBOImpl::OnBackendContextDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
StateGuardCallLog log;
|
||||||
|
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
BufferImpl::BindPixelPackBufferId(5);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u);
|
||||||
|
BufferImpl::BindPixelPackBufferId(5); // redundant: must not reach the driver
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u);
|
||||||
|
BufferImpl::BindPixelPackBufferId(0); // scope exit: resting state
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u);
|
||||||
|
BufferImpl::BindPixelPackBufferId(0);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u);
|
||||||
|
|
||||||
|
// After invalidation (MakeCurrent / context reset) the first bind must reach
|
||||||
|
// the driver again even for the same value.
|
||||||
|
BufferImpl::InvalidatePixelBufferBindingCaches();
|
||||||
|
BufferImpl::BindPixelPackBufferId(0);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindBuffer:"), 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, FramebufferBindingShadowPinsOnceThenSkips) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
// Cold path: one driver query pins the shadow; further reads are free.
|
||||||
|
(void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read);
|
||||||
|
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u);
|
||||||
|
(void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read);
|
||||||
|
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u);
|
||||||
|
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u);
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u);
|
||||||
|
// GL_FRAMEBUFFER touches both targets; DRAW is still unknown so it must bind.
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u);
|
||||||
|
// Both halves now match: no further calls for either single target.
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7);
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7);
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7);
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u);
|
||||||
|
EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 7u);
|
||||||
|
EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u); // shadow answered, no new query
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, PackStateShadowAppliesMinimalDeltas) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
// First application pins all four parameters.
|
||||||
|
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||||
|
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u);
|
||||||
|
// Identical state: zero driver calls.
|
||||||
|
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||||
|
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u);
|
||||||
|
// One field changed: exactly one driver call.
|
||||||
|
PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||||
|
EXPECT_EQ(mocks.log.Count("PixelStorei:"), 5u);
|
||||||
|
|
||||||
|
const auto current = PixelStoreImpl::CurrentPackState();
|
||||||
|
EXPECT_EQ(current.Alignment, 1);
|
||||||
|
EXPECT_EQ(current.RowLength, 0);
|
||||||
|
EXPECT_EQ(current.SkipRows, 0);
|
||||||
|
EXPECT_EQ(current.SkipPixels, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, ScratchFBODetachesCrossAspectResidue) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
auto& fb = ScratchFBOImpl::TempFramebuffer();
|
||||||
|
EXPECT_NE(ScratchFBOImpl::EnsureId(fb), 0u);
|
||||||
|
|
||||||
|
// A depth copy leaves a DEPTH_STENCIL attachment (the pre-fix code never
|
||||||
|
// detached it, wedging every later color readback through this FBO).
|
||||||
|
ScratchFBOImpl::EnsureDepthAttachment2D(fb, GL_DRAW_FRAMEBUFFER, 11, GL_TEXTURE_2D, 0, /*withStencil=*/true);
|
||||||
|
const MobileGL::String dsAttach = "FramebufferTexture2D:" + std::to_string(GL_DRAW_FRAMEBUFFER) + ":" +
|
||||||
|
std::to_string(GL_DEPTH_STENCIL_ATTACHMENT);
|
||||||
|
EXPECT_EQ(mocks.log.Count(dsAttach), 1u);
|
||||||
|
|
||||||
|
// The next color use must detach the stale depth-stencil attachment exactly once.
|
||||||
|
mocks.log.calls.clear();
|
||||||
|
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||||
|
const MobileGL::String dsDetach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||||
|
std::to_string(GL_DEPTH_STENCIL_ATTACHMENT) + ":" +
|
||||||
|
std::to_string(GL_TEXTURE_2D) + ":0:0";
|
||||||
|
const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||||
|
std::to_string(GL_COLOR_ATTACHMENT0) + ":" +
|
||||||
|
std::to_string(GL_TEXTURE_2D) + ":22:0";
|
||||||
|
EXPECT_EQ(mocks.log.Count(dsDetach), 1u);
|
||||||
|
EXPECT_EQ(mocks.log.Count(colorAttach), 1u);
|
||||||
|
|
||||||
|
// Back-to-back identical color use: no driver traffic at all.
|
||||||
|
mocks.log.calls.clear();
|
||||||
|
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||||
|
EXPECT_EQ(mocks.log.Count("FramebufferTexture2D:"), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, ScratchFBOTextureDeletionForcesFullScrub) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
auto& fb = ScratchFBOImpl::TempFramebuffer();
|
||||||
|
ScratchFBOImpl::EnsureId(fb);
|
||||||
|
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
// The attached texture id dies: the shadow can no longer vouch for the FBO
|
||||||
|
// (ES does not auto-detach from unbound FBOs, and the name may be recycled),
|
||||||
|
// so the next use must scrub and re-attach instead of skipping.
|
||||||
|
ScratchFBOImpl::NoteTextureIdDeleted(22);
|
||||||
|
mocks.log.calls.clear();
|
||||||
|
ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0);
|
||||||
|
EXPECT_GE(mocks.log.Count("FramebufferTexture2D:"), 2u); // scrub (color + depth) ...
|
||||||
|
const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" +
|
||||||
|
std::to_string(GL_COLOR_ATTACHMENT0) + ":" +
|
||||||
|
std::to_string(GL_TEXTURE_2D) + ":22:0";
|
||||||
|
EXPECT_EQ(mocks.log.Count(colorAttach), 1u); // ... then the real re-attach
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, ScratchFBOReadDrawBufferStateCached) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
auto& fb = ScratchFBOImpl::BlitReadFramebuffer();
|
||||||
|
ScratchFBOImpl::EnsureId(fb);
|
||||||
|
|
||||||
|
// Fresh FBOs default to COLOR_ATTACHMENT0 for both buffers: no call needed.
|
||||||
|
ScratchFBOImpl::EnsureReadBuffer(fb, GL_COLOR_ATTACHMENT0);
|
||||||
|
EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 0u);
|
||||||
|
// Depth blits want GL_NONE; the transition costs one call, repeats are free.
|
||||||
|
ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE);
|
||||||
|
ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE);
|
||||||
|
EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 1u);
|
||||||
|
ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE);
|
||||||
|
ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE);
|
||||||
|
EXPECT_EQ(mocks.log.Count("DrawBuffers:"), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
MobileGL::Vector<GLuint>* g_deletedTextureIds = nullptr;
|
||||||
|
|
||||||
|
void SG_DeleteTextures(GLsizei count, const GLuint* textures) {
|
||||||
|
if (!g_deletedTextureIds) return;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) g_deletedTextureIds->push_back(textures[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears the recording hook even when a gtest assertion unwinds the test body
|
||||||
|
// (a dangling pointer to the dead stack vector would corrupt later tests).
|
||||||
|
struct ScopedDeletedTextureRecording {
|
||||||
|
explicit ScopedDeletedTextureRecording(MobileGL::Vector<GLuint>& sink) { g_deletedTextureIds = &sink; }
|
||||||
|
~ScopedDeletedTextureRecording() { g_deletedTextureIds = nullptr; }
|
||||||
|
ScopedDeletedTextureRecording(const ScopedDeletedTextureRecording&) = delete;
|
||||||
|
ScopedDeletedTextureRecording& operator=(const ScopedDeletedTextureRecording&) = delete;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedDirectGLESTextureBindings scoped; // installs glGenTextures/glBindTexture mocks + resets caches
|
||||||
|
MobileGL::Vector<GLuint> deleted;
|
||||||
|
ScopedDeletedTextureRecording recording(deleted);
|
||||||
|
auto functions = g_GLESFuncs;
|
||||||
|
functions.glDeleteTextures = SG_DeleteTextures;
|
||||||
|
SetGLESFuncsTable(functions);
|
||||||
|
|
||||||
|
const auto texture2DSlot = static_cast<MobileGL::SizeT>(MobileGL::TextureTarget::Texture2D);
|
||||||
|
GLuint id = 0;
|
||||||
|
{
|
||||||
|
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||||
|
id = backendTexture->GetBackendTextureId();
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
backendTexture->Bind(GL_TEXTURE_2D, 0);
|
||||||
|
ASSERT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], backendTexture.get());
|
||||||
|
}
|
||||||
|
// Frontend glDeleteTextures used to leak the backend id forever and leave the
|
||||||
|
// cache pointer dangling (heap-address reuse then false-skips a later Bind).
|
||||||
|
ASSERT_EQ(deleted.size(), 1u);
|
||||||
|
EXPECT_EQ(deleted[0], id);
|
||||||
|
EXPECT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], nullptr);
|
||||||
|
|
||||||
|
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
||||||
|
{
|
||||||
|
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||||
|
++TextureImpl::g_textureContextGeneration;
|
||||||
|
backendTexture.reset();
|
||||||
|
--TextureImpl::g_textureContextGeneration; // restore for later tests
|
||||||
|
EXPECT_EQ(deleted.size(), 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
||||||
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
|
ScopedStateGuardMocks mocks;
|
||||||
|
|
||||||
|
// The regression this guards against: binding framebuffer 0 raw while the
|
||||||
|
// shadow keeps a user-FBO id makes the next re-bind of that FBO false-skip.
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7);
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 0); // default-FBO path must use this API
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
||||||
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
SpirvPassTest
|
||||||
|
SpirvPassTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(SpirvPassTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
SpirvPassTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
|
||||||
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// glslangValidator -V output. Both are vertex shaders writing gl_Position through
|
||||||
|
// the gl_PerVertex block, i.e. the Position builtin arrives as OpMemberDecorate rather
|
||||||
|
// than a plain OpDecorate - the shape real glslang output actually takes.
|
||||||
|
|
||||||
|
// #version 450
|
||||||
|
// layout(location = 0) in vec4 inPos;
|
||||||
|
// void main() { gl_Position = inPos; }
|
||||||
|
constexpr Uint32 kPlainVertexSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u,
|
||||||
|
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||||
|
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u,
|
||||||
|
0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u,
|
||||||
|
0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
|
||||||
|
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
|
||||||
|
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u,
|
||||||
|
0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u,
|
||||||
|
0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu,
|
||||||
|
0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u,
|
||||||
|
0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u,
|
||||||
|
0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu,
|
||||||
|
0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu,
|
||||||
|
0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
|
||||||
|
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
|
||||||
|
0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu,
|
||||||
|
0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u,
|
||||||
|
0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au,
|
||||||
|
0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu,
|
||||||
|
0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u,
|
||||||
|
0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u,
|
||||||
|
0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u,
|
||||||
|
0x00000001u, 0x00040020u, 0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u,
|
||||||
|
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u,
|
||||||
|
0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u,
|
||||||
|
0x00000014u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u,
|
||||||
|
0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... plus `invariant gl_Position;` - already carries OpMemberDecorate %gl_PerVertex 0
|
||||||
|
// Invariant, so the pass must not add a duplicate.
|
||||||
|
constexpr Uint32 kAlreadyInvariantVertexSpirv[] = {
|
||||||
|
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u,
|
||||||
|
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||||
|
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u,
|
||||||
|
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u,
|
||||||
|
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||||
|
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u,
|
||||||
|
0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u,
|
||||||
|
0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
|
||||||
|
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
|
||||||
|
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u,
|
||||||
|
0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u,
|
||||||
|
0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu,
|
||||||
|
0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u,
|
||||||
|
0x00040048u, 0x0000000bu, 0x00000000u, 0x00000012u, 0x00050048u, 0x0000000bu,
|
||||||
|
0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u,
|
||||||
|
0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu,
|
||||||
|
0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu, 0x00000000u, 0x00020013u,
|
||||||
|
0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u,
|
||||||
|
0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u,
|
||||||
|
0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, 0x00000009u,
|
||||||
|
0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu,
|
||||||
|
0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u,
|
||||||
|
0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du,
|
||||||
|
0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, 0x0004002bu,
|
||||||
|
0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u, 0x00000001u,
|
||||||
|
0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u, 0x00040020u,
|
||||||
|
0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u,
|
||||||
|
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u,
|
||||||
|
0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u, 0x00000014u, 0x0000000du,
|
||||||
|
0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u, 0x000100fdu, 0x00010038u,
|
||||||
|
};
|
||||||
|
|
||||||
|
// OpMemberDecorate <struct-id> <member> <decoration>
|
||||||
|
constexpr Uint32 kOpMemberDecorate = 72;
|
||||||
|
constexpr Uint32 kDecorationInvariant = 18;
|
||||||
|
constexpr Uint32 kSpirvHeaderWordCount = 5;
|
||||||
|
|
||||||
|
// Test-side reference walker. Deliberately independent of the production code so a bug in
|
||||||
|
// the pass cannot hide behind the same helper; only used to count what the pass emitted.
|
||||||
|
Uint32 CountInvariantMemberDecorations(const Vector<Uint32>& spirv) {
|
||||||
|
Uint32 count = 0;
|
||||||
|
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
|
||||||
|
const Uint32 wordCount = spirv[i] >> 16;
|
||||||
|
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||||
|
if (wordCount == 0 || i + wordCount > spirv.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (opcode == kOpMemberDecorate && wordCount >= 4 && spirv[i + 3] == kDecorationInvariant) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
i += wordCount;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <SizeT WordCount>
|
||||||
|
Vector<Uint32> ToVector(const Uint32 (&words)[WordCount]) {
|
||||||
|
return Vector<Uint32>(words, words + WordCount);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// --- DecoratePositionInvariantPass ---
|
||||||
|
|
||||||
|
TEST(DecoratePositionInvariant, AddsInvariantToThePositionMember) {
|
||||||
|
const Vector<Uint32> input = ToVector(kPlainVertexSpirv);
|
||||||
|
ASSERT_EQ(CountInvariantMemberDecorations(input), 0u);
|
||||||
|
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output));
|
||||||
|
EXPECT_EQ(CountInvariantMemberDecorations(output), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DecoratePositionInvariant, DoesNotDuplicateAnExistingInvariant) {
|
||||||
|
const Vector<Uint32> input = ToVector(kAlreadyInvariantVertexSpirv);
|
||||||
|
ASSERT_EQ(CountInvariantMemberDecorations(input), 1u);
|
||||||
|
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output));
|
||||||
|
EXPECT_EQ(CountInvariantMemberDecorations(output), 1u);
|
||||||
|
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
|
||||||
|
// builds) that such a run round-trips byte-identically. Pin that from the outside so an
|
||||||
|
// assert-enabled CI build cannot be the first thing to discover a violation.
|
||||||
|
EXPECT_EQ(output, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DecoratePositionInvariant, IsIdempotent) {
|
||||||
|
Vector<Uint32> once;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), once));
|
||||||
|
Vector<Uint32> twice;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(once, twice));
|
||||||
|
EXPECT_EQ(CountInvariantMemberDecorations(twice), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DecoratePositionInvariant, OutputStaysAReflectableModule) {
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), output));
|
||||||
|
|
||||||
|
SpvReflectShaderModule module{};
|
||||||
|
ASSERT_EQ(spvReflectCreateShaderModule(output.size() * sizeof(Uint32), output.data(), &module),
|
||||||
|
SPV_REFLECT_RESULT_SUCCESS);
|
||||||
|
EXPECT_EQ(module.entry_point_count, 1u);
|
||||||
|
spvReflectDestroyShaderModule(&module);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DecoratePositionInvariant, RejectsGarbageInput) {
|
||||||
|
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
|
||||||
|
Vector<Uint32> output;
|
||||||
|
EXPECT_FALSE(ShaderCompiler::DecoratePositionInvariantForVulkan(notSpirv, output));
|
||||||
|
}
|
||||||
@@ -158,6 +158,39 @@ namespace {
|
|||||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level));
|
return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ScopedTextureBackendFunctionsOverride {
|
||||||
|
public:
|
||||||
|
ScopedTextureBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {}
|
||||||
|
~ScopedTextureBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CopyTexSubImage2DCall {
|
||||||
|
Bool Called = false;
|
||||||
|
GLenum Target = GL_NONE;
|
||||||
|
GLint Level = -1;
|
||||||
|
GLint XOffset = -1;
|
||||||
|
GLint YOffset = -1;
|
||||||
|
GLint X = -1;
|
||||||
|
GLint Y = -1;
|
||||||
|
GLsizei Width = -1;
|
||||||
|
GLsizei Height = -1;
|
||||||
|
GLuint BoundTexture = 0;
|
||||||
|
} g_copyTexSubImage2DCall;
|
||||||
|
|
||||||
|
void RecordCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height) {
|
||||||
|
g_copyTexSubImage2DCall = {
|
||||||
|
true, target, level, xoffset, yoffset, x, y, width, height,
|
||||||
|
MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit())
|
||||||
|
.GetBindingSlot(TextureTarget::Texture2D)
|
||||||
|
.GetBoundObject()
|
||||||
|
->GetExternalIndex(),
|
||||||
|
};
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
|
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
|
||||||
@@ -178,6 +211,156 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, ClearTexImageNullClearsWholeNamedTextureAndMarksStorageDirty) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
const Uint8 initialPixels[] = {
|
||||||
|
1, 2, 3, 4,
|
||||||
|
5, 6, 7, 8,
|
||||||
|
9, 10, 11, 12,
|
||||||
|
13, 14, 15, 16,
|
||||||
|
};
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
|
mipmapObject->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, false);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
|
||||||
|
ASSERT_NE(stored, nullptr);
|
||||||
|
const Uint8 zeros[sizeof(initialPixels)] = {};
|
||||||
|
EXPECT_EQ(std::memcmp(stored, zeros, sizeof(zeros)), 0);
|
||||||
|
EXPECT_TRUE(mipmapObject->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, ClearTexImageRepeatsConvertedClearPixel) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const Uint8 clearPixel[] = {17, 34, 51, 68};
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, clearPixel);
|
||||||
|
|
||||||
|
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
|
||||||
|
ASSERT_NE(stored, nullptr);
|
||||||
|
const Uint8 expected[] = {
|
||||||
|
17, 34, 51, 68,
|
||||||
|
17, 34, 51, 68,
|
||||||
|
17, 34, 51, 68,
|
||||||
|
17, 34, 51, 68,
|
||||||
|
};
|
||||||
|
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, ClearTexSubImageClearsOnlyRequestedRectangle) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
Uint8 initialPixels[3 * 2 * 4];
|
||||||
|
std::memset(initialPixels, 0x7f, sizeof(initialPixels));
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::ClearTexSubImage(texture, 0, 1, 0, 0, 1, 2, 1,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
|
||||||
|
ASSERT_NE(stored, nullptr);
|
||||||
|
for (Int y = 0; y < 2; ++y) {
|
||||||
|
for (Int x = 0; x < 3; ++x) {
|
||||||
|
for (Int channel = 0; channel < 4; ++channel) {
|
||||||
|
EXPECT_EQ(stored[(y * 3 + x) * 4 + channel], x == 1 ? 0 : 0x7f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
|
||||||
|
g_copyTexSubImage2DCall = {};
|
||||||
|
|
||||||
|
GLuint namedTexture = 0;
|
||||||
|
GLuint boundTexture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &namedTexture);
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &boundTexture);
|
||||||
|
MG_Impl::GLImpl::BindTextureUnit(0, boundTexture);
|
||||||
|
|
||||||
|
const auto boundBefore = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||||
|
.GetBindingSlot(TextureTarget::Texture2D)
|
||||||
|
.GetBoundObject();
|
||||||
|
MG_Impl::GLImpl::CopyTextureSubImage2D(namedTexture, 2, 3, 4, 5, 6, 7, 8);
|
||||||
|
|
||||||
|
EXPECT_TRUE(g_copyTexSubImage2DCall.Called);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.Target, GL_TEXTURE_2D);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.Level, 2);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.XOffset, 3);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.YOffset, 4);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.X, 5);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.Y, 6);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.Width, 7);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.Height, 8);
|
||||||
|
EXPECT_EQ(g_copyTexSubImage2DCall.BoundTexture, namedTexture);
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0)
|
||||||
|
.GetBindingSlot(TextureTarget::Texture2D)
|
||||||
|
.GetBoundObject(),
|
||||||
|
boundBefore);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, CopyTextureSubImage2DRejectsCubeMapTargets) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
|
||||||
|
g_copyTexSubImage2DCall = {};
|
||||||
|
|
||||||
|
GLuint cubeTexture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &cubeTexture);
|
||||||
|
MG_Impl::GLImpl::CopyTextureSubImage2D(cubeTexture, 0, 0, 0, 0, 0, 1, 1);
|
||||||
|
|
||||||
|
// GL 4.6 sec. 8.8: the 2D form only accepts 2D/1D-array/rectangle effective targets.
|
||||||
|
EXPECT_FALSE(g_copyTexSubImage2DCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, ClearTexImageErrorContracts) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
// Zero texture name is INVALID_OPERATION (ARB_clear_texture).
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||||
|
|
||||||
|
// A negative level is INVALID_VALUE...
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(texture, -1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||||
|
|
||||||
|
// ...but clearing a level that was never defined is INVALID_OPERATION.
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(texture, 5, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||||
|
|
||||||
|
// A clear region outside the level is INVALID_VALUE.
|
||||||
|
MG_Impl::GLImpl::ClearTexSubImage(texture, 0, 1, 1, 0, 4, 4, 1,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||||
|
|
||||||
|
// An invalid pixel-transfer format is INVALID_ENUM from the shared validators.
|
||||||
|
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_NONE, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
|
||||||
|
}
|
||||||
|
|
||||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
|
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
|
||||||
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
||||||
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
||||||
@@ -1244,6 +1427,158 @@ TEST_F(TextureTest, TextureStorage1DAndSubImageModifyNamedObjectOnly) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Building a mip chain top-down - upload level N, then level 0 - must not destroy the levels
|
||||||
|
// already uploaded. AllocateLevel used to resize() the storage down to level+1 on every call, so
|
||||||
|
// the level-0 upload truncated the chain to a single level; the higher level then read back as
|
||||||
|
// {0,0,0}, IsComplete() rejected the zero-then-nonzero pattern, and DirectGLES answered that by
|
||||||
|
// skipping the texture's sync entirely. This is the shape KHR-GL33.texture_repeat_mode uses, and
|
||||||
|
// it accounted for 108 CTS failures in every GL version.
|
||||||
|
TEST_F(TextureTest, TexImage2DOnLevelZeroKeepsAnAlreadyUploadedHigherLevel) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 49, 23, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 98, 46, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
|
ASSERT_NE(mipmapObject, nullptr);
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u);
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(98, 46, 1));
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 1), IntVec3(49, 23, 1));
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half of the contract: respecifying a level 0 that already held an image still drops
|
||||||
|
// the chain, exactly as before. Minecraft rebinds the block-atlas name and calls glTexImage2D on
|
||||||
|
// level 0 before uploading the new levels; leaving the previous chain in place would strand a tail
|
||||||
|
// at the wrong sizes and - because Mojang terminates its chains with a 0x0 level - reproduce the
|
||||||
|
// same incomplete-texture black atlas the fix above exists to prevent.
|
||||||
|
TEST_F(TextureTest, TexImage2DRespecifyingAnExistingLevelZeroDropsTheStaleChain) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
|
ASSERT_NE(mipmapObject, nullptr);
|
||||||
|
ASSERT_EQ(mipmapObject->GetMipmapLevelCount(), 3u);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u);
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(16, 16, 1));
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same-size respecification has to drop the chain too. The Mipmap Levels video setting rebuilds
|
||||||
|
// the atlas at identical dimensions with a different level count, so a size-change-only test would
|
||||||
|
// let the old tail survive.
|
||||||
|
TEST_F(TextureTest, TexImage2DRespecifyingLevelZeroAtTheSameSizeStillDropsTheChain) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
|
ASSERT_NE(mipmapObject, nullptr);
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glTexStorage2D defines exactly `levels` levels. AllocateStorage only grows now, so the immutable
|
||||||
|
// path has to drop a longer pre-existing chain explicitly.
|
||||||
|
TEST_F(TextureTest, TexStorage2DTrimsALongerPreExistingMipChain) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 3, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
|
ASSERT_NE(mipmapObject, nullptr);
|
||||||
|
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// glTexImage2D used to reject every GL_COMPRESSED_* internal format with GL_INVALID_ENUM, because
|
||||||
|
// none of them mapped to a TextureInternalFormat and the "unknown format" gate fired. They now
|
||||||
|
// resolve to the uncompressed storage that backs them - what GL prescribes for the generic formats,
|
||||||
|
// and a deliberate deviation for RGTC, which ES cannot compress. The (format, type) pairs below are
|
||||||
|
// the ones KHR-GL33.packed_pixels uploads with, so this table doubles as a pin for those 480 cases.
|
||||||
|
TEST_F(TextureTest, CompressedInternalFormatsResolveToTheirUncompressedStorage) {
|
||||||
|
struct Case {
|
||||||
|
GLenum internalFormat;
|
||||||
|
GLenum format;
|
||||||
|
GLenum type;
|
||||||
|
TextureInternalFormat expected;
|
||||||
|
};
|
||||||
|
const Case cases[] = {
|
||||||
|
{GL_COMPRESSED_RED, GL_RED, GL_UNSIGNED_BYTE, TextureInternalFormat::R8},
|
||||||
|
{GL_COMPRESSED_RG, GL_RG, GL_UNSIGNED_BYTE, TextureInternalFormat::RG8},
|
||||||
|
{GL_COMPRESSED_RGB, GL_RGB, GL_UNSIGNED_BYTE, TextureInternalFormat::RGB8},
|
||||||
|
{GL_COMPRESSED_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, TextureInternalFormat::RGBA8},
|
||||||
|
{GL_COMPRESSED_SRGB, GL_RGB, GL_UNSIGNED_BYTE, TextureInternalFormat::SRGB8},
|
||||||
|
{GL_COMPRESSED_SRGB_ALPHA, GL_RGBA, GL_UNSIGNED_BYTE, TextureInternalFormat::SRGB8Alpha8},
|
||||||
|
{GL_COMPRESSED_RED_RGTC1, GL_RED, GL_UNSIGNED_BYTE, TextureInternalFormat::R8},
|
||||||
|
{GL_COMPRESSED_RG_RGTC2, GL_RG, GL_UNSIGNED_BYTE, TextureInternalFormat::RG8},
|
||||||
|
// The signed RGTC pair is uploaded as GL_BYTE and must land on SNORM storage - resolving
|
||||||
|
// them to plain R8/RG8 would silently reinterpret negative texels.
|
||||||
|
{GL_COMPRESSED_SIGNED_RED_RGTC1, GL_RED, GL_BYTE, TextureInternalFormat::R8Snorm},
|
||||||
|
{GL_COMPRESSED_SIGNED_RG_RGTC2, GL_RG, GL_BYTE, TextureInternalFormat::RG8Snorm},
|
||||||
|
};
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
for (const auto& c : cases) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, c.internalFormat, 4, 4, 0, c.format, c.type, nullptr);
|
||||||
|
|
||||||
|
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||||
|
ASSERT_NE(textureObject, nullptr) << "internalFormat 0x" << std::hex << c.internalFormat;
|
||||||
|
EXPECT_EQ(textureObject->GetFormat(), c.expected) << "internalFormat 0x" << std::hex << c.internalFormat;
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "internalFormat 0x" << std::hex << c.internalFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGTC compresses 4x4 blocks of a 2D image and has no 3D form, so glTexImage3D must reject it even
|
||||||
|
// though the same enum is accepted on a 2D target. The generic compressed formats carry no such
|
||||||
|
// restriction and stay legal in 3D.
|
||||||
|
TEST_F(TextureTest, RgtcInternalFormatsAreRejectedOnThreeDimensionalTargets) {
|
||||||
|
const GLenum rgtc[] = {GL_COMPRESSED_RED_RGTC1, GL_COMPRESSED_SIGNED_RED_RGTC1, GL_COMPRESSED_RG_RGTC2,
|
||||||
|
GL_COMPRESSED_SIGNED_RG_RGTC2};
|
||||||
|
for (const GLenum internalFormat : rgtc) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture);
|
||||||
|
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, internalFormat, 4, 4, 4, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION)
|
||||||
|
<< "internalFormat 0x" << std::hex << internalFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint generic = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &generic);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, generic);
|
||||||
|
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RGBA, 4, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(TextureTest, TextureStorage3DAndSubImageModifyNamedObjectOnly) {
|
TEST_F(TextureTest, TextureStorage3DAndSubImageModifyNamedObjectOnly) {
|
||||||
GLuint texture = 0;
|
GLuint texture = 0;
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
|
||||||
|
|||||||
@@ -811,6 +811,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
|
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
|
||||||
caps.SupportsDualSourceBlend = true;
|
caps.SupportsDualSourceBlend = true;
|
||||||
}
|
}
|
||||||
|
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||||
|
caps.SupportsNoperspectiveInterpolation = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,6 +868,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLint maxUniformBlockSize = 16384;
|
GLint maxUniformBlockSize = 16384;
|
||||||
GLint maxImageUnits = 8;
|
GLint maxImageUnits = 8;
|
||||||
GLint maxCombinedImageUniforms = 8;
|
GLint maxCombinedImageUniforms = 8;
|
||||||
|
GLint maxVertexImageUniforms = 0;
|
||||||
|
GLint maxGeometryImageUniforms = 0;
|
||||||
|
GLint maxFragmentImageUniforms = 8;
|
||||||
GLint maxComputeImageUniforms = 8;
|
GLint maxComputeImageUniforms = 8;
|
||||||
GLint maxDrawBuffers = 8;
|
GLint maxDrawBuffers = 8;
|
||||||
GLint maxColorAttachments = 8;
|
GLint maxColorAttachments = 8;
|
||||||
@@ -904,7 +910,16 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
|
glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms);
|
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms);
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImageUniforms);
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
// Geometry shaders and their image-uniform query are core only in ES 3.2. DirectGLES
|
||||||
|
// emits ESSL 3.10 on an ES 3.1 context, so reporting zero there is both legal and an
|
||||||
|
// accurate description of what the backend compiler can consume.
|
||||||
|
if (caps.GLESVersion.Major > 3 ||
|
||||||
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
||||||
|
}
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||||
@@ -955,6 +970,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxUniformBlockSize = maxUniformBlockSize;
|
caps.MaxUniformBlockSize = maxUniformBlockSize;
|
||||||
caps.MaxImageUnits = maxImageUnits;
|
caps.MaxImageUnits = maxImageUnits;
|
||||||
caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
|
caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
|
||||||
|
caps.MaxVertexImageUniforms = maxVertexImageUniforms;
|
||||||
|
caps.MaxGeometryImageUniforms = maxGeometryImageUniforms;
|
||||||
|
caps.MaxFragmentImageUniforms = maxFragmentImageUniforms;
|
||||||
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
||||||
caps.MaxDrawBuffers = maxDrawBuffers;
|
caps.MaxDrawBuffers = maxDrawBuffers;
|
||||||
caps.MaxColorAttachments = maxColorAttachments;
|
caps.MaxColorAttachments = maxColorAttachments;
|
||||||
@@ -1000,6 +1018,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize);
|
MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize);
|
||||||
MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits);
|
MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits);
|
||||||
MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms);
|
MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms);
|
||||||
|
MGLOG_I(" GL_MAX_VERTEX_IMAGE_UNIFORMS: %d", caps.MaxVertexImageUniforms);
|
||||||
|
MGLOG_I(" GL_MAX_GEOMETRY_IMAGE_UNIFORMS: %d", caps.MaxGeometryImageUniforms);
|
||||||
|
MGLOG_I(" GL_MAX_FRAGMENT_IMAGE_UNIFORMS: %d", caps.MaxFragmentImageUniforms);
|
||||||
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
|
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
|
||||||
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
||||||
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
||||||
|
|||||||
@@ -1050,6 +1050,11 @@ namespace MobileGL {
|
|||||||
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
|
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
|
||||||
// so without this a draw using a SRC1 factor cannot proceed.
|
// so without this a draw using a SRC1 factor cannot proceed.
|
||||||
Bool SupportsDualSourceBlend = false;
|
Bool SupportsDualSourceBlend = false;
|
||||||
|
// GL_NV_shader_noperspective_interpolation is present: the driver accepts the
|
||||||
|
// `noperspective` interpolation qualifier in ESSL. GLES core has none, so without this
|
||||||
|
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||||
|
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||||
|
Bool SupportsNoperspectiveInterpolation = false;
|
||||||
// GL_RENDERER contains "ANGLE".
|
// GL_RENDERER contains "ANGLE".
|
||||||
Bool IsAngleRenderer = false;
|
Bool IsAngleRenderer = false;
|
||||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
@@ -1102,6 +1107,9 @@ namespace MobileGL {
|
|||||||
Int MaxUniformBlockSize = 16384;
|
Int MaxUniformBlockSize = 16384;
|
||||||
Int MaxImageUnits = 8;
|
Int MaxImageUnits = 8;
|
||||||
Int MaxCombinedImageUniforms = 8;
|
Int MaxCombinedImageUniforms = 8;
|
||||||
|
Int MaxVertexImageUniforms = 0;
|
||||||
|
Int MaxGeometryImageUniforms = 0;
|
||||||
|
Int MaxFragmentImageUniforms = 8;
|
||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.VulkanAPIVersion = DecodeApiVersion(p.apiVersion);
|
caps.VulkanAPIVersion = DecodeApiVersion(p.apiVersion);
|
||||||
caps.DeviceName = p.deviceName;
|
caps.DeviceName = p.deviceName;
|
||||||
caps.DriverVersionString = DecodeDriverVersion(p.driverVersion);
|
caps.DriverVersionString = DecodeDriverVersion(p.driverVersion);
|
||||||
|
caps.VendorId = p.vendorID;
|
||||||
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
||||||
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
||||||
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
||||||
@@ -174,6 +175,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||||
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
|
||||||
|
caps.SupportsVertexPipelineStoresAndAtomics =
|
||||||
|
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
|
||||||
|
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
||||||
|
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
|
||||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
||||||
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
||||||
HasUsableShaderSubgroupSupport(subgroupProps);
|
HasUsableShaderSubgroupSupport(subgroupProps);
|
||||||
@@ -206,6 +211,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.VulkanAPIVersion = DecodeApiVersion(properties.apiVersion);
|
caps.VulkanAPIVersion = DecodeApiVersion(properties.apiVersion);
|
||||||
caps.DeviceName = properties.deviceName;
|
caps.DeviceName = properties.deviceName;
|
||||||
caps.DriverVersionString = DecodeDriverVersion(properties.driverVersion);
|
caps.DriverVersionString = DecodeDriverVersion(properties.driverVersion);
|
||||||
|
caps.VendorId = properties.vendorID;
|
||||||
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
||||||
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
||||||
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
||||||
@@ -256,6 +262,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||||
caps.SupportsWideLines = false;
|
caps.SupportsWideLines = false;
|
||||||
|
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
|
||||||
|
// stage writes disabled rather than inferring them from descriptor limits alone.
|
||||||
|
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
||||||
|
caps.SupportsFragmentStoresAndAtomics = false;
|
||||||
|
caps.SupportsGeometryShader = false;
|
||||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
||||||
caps.SupportsShaderSubgroup = false;
|
caps.SupportsShaderSubgroup = false;
|
||||||
caps.SubgroupSize = 0;
|
caps.SubgroupSize = 0;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace MobileGL {
|
|||||||
Version VulkanAPIVersion{1, 0, 0};
|
Version VulkanAPIVersion{1, 0, 0};
|
||||||
String DeviceName;
|
String DeviceName;
|
||||||
String DriverVersionString;
|
String DriverVersionString;
|
||||||
|
// VkPhysicalDeviceProperties::vendorID, for device-quirk vendor gating.
|
||||||
|
Uint32 VendorId = 0;
|
||||||
Int UniformBufferOffsetAlignment = 256;
|
Int UniformBufferOffsetAlignment = 256;
|
||||||
Float AliasedLineWidthRangeMin = 1.0f;
|
Float AliasedLineWidthRangeMin = 1.0f;
|
||||||
Float AliasedLineWidthRangeMax = 1.0f;
|
Float AliasedLineWidthRangeMax = 1.0f;
|
||||||
@@ -67,6 +69,12 @@ namespace MobileGL {
|
|||||||
Float ViewportBoundsRangeMax = 0.0f;
|
Float ViewportBoundsRangeMax = 0.0f;
|
||||||
Int ViewportSubpixelBits = 0;
|
Int ViewportSubpixelBits = 0;
|
||||||
Bool SupportsWideLines = false;
|
Bool SupportsWideLines = 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.
|
||||||
|
Bool SupportsVertexPipelineStoresAndAtomics = false;
|
||||||
|
Bool SupportsFragmentStoresAndAtomics = false;
|
||||||
|
Bool SupportsGeometryShader = false;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Bool SupportsShaderSubgroup = false;
|
Bool SupportsShaderSubgroup = false;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
|
|||||||
@@ -255,6 +255,37 @@ namespace MobileGL {
|
|||||||
return TextureInternalFormat::DepthComponent;
|
return TextureInternalFormat::DepthComponent;
|
||||||
case GL_DEPTH_STENCIL:
|
case GL_DEPTH_STENCIL:
|
||||||
return TextureInternalFormat::DepthStencil;
|
return TextureInternalFormat::DepthStencil;
|
||||||
|
// Compressed internal formats resolve to the uncompressed storage that backs them.
|
||||||
|
//
|
||||||
|
// For the six generic formats this is exactly what GL prescribes: the implementation
|
||||||
|
// picks a specific compressed format, and when none is available it falls back to the
|
||||||
|
// corresponding base format. Nothing downstream ever sees a compressed enum, so the
|
||||||
|
// metrics, pixel-store and backend tables keep their "one format, N bytes per texel"
|
||||||
|
// invariant instead of each needing a compressed-aware arm.
|
||||||
|
//
|
||||||
|
// The four RGTC formats are a deliberate deviation: they are specific formats that GL
|
||||||
|
// 3.3 requires, but ES exposes no RGTC compressor to hand the data to. Storing the
|
||||||
|
// texels uncompressed keeps them renderable at the cost of the memory saving, which is
|
||||||
|
// strictly better than the INVALID_ENUM the application used to get. Note the signed
|
||||||
|
// variants must land on SNORM storage - CTS uploads them as GL_BYTE.
|
||||||
|
case GL_COMPRESSED_RED:
|
||||||
|
case GL_COMPRESSED_RED_RGTC1:
|
||||||
|
return TextureInternalFormat::R8;
|
||||||
|
case GL_COMPRESSED_SIGNED_RED_RGTC1:
|
||||||
|
return TextureInternalFormat::R8Snorm;
|
||||||
|
case GL_COMPRESSED_RG:
|
||||||
|
case GL_COMPRESSED_RG_RGTC2:
|
||||||
|
return TextureInternalFormat::RG8;
|
||||||
|
case GL_COMPRESSED_SIGNED_RG_RGTC2:
|
||||||
|
return TextureInternalFormat::RG8Snorm;
|
||||||
|
case GL_COMPRESSED_RGB:
|
||||||
|
return TextureInternalFormat::RGB8;
|
||||||
|
case GL_COMPRESSED_RGBA:
|
||||||
|
return TextureInternalFormat::RGBA8;
|
||||||
|
case GL_COMPRESSED_SRGB:
|
||||||
|
return TextureInternalFormat::SRGB8;
|
||||||
|
case GL_COMPRESSED_SRGB_ALPHA:
|
||||||
|
return TextureInternalFormat::SRGB8Alpha8;
|
||||||
case GL_ALPHA:
|
case GL_ALPHA:
|
||||||
case GL_RED:
|
case GL_RED:
|
||||||
return TextureInternalFormat::Red;
|
return TextureInternalFormat::Red;
|
||||||
|
|||||||
@@ -401,6 +401,230 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
disabledNote);
|
disabledNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compiles + links a two-stage program on the probe context. Returns 0 on failure and writes a
|
||||||
|
// human-readable reason into |detail|.
|
||||||
|
GLuint CompileLinkProgram(const MG_External::GLESFunctionsTable& g, const char* vs, const char* fs,
|
||||||
|
String& detail) {
|
||||||
|
const auto compile = [&](GLenum stage, const char* src, GLuint& out) -> bool {
|
||||||
|
out = g.glCreateShader(stage);
|
||||||
|
if (out == 0) {
|
||||||
|
detail = "glCreateShader returned 0";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g.glShaderSource(out, 1, &src, nullptr);
|
||||||
|
g.glCompileShader(out);
|
||||||
|
GLint ok = GL_FALSE;
|
||||||
|
g.glGetShaderiv(out, GL_COMPILE_STATUS, &ok);
|
||||||
|
if (ok != GL_TRUE) {
|
||||||
|
GLchar log[512] = {};
|
||||||
|
GLsizei len = 0;
|
||||||
|
g.glGetShaderInfoLog(out, static_cast<GLsizei>(sizeof(log) - 1), &len, log);
|
||||||
|
detail = format("{} shader compile failed: {}",
|
||||||
|
stage == GL_VERTEX_SHADER ? "vertex" : "fragment",
|
||||||
|
len > 0 ? log : "(no info log)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
GLuint v = 0, f = 0;
|
||||||
|
const ScopeGuard delV([&]() { if (v) g.glDeleteShader(v); });
|
||||||
|
const ScopeGuard delF([&]() { if (f) g.glDeleteShader(f); });
|
||||||
|
if (!compile(GL_VERTEX_SHADER, vs, v) || !compile(GL_FRAGMENT_SHADER, fs, f)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint prog = g.glCreateProgram();
|
||||||
|
if (prog == 0) {
|
||||||
|
detail = "glCreateProgram returned 0";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
g.glAttachShader(prog, v);
|
||||||
|
g.glAttachShader(prog, f);
|
||||||
|
g.glLinkProgram(prog);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
g.glGetProgramiv(prog, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked != GL_TRUE) {
|
||||||
|
detail = "program link failed";
|
||||||
|
g.glDeleteProgram(prog);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "noperspective interpolation" row - a real correctness render, not just a compile. A viewport-
|
||||||
|
// filling quad is drawn with strong perspective (left clip-w 1, right clip-w 8) and a varying that
|
||||||
|
// runs 0..1 across it. At the screen centre screen-linear interpolation gives 0.5 while perspective-
|
||||||
|
// correct gives 1/(w+1) ~= 0.11, so reading the centre texel tells the two apart. The varying is
|
||||||
|
// carried either through the native `noperspective` qualifier (extension present) or through the
|
||||||
|
// exact gl_Position.w / gl_FragCoord.w rewrite MobileGL applies when it is absent. Verdict:
|
||||||
|
// PASS - extension present and the native noperspective result is screen-linear;
|
||||||
|
// WARN - extension absent but the gl_Position.w/gl_FragCoord.w emulation renders screen-linear
|
||||||
|
// (correct, just the fallback path shipping shader packs hit on such devices);
|
||||||
|
// FAIL - either path renders perspective-correct / wrong (noperspective does not actually work),
|
||||||
|
// or the program will not compile/link, or the render errors.
|
||||||
|
// Requires the probe context to still be current.
|
||||||
|
void ProbeGlesNoperspective(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
|
||||||
|
const MG_External::GLESFunctionsTable& g) {
|
||||||
|
const Bool native = caps.SupportsNoperspectiveInterpolation;
|
||||||
|
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
|
||||||
|
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
|
||||||
|
"gl_FragCoord.w emulation path)";
|
||||||
|
const auto fail = [&](const String& detail) {
|
||||||
|
builder.Fail("noperspective interpolation", pathNote + "; " + detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
|
||||||
|
!g.glGetShaderInfoLog || !g.glDeleteShader || !g.glCreateProgram || !g.glAttachShader ||
|
||||||
|
!g.glLinkProgram || !g.glGetProgramiv || !g.glUseProgram || !g.glDeleteProgram ||
|
||||||
|
!g.glGenFramebuffers || !g.glBindFramebuffer || !g.glDeleteFramebuffers ||
|
||||||
|
!g.glGenRenderbuffers || !g.glBindRenderbuffer || !g.glRenderbufferStorage ||
|
||||||
|
!g.glFramebufferRenderbuffer || !g.glDeleteRenderbuffers || !g.glCheckFramebufferStatus ||
|
||||||
|
!g.glGenBuffers || !g.glBindBuffer || !g.glBufferData || !g.glDeleteBuffers ||
|
||||||
|
!g.glGetAttribLocation || !g.glVertexAttribPointer || !g.glEnableVertexAttribArray ||
|
||||||
|
!g.glViewport || !g.glClearColor || !g.glClear || !g.glDrawArrays || !g.glReadPixels ||
|
||||||
|
!g.glFinish || !g.glGetError) {
|
||||||
|
fail("the render entry points did not resolve through eglGetProcAddress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match MobileGL's own ESSL target (the device's version). At #version 300 es some drivers
|
||||||
|
// (Adreno) still treat `noperspective` as reserved even with the extension enabled; the ES 3.2
|
||||||
|
// form the backend actually emits compiles. Emulated shaders are version-agnostic but use the
|
||||||
|
// same header for consistency.
|
||||||
|
const Int esslVer = caps.GLESVersion.Major * 100 + caps.GLESVersion.Minor * 10;
|
||||||
|
const String header = format("#version {} es\n", esslVer >= 300 ? esslVer : 300);
|
||||||
|
static const char* const kVsNativeBody =
|
||||||
|
"#extension GL_NV_shader_noperspective_interpolation : require\n"
|
||||||
|
"in vec4 a_pos;\n"
|
||||||
|
"in float a_v;\n"
|
||||||
|
"noperspective out highp float v_out;\n"
|
||||||
|
"void main() { gl_Position = a_pos; v_out = a_v; }\n";
|
||||||
|
static const char* const kFsNativeBody =
|
||||||
|
"#extension GL_NV_shader_noperspective_interpolation : require\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"noperspective in highp float v_out;\n"
|
||||||
|
"out vec4 fragColor;\n"
|
||||||
|
"void main() { fragColor = vec4(v_out, 0.0, 0.0, 1.0); }\n";
|
||||||
|
// Exactly MobileGL's emulation (verified against EmulateNoPerspectivePass output): pre-multiply
|
||||||
|
// the varying by clip-w in the vertex stage, recover with gl_FragCoord.w in the fragment stage,
|
||||||
|
// no noperspective qualifier (so the driver interpolates it perspective-correct).
|
||||||
|
static const char* const kVsEmuBody =
|
||||||
|
"in vec4 a_pos;\n"
|
||||||
|
"in float a_v;\n"
|
||||||
|
"out highp float v_out;\n"
|
||||||
|
"void main() { gl_Position = a_pos; v_out = a_v * gl_Position.w; }\n";
|
||||||
|
static const char* const kFsEmuBody =
|
||||||
|
"precision highp float;\n"
|
||||||
|
"in highp float v_out;\n"
|
||||||
|
"out vec4 fragColor;\n"
|
||||||
|
"void main() { fragColor = vec4(v_out * gl_FragCoord.w, 0.0, 0.0, 1.0); }\n";
|
||||||
|
|
||||||
|
while (g.glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const String vsSrc = header + (native ? kVsNativeBody : kVsEmuBody);
|
||||||
|
const String fsSrc = header + (native ? kFsNativeBody : kFsEmuBody);
|
||||||
|
String linkDetail;
|
||||||
|
const GLuint prog = CompileLinkProgram(g, vsSrc.c_str(), fsSrc.c_str(), linkDetail);
|
||||||
|
if (prog == 0) {
|
||||||
|
fail(native ? "a noperspective program failed to build though the extension is advertised: " +
|
||||||
|
linkDetail
|
||||||
|
: "the emulation program failed to build: " + linkDetail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ScopeGuard delProg([&]() { g.glDeleteProgram(prog); });
|
||||||
|
|
||||||
|
// 9x9 so the centre texel (4,4) sits exactly at NDC (0,0).
|
||||||
|
constexpr GLsizei kDim = 9;
|
||||||
|
GLuint rbo = 0, fbo = 0, vbo = 0;
|
||||||
|
g.glGenRenderbuffers(1, &rbo);
|
||||||
|
const ScopeGuard delRbo([&]() { if (rbo) g.glDeleteRenderbuffers(1, &rbo); });
|
||||||
|
g.glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||||
|
g.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kDim, kDim);
|
||||||
|
g.glGenFramebuffers(1, &fbo);
|
||||||
|
const ScopeGuard delFbo([&]() {
|
||||||
|
if (fbo) {
|
||||||
|
g.glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
g.glDeleteFramebuffers(1, &fbo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
g.glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
g.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
||||||
|
if (g.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
fail("the probe framebuffer is incomplete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interleaved [vec4 clip-pos, float v]. Left w=1, right w=8; x/y pre-multiplied by w so the quad
|
||||||
|
// still fills NDC after the perspective divide.
|
||||||
|
const GLfloat verts[] = {
|
||||||
|
-1.f, -1.f, 0.f, 1.f, 0.f, //
|
||||||
|
8.f, -8.f, 0.f, 8.f, 1.f, //
|
||||||
|
-1.f, 1.f, 0.f, 1.f, 0.f, //
|
||||||
|
8.f, 8.f, 0.f, 8.f, 1.f, //
|
||||||
|
};
|
||||||
|
g.glGenBuffers(1, &vbo);
|
||||||
|
const ScopeGuard delVbo([&]() { if (vbo) g.glDeleteBuffers(1, &vbo); });
|
||||||
|
g.glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
g.glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
g.glUseProgram(prog);
|
||||||
|
const GLint posLoc = g.glGetAttribLocation(prog, "a_pos");
|
||||||
|
const GLint vLoc = g.glGetAttribLocation(prog, "a_v");
|
||||||
|
if (posLoc < 0 || vLoc < 0) {
|
||||||
|
fail("the probe vertex attributes did not resolve");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g.glEnableVertexAttribArray(static_cast<GLuint>(posLoc));
|
||||||
|
g.glVertexAttribPointer(static_cast<GLuint>(posLoc), 4, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
|
||||||
|
reinterpret_cast<const void*>(0));
|
||||||
|
g.glEnableVertexAttribArray(static_cast<GLuint>(vLoc));
|
||||||
|
g.glVertexAttribPointer(static_cast<GLuint>(vLoc), 1, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
|
||||||
|
reinterpret_cast<const void*>(4 * sizeof(GLfloat)));
|
||||||
|
|
||||||
|
g.glViewport(0, 0, kDim, kDim);
|
||||||
|
g.glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
|
g.glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
g.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
g.glFinish();
|
||||||
|
|
||||||
|
const GLenum drawError = g.glGetError();
|
||||||
|
if (drawError != GL_NO_ERROR) {
|
||||||
|
fail(format("GL error 0x{:x} while rendering the probe quad", drawError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLubyte center[4] = {};
|
||||||
|
g.glReadPixels(kDim / 2, kDim / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, center);
|
||||||
|
const GLenum readError = g.glGetError();
|
||||||
|
if (readError != GL_NO_ERROR) {
|
||||||
|
fail(format("GL error 0x{:x} while reading the probe pixel back", readError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the centre: screen-linear -> 0.5 (~128); perspective-correct -> 1/(8+1) ~= 0.111 (~28).
|
||||||
|
const float observed = static_cast<float>(center[0]) / 255.0f;
|
||||||
|
const int observedByte = center[0];
|
||||||
|
constexpr float kScreenLinear = 0.5f;
|
||||||
|
const bool screenLinear = observed > 0.5f * (kScreenLinear + 1.0f / 9.0f); // midpoint ~= 0.306
|
||||||
|
if (!screenLinear) {
|
||||||
|
fail(format("the centre texel read {} (~{:.3f}); expected the screen-linear ~0.5 - "
|
||||||
|
"interpolation came out perspective-correct, so noperspective does not work here",
|
||||||
|
observedByte, observed));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (native) {
|
||||||
|
builder.Pass("noperspective interpolation",
|
||||||
|
pathNote + format("; native noperspective renders screen-linear (centre {} ~= 0.5)",
|
||||||
|
observedByte));
|
||||||
|
} else {
|
||||||
|
builder.Warn("noperspective interpolation",
|
||||||
|
pathNote +
|
||||||
|
format("; the emulation renders screen-linear correctly (centre {} ~= 0.5), "
|
||||||
|
"but this is the fallback path with less driver coverage",
|
||||||
|
observedByte));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Everything the "MobileGL reported ..." rows need from the GLES device probe.
|
// Everything the "MobileGL reported ..." rows need from the GLES device probe.
|
||||||
struct GlesProbeSummary {
|
struct GlesProbeSummary {
|
||||||
Bool capsValid = false;
|
Bool capsValid = false;
|
||||||
@@ -527,6 +751,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString);
|
builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString);
|
||||||
EvaluateGlesChecklist(builder, caps, glesFuncs);
|
EvaluateGlesChecklist(builder, caps, glesFuncs);
|
||||||
ProbeGlesTimerQuery(builder, caps, glesFuncs);
|
ProbeGlesTimerQuery(builder, caps, glesFuncs);
|
||||||
|
ProbeGlesNoperspective(builder, caps, glesFuncs);
|
||||||
builder.report.formatCapabilities.emplace();
|
builder.report.formatCapabilities.emplace();
|
||||||
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
||||||
glesFuncs, caps, builder.report.formatCapabilities.value());
|
glesFuncs, caps, builder.report.formatCapabilities.value());
|
||||||
|
|||||||
@@ -6,27 +6,36 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
|
#define SPV_ENABLE_UTILITY_CODE
|
||||||
|
#include "glslang/SPIRV/spirv.hpp11"
|
||||||
|
#undef SPV_ENABLE_UTILITY_CODE
|
||||||
|
|
||||||
#include "ShaderCompiler.h"
|
#include "ShaderCompiler.h"
|
||||||
|
|
||||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||||
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
||||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||||
|
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
|
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||||
|
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||||
#include "spirv-tools/libspirv.h"
|
#include "spirv-tools/libspirv.h"
|
||||||
#include "spirv-tools/optimizer.hpp"
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
#include "ShaderSourceProcessor.h"
|
#include "ShaderSourceProcessor.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
TBuiltInResource& GetTBuiltInResourceInstance() {
|
TBuiltInResource BuildTBuiltInResource() {
|
||||||
static TBuiltInResource Resources{};
|
TBuiltInResource Resources{};
|
||||||
Resources.maxLights = 32;
|
Resources.maxLights = 32;
|
||||||
Resources.maxClipPlanes = 6;
|
Resources.maxClipPlanes = 6;
|
||||||
Resources.maxTextureUnits = 32;
|
Resources.maxTextureUnits = 32;
|
||||||
@@ -121,6 +130,22 @@ namespace MobileGL {
|
|||||||
Resources.maxTaskWorkGroupSizeZ_NV = 1;
|
Resources.maxTaskWorkGroupSizeZ_NV = 1;
|
||||||
Resources.maxMeshViewCountNV = 4;
|
Resources.maxMeshViewCountNV = 4;
|
||||||
|
|
||||||
|
// Resource checking must describe the same backend contract exposed through
|
||||||
|
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
|
||||||
|
// TBuiltInResource when Iris compiles shaders concurrently.
|
||||||
|
const MG_Backend::DynamicBackendParameters fallbackParameters{};
|
||||||
|
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
||||||
|
const auto& dynamicParameters =
|
||||||
|
activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters;
|
||||||
|
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
|
||||||
|
Resources.maxCombinedImageUnitsAndFragmentOutputs =
|
||||||
|
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
|
||||||
|
Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms;
|
||||||
|
Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
|
||||||
|
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
|
||||||
|
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
|
||||||
|
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
|
||||||
|
|
||||||
Resources.limits.nonInductiveForLoops = true;
|
Resources.limits.nonInductiveForLoops = true;
|
||||||
Resources.limits.whileLoops = true;
|
Resources.limits.whileLoops = true;
|
||||||
Resources.limits.doWhileLoops = true;
|
Resources.limits.doWhileLoops = true;
|
||||||
@@ -166,7 +191,8 @@ namespace MobileGL {
|
|||||||
tshader->setAutoMapLocations(true);
|
tshader->setAutoMapLocations(true);
|
||||||
tshader->setAutoMapBindings(true);
|
tshader->setAutoMapBindings(true);
|
||||||
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
|
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
|
||||||
if (!tshader->parse(&GetTBuiltInResourceInstance(), 460, ECoreProfile,
|
auto resources = BuildTBuiltInResource();
|
||||||
|
if (!tshader->parse(&resources, 460, ECoreProfile,
|
||||||
/*forceDefaultVersionAndProfile: */ false,
|
/*forceDefaultVersionAndProfile: */ false,
|
||||||
/*forwardCompatible: */ true, EShMsgDefault)) {
|
/*forwardCompatible: */ true, EShMsgDefault)) {
|
||||||
ResultInfo r;
|
ResultInfo r;
|
||||||
@@ -310,6 +336,30 @@ namespace MobileGL {
|
|||||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::StripNoPerspectiveForEssl(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(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::EmulateNoPerspectiveForEssl(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(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
@@ -322,6 +372,148 @@ namespace MobileGL {
|
|||||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::DecoratePositionInvariantForVulkan(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(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||||
|
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
|
||||||
|
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||||
|
outputBinary.clear();
|
||||||
|
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Uint32> floatTypeIds;
|
||||||
|
Vector<Uint32> resultTypeById(inputBinary[3], 0);
|
||||||
|
Vector<Uint32> pointerPointeeTypeById(inputBinary[3], 0);
|
||||||
|
Bool hasReadWithoutFormatCapability = false;
|
||||||
|
Bool hasWriteWithoutFormatCapability = false;
|
||||||
|
SizeT capabilityInsertOffset = kSpirvHeaderWordCount;
|
||||||
|
|
||||||
|
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||||
|
const Uint32 instructionWord = inputBinary[offset];
|
||||||
|
const Uint32 wordCount = instructionWord >> 16u;
|
||||||
|
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||||
|
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opcode == spv::Op::OpCapability && wordCount >= 2) {
|
||||||
|
capabilityInsertOffset = offset + wordCount;
|
||||||
|
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
|
||||||
|
hasReadWithoutFormatCapability |=
|
||||||
|
capability == spv::Capability::StorageImageReadWithoutFormat;
|
||||||
|
hasWriteWithoutFormatCapability |=
|
||||||
|
capability == spv::Capability::StorageImageWriteWithoutFormat;
|
||||||
|
} else if (opcode == spv::Op::OpTypeFloat && wordCount >= 3) {
|
||||||
|
floatTypeIds.push_back(inputBinary[offset + 1]);
|
||||||
|
} else if (opcode == spv::Op::OpTypePointer && wordCount >= 4) {
|
||||||
|
const Uint32 pointerTypeId = inputBinary[offset + 1];
|
||||||
|
if (pointerTypeId >= pointerPointeeTypeById.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pointerPointeeTypeById[pointerTypeId] = inputBinary[offset + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasResult = false;
|
||||||
|
bool hasResultType = false;
|
||||||
|
spv::HasResultAndType(opcode, &hasResult, &hasResultType);
|
||||||
|
if (hasResult && hasResultType && wordCount >= 3) {
|
||||||
|
const Uint32 resultTypeId = inputBinary[offset + 1];
|
||||||
|
const Uint32 resultId = inputBinary[offset + 2];
|
||||||
|
if (resultId >= resultTypeById.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resultTypeById[resultId] = resultTypeId;
|
||||||
|
}
|
||||||
|
offset += wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpImageTexelPointer is the bridge to image atomic instructions. Vulkan requires
|
||||||
|
// those image types to retain an atomic-compatible declared format, so exclude only
|
||||||
|
// the exact image types used by an atomic path rather than disabling formatless
|
||||||
|
// access for unrelated float images in the same module.
|
||||||
|
Vector<Uint32> atomicImageTypeIds;
|
||||||
|
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||||
|
const Uint32 instructionWord = inputBinary[offset];
|
||||||
|
const Uint32 wordCount = instructionWord >> 16u;
|
||||||
|
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||||
|
if (opcode == spv::Op::OpImageTexelPointer && wordCount >= 6) {
|
||||||
|
const Uint32 imageId = inputBinary[offset + 3];
|
||||||
|
if (imageId >= resultTypeById.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uint32 imageTypeId = resultTypeById[imageId];
|
||||||
|
if (imageTypeId < pointerPointeeTypeById.size() &&
|
||||||
|
pointerPointeeTypeById[imageTypeId] != 0) {
|
||||||
|
imageTypeId = pointerPointeeTypeById[imageTypeId];
|
||||||
|
}
|
||||||
|
if (imageTypeId != 0 &&
|
||||||
|
std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) ==
|
||||||
|
atomicImageTypeIds.end()) {
|
||||||
|
atomicImageTypeIds.push_back(imageTypeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputBinary = inputBinary;
|
||||||
|
Bool hasFloatStorageImage = false;
|
||||||
|
for (SizeT offset = kSpirvHeaderWordCount; offset < outputBinary.size();) {
|
||||||
|
const Uint32 instructionWord = outputBinary[offset];
|
||||||
|
const Uint32 wordCount = instructionWord >> 16u;
|
||||||
|
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||||
|
|
||||||
|
// OpTypeImage operands are: result id, sampled type, dim, depth, arrayed,
|
||||||
|
// multisampled, sampled, image format, and an optional access qualifier.
|
||||||
|
if (opcode == spv::Op::OpTypeImage && wordCount >= 9) {
|
||||||
|
const Uint32 imageTypeId = outputBinary[offset + 1];
|
||||||
|
const Uint32 sampledTypeId = outputBinary[offset + 2];
|
||||||
|
const Uint32 sampled = outputBinary[offset + 7];
|
||||||
|
const Bool hasFloatSampledType =
|
||||||
|
std::find(floatTypeIds.begin(), floatTypeIds.end(), sampledTypeId) != floatTypeIds.end();
|
||||||
|
const Bool usedByAtomic =
|
||||||
|
std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) !=
|
||||||
|
atomicImageTypeIds.end();
|
||||||
|
if (sampled == 2 && hasFloatSampledType && !usedByAtomic) {
|
||||||
|
outputBinary[offset + 8] = static_cast<Uint32>(spv::ImageFormat::Unknown);
|
||||||
|
hasFloatStorageImage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFloatStorageImage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Uint32> addedCapabilities;
|
||||||
|
const Uint32 capabilityInstruction =
|
||||||
|
(2u << 16u) | static_cast<Uint32>(spv::Op::OpCapability);
|
||||||
|
if (!hasReadWithoutFormatCapability) {
|
||||||
|
addedCapabilities.push_back(capabilityInstruction);
|
||||||
|
addedCapabilities.push_back(
|
||||||
|
static_cast<Uint32>(spv::Capability::StorageImageReadWithoutFormat));
|
||||||
|
}
|
||||||
|
if (!hasWriteWithoutFormatCapability) {
|
||||||
|
addedCapabilities.push_back(capabilityInstruction);
|
||||||
|
addedCapabilities.push_back(
|
||||||
|
static_cast<Uint32>(spv::Capability::StorageImageWriteWithoutFormat));
|
||||||
|
}
|
||||||
|
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
|
||||||
|
addedCapabilities.begin(), addedCapabilities.end());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
||||||
spvc_compiler_options options;
|
spvc_compiler_options options;
|
||||||
session.CreateOptions(&options);
|
session.CreateOptions(&options);
|
||||||
|
|||||||
@@ -33,12 +33,37 @@ namespace MobileGL {
|
|||||||
// Only for the DirectGLES transpile path.
|
// Only for the DirectGLES transpile path.
|
||||||
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
|
||||||
|
// DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
|
||||||
|
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
|
||||||
|
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Emulates noperspective (screen-linear) interpolation via gl_Position.w / gl_FragCoord.w
|
||||||
|
// so no NV extension is needed; strips what it cannot emulate. DirectGLES fallback for
|
||||||
|
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||||
|
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||||
// which wrongly includes baseInstance).
|
// which wrongly includes baseInstance).
|
||||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
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
|
||||||
|
// mobile drivers that optimize per-pipeline break that without the
|
||||||
|
// decoration. DirectVulkan only.
|
||||||
|
static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Replaces the declared format of float storage images with Unknown and adds the
|
||||||
|
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
|
||||||
|
// shaderStorageImage*WithoutFormat features are enabled, allowing the
|
||||||
|
// glBindImageTexture format to select the descriptor view at runtime. Integer
|
||||||
|
// storage images deliberately keep their declared format for GL-compatible bit
|
||||||
|
// reinterpretation paths (for example, R32F storage accessed as r32ui).
|
||||||
|
static bool UseUnformattedFloatStorageImagesForVulkan(
|
||||||
|
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||||
static Result<String> DecompileShader(SpvcSession& session);
|
static Result<String> DecompileShader(SpvcSession& session);
|
||||||
};
|
};
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
|
|||||||
@@ -10,10 +10,15 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <utility>
|
||||||
|
#include <Config.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using MobileGL::SizeT;
|
using MobileGL::SizeT;
|
||||||
|
using MobileGL::String;
|
||||||
|
using MobileGL::Vector;
|
||||||
|
|
||||||
bool IsIdentifierChar(char ch) {
|
bool IsIdentifierChar(char ch) {
|
||||||
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
|
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
|
||||||
@@ -91,6 +96,460 @@ namespace {
|
|||||||
return masked;
|
return masked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blank out block comments in place, leaving line comments and every other byte where it is.
|
||||||
|
//
|
||||||
|
// The passes that follow scan the source as raw text, so block comments have to stop being
|
||||||
|
// visible to them - but they must not be *deleted*: replacing the bytes with spaces keeps every
|
||||||
|
// later offset valid and keeps newlines, so glslang's diagnostics still point at the line the
|
||||||
|
// application wrote. It also has to be lexically aware. A banner line such as
|
||||||
|
//
|
||||||
|
// //*** lighting pass ***
|
||||||
|
//
|
||||||
|
// contains "/*" one byte in, and a naive search for that opener treats the rest of the file as
|
||||||
|
// an unterminated comment.
|
||||||
|
void BlankBlockComments(MobileGL::String& source) {
|
||||||
|
enum class Region { Code, SingleLineComment, MultiLineComment, QuotedText };
|
||||||
|
|
||||||
|
Region region = Region::Code;
|
||||||
|
char quote = '\0';
|
||||||
|
bool escaped = false;
|
||||||
|
|
||||||
|
for (SizeT pos = 0; pos < source.size(); pos++) {
|
||||||
|
const char ch = source[pos];
|
||||||
|
const char next = pos + 1 < source.size() ? source[pos + 1] : '\0';
|
||||||
|
|
||||||
|
if (region == Region::Code) {
|
||||||
|
if (ch == '/' && next == '/') {
|
||||||
|
pos++;
|
||||||
|
region = Region::SingleLineComment;
|
||||||
|
} else if (ch == '/' && next == '*') {
|
||||||
|
source[pos] = ' ';
|
||||||
|
source[pos + 1] = ' ';
|
||||||
|
pos++;
|
||||||
|
region = Region::MultiLineComment;
|
||||||
|
} else if (ch == '"' || ch == '\'') {
|
||||||
|
quote = ch;
|
||||||
|
escaped = false;
|
||||||
|
region = Region::QuotedText;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region == Region::SingleLineComment) {
|
||||||
|
if (ch == '\n' || ch == '\r') region = Region::Code;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region == Region::MultiLineComment) {
|
||||||
|
if (ch == '*' && next == '/') {
|
||||||
|
source[pos] = ' ';
|
||||||
|
source[pos + 1] = ' ';
|
||||||
|
pos++;
|
||||||
|
region = Region::Code;
|
||||||
|
} else if (ch != '\n' && ch != '\r') {
|
||||||
|
source[pos] = ' ';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLSL has no multi-line string literals, so a quote that reaches end of line was never
|
||||||
|
// a literal to begin with - most likely an apostrophe in a #error or #pragma message.
|
||||||
|
// Ending the region here keeps one stray apostrophe from swallowing the rest of the file.
|
||||||
|
if (ch == '\n' || ch == '\r') {
|
||||||
|
region = Region::Code;
|
||||||
|
} else if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (ch == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (ch == quote) {
|
||||||
|
region = Region::Code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CodeToken {
|
||||||
|
String text;
|
||||||
|
SizeT begin = 0;
|
||||||
|
SizeT end = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Vector<CodeToken> TokenizeCode(const String& source) {
|
||||||
|
const String masked = MaskCommentsAndQuotedText(source);
|
||||||
|
Vector<CodeToken> tokens;
|
||||||
|
tokens.reserve(source.size() / 4);
|
||||||
|
|
||||||
|
SizeT pos = 0;
|
||||||
|
while (pos < masked.size()) {
|
||||||
|
const char ch = masked[pos];
|
||||||
|
if (std::isspace(static_cast<unsigned char>(ch))) {
|
||||||
|
++pos;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT begin = pos;
|
||||||
|
if (IsIdentifierStart(ch)) {
|
||||||
|
++pos;
|
||||||
|
while (pos < masked.size() && IsIdentifierChar(masked[pos])) {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
} else if (std::isdigit(static_cast<unsigned char>(ch))) {
|
||||||
|
++pos;
|
||||||
|
while (pos < masked.size()) {
|
||||||
|
const char numberChar = masked[pos];
|
||||||
|
if (!IsIdentifierChar(numberChar) && numberChar != '.') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++pos;
|
||||||
|
if (pos < masked.size()) {
|
||||||
|
const String twoChars = masked.substr(begin, 2);
|
||||||
|
if (twoChars == "==" || twoChars == "!=" || twoChars == "<=" || twoChars == ">=" ||
|
||||||
|
twoChars == "+=" || twoChars == "-=" || twoChars == "<<" || twoChars == ">>" ||
|
||||||
|
twoChars == "++" || twoChars == "--" || twoChars == "&&" || twoChars == "||") {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push_back(CodeToken{source.substr(begin, pos - begin), begin, pos});
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsIdentifierToken(const CodeToken& token) {
|
||||||
|
if (token.text.empty() || !IsIdentifierStart(token.text.front())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::all_of(token.text.begin() + 1, token.text.end(), IsIdentifierChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TokenCursor {
|
||||||
|
public:
|
||||||
|
TokenCursor(const Vector<CodeToken>& tokens, SizeT position) : m_tokens(tokens), m_position(position) {}
|
||||||
|
|
||||||
|
bool Consume(const char* expected) {
|
||||||
|
if (m_position >= m_tokens.size() || m_tokens[m_position].text != expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++m_position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConsumeAnyIdentifier(String& identifier) {
|
||||||
|
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
identifier = m_tokens[m_position++].text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConsumeAnyIdentifier() {
|
||||||
|
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++m_position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConsumeIdentifier(const String& expected) {
|
||||||
|
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position]) ||
|
||||||
|
m_tokens[m_position].text != expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++m_position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT Position() const { return m_position; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Vector<CodeToken>& m_tokens;
|
||||||
|
SizeT m_position;
|
||||||
|
};
|
||||||
|
|
||||||
|
SizeT CountToken(const Vector<CodeToken>& tokens, const String& tokenText) {
|
||||||
|
return static_cast<SizeT>(std::count_if(tokens.begin(), tokens.end(),
|
||||||
|
[&](const CodeToken& token) { return token.text == tokenText; }));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasIdentifierWithPrefixOutsideAllowed(const Vector<CodeToken>& tokens, const String& prefix,
|
||||||
|
std::initializer_list<const char*> allowedIdentifiers) {
|
||||||
|
return std::any_of(tokens.begin(), tokens.end(), [&](const CodeToken& token) {
|
||||||
|
if (!IsIdentifierToken(token) || !token.text.starts_with(prefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::none_of(allowedIdentifiers.begin(), allowedIdentifiers.end(),
|
||||||
|
[&](const char* allowed) { return token.text == allowed; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchTokenSequence(const Vector<CodeToken>& tokens, SizeT position,
|
||||||
|
std::initializer_list<const char*> expected) {
|
||||||
|
if (position + expected.size() > tokens.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const char* token : expected) {
|
||||||
|
if (tokens[position++].text != token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LinearPrefixScanMatch {
|
||||||
|
SizeT sharedArraySizeBegin = 0;
|
||||||
|
SizeT sharedArraySizeEnd = 0;
|
||||||
|
SizeT scanBegin = 0;
|
||||||
|
SizeT scanEnd = 0;
|
||||||
|
String cache;
|
||||||
|
String importance;
|
||||||
|
String prefixSum;
|
||||||
|
String loopLength;
|
||||||
|
String loopIndex;
|
||||||
|
String sum;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ParseLinearPrefixScanTemplate(const Vector<CodeToken>& tokens, LinearPrefixScanMatch& match) {
|
||||||
|
// The workaround deliberately recognizes one complete algorithm, not merely the
|
||||||
|
// subgroupInclusiveAdd token. Changing scratch storage is only safe when that storage is
|
||||||
|
// private to this scan and the workgroup has exactly 1024 X invocations.
|
||||||
|
SizeT localSizeDeclarationCount = 0;
|
||||||
|
for (SizeT i = 0; i < tokens.size(); ++i) {
|
||||||
|
if (MatchTokenSequence(tokens, i, {"layout", "(", "local_size_x", "=", "1024", ")", "in", ";"})) {
|
||||||
|
++localSizeDeclarationCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (localSizeDeclarationCount != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT sharedDeclarationIndex = String::npos;
|
||||||
|
SizeT sharedDeclarationCount = 0;
|
||||||
|
String cacheName;
|
||||||
|
for (SizeT i = 0; i + 6 < tokens.size(); ++i) {
|
||||||
|
if (tokens[i].text != "shared" || tokens[i + 1].text != "float" || !IsIdentifierToken(tokens[i + 2]) ||
|
||||||
|
tokens[i + 3].text != "[" || tokens[i + 4].text != "64" || tokens[i + 5].text != "]" ||
|
||||||
|
tokens[i + 6].text != ";") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++sharedDeclarationCount;
|
||||||
|
sharedDeclarationIndex = i;
|
||||||
|
cacheName = tokens[i + 2].text;
|
||||||
|
}
|
||||||
|
if (sharedDeclarationCount != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT scanTokenIndex = String::npos;
|
||||||
|
SizeT scanCount = 0;
|
||||||
|
for (SizeT i = 0; i + 7 < tokens.size(); ++i) {
|
||||||
|
if (tokens[i].text == "float" && IsIdentifierToken(tokens[i + 1]) && tokens[i + 2].text == "=" &&
|
||||||
|
tokens[i + 3].text == "subgroupInclusiveAdd" && tokens[i + 4].text == "(" &&
|
||||||
|
IsIdentifierToken(tokens[i + 5]) && tokens[i + 6].text == ")" && tokens[i + 7].text == ";") {
|
||||||
|
++scanCount;
|
||||||
|
scanTokenIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scanCount != 1 || sharedDeclarationIndex >= scanTokenIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenCursor cursor(tokens, scanTokenIndex);
|
||||||
|
String prefixSum;
|
||||||
|
String importance;
|
||||||
|
String loopLength;
|
||||||
|
String loopIndex;
|
||||||
|
String sum;
|
||||||
|
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(prefixSum) || !cursor.Consume("=") ||
|
||||||
|
!cursor.Consume("subgroupInclusiveAdd") || !cursor.Consume("(") ||
|
||||||
|
!cursor.ConsumeAnyIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume(";") ||
|
||||||
|
!cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") ||
|
||||||
|
!cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") ||
|
||||||
|
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) ||
|
||||||
|
!cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") ||
|
||||||
|
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("uint") ||
|
||||||
|
!cursor.ConsumeAnyIdentifier(loopLength) || !cursor.Consume("=") || !cursor.Consume("uint") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("findMSB") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume("gl_NumSubgroups") || !cursor.Consume(")") || !cursor.Consume(")") ||
|
||||||
|
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("+=") ||
|
||||||
|
!cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") ||
|
||||||
|
!cursor.Consume("-") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") ||
|
||||||
|
!cursor.Consume("(") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("-") ||
|
||||||
|
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") ||
|
||||||
|
!cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("for") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopIndex) ||
|
||||||
|
!cursor.Consume("=") || !cursor.Consume("0") || !cursor.Consume(";") ||
|
||||||
|
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<") || !cursor.ConsumeIdentifier(loopLength) ||
|
||||||
|
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("++") ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.Consume("if") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("&") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.ConsumeIdentifier(prefixSum) ||
|
||||||
|
!cursor.Consume("+=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume(">>") ||
|
||||||
|
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume("]") ||
|
||||||
|
!cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") ||
|
||||||
|
!cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") ||
|
||||||
|
!cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") ||
|
||||||
|
!cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) ||
|
||||||
|
!cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("barrier") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("if") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
|
||||||
|
!cursor.Consume("x") || !cursor.Consume("==") || !cursor.Consume("uint") || !cursor.Consume("(") ||
|
||||||
|
!cursor.Consume("1024") || !cursor.Consume("-") || !cursor.Consume("1") || !cursor.Consume(")") ||
|
||||||
|
!cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
|
||||||
|
!cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume("=") ||
|
||||||
|
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("float") ||
|
||||||
|
!cursor.ConsumeAnyIdentifier(sum) || !cursor.Consume("=") || !cursor.ConsumeIdentifier(cacheName) ||
|
||||||
|
!cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume(";")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SizeT scanEndToken = cursor.Position() - 1;
|
||||||
|
|
||||||
|
// Require the scan's immediate consumer as well. This makes the match specific to a
|
||||||
|
// linear distribution warp, and avoids changing unrelated prefix scans which may rely on
|
||||||
|
// the implementation's native subgroup partitioning.
|
||||||
|
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier() || !cursor.Consume("=") ||
|
||||||
|
!cursor.Consume("(") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("-") ||
|
||||||
|
!cursor.ConsumeIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume("/") ||
|
||||||
|
!cursor.ConsumeIdentifier(sum) || !cursor.Consume("-") || !cursor.Consume("float") ||
|
||||||
|
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
|
||||||
|
!cursor.Consume("x") || !cursor.Consume("+") || !cursor.Consume("1u") || !cursor.Consume(")") ||
|
||||||
|
!cursor.Consume("/") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("1024") ||
|
||||||
|
!cursor.Consume(")") || !cursor.Consume(";")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No other use may share the scratch array, and no additional subgroup operation or
|
||||||
|
// builtin may silently retain native-64 semantics after this module becomes virtual-32.
|
||||||
|
if (CountToken(tokens, cacheName) != 6 || CountToken(tokens, "subgroupInclusiveAdd") != 1 ||
|
||||||
|
CountToken(tokens, "gl_SubgroupInvocationID") != 2 || CountToken(tokens, "gl_SubgroupSize") != 2 ||
|
||||||
|
CountToken(tokens, "gl_SubgroupID") != 4 || CountToken(tokens, "gl_NumSubgroups") != 2 ||
|
||||||
|
CountToken(tokens, "gl_LocalInvocationID") != 2 || CountToken(tokens, "barrier") != 3 ||
|
||||||
|
CountToken(tokens, "findMSB") != 1 ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(
|
||||||
|
tokens, "gl_Subgroup",
|
||||||
|
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) ||
|
||||||
|
// ARB/NV spellings of lane-width-sensitive builtins and functions
|
||||||
|
// (gl_SubGroupSizeARB, ballotARB, gl_WarpSizeNV, shuffleNV, ...) must block the
|
||||||
|
// rewrite just like their KHR counterparts: they would silently keep native-width
|
||||||
|
// semantics in a module rewritten to the virtual 32-lane model.
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SubGroup", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Warp", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_Thread", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "gl_SMID", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "ballot", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "shuffle", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "readInvocation", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "readFirstInvocation", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "anyInvocation", {}) ||
|
||||||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "allInvocations", {})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The scan must be at the top level of the sole main() body. Its existing barriers already
|
||||||
|
// require uniform control flow; this check prevents us from introducing extra barriers in
|
||||||
|
// a nested branch or loop.
|
||||||
|
SizeT mainOpenBrace = String::npos;
|
||||||
|
SizeT mainCloseBrace = String::npos;
|
||||||
|
SizeT mainCount = 0;
|
||||||
|
for (SizeT i = 0; i + 4 < tokens.size(); ++i) {
|
||||||
|
if (!MatchTokenSequence(tokens, i, {"void", "main", "(", ")", "{"})) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++mainCount;
|
||||||
|
mainOpenBrace = i + 4;
|
||||||
|
int depth = 1;
|
||||||
|
for (SizeT j = mainOpenBrace + 1; j < tokens.size(); ++j) {
|
||||||
|
if (tokens[j].text == "{")
|
||||||
|
++depth;
|
||||||
|
else if (tokens[j].text == "}" && --depth == 0) {
|
||||||
|
mainCloseBrace = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mainCount != 1 || mainCloseBrace == String::npos || scanTokenIndex <= mainOpenBrace ||
|
||||||
|
scanEndToken >= mainCloseBrace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int depthAtScan = 1;
|
||||||
|
for (SizeT i = mainOpenBrace + 1; i < scanTokenIndex; ++i) {
|
||||||
|
if (tokens[i].text == "{")
|
||||||
|
++depthAtScan;
|
||||||
|
else if (tokens[i].text == "}")
|
||||||
|
--depthAtScan;
|
||||||
|
}
|
||||||
|
if (depthAtScan != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr const char* injectedNames[] = {"mglPrefixScanLane", "mglVirtualSubgroupInvocation",
|
||||||
|
"mglVirtualSubgroup", "mglVirtualSubgroupBase",
|
||||||
|
"mglPrefixLane", "mglVirtualSubgroupCount"};
|
||||||
|
for (const char* injectedName : injectedNames) {
|
||||||
|
if (CountToken(tokens, injectedName) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match.sharedArraySizeBegin = tokens[sharedDeclarationIndex + 4].begin;
|
||||||
|
match.sharedArraySizeEnd = tokens[sharedDeclarationIndex + 4].end;
|
||||||
|
match.scanBegin = tokens[scanTokenIndex].begin;
|
||||||
|
match.scanEnd = tokens[scanEndToken].end;
|
||||||
|
match.cache = std::move(cacheName);
|
||||||
|
match.importance = std::move(importance);
|
||||||
|
match.prefixSum = std::move(prefixSum);
|
||||||
|
match.loopLength = std::move(loopLength);
|
||||||
|
match.loopIndex = std::move(loopIndex);
|
||||||
|
match.sum = std::move(sum);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String BuildLinearPrefixScanReplacement(const LinearPrefixScanMatch& match) {
|
||||||
|
String replacement;
|
||||||
|
replacement.reserve(1800);
|
||||||
|
replacement += "uint mglPrefixScanLane = gl_LocalInvocationID.x;\n";
|
||||||
|
replacement += "uint mglVirtualSubgroupInvocation = mglPrefixScanLane & 31u;\n";
|
||||||
|
replacement += "uint mglVirtualSubgroup = mglPrefixScanLane >> 5u;\n";
|
||||||
|
replacement += "const uint mglVirtualSubgroupCount = 32u;\n";
|
||||||
|
replacement += match.cache + "[mglPrefixScanLane] = " + match.importance + ";\n";
|
||||||
|
replacement += "barrier();\n";
|
||||||
|
replacement += "float " + match.prefixSum + " = 0.0f;\n";
|
||||||
|
replacement += "uint mglVirtualSubgroupBase = mglVirtualSubgroup << 5u;\n";
|
||||||
|
replacement += "for (uint mglPrefixLane = mglVirtualSubgroupBase; "
|
||||||
|
"mglPrefixLane <= mglPrefixScanLane; ++mglPrefixLane) {\n";
|
||||||
|
replacement += match.prefixSum + " += " + match.cache + "[mglPrefixLane];\n";
|
||||||
|
replacement += "}\n";
|
||||||
|
replacement += "barrier();\n";
|
||||||
|
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
|
||||||
|
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
|
||||||
|
replacement += "barrier();\n";
|
||||||
|
replacement += "uint " + match.loopLength + " = uint(findMSB(mglVirtualSubgroupCount));\n";
|
||||||
|
replacement +=
|
||||||
|
match.loopLength + " += uint(mglVirtualSubgroupCount - (1u << (" + match.loopLength + " - 1u)) > 0u);\n";
|
||||||
|
replacement += "for (uint " + match.loopIndex + " = 0u; " + match.loopIndex + " < " + match.loopLength +
|
||||||
|
"; ++" + match.loopIndex + ") {\n";
|
||||||
|
replacement += "if ((mglVirtualSubgroup & (1u << " + match.loopIndex + ")) > 0u) {\n";
|
||||||
|
replacement += match.prefixSum + " += " + match.cache + "[(mglVirtualSubgroup >> " + match.loopIndex + " << " +
|
||||||
|
match.loopIndex + ") - 1u];\n";
|
||||||
|
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
|
||||||
|
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
|
||||||
|
replacement += "}\nbarrier();\n}\n";
|
||||||
|
replacement += "if (mglPrefixScanLane == 1023u) " + match.cache + "[0] = " + match.prefixSum + ";\n";
|
||||||
|
replacement += "barrier();\n";
|
||||||
|
replacement += "float " + match.sum + " = " + match.cache + "[0];";
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
|
||||||
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
|
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
|
||||||
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
|
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
|
||||||
pos++;
|
pos++;
|
||||||
@@ -114,6 +573,23 @@ namespace {
|
|||||||
static_cast<unsigned char>(source[1]) == 0xbb && static_cast<unsigned char>(source[2]) == 0xbf;
|
static_cast<unsigned char>(source[1]) == 0xbb && static_cast<unsigned char>(source[2]) == 0xbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The GLSL versions MobileGL is willing to normalize. Anything else in a #version line - a number
|
||||||
|
// that is not a real language version (329, 331), a bad profile keyword, a float/identifier where
|
||||||
|
// the integer belongs, or trailing tokens - is left untouched so glslang rejects it, matching
|
||||||
|
// KHR-GL33.shaders.preprocessor.directive.version_*. The set is deliberately generous (every real
|
||||||
|
// desktop and ES version) so the normalizer never starts rejecting a form it used to accept.
|
||||||
|
bool IsRecognizedGlslVersion(unsigned version) {
|
||||||
|
switch (version) {
|
||||||
|
case 100: case 110: case 120: case 130: case 140: case 150:
|
||||||
|
case 300: case 310: case 320:
|
||||||
|
case 330: case 400: case 410: case 420: case 430:
|
||||||
|
case 440: case 450: case 460:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ShaderLanguageInfo {
|
struct ShaderLanguageInfo {
|
||||||
unsigned version = 110;
|
unsigned version = 110;
|
||||||
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
|
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
|
||||||
@@ -121,6 +597,9 @@ namespace {
|
|||||||
SizeT versionDirectiveEnd = MobileGL::String::npos;
|
SizeT versionDirectiveEnd = MobileGL::String::npos;
|
||||||
bool hasUtf8Bom = false;
|
bool hasUtf8Bom = false;
|
||||||
bool enablesGpuShader5 = false;
|
bool enablesGpuShader5 = false;
|
||||||
|
// Whether the parsed #version directive is a well-formed one MobileGL should rewrite. A
|
||||||
|
// malformed directive (see IsRecognizedGlslVersion) is left alone for glslang to reject.
|
||||||
|
bool hasValidVersionDirective = false;
|
||||||
|
|
||||||
bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; }
|
bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; }
|
||||||
};
|
};
|
||||||
@@ -164,13 +643,25 @@ namespace {
|
|||||||
info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
|
info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
|
||||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||||
const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd);
|
const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd);
|
||||||
if (profile == "es" || profile == "ES") {
|
bool profileTokenValid = true;
|
||||||
|
if (profile.empty() || profile == "core") {
|
||||||
|
info.profile = MobileGL::ShaderProfile::Core;
|
||||||
|
} else if (profile == "es" || profile == "ES") {
|
||||||
info.profile = MobileGL::ShaderProfile::ES;
|
info.profile = MobileGL::ShaderProfile::ES;
|
||||||
} else if (profile == "compatibility") {
|
} else if (profile == "compatibility") {
|
||||||
info.profile = MobileGL::ShaderProfile::Compatibility;
|
info.profile = MobileGL::ShaderProfile::Compatibility;
|
||||||
} else {
|
} else {
|
||||||
|
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any
|
||||||
|
// downstream routing, but mark the directive malformed.
|
||||||
info.profile = MobileGL::ShaderProfile::Core;
|
info.profile = MobileGL::ShaderProfile::Core;
|
||||||
|
profileTokenValid = false;
|
||||||
}
|
}
|
||||||
|
// Comments are already masked to spaces, so anything non-blank left on the
|
||||||
|
// line is real trailing garbage: "#version 330 foobar" / "#version 330.0".
|
||||||
|
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||||
|
const bool hasTrailingTokens = probe < lineEnd;
|
||||||
|
info.hasValidVersionDirective =
|
||||||
|
IsRecognizedGlslVersion(info.version) && profileTokenValid && !hasTrailingTokens;
|
||||||
}
|
}
|
||||||
} else if (directive == "extension") {
|
} else if (directive == "extension") {
|
||||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||||
@@ -217,6 +708,17 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
||||||
|
// A malformed #version (329, 331, bad profile, float/trailing tokens) is left exactly as the
|
||||||
|
// application wrote it so glslang rejects it - rewriting it to "#version 330 core" would
|
||||||
|
// silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so
|
||||||
|
// the reported error is the bad version rather than a stray byte-order mark.
|
||||||
|
if (info.HasVersionDirective() && !info.hasValidVersionDirective) {
|
||||||
|
if (info.hasUtf8Bom) {
|
||||||
|
source.erase(0, 3);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const MobileGL::String replacement = GetNormalizedVersionDirective(info);
|
const MobileGL::String replacement = GetNormalizedVersionDirective(info);
|
||||||
if (info.HasVersionDirective()) {
|
if (info.HasVersionDirective()) {
|
||||||
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
||||||
@@ -298,7 +800,10 @@ namespace {
|
|||||||
|
|
||||||
void RenameBuiltinShadowingFunction(MobileGL::String& source, const char* from, const char* to) {
|
void RenameBuiltinShadowingFunction(MobileGL::String& source, const char* from, const char* to) {
|
||||||
const MobileGL::String fromName = from;
|
const MobileGL::String fromName = from;
|
||||||
if (!HasSingleLineFunctionDefinition(source, fromName)) {
|
// Decide from a comment-free view. A commented-out definition is not a definition, and
|
||||||
|
// acting on one renames every genuine call to the builtin to a name nothing defines - which
|
||||||
|
// then fails to resolve. Line comments survive BlankBlockComments, so this matters.
|
||||||
|
if (!HasSingleLineFunctionDefinition(MaskCommentsAndQuotedText(source), fromName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +876,58 @@ namespace {
|
|||||||
return info.HasVersionDirective() ? info.versionDirectiveEnd : 0;
|
return info.HasVersionDirective() ? info.versionDirectiveEnd : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GLSL's #line takes integer expressions only, but plenty of shader-pack preprocessors emit the
|
||||||
|
// C form with a quoted filename. Deleting every #line outright made those harmless - at the cost
|
||||||
|
// of __LINE__ reporting the position in MobileGL's rewritten text rather than the one the pack
|
||||||
|
// author wrote, and of every later diagnostic pointing at the wrong line. Dropping just the
|
||||||
|
// quoted operand keeps the directive doing its job and still hands glslang something it accepts.
|
||||||
|
void NormalizeLineDirectives(MobileGL::String& source) {
|
||||||
|
const MobileGL::String masked = MaskCommentsAndQuotedText(source);
|
||||||
|
const SizeT versionEnd = FindAfterVersionDirective(source);
|
||||||
|
MobileGL::String result;
|
||||||
|
result.reserve(source.size());
|
||||||
|
|
||||||
|
SizeT lineStart = 0;
|
||||||
|
while (lineStart <= source.size()) {
|
||||||
|
SizeT lineEnd = source.find('\n', lineStart);
|
||||||
|
const bool lastLine = lineEnd == MobileGL::String::npos;
|
||||||
|
if (lastLine) lineEnd = source.size();
|
||||||
|
|
||||||
|
SizeT probe = lineStart;
|
||||||
|
while (probe < lineEnd && (source[probe] == ' ' || source[probe] == '\t')) probe++;
|
||||||
|
|
||||||
|
const bool isLineDirective = masked.compare(probe, 5, "#line") == 0 &&
|
||||||
|
(probe + 5 >= lineEnd || !IsIdentifierChar(source[probe + 5]));
|
||||||
|
if (isLineDirective && lineStart < versionEnd) {
|
||||||
|
// #version has to be the first token in the shader, so a #line ahead of it could
|
||||||
|
// never have taken effect. Drop it rather than hand glslang a source it must reject
|
||||||
|
// - some pack preprocessors emit their directives before the version line.
|
||||||
|
} else if (isLineDirective) {
|
||||||
|
// Keep everything up to the first quote that the masker identified as string text.
|
||||||
|
SizeT quotePos = MobileGL::String::npos;
|
||||||
|
for (SizeT i = probe + 5; i < lineEnd; i++) {
|
||||||
|
if (source[i] == '"' || source[i] == '\'') {
|
||||||
|
quotePos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (quotePos != MobileGL::String::npos) {
|
||||||
|
result.append(source, lineStart, quotePos - lineStart);
|
||||||
|
} else {
|
||||||
|
result.append(source, lineStart, lineEnd - lineStart);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.append(source, lineStart, lineEnd - lineStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastLine) break;
|
||||||
|
result.push_back('\n');
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
source = std::move(result);
|
||||||
|
}
|
||||||
|
|
||||||
bool IsExtensionAdvertised(MobileGL::GLExtension extension) {
|
bool IsExtensionAdvertised(MobileGL::GLExtension extension) {
|
||||||
const auto& activeBackendObject = MobileGL::MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MobileGL::MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
@@ -399,15 +956,28 @@ namespace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect the directive on a comment/string-masked copy so a commented-out
|
||||||
|
// "#extension GL_ARB_gpu_shader_int64" is never turned into a synthesized #error. Comments are
|
||||||
|
// no longer blanked in the delivered source (glslang handles them), so this pass must mask
|
||||||
|
// locally like its siblings. Masking preserves offsets, so edits collected against the scan
|
||||||
|
// apply verbatim to `source`; they are applied back-to-front to keep earlier offsets valid.
|
||||||
|
const MobileGL::String scan = MaskCommentsAndQuotedText(source);
|
||||||
|
struct DirectiveEdit {
|
||||||
|
SizeT pos;
|
||||||
|
SizeT len;
|
||||||
|
MobileGL::String replacement;
|
||||||
|
};
|
||||||
|
Vector<DirectiveEdit> edits;
|
||||||
|
|
||||||
SizeT lineStart = 0;
|
SizeT lineStart = 0;
|
||||||
while (lineStart < source.size()) {
|
while (lineStart < scan.size()) {
|
||||||
SizeT lineEnd = source.find('\n', lineStart);
|
SizeT lineEnd = scan.find('\n', lineStart);
|
||||||
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
|
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
|
||||||
if (!hasLineBreak) {
|
if (!hasLineBreak) {
|
||||||
lineEnd = source.size();
|
lineEnd = scan.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
const MobileGL::String line = source.substr(lineStart, lineEnd - lineStart);
|
const MobileGL::String line = scan.substr(lineStart, lineEnd - lineStart);
|
||||||
SizeT probe = 0;
|
SizeT probe = 0;
|
||||||
while (probe < line.size() && std::isspace(static_cast<unsigned char>(line[probe]))) {
|
while (probe < line.size() && std::isspace(static_cast<unsigned char>(line[probe]))) {
|
||||||
probe++;
|
probe++;
|
||||||
@@ -449,16 +1019,12 @@ namespace {
|
|||||||
const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe));
|
const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe));
|
||||||
const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0);
|
const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0);
|
||||||
if (behavior == "require") {
|
if (behavior == "require") {
|
||||||
const MobileGL::String replacement =
|
edits.push_back({lineStart, replaceLen,
|
||||||
"#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n";
|
"#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n"});
|
||||||
source.replace(lineStart, replaceLen, replacement);
|
|
||||||
lineStart += replacement.size();
|
|
||||||
} else if (behavior == "enable" || behavior == "warn") {
|
} else if (behavior == "enable" || behavior == "warn") {
|
||||||
source.replace(lineStart, replaceLen, "\n");
|
edits.push_back({lineStart, replaceLen, "\n"});
|
||||||
lineStart++;
|
|
||||||
} else {
|
|
||||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
|
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,6 +1034,10 @@ namespace {
|
|||||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
|
||||||
|
source.replace(it->pos, it->len, it->replacement);
|
||||||
|
}
|
||||||
|
|
||||||
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,48 +1146,136 @@ namespace {
|
|||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
|
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
|
||||||
|
String& source) {
|
||||||
|
constexpr Uint32 capturedSubgroupSize = 32;
|
||||||
|
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
|
||||||
|
nativeSubgroupSize % capturedSubgroupSize != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
|
||||||
|
// wider widths which are a power-of-two multiple of the captured 32-lane model.
|
||||||
|
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
|
||||||
|
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
|
LinearPrefixScanMatch match;
|
||||||
|
if (!ParseLinearPrefixScanTemplate(tokens, match)) {
|
||||||
|
// Diagnosability: when the trigger op is present but the template no longer
|
||||||
|
// matches (e.g. the pack shipped a new shader revision), the affected device
|
||||||
|
// silently falls back to the driver's miscompiled path. Make that visible.
|
||||||
|
if (CountToken(tokens, "subgroupInclusiveAdd") > 0) {
|
||||||
|
MGLOG_W("%s: subgroupInclusiveAdd present but the linear prefix-scan template "
|
||||||
|
"did not match; the wide-subgroup rewrite was NOT applied",
|
||||||
|
__func__);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const String replacement = BuildLinearPrefixScanReplacement(match);
|
||||||
|
source.replace(match.scanBegin, match.scanEnd - match.scanBegin, replacement);
|
||||||
|
// The declaration occurs before the replaced scan, so its original offsets remain
|
||||||
|
// valid after the first replacement.
|
||||||
|
source.replace(match.sharedArraySizeBegin, match.sharedArraySizeEnd - match.sharedArraySizeBegin,
|
||||||
|
"1024");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct ShaderSourceQuirkContext {
|
||||||
|
ShaderStage stage = ShaderStage::Unknown;
|
||||||
|
BackendType backend = BackendType::Unknown;
|
||||||
|
MG_Backend::GpuVendorKind vendor = MG_Backend::GpuVendorKind::Unknown;
|
||||||
|
Uint32 subgroupSize = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Device-quirk registry. Every entry is a narrowly scoped source rewrite that
|
||||||
|
// works around a specific driver defect. A quirk runs when its env override
|
||||||
|
// forces it on, or when the override is Auto and DeviceApplies matches the
|
||||||
|
// detected device. ForceOn bypasses only the device gate - each Apply keeps
|
||||||
|
// its own structural safety checks. Add new per-device workarounds here
|
||||||
|
// instead of open-coding them in PreprocessShaderSource.
|
||||||
|
struct ShaderSourceQuirk {
|
||||||
|
const char* name;
|
||||||
|
MG_Config::QuirkOverride (*GetOverride)();
|
||||||
|
Bool (*DeviceApplies)(const ShaderSourceQuirkContext&);
|
||||||
|
Bool (*Apply)(const ShaderSourceQuirkContext&, String&);
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr ShaderSourceQuirk kShaderSourceQuirks[] = {
|
||||||
|
{
|
||||||
|
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN
|
||||||
|
"subgroup-prefix-scan-rewrite",
|
||||||
|
[] { return MG_Config::Features.SubgroupPrefixScanQuirk; },
|
||||||
|
[](const ShaderSourceQuirkContext& ctx) {
|
||||||
|
// Qualcomm's Vulkan driver miscompiles the recognized float
|
||||||
|
// InclusiveScan pattern for native subgroups wider than the
|
||||||
|
// captured 32 lanes; other vendors compile it correctly and
|
||||||
|
// should keep their native scan.
|
||||||
|
return ctx.backend == BackendType::DirectVulkan &&
|
||||||
|
ctx.vendor == MG_Backend::GpuVendorKind::Qualcomm;
|
||||||
|
},
|
||||||
|
[](const ShaderSourceQuirkContext& ctx, String& source) {
|
||||||
|
return RewriteLinearSubgroupPrefixScanForVulkan(ctx.stage, ctx.subgroupSize,
|
||||||
|
source);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
void ApplyShaderSourceQuirks(ShaderStage stage, String& source) {
|
||||||
|
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
||||||
|
if (!activeBackend) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& dynamicParameters = activeBackend->GetDynamicParameters();
|
||||||
|
const ShaderSourceQuirkContext quirkContext{
|
||||||
|
stage,
|
||||||
|
activeBackend->GetBackendType(),
|
||||||
|
dynamicParameters.GpuVendor,
|
||||||
|
dynamicParameters.SubgroupSize,
|
||||||
|
};
|
||||||
|
for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) {
|
||||||
|
const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride();
|
||||||
|
if (quirkOverride == MG_Config::QuirkOverride::ForceOff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (quirkOverride == MG_Config::QuirkOverride::Auto &&
|
||||||
|
!quirk.DeviceApplies(quirkContext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (quirk.Apply(quirkContext, source)) {
|
||||||
|
MGLOG_I("ApplyShaderSourceQuirks: applied '%s'%s", quirk.name,
|
||||||
|
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void PreprocessShaderSource(ShaderStage stage, String& source) {
|
void PreprocessShaderSource(ShaderStage stage, String& source) {
|
||||||
// Normalize while the inspector's source span still refers to the untouched input. Later passes
|
// Normalize while the inspector's source span still refers to the untouched input. Later passes
|
||||||
// remove comments and directives, so any subsequent insertion re-inspects the current source.
|
// remove comments and directives, so any subsequent insertion re-inspects the current source.
|
||||||
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
||||||
NormalizeVersionDirective(source, originalLanguage);
|
NormalizeVersionDirective(source, originalLanguage);
|
||||||
|
|
||||||
// remove multi-line comment
|
// Comments are left intact for glslang's own preprocessor: a block comment is a single
|
||||||
size_t commentStartPos = source.find("/*");
|
// preprocessing token that collapses to one space even across newlines and inside a
|
||||||
while (commentStartPos != String::npos) {
|
// directive, so blanking it here (which preserved the interior newlines) truncated
|
||||||
size_t commentEndPos = source.find("*/", commentStartPos);
|
// multi-line #define bodies and broke otherwise-valid shaders (KHR-GL3x.shaders.
|
||||||
if (commentEndPos == String::npos) {
|
// preprocessor multiline_comment_define / redefine_object / function_redefinition).
|
||||||
source.erase(commentStartPos);
|
// Every MobileGL pass that must ignore comment/string text already masks them locally
|
||||||
break;
|
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
|
||||||
}
|
NormalizeLineDirectives(source);
|
||||||
// + length of "*/"
|
|
||||||
source = source.replace(commentStartPos, commentEndPos - commentStartPos + 2, "");
|
|
||||||
commentStartPos = source.find("/*", commentStartPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove #line directives
|
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
|
||||||
SizeT linedirPos = source.find("#line");
|
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
|
||||||
while (linedirPos != String::npos) {
|
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
|
||||||
SizeT newlinePos = source.find('\n', linedirPos);
|
// GL_NV_shader_noperspective_interpolation extension. The old naked substring erase
|
||||||
if (newlinePos == String::npos) {
|
// both discarded that interpolation (shader packs need it) and corrupted any
|
||||||
source.erase(linedirPos);
|
// identifier that merely contained the word. The GLES fallback for devices without
|
||||||
break;
|
// the extension lives in the backend, where device capabilities are known.
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve a line break so adjacent preprocessor directives do not merge.
|
|
||||||
source = source.replace(linedirPos, newlinePos - linedirPos + 1, "\n");
|
|
||||||
linedirPos = source.find("#line", linedirPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove "noperspective"
|
|
||||||
const char* str_np = "noperspective";
|
|
||||||
const SizeT len_np = strlen(str_np);
|
|
||||||
SizeT noperspectivePos = source.find(str_np);
|
|
||||||
while (noperspectivePos != String::npos) {
|
|
||||||
// + length of "\n"
|
|
||||||
source = source.replace(noperspectivePos, len_np, "");
|
|
||||||
noperspectivePos = source.find(str_np);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterUnsupportedGpuShaderInt64(source);
|
FilterUnsupportedGpuShaderInt64(source);
|
||||||
CoerceUniformBlockPackingToStd140(source);
|
CoerceUniformBlockPackingToStd140(source);
|
||||||
@@ -631,6 +1289,8 @@ namespace MobileGL {
|
|||||||
RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
|
RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
|
||||||
ModernizeLegacyGLSL(stage, source);
|
ModernizeLegacyGLSL(stage, source);
|
||||||
InjectDepthRangeBuiltinShim(stage, source);
|
InjectDepthRangeBuiltinShim(stage, source);
|
||||||
|
|
||||||
|
ApplyShaderSourceQuirks(stage, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
||||||
@@ -639,6 +1299,10 @@ namespace MobileGL {
|
|||||||
// must not be mistaken for the real one.
|
// must not be mistaken for the real one.
|
||||||
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
||||||
if (!info.HasVersionDirective()) return false;
|
if (!info.HasVersionDirective()) return false;
|
||||||
|
// Never rescue a malformed directive to 460: that is precisely what re-legalized the
|
||||||
|
// CTS directive.version_* rejection cases after the first compile failed. The shader-
|
||||||
|
// pack retry this exists for only ever sees a valid low version (a real "#version 330").
|
||||||
|
if (!info.hasValidVersionDirective) return false;
|
||||||
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
||||||
// compatibility shaders keep whatever they declared.
|
// compatibility shaders keep whatever they declared.
|
||||||
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ namespace MobileGL {
|
|||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
void PreprocessShaderSource(ShaderStage stage, String& source);
|
void PreprocessShaderSource(ShaderStage stage, String& source);
|
||||||
|
|
||||||
|
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
|
||||||
|
// from subgroupInclusiveAdd plus a shared array of subgroup totals. Qualcomm's
|
||||||
|
// Vulkan driver miscompiles that exact float InclusiveScan path for native subgroups
|
||||||
|
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
|
||||||
|
// flow template, replace the subgroup-local scan with a shared-memory, strict
|
||||||
|
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
|
||||||
|
// template was recognized and rewritten. PreprocessShaderSource reaches this through
|
||||||
|
// its device-quirk registry: by default only on detected Qualcomm Vulkan devices,
|
||||||
|
// overridable either way with MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN=1/0. The explicit
|
||||||
|
// entry point exists for deterministic tests.
|
||||||
|
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source);
|
||||||
|
|
||||||
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
|
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
|
||||||
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
|
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
|
||||||
// the source untouched) for anything else: ES, compatibility, or an already-modern
|
// the source untouched) for anything else: ES, compatibility, or an already-modern
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 "DecoratePositionInvariantPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
|
||||||
|
// Identifies one member of a decorated struct (gl_PerVertex's Position slot).
|
||||||
|
struct MemberKey {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t member = 0;
|
||||||
|
bool operator==(const MemberKey& other) const {
|
||||||
|
return id == other.id && member == other.member;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// OpDecorate <target-id> <decoration> [literals...]
|
||||||
|
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
|
||||||
|
constexpr uint32_t kDecorateTargetOperand = 0;
|
||||||
|
constexpr uint32_t kDecorateDecorationOperand = 1;
|
||||||
|
constexpr uint32_t kDecorateBuiltInOperand = 2;
|
||||||
|
constexpr uint32_t kMemberDecorateStructOperand = 0;
|
||||||
|
constexpr uint32_t kMemberDecorateMemberOperand = 1;
|
||||||
|
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
|
||||||
|
constexpr uint32_t kMemberDecorateBuiltInOperand = 3;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status DecoratePositionInvariantPass::Process() {
|
||||||
|
auto* irContext = context();
|
||||||
|
|
||||||
|
// Collect first: AddAnnotationInst mutates the list being walked.
|
||||||
|
std::vector<uint32_t> invariantIds;
|
||||||
|
std::vector<MemberKey> invariantMembers;
|
||||||
|
std::vector<uint32_t> positionIds;
|
||||||
|
std::vector<MemberKey> positionMembers;
|
||||||
|
|
||||||
|
for (const Instruction& annotation : irContext->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||||
|
if (annotation.NumInOperands() <= kDecorateDecorationOperand) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto decoration = static_cast<spv::Decoration>(
|
||||||
|
annotation.GetSingleWordInOperand(kDecorateDecorationOperand));
|
||||||
|
const uint32_t target = annotation.GetSingleWordInOperand(kDecorateTargetOperand);
|
||||||
|
if (decoration == spv::Decoration::Invariant) {
|
||||||
|
invariantIds.push_back(target);
|
||||||
|
} else if (decoration == spv::Decoration::BuiltIn &&
|
||||||
|
annotation.NumInOperands() > kDecorateBuiltInOperand &&
|
||||||
|
static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(
|
||||||
|
kDecorateBuiltInOperand)) == spv::BuiltIn::Position) {
|
||||||
|
positionIds.push_back(target);
|
||||||
|
}
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||||
|
if (annotation.NumInOperands() <= kMemberDecorateDecorationOperand) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto decoration = static_cast<spv::Decoration>(
|
||||||
|
annotation.GetSingleWordInOperand(kMemberDecorateDecorationOperand));
|
||||||
|
const MemberKey key{
|
||||||
|
annotation.GetSingleWordInOperand(kMemberDecorateStructOperand),
|
||||||
|
annotation.GetSingleWordInOperand(kMemberDecorateMemberOperand)};
|
||||||
|
if (decoration == spv::Decoration::Invariant) {
|
||||||
|
invariantMembers.push_back(key);
|
||||||
|
} else if (decoration == spv::Decoration::BuiltIn &&
|
||||||
|
annotation.NumInOperands() > kMemberDecorateBuiltInOperand &&
|
||||||
|
static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(
|
||||||
|
kMemberDecorateBuiltInOperand)) == spv::BuiltIn::Position) {
|
||||||
|
positionMembers.push_back(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool changed = false;
|
||||||
|
|
||||||
|
for (const uint32_t target : positionIds) {
|
||||||
|
if (std::find(invariantIds.begin(), invariantIds.end(), target) != invariantIds.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
irContext->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||||
|
irContext, spv::Op::OpDecorate, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {target}},
|
||||||
|
{SPV_OPERAND_TYPE_DECORATION,
|
||||||
|
{static_cast<uint32_t>(spv::Decoration::Invariant)}}}));
|
||||||
|
// Guard against a second Position decoration on the same target.
|
||||||
|
invariantIds.push_back(target);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const MemberKey& key : positionMembers) {
|
||||||
|
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) !=
|
||||||
|
invariantMembers.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
irContext->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||||
|
irContext, spv::Op::OpMemberDecorate, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {key.id}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {key.member}},
|
||||||
|
{SPV_OPERAND_TYPE_DECORATION,
|
||||||
|
{static_cast<uint32_t>(spv::Decoration::Invariant)}}}));
|
||||||
|
invariantMembers.push_back(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken
|
||||||
|
DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<DecoratePositionInvariantPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 {
|
||||||
|
// Adds the Invariant decoration to every Position builtin output. GL apps
|
||||||
|
// routinely rely on cross-program position invariance for multi-pass equality
|
||||||
|
// depth tests - MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||||
|
// depth its own first pass wrote - and a driver that optimizes each pipeline
|
||||||
|
// separately may otherwise vary the position math between passes, dropping whole
|
||||||
|
// primitives from the later ones. Both the plain (OpDecorate on a Position
|
||||||
|
// variable) and the block-member (OpMemberDecorate on gl_PerVertex) spellings are
|
||||||
|
// handled; targets that already carry Invariant are left alone. DirectVulkan only.
|
||||||
|
class DecoratePositionInvariantPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "decorate-position-invariant"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateDecoratePositionInvariantPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 "EmulateNoPerspectivePass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/constants.h"
|
||||||
|
#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/type_manager.h"
|
||||||
|
#include "source/opt/types.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
namespace analysis = spvtools::opt::analysis;
|
||||||
|
|
||||||
|
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
return spv::ExecutionModel::Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||||
|
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||||
|
// OpTypePointer <storage-class> <pointee>
|
||||||
|
return ptrType->GetSingleWordInOperand(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If |typeId| is float or a vector of float, returns true and reports the scalar float
|
||||||
|
// type and whether it is a vector. Matrices, structs, ints etc. are not emulatable.
|
||||||
|
bool IsFloatScalarOrVector(IRContext* ctx, uint32_t typeId, uint32_t& floatTypeId, bool& isVector) {
|
||||||
|
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
||||||
|
if (t == nullptr) return false;
|
||||||
|
if (t->opcode() == spv::Op::OpTypeFloat) {
|
||||||
|
floatTypeId = typeId;
|
||||||
|
isVector = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (t->opcode() == spv::Op::OpTypeVector) {
|
||||||
|
const uint32_t comp = t->GetSingleWordInOperand(0);
|
||||||
|
Instruction* ct = ctx->get_def_use_mgr()->GetDef(comp);
|
||||||
|
if (ct != nullptr && ct->opcode() == spv::Op::OpTypeFloat) {
|
||||||
|
floatTypeId = comp;
|
||||||
|
isVector = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||||
|
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||||
|
analysis::Pointer ptr(pointee, sc);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t V4FloatType(IRContext* ctx) {
|
||||||
|
analysis::Float f(32);
|
||||||
|
analysis::Type* freg = ctx->get_type_mgr()->GetRegisteredType(&f);
|
||||||
|
analysis::Vector v(freg, 4);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&v);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t FloatType(IRContext* ctx) {
|
||||||
|
analysis::Float f(32);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&f);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t SignedIntConstant(IRContext* ctx, int32_t value) {
|
||||||
|
analysis::Integer i(32, true);
|
||||||
|
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||||
|
const analysis::Constant* c =
|
||||||
|
ctx->get_constant_mgr()->GetConstant(reg, {static_cast<uint32_t>(value)});
|
||||||
|
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply |valueId| (of type |valueTypeId|) by the scalar |scalarId|, inserting the op
|
||||||
|
// before |before|. Returns the product's id.
|
||||||
|
uint32_t InsertScale(IRContext* ctx, Instruction* before, uint32_t valueTypeId,
|
||||||
|
uint32_t valueId, uint32_t scalarId, bool isVector) {
|
||||||
|
const uint32_t productId = ctx->TakeNextId();
|
||||||
|
const spv::Op op = isVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul;
|
||||||
|
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, op, valueTypeId, productId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {valueId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {scalarId}}}));
|
||||||
|
return productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vertex stage: gl_Position discovery ------------------------------------------
|
||||||
|
|
||||||
|
// Finds gl_Position as member |memberIndex| of a gl_PerVertex-style block whose Output
|
||||||
|
// variable is |blockVarId|; |v4floatTypeId| is that member's (vec4) type. Returns false
|
||||||
|
// if gl_Position is not a block member (older plain-variable form is left to the strip).
|
||||||
|
bool FindPositionBlock(IRContext* ctx, uint32_t& blockVarId, uint32_t& memberIndex,
|
||||||
|
uint32_t& v4floatTypeId) {
|
||||||
|
uint32_t structId = 0;
|
||||||
|
uint32_t member = 0;
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn &&
|
||||||
|
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||||
|
spv::BuiltIn::Position) {
|
||||||
|
structId = ann.GetSingleWordInOperand(0);
|
||||||
|
member = ann.GetSingleWordInOperand(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (structId == 0) return false;
|
||||||
|
|
||||||
|
Instruction* structType = ctx->get_def_use_mgr()->GetDef(structId);
|
||||||
|
if (structType == nullptr || member >= structType->NumInOperands()) return false;
|
||||||
|
v4floatTypeId = structType->GetSingleWordInOperand(member);
|
||||||
|
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (inst.opcode() == spv::Op::OpVariable &&
|
||||||
|
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) ==
|
||||||
|
spv::StorageClass::Output &&
|
||||||
|
VariablePointeeType(ctx, &inst) == structId) {
|
||||||
|
blockVarId = inst.result_id();
|
||||||
|
memberIndex = member;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fragment stage: gl_FragCoord discovery/synthesis -----------------------------
|
||||||
|
|
||||||
|
Instruction* FindBuiltinInput(IRContext* ctx, spv::BuiltIn builtin) {
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() != spv::Op::OpDecorate || ann.NumInOperands() < 3) continue;
|
||||||
|
if (static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) !=
|
||||||
|
spv::Decoration::BuiltIn)
|
||||||
|
continue;
|
||||||
|
if (static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) != builtin) continue;
|
||||||
|
Instruction* var = ctx->get_def_use_mgr()->GetDef(ann.GetSingleWordInOperand(0));
|
||||||
|
if (var != nullptr && var->opcode() == spv::Op::OpVariable &&
|
||||||
|
static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) ==
|
||||||
|
spv::StorageClass::Input) {
|
||||||
|
return var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t SynthesizeFragCoord(IRContext* ctx, uint32_t v4floatTypeId) {
|
||||||
|
const uint32_t ptrType = PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Input);
|
||||||
|
const uint32_t varId = ctx->TakeNextId();
|
||||||
|
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||||
|
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
|
||||||
|
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpDecorate, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||||
|
{SPV_OPERAND_TYPE_DECORATION,
|
||||||
|
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||||
|
{static_cast<uint32_t>(spv::BuiltIn::FragCoord)}}}));
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
ep.AddOperand({SPV_OPERAND_TYPE_ID, {varId}});
|
||||||
|
}
|
||||||
|
return varId;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status EmulateNoPerspectivePass::Process() {
|
||||||
|
auto* ctx = context();
|
||||||
|
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||||
|
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||||
|
const bool isFragment = model == spv::ExecutionModel::Fragment;
|
||||||
|
|
||||||
|
// Collect NoPerspective-decorated plain variables and every NoPerspective annotation.
|
||||||
|
std::vector<uint32_t> plainVarIds;
|
||||||
|
std::vector<Instruction*> decorationsToKill;
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::NoPerspective) {
|
||||||
|
plainVarIds.push_back(ann.GetSingleWordInOperand(0));
|
||||||
|
decorationsToKill.push_back(&ann);
|
||||||
|
} else if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 3 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::NoPerspective) {
|
||||||
|
// Block-member noperspective is not emulated here; the decoration is stripped
|
||||||
|
// (smooth fallback) so SPIRV-Cross does not require the NV extension.
|
||||||
|
decorationsToKill.push_back(&ann);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decorationsToKill.empty()) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spv::StorageClass wantStorage =
|
||||||
|
isVertex ? spv::StorageClass::Output : spv::StorageClass::Input;
|
||||||
|
|
||||||
|
// Emulatable = plain variable of the stage's interface direction, float or floatN.
|
||||||
|
struct Target {
|
||||||
|
Instruction* var;
|
||||||
|
uint32_t typeId;
|
||||||
|
uint32_t floatTypeId;
|
||||||
|
bool isVector;
|
||||||
|
};
|
||||||
|
std::vector<Target> targets;
|
||||||
|
if (isVertex || isFragment) {
|
||||||
|
for (const uint32_t id : plainVarIds) {
|
||||||
|
Instruction* var = ctx->get_def_use_mgr()->GetDef(id);
|
||||||
|
if (var == nullptr || var->opcode() != spv::Op::OpVariable) continue;
|
||||||
|
if (static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) != wantStorage)
|
||||||
|
continue;
|
||||||
|
const uint32_t pointee = VariablePointeeType(ctx, var);
|
||||||
|
uint32_t floatTypeId = 0;
|
||||||
|
bool isVector = false;
|
||||||
|
if (IsFloatScalarOrVector(ctx, pointee, floatTypeId, isVector)) {
|
||||||
|
targets.push_back({var, pointee, floatTypeId, isVector});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force highp on the varyings we emulate: the a*w round-trip overflows a mediump (fp16)
|
||||||
|
// varying at large clip-space w. Dropping RelaxedPrecision makes SPIRV-Cross emit them
|
||||||
|
// highp on both stages, keeping the emulation exact. Only touches emulated variables.
|
||||||
|
if (!targets.empty()) {
|
||||||
|
std::vector<uint32_t> targetIds;
|
||||||
|
targetIds.reserve(targets.size());
|
||||||
|
for (const Target& t : targets) targetIds.push_back(t.var->result_id());
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::RelaxedPrecision &&
|
||||||
|
std::find(targetIds.begin(), targetIds.end(),
|
||||||
|
ann.GetSingleWordInOperand(0)) != targetIds.end()) {
|
||||||
|
decorationsToKill.push_back(&ann);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVertex && !targets.empty()) {
|
||||||
|
uint32_t blockVarId = 0;
|
||||||
|
uint32_t memberIndex = 0;
|
||||||
|
uint32_t v4floatTypeId = 0;
|
||||||
|
if (FindPositionBlock(ctx, blockVarId, memberIndex, v4floatTypeId)) {
|
||||||
|
const uint32_t ptrOutV4 =
|
||||||
|
PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Output);
|
||||||
|
const uint32_t memberConst = SignedIntConstant(ctx, static_cast<int32_t>(memberIndex));
|
||||||
|
const uint32_t floatTy = FloatType(ctx);
|
||||||
|
|
||||||
|
uint32_t entryFuncId = 0;
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
// OpEntryPoint <model> <function> "name" <interface...>
|
||||||
|
entryFuncId = ep.GetSingleWordInOperand(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-multiply every target output by gl_Position.w before each return of the
|
||||||
|
// ENTRY function only. glslang does not inline, so a called helper survives as
|
||||||
|
// its own OpFunction; instrumenting its returns too would scale the varying
|
||||||
|
// more than once (w^2), breaking the identity.
|
||||||
|
for (auto funcIt = ctx->module()->begin(); funcIt != ctx->module()->end(); ++funcIt) {
|
||||||
|
if (funcIt->result_id() != entryFuncId) continue;
|
||||||
|
funcIt->ForEachInst([&](Instruction* inst) {
|
||||||
|
if (inst->opcode() != spv::Op::OpReturn &&
|
||||||
|
inst->opcode() != spv::Op::OpReturnValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t posPtrId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpAccessChain, ptrOutV4, posPtrId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {blockVarId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {memberConst}}}));
|
||||||
|
const uint32_t posId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, v4floatTypeId, posId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posPtrId}}}));
|
||||||
|
const uint32_t wId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posId}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||||
|
for (const Target& t : targets) {
|
||||||
|
const uint32_t valId = ctx->TakeNextId();
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, t.typeId, valId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}}}));
|
||||||
|
const uint32_t scaledId =
|
||||||
|
InsertScale(ctx, inst, t.typeId, valId, wId, t.isVector);
|
||||||
|
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpStore, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {scaledId}}}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFragment && !targets.empty()) {
|
||||||
|
Instruction* fragCoord = FindBuiltinInput(ctx, spv::BuiltIn::FragCoord);
|
||||||
|
uint32_t fragCoordId = 0;
|
||||||
|
uint32_t v4floatTypeId = 0;
|
||||||
|
if (fragCoord != nullptr) {
|
||||||
|
fragCoordId = fragCoord->result_id();
|
||||||
|
v4floatTypeId = VariablePointeeType(ctx, fragCoord);
|
||||||
|
} else {
|
||||||
|
v4floatTypeId = V4FloatType(ctx);
|
||||||
|
fragCoordId = SynthesizeFragCoord(ctx, v4floatTypeId);
|
||||||
|
}
|
||||||
|
const uint32_t floatTy = FloatType(ctx);
|
||||||
|
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
for (const Target& t : targets) {
|
||||||
|
// Collect every load that reads the varying. glslang lowers a whole-variable
|
||||||
|
// read to OpLoad(var), but a single-component read (v.x) to
|
||||||
|
// OpAccessChain(var) + OpLoad(chain). Both must be scaled; the identity is
|
||||||
|
// per-component, so scaling one loaded component by gl_FragCoord.w is valid.
|
||||||
|
std::vector<Instruction*> loads;
|
||||||
|
defUse->ForEachUser(t.var, [&](Instruction* user) {
|
||||||
|
if (user->opcode() == spv::Op::OpLoad &&
|
||||||
|
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||||
|
loads.push_back(user);
|
||||||
|
} else if (user->opcode() == spv::Op::OpAccessChain &&
|
||||||
|
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||||
|
const uint32_t chainId = user->result_id();
|
||||||
|
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||||
|
if (chainUser->opcode() == spv::Op::OpLoad &&
|
||||||
|
chainUser->GetSingleWordInOperand(0) == chainId) {
|
||||||
|
loads.push_back(chainUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rewrite `%r = OpLoad %ty %ptr` into
|
||||||
|
// %orig = OpLoad %ty %ptr
|
||||||
|
// %fc = OpLoad %v4float %fragCoord
|
||||||
|
// %w = OpCompositeExtract %float %fc 3
|
||||||
|
// %r = OpVectorTimesScalar/OpFMul %ty %orig %w (reuse %r: uses stay intact)
|
||||||
|
// The op is chosen from the LOAD's own result type: a whole-vector load scales
|
||||||
|
// with OpVectorTimesScalar, a scalar component load with OpFMul.
|
||||||
|
for (Instruction* load : loads) {
|
||||||
|
const uint32_t loadType = load->type_id();
|
||||||
|
uint32_t componentFloat = 0;
|
||||||
|
bool loadIsVector = false;
|
||||||
|
if (!IsFloatScalarOrVector(ctx, loadType, componentFloat, loadIsVector)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t ptrId = load->GetSingleWordInOperand(0);
|
||||||
|
const uint32_t origId = ctx->TakeNextId();
|
||||||
|
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, loadType, origId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {ptrId}}}));
|
||||||
|
const uint32_t fcId = ctx->TakeNextId();
|
||||||
|
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, v4floatTypeId, fcId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fragCoordId}}}));
|
||||||
|
const uint32_t wId = ctx->TakeNextId();
|
||||||
|
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fcId}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||||
|
load->SetOpcode(loadIsVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul);
|
||||||
|
load->SetInOperands(Instruction::OperandList{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {origId}}, {SPV_OPERAND_TYPE_ID, {wId}}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip every NoPerspective decoration: emulated varyings now transport smooth, and
|
||||||
|
// non-emulatable ones fall back to smooth.
|
||||||
|
for (Instruction* dec : decorationsToKill) {
|
||||||
|
ctx->KillInst(dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<EmulateNoPerspectivePass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 {
|
||||||
|
// Emulates 'noperspective' (screen-linear) interpolation on GLES devices that lack
|
||||||
|
// GL_NV_shader_noperspective_interpolation, so no NV extension is required. The hardware
|
||||||
|
// interpolates perspective-correct; screen-linear L(a) is recovered from the identity
|
||||||
|
// L(a) = P(a * w) * gl_FragCoord.w
|
||||||
|
// where P is perspective-correct interpolation and w is the vertex clip-space w. So each
|
||||||
|
// NoPerspective-decorated output is pre-multiplied by gl_Position.w in the vertex stage
|
||||||
|
// and each NoPerspective-decorated input is multiplied by gl_FragCoord.w in the fragment
|
||||||
|
// stage; the decoration is then removed so the varying transports smooth. This is exact
|
||||||
|
// (modulo float precision - the emulated varyings want highp).
|
||||||
|
//
|
||||||
|
// Scope: plain interface variables of float or floatN type. Anything it cannot emulate
|
||||||
|
// (interface-block members, matrices, or a stage lacking the needed builtin) has its
|
||||||
|
// NoPerspective decoration stripped instead, degrading to smooth - the same result the
|
||||||
|
// extension-less fallback produced before, and never invalid SPIR-V. DirectGLES only.
|
||||||
|
class EmulateNoPerspectivePass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "emulate-noperspective"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateEmulateNoPerspectivePass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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 "StripNoPerspectivePass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
|
||||||
|
// OpDecorate <target-id> <decoration> [literals...]
|
||||||
|
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
|
||||||
|
constexpr uint32_t kDecorateDecorationOperand = 1;
|
||||||
|
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status StripNoPerspectivePass::Process() {
|
||||||
|
auto* irContext = context();
|
||||||
|
|
||||||
|
// Collect first: KillInst mutates the annotation list being walked.
|
||||||
|
std::vector<Instruction*> toKill;
|
||||||
|
for (Instruction& annotation : irContext->annotations()) {
|
||||||
|
uint32_t decorationOperand = 0;
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||||
|
decorationOperand = kDecorateDecorationOperand;
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||||
|
decorationOperand = kMemberDecorateDecorationOperand;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotation.NumInOperands() <= decorationOperand) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(decorationOperand)) ==
|
||||||
|
spv::Decoration::NoPerspective) {
|
||||||
|
toKill.push_back(&annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toKill.empty()) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Instruction* inst : toKill) {
|
||||||
|
irContext->KillInst(inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken StripNoPerspectivePass::CreateStripNoPerspectivePass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<StripNoPerspectivePass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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 {
|
||||||
|
// Removes the NoPerspective decoration from every interface variable and block member.
|
||||||
|
// DirectGLES fallback only, for devices that lack GL_NV_shader_noperspective_interpolation:
|
||||||
|
// SPIRV-Cross renders a NoPerspective-decorated varying as ESSL `noperspective` plus
|
||||||
|
// `#extension GL_NV_shader_noperspective_interpolation : require`, which such a driver
|
||||||
|
// rejects. Dropping the decoration falls the varying back to smooth (perspective-correct)
|
||||||
|
// interpolation - the same visible result the old text-level strip produced, but without
|
||||||
|
// corrupting identifiers and without touching DirectVulkan, where NoPerspective is native.
|
||||||
|
// (The exact screen-linear emulation via gl_Position.w / gl_FragCoord.w is a later step.)
|
||||||
|
class StripNoPerspectivePass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "strip-noperspective"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateStripNoPerspectivePass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -63,7 +63,17 @@ namespace MobileGL {
|
|||||||
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||||
const glslang::TType& type = ent.symbol->getType();
|
const glslang::TType& type = ent.symbol->getType();
|
||||||
const glslang::TString& name = ent.symbol->getAccessName();
|
const glslang::TString& name = ent.symbol->getAccessName();
|
||||||
if (currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
|
// OpenGL assigns generic vertex attribute locations only to active inputs. glslang gathers
|
||||||
|
// both live and dead declarations before mapping, so allowing the default collector to
|
||||||
|
// reserve a dead vertex input would make it consume a location that an active input should
|
||||||
|
// reuse. Other stage interfaces still need the default cross-stage matching behavior.
|
||||||
|
if (!ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// glBindAttribLocation only affects active inputs in the linked program. Applying an API
|
||||||
|
// binding to an inactive declaration would reserve its slot in glslang's collector and
|
||||||
|
// incorrectly push an active, automatically mapped input to a different location.
|
||||||
|
if (ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
|
||||||
auto it = m_explicitVertexIns.find(name.c_str());
|
auto it = m_explicitVertexIns.find(name.c_str());
|
||||||
if (it != m_explicitVertexIns.end()) {
|
if (it != m_explicitVertexIns.end()) {
|
||||||
auto& writableType = ent.symbol->getWritableType();
|
auto& writableType = ent.symbol->getWritableType();
|
||||||
@@ -94,6 +104,13 @@ namespace MobileGL {
|
|||||||
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
|
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
||||||
|
if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) {
|
||||||
|
return ent.newLocation = -1;
|
||||||
|
}
|
||||||
|
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
||||||
|
}
|
||||||
|
|
||||||
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||||
const glslang::TType& type = ent.symbol->getType();
|
const glslang::TType& type = ent.symbol->getType();
|
||||||
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace MobileGL {
|
|||||||
opaqueUniformBindings) {}
|
opaqueUniformBindings) {}
|
||||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||||
|
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||||
int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android
|
||||||
|
|
||||||
|
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
|
||||||
|
passes, separately for each backend (`DirectGLES`, `DirectVulkan`).
|
||||||
|
|
||||||
|
## How MobileGL is reached from a test binary
|
||||||
|
|
||||||
|
MobileGL ships its own EGL implementation alongside its desktop-GL implementation
|
||||||
|
in a single `libMobileGL.so`. A plain arm64 ELF in `/data/local/tmp` can therefore
|
||||||
|
drive it with no APK and no Activity:
|
||||||
|
|
||||||
|
1. `setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES"|"DirectVulkan")` **before** the
|
||||||
|
library is mapped — MobileGL parses its configuration from an ELF constructor.
|
||||||
|
2. `dlopen("libMobileGL.so")`, then `dlsym` the `egl*` and `gl*` entry points.
|
||||||
|
MobileGL exports 45 EGL symbols and the desktop GL functions directly;
|
||||||
|
`eglGetProcAddress` resolves the same set.
|
||||||
|
3. `eglBindAPI(EGL_OPENGL_API)`, choose a config with `EGL_RENDERABLE_TYPE =
|
||||||
|
EGL_OPENGL_BIT`, then `eglCreateContext` with
|
||||||
|
`EGL_CONTEXT_OPENGL_PROFILE_MASK = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT` and
|
||||||
|
major/minor `3`/`3`.
|
||||||
|
|
||||||
|
This yields a genuine GL 3.3 core context (`GL_CONTEXT_PROFILE_MASK == 0x1`).
|
||||||
|
|
||||||
|
## Surface type, per backend
|
||||||
|
|
||||||
|
| backend | pbuffer (headless) | window |
|
||||||
|
|---|---|---|
|
||||||
|
| `DirectGLES` | works | works |
|
||||||
|
| `DirectVulkan` | **unusable** | works |
|
||||||
|
|
||||||
|
`DirectVulkan`'s pbuffer path builds a headless `VkSurfaceKHR` and so requires the
|
||||||
|
`VK_EXT_headless_surface` instance extension, which Adreno's Android driver does
|
||||||
|
not expose. It fails inside `eglMakeCurrent`, not at surface creation.
|
||||||
|
|
||||||
|
The workaround that keeps everything in a shell process: obtain a real
|
||||||
|
`ANativeWindow` from **`AImageReader`** (`AImageReader_newWithUsage` +
|
||||||
|
`AImageReader_getWindow`). It is an ordinary BufferQueue producer, so
|
||||||
|
`vkCreateAndroidSurfaceKHR` accepts it, and no Activity is involved. Register an
|
||||||
|
`onImageAvailable` listener that acquires and deletes each image — otherwise the
|
||||||
|
producer blocks once `maxImages` buffers are in flight and the next swap hangs.
|
||||||
|
|
||||||
|
## Why the suite must render into an FBO
|
||||||
|
|
||||||
|
On a window surface, `DirectVulkan`'s `glReadPixels` from the **default
|
||||||
|
framebuffer** returns all zeros, with no GL error, both before and after
|
||||||
|
`eglSwapBuffers`. `DirectGLES` on the identical window is correct, and readback
|
||||||
|
from a **user FBO is correct on both backends**.
|
||||||
|
|
||||||
|
Verified on two SoCs and two drivers, so this is MobileGL's behaviour rather than
|
||||||
|
a driver quirk:
|
||||||
|
|
||||||
|
| device | GPU | driver | default-FB | user FBO |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Xiaomi 24129PN74C | Adreno 830 | Vulkan 1.3.284 / 512.800.46 | zeros | ok |
|
||||||
|
| Lenovo TB321FU | Adreno 750 | Vulkan 1.3.128 / 512.762.28 | zeros | ok |
|
||||||
|
|
||||||
|
dEQP verifies nearly every case through `glReadPixels`, so running it against the
|
||||||
|
default framebuffer would score `DirectVulkan` near zero for a reason unrelated to
|
||||||
|
conformance. The runs therefore use `--deqp-surface-type=fbo`, uniformly for both
|
||||||
|
backends so the two numbers stay comparable.
|
||||||
|
|
||||||
|
## Other constraints the harness must respect
|
||||||
|
|
||||||
|
- `eglMakeCurrent` requires **draw == read** and rejects `EGL_NO_SURFACE` with
|
||||||
|
`EGL_BAD_MATCH`. dEQP's `surfaceless` platform is therefore unusable, which is
|
||||||
|
why this port supplies its own `tcu::Platform`.
|
||||||
|
- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on a
|
||||||
|
destroyed mutex`) *after* all work completes. Flush and `_exit()` so the exit
|
||||||
|
code and the `.qpa` log survive.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
probe/mgprobe.c preflight gate: one backend x one surface type, checks
|
||||||
|
context version/profile and both readback paths
|
||||||
|
scripts/qpa_report.py .qpa -> pass rate, status histogram, worst groups
|
||||||
|
|
||||||
|
### Preflight
|
||||||
|
|
||||||
|
aarch64-linux-android26-clang -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk
|
||||||
|
adb push mgprobe libMobileGL.so /data/local/tmp/mgcts/
|
||||||
|
adb shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \
|
||||||
|
--backend DirectVulkan --surface imagereader --lib ./libMobileGL.so'
|
||||||
|
|
||||||
|
Exit status is 0 when a 3.3 core context came up and FBO readback is correct.
|
||||||
|
Default-framebuffer readback is reported but deliberately does not gate.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
diff --git a/framework/opengl/gluFboRenderContext.cpp b/framework/opengl/gluFboRenderContext.cpp
|
||||||
|
index 588cf7d2a..0721ffee7 100644
|
||||||
|
--- a/framework/opengl/gluFboRenderContext.cpp
|
||||||
|
+++ b/framework/opengl/gluFboRenderContext.cpp
|
||||||
|
@@ -132,6 +132,7 @@ FboRenderContext::FboRenderContext(RenderContext *context, const RenderConfig &c
|
||||||
|
: m_context(context)
|
||||||
|
, m_framebuffer(0)
|
||||||
|
, m_colorBuffer(0)
|
||||||
|
+ , m_colorIsTexture(false)
|
||||||
|
, m_depthStencilBuffer(0)
|
||||||
|
, m_renderTarget()
|
||||||
|
{
|
||||||
|
@@ -151,6 +152,7 @@ FboRenderContext::FboRenderContext(const ContextFactory &factory, const RenderCo
|
||||||
|
: m_context(nullptr)
|
||||||
|
, m_framebuffer(0)
|
||||||
|
, m_colorBuffer(0)
|
||||||
|
+ , m_colorIsTexture(false)
|
||||||
|
, m_depthStencilBuffer(0)
|
||||||
|
, m_renderTarget()
|
||||||
|
{
|
||||||
|
@@ -215,19 +217,41 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config)
|
||||||
|
height = (height == glu::RenderConfig::DONT_CARE) ? maxSize : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // MOBILEGL: allow the colour attachment to be a texture instead of a
|
||||||
|
+ // renderbuffer. MobileGL's DirectVulkan backend returns zeros when reading
|
||||||
|
+ // back a renderbuffer-attached FBO, which makes every image comparison fail
|
||||||
|
+ // for one reason and hides everything else. Setting
|
||||||
|
+ // MOBILEGL_CTS_FBO_COLOR_TEXTURE=1 isolates that single defect so the rest
|
||||||
|
+ // of the suite can be measured. Off by default: stock behaviour.
|
||||||
|
{
|
||||||
|
- pixelFormat = getPixelFormat(colorFormat);
|
||||||
|
+ const char *useTexEnv = getenv("MOBILEGL_CTS_FBO_COLOR_TEXTURE");
|
||||||
|
+ m_colorIsTexture = (useTexEnv && useTexEnv[0] == '1' && config.numSamples <= 0);
|
||||||
|
|
||||||
|
- gl.genRenderbuffers(1, &m_colorBuffer);
|
||||||
|
- gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer);
|
||||||
|
+ pixelFormat = getPixelFormat(colorFormat);
|
||||||
|
|
||||||
|
- if (config.numSamples > 0)
|
||||||
|
- gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height);
|
||||||
|
+ if (m_colorIsTexture)
|
||||||
|
+ {
|
||||||
|
+ gl.genTextures(1, &m_colorBuffer);
|
||||||
|
+ gl.bindTexture(GL_TEXTURE_2D, m_colorBuffer);
|
||||||
|
+ gl.texStorage2D(GL_TEXTURE_2D, 1, colorFormat, width, height);
|
||||||
|
+ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
+ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
+ gl.bindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
+ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color texture");
|
||||||
|
+ }
|
||||||
|
else
|
||||||
|
- gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height);
|
||||||
|
-
|
||||||
|
- gl.bindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
- GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer");
|
||||||
|
+ {
|
||||||
|
+ gl.genRenderbuffers(1, &m_colorBuffer);
|
||||||
|
+ gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer);
|
||||||
|
+
|
||||||
|
+ if (config.numSamples > 0)
|
||||||
|
+ gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height);
|
||||||
|
+ else
|
||||||
|
+ gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height);
|
||||||
|
+
|
||||||
|
+ gl.bindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
+ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer");
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depthStencilFormat != GL_NONE)
|
||||||
|
@@ -250,7 +274,12 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config)
|
||||||
|
gl.bindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
|
||||||
|
|
||||||
|
if (m_colorBuffer)
|
||||||
|
- gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer);
|
||||||
|
+ {
|
||||||
|
+ if (m_colorIsTexture)
|
||||||
|
+ gl.framebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffer, 0);
|
||||||
|
+ else
|
||||||
|
+ gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer);
|
||||||
|
+ }
|
||||||
|
|
||||||
|
if (m_depthStencilBuffer)
|
||||||
|
{
|
||||||
|
@@ -290,7 +319,10 @@ void FboRenderContext::destroyFramebuffer(void)
|
||||||
|
|
||||||
|
if (m_colorBuffer)
|
||||||
|
{
|
||||||
|
- gl.deleteRenderbuffers(1, &m_colorBuffer);
|
||||||
|
+ if (m_colorIsTexture)
|
||||||
|
+ gl.deleteTextures(1, &m_colorBuffer);
|
||||||
|
+ else
|
||||||
|
+ gl.deleteRenderbuffers(1, &m_colorBuffer);
|
||||||
|
m_colorBuffer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/framework/opengl/gluFboRenderContext.hpp b/framework/opengl/gluFboRenderContext.hpp
|
||||||
|
index 75a0ff6b7..09ff1e7a9 100644
|
||||||
|
--- a/framework/opengl/gluFboRenderContext.hpp
|
||||||
|
+++ b/framework/opengl/gluFboRenderContext.hpp
|
||||||
|
@@ -80,6 +80,7 @@ private:
|
||||||
|
RenderContext *m_context;
|
||||||
|
uint32_t m_framebuffer;
|
||||||
|
uint32_t m_colorBuffer;
|
||||||
|
+ bool m_colorIsTexture;
|
||||||
|
uint32_t m_depthStencilBuffer;
|
||||||
|
tcu::RenderTarget m_renderTarget;
|
||||||
|
};
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* dEQP platform port for MobileGL on Android
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*//*!
|
||||||
|
* \file
|
||||||
|
* \brief MobileGL platform.
|
||||||
|
*
|
||||||
|
* Modelled on the surfaceless platform, but adapted to MobileGL, which ships
|
||||||
|
* its own EGL implementation inside libMobileGL.so:
|
||||||
|
*
|
||||||
|
* - Every EGL call goes through the dynamically loaded library. The
|
||||||
|
* surfaceless port mixes wrapper calls with globally linked egl* symbols;
|
||||||
|
* doing that here would silently reach Android's system EGL instead.
|
||||||
|
* - Desktop-GL configs are selected with EGL_OPENGL_BIT. The surfaceless port
|
||||||
|
* always asks for an ES bit, which cannot satisfy a GL 3.3 core context.
|
||||||
|
* - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with
|
||||||
|
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
|
||||||
|
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
|
||||||
|
* - Window surfaces are backed by an AImageReader rather than an Activity,
|
||||||
|
* which is what lets the suite run as a plain adb-shell binary. DirectVulkan
|
||||||
|
* needs this: its pbuffer path requires VK_EXT_headless_surface, which
|
||||||
|
* Adreno's Android driver does not expose.
|
||||||
|
*
|
||||||
|
* Environment:
|
||||||
|
* MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so)
|
||||||
|
* MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer"
|
||||||
|
* MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching
|
||||||
|
*//*--------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
#include "tcuMobileGLPlatform.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "deDynamicLibrary.hpp"
|
||||||
|
#include "egluUtil.hpp"
|
||||||
|
#include "eglwEnums.hpp"
|
||||||
|
#include "eglwLibrary.hpp"
|
||||||
|
#include "gluPlatform.hpp"
|
||||||
|
#include "gluRenderConfig.hpp"
|
||||||
|
#include "gluRenderContext.hpp"
|
||||||
|
#include "glwInitFunctions.hpp"
|
||||||
|
#include "tcuCommandLine.hpp"
|
||||||
|
#include "tcuPixelFormat.hpp"
|
||||||
|
#include "tcuPlatform.hpp"
|
||||||
|
#include "tcuRenderTarget.hpp"
|
||||||
|
|
||||||
|
#include <android/hardware_buffer.h>
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <media/NdkImageReader.h>
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::vector;
|
||||||
|
|
||||||
|
#if !defined(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR)
|
||||||
|
#define EGL_CONTEXT_FLAGS_KHR 0x30FC
|
||||||
|
#define EGL_CONTEXT_MAJOR_VERSION_KHR 0x3098
|
||||||
|
#define EGL_CONTEXT_MINOR_VERSION_KHR 0x30FB
|
||||||
|
#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR 0x00000002
|
||||||
|
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR 0x00000001
|
||||||
|
#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001
|
||||||
|
#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002
|
||||||
|
#define EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR 0x30FD
|
||||||
|
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace tcu
|
||||||
|
{
|
||||||
|
namespace mobilegl
|
||||||
|
{
|
||||||
|
|
||||||
|
static string getLibraryName(void)
|
||||||
|
{
|
||||||
|
const char *env = std::getenv("MOBILEGL_CTS_LIB");
|
||||||
|
return (env && env[0]) ? string(env) : string("libMobileGL.so");
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Window surfaces default on: they are the only kind DirectVulkan can use.
|
||||||
|
static bool useWindowSurface(void)
|
||||||
|
{
|
||||||
|
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
|
||||||
|
return !(env && string(env) == "pbuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------------------------------------------------*//*!
|
||||||
|
* \brief A real ANativeWindow with no Activity behind it.
|
||||||
|
*
|
||||||
|
* AImageReader's window is an ordinary BufferQueue producer, so both
|
||||||
|
* eglCreateWindowSurface and vkCreateAndroidSurfaceKHR accept it. The image
|
||||||
|
* listener must drain the queue: without it the producer blocks once maxImages
|
||||||
|
* buffers are in flight and the next swap deadlocks.
|
||||||
|
*//*--------------------------------------------------------------------*/
|
||||||
|
class ImageReaderWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImageReaderWindow(int width, int height) : m_reader(nullptr), m_window(nullptr)
|
||||||
|
{
|
||||||
|
const media_status_t status =
|
||||||
|
AImageReader_newWithUsage(width, height, AIMAGE_FORMAT_RGBA_8888,
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
||||||
|
kMaxImages, &m_reader);
|
||||||
|
if (status != AMEDIA_OK || m_reader == nullptr)
|
||||||
|
throw tcu::ResourceError("AImageReader_newWithUsage() failed");
|
||||||
|
|
||||||
|
AImageReader_ImageListener listener = {this, onImageAvailable};
|
||||||
|
AImageReader_setImageListener(m_reader, &listener);
|
||||||
|
|
||||||
|
if (AImageReader_getWindow(m_reader, &m_window) != AMEDIA_OK || m_window == nullptr)
|
||||||
|
{
|
||||||
|
AImageReader_delete(m_reader);
|
||||||
|
m_reader = nullptr;
|
||||||
|
throw tcu::ResourceError("AImageReader_getWindow() failed");
|
||||||
|
}
|
||||||
|
ANativeWindow_acquire(m_window);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ImageReaderWindow(void)
|
||||||
|
{
|
||||||
|
if (m_window != nullptr)
|
||||||
|
ANativeWindow_release(m_window);
|
||||||
|
if (m_reader != nullptr)
|
||||||
|
{
|
||||||
|
AImageReader_setImageListener(m_reader, nullptr);
|
||||||
|
AImageReader_delete(m_reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ANativeWindow *getWindow(void) const
|
||||||
|
{
|
||||||
|
return m_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const int kMaxImages = 4;
|
||||||
|
|
||||||
|
static void onImageAvailable(void *, AImageReader *reader)
|
||||||
|
{
|
||||||
|
AImage *image = nullptr;
|
||||||
|
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr)
|
||||||
|
AImage_delete(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageReaderWindow(const ImageReaderWindow &);
|
||||||
|
ImageReaderWindow &operator=(const ImageReaderWindow &);
|
||||||
|
|
||||||
|
AImageReader *m_reader;
|
||||||
|
ANativeWindow *m_window;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GetProcFuncLoader : public glw::FunctionLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GetProcFuncLoader(const eglw::Library &egl) : m_egl(egl)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
glw::GenericFuncType get(const char *name) const
|
||||||
|
{
|
||||||
|
return (glw::GenericFuncType)m_egl.getProcAddress(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const eglw::Library &m_egl;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EglRenderContext : public glu::RenderContext
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
|
||||||
|
const glu::RenderContext *sharedContext);
|
||||||
|
~EglRenderContext(void);
|
||||||
|
|
||||||
|
glu::ContextType getType(void) const
|
||||||
|
{
|
||||||
|
return m_contextType;
|
||||||
|
}
|
||||||
|
eglw::EGLContext getEglContext(void) const
|
||||||
|
{
|
||||||
|
return m_eglContext;
|
||||||
|
}
|
||||||
|
const glw::Functions &getFunctions(void) const
|
||||||
|
{
|
||||||
|
return m_glFunctions;
|
||||||
|
}
|
||||||
|
const tcu::RenderTarget &getRenderTarget(void) const
|
||||||
|
{
|
||||||
|
return m_renderTarget;
|
||||||
|
}
|
||||||
|
void postIterate(void);
|
||||||
|
void makeCurrent(void);
|
||||||
|
|
||||||
|
glw::GenericFuncType getProcAddress(const char *name) const
|
||||||
|
{
|
||||||
|
return (glw::GenericFuncType)m_egl.getProcAddress(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const eglw::DefaultLibrary m_egl;
|
||||||
|
const glu::ContextType m_contextType;
|
||||||
|
eglw::EGLDisplay m_eglDisplay;
|
||||||
|
eglw::EGLContext m_eglContext;
|
||||||
|
eglw::EGLSurface m_eglSurface;
|
||||||
|
ImageReaderWindow *m_window;
|
||||||
|
glw::Functions m_glFunctions;
|
||||||
|
tcu::RenderTarget m_renderTarget;
|
||||||
|
eglw::EGLContext m_sharedEglContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ContextFactory : public glu::ContextFactory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ContextFactory(void) : glu::ContextFactory("default", "MobileGL EGL context")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
glu::RenderContext *createContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
|
||||||
|
const glu::RenderContext *sharedContext) const
|
||||||
|
{
|
||||||
|
return new EglRenderContext(config, cmdLine, sharedContext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Platform : public tcu::Platform, public glu::Platform
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Platform(void)
|
||||||
|
{
|
||||||
|
m_contextFactoryRegistry.registerFactory(new ContextFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
const glu::Platform &getGLPlatform(void) const
|
||||||
|
{
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
|
||||||
|
const glu::RenderContext *sharedContext)
|
||||||
|
: m_egl(getLibraryName().c_str())
|
||||||
|
, m_contextType(config.type)
|
||||||
|
, m_eglDisplay(EGL_NO_DISPLAY)
|
||||||
|
, m_eglContext(EGL_NO_CONTEXT)
|
||||||
|
, m_eglSurface(EGL_NO_SURFACE)
|
||||||
|
, m_window(nullptr)
|
||||||
|
, m_renderTarget(config.width, config.height,
|
||||||
|
tcu::PixelFormat(config.redBits, config.greenBits, config.blueBits, config.alphaBits),
|
||||||
|
config.depthBits, config.stencilBits, config.numSamples)
|
||||||
|
, m_sharedEglContext(EGL_NO_CONTEXT)
|
||||||
|
{
|
||||||
|
DE_UNREF(cmdLine);
|
||||||
|
|
||||||
|
const glu::ContextType &contextType = config.type;
|
||||||
|
const bool isES = glu::isContextTypeES(contextType);
|
||||||
|
eglw::EGLint eglMajorVersion = 0;
|
||||||
|
eglw::EGLint eglMinorVersion = 0;
|
||||||
|
|
||||||
|
m_eglDisplay = m_egl.getDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
EGLU_CHECK_MSG(m_egl, "eglGetDisplay()");
|
||||||
|
if (m_eglDisplay == EGL_NO_DISPLAY)
|
||||||
|
throw tcu::ResourceError("eglGetDisplay() failed");
|
||||||
|
|
||||||
|
EGLU_CHECK_CALL(m_egl, initialize(m_eglDisplay, &eglMajorVersion, &eglMinorVersion));
|
||||||
|
|
||||||
|
// MobileGL cannot make a context current without a surface, so
|
||||||
|
// SURFACETYPE_DONT_CARE (which is what --deqp-surface-type=fbo requests)
|
||||||
|
// still gets a real one.
|
||||||
|
bool wantWindow = false;
|
||||||
|
switch (config.surfaceType)
|
||||||
|
{
|
||||||
|
case glu::RenderConfig::SURFACETYPE_WINDOW:
|
||||||
|
wantWindow = true;
|
||||||
|
break;
|
||||||
|
case glu::RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE:
|
||||||
|
case glu::RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC:
|
||||||
|
wantWindow = false;
|
||||||
|
break;
|
||||||
|
case glu::RenderConfig::SURFACETYPE_DONT_CARE:
|
||||||
|
wantWindow = useWindowSurface();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
TCU_CHECK_INTERNAL(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int width = (config.width == glu::RenderConfig::DONT_CARE) ? 256 : config.width;
|
||||||
|
const int height = (config.height == glu::RenderConfig::DONT_CARE) ? 256 : config.height;
|
||||||
|
|
||||||
|
vector<eglw::EGLint> cfgAttribs;
|
||||||
|
cfgAttribs.push_back(EGL_RENDERABLE_TYPE);
|
||||||
|
if (isES)
|
||||||
|
{
|
||||||
|
switch (contextType.getMajorVersion())
|
||||||
|
{
|
||||||
|
case 3:
|
||||||
|
cfgAttribs.push_back(EGL_OPENGL_ES3_BIT);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
cfgAttribs.push_back(EGL_OPENGL_ES2_BIT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cfgAttribs.push_back(EGL_OPENGL_ES_BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Desktop GL, which is the whole point of this port.
|
||||||
|
cfgAttribs.push_back(EGL_OPENGL_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgAttribs.push_back(EGL_SURFACE_TYPE);
|
||||||
|
cfgAttribs.push_back(wantWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT);
|
||||||
|
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
eglw::EGLint attrib;
|
||||||
|
int glu::RenderConfig::*field;
|
||||||
|
} s_sizeAttribs[] = {
|
||||||
|
{EGL_RED_SIZE, &glu::RenderConfig::redBits}, {EGL_GREEN_SIZE, &glu::RenderConfig::greenBits},
|
||||||
|
{EGL_BLUE_SIZE, &glu::RenderConfig::blueBits}, {EGL_ALPHA_SIZE, &glu::RenderConfig::alphaBits},
|
||||||
|
{EGL_DEPTH_SIZE, &glu::RenderConfig::depthBits}, {EGL_STENCIL_SIZE, &glu::RenderConfig::stencilBits},
|
||||||
|
{EGL_SAMPLES, &glu::RenderConfig::numSamples},
|
||||||
|
};
|
||||||
|
for (size_t ndx = 0; ndx < DE_LENGTH_OF_ARRAY(s_sizeAttribs); ndx++)
|
||||||
|
{
|
||||||
|
const int value = config.*(s_sizeAttribs[ndx].field);
|
||||||
|
if (value != glu::RenderConfig::DONT_CARE)
|
||||||
|
{
|
||||||
|
cfgAttribs.push_back(s_sizeAttribs[ndx].attrib);
|
||||||
|
cfgAttribs.push_back(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfgAttribs.push_back(EGL_NONE);
|
||||||
|
|
||||||
|
eglw::EGLConfig eglConfig = nullptr;
|
||||||
|
eglw::EGLint numConfigs = 0;
|
||||||
|
EGLU_CHECK_CALL(m_egl, chooseConfig(m_eglDisplay, &cfgAttribs[0], &eglConfig, 1, &numConfigs));
|
||||||
|
if (numConfigs < 1)
|
||||||
|
throw tcu::NotSupportedError("No matching EGL config for the requested context");
|
||||||
|
|
||||||
|
if (wantWindow)
|
||||||
|
{
|
||||||
|
m_window = new ImageReaderWindow(width, height);
|
||||||
|
|
||||||
|
eglw::EGLint visualId = 0;
|
||||||
|
if (m_egl.getConfigAttrib(m_eglDisplay, eglConfig, EGL_NATIVE_VISUAL_ID, &visualId) && visualId != 0)
|
||||||
|
ANativeWindow_setBuffersGeometry(m_window->getWindow(), width, height, visualId);
|
||||||
|
|
||||||
|
m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig,
|
||||||
|
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
|
||||||
|
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const eglw::EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
|
||||||
|
m_eglSurface = m_egl.createPbufferSurface(m_eglDisplay, eglConfig, surfaceAttribs);
|
||||||
|
EGLU_CHECK_MSG(m_egl, "eglCreatePbufferSurface()");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_eglSurface == EGL_NO_SURFACE)
|
||||||
|
throw tcu::ResourceError("Failed to create EGL surface");
|
||||||
|
|
||||||
|
vector<eglw::EGLint> ctxAttribs;
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR);
|
||||||
|
ctxAttribs.push_back(contextType.getMajorVersion());
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_MINOR_VERSION_KHR);
|
||||||
|
ctxAttribs.push_back(contextType.getMinorVersion());
|
||||||
|
|
||||||
|
switch (contextType.getProfile())
|
||||||
|
{
|
||||||
|
case glu::PROFILE_ES:
|
||||||
|
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_ES_API));
|
||||||
|
break;
|
||||||
|
case glu::PROFILE_CORE:
|
||||||
|
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API));
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR);
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR);
|
||||||
|
break;
|
||||||
|
case glu::PROFILE_COMPATIBILITY:
|
||||||
|
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API));
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR);
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
TCU_CHECK_INTERNAL(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
eglw::EGLint flags = 0;
|
||||||
|
if ((contextType.getFlags() & glu::CONTEXT_DEBUG) != 0)
|
||||||
|
flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR;
|
||||||
|
if ((contextType.getFlags() & glu::CONTEXT_ROBUST) != 0)
|
||||||
|
flags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR;
|
||||||
|
if ((contextType.getFlags() & glu::CONTEXT_FORWARD_COMPATIBLE) != 0)
|
||||||
|
flags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR;
|
||||||
|
if (flags != 0)
|
||||||
|
{
|
||||||
|
ctxAttribs.push_back(EGL_CONTEXT_FLAGS_KHR);
|
||||||
|
ctxAttribs.push_back(flags);
|
||||||
|
}
|
||||||
|
ctxAttribs.push_back(EGL_NONE);
|
||||||
|
|
||||||
|
const EglRenderContext *sharedEglRenderContext = dynamic_cast<const EglRenderContext *>(sharedContext);
|
||||||
|
m_sharedEglContext = sharedEglRenderContext ? sharedEglRenderContext->getEglContext() : EGL_NO_CONTEXT;
|
||||||
|
|
||||||
|
m_eglContext = m_egl.createContext(m_eglDisplay, eglConfig, m_sharedEglContext, &ctxAttribs[0]);
|
||||||
|
EGLU_CHECK_MSG(m_egl, "eglCreateContext()");
|
||||||
|
if (!m_eglContext)
|
||||||
|
throw tcu::ResourceError("eglCreateContext() failed");
|
||||||
|
|
||||||
|
// MobileGL requires draw == read.
|
||||||
|
EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext));
|
||||||
|
|
||||||
|
// MobileGL advertises EGL 1.5, so eglGetProcAddress resolves core entry
|
||||||
|
// points too; there is no separate GL library to dlopen.
|
||||||
|
GetProcFuncLoader funcLoader(m_egl);
|
||||||
|
glu::initCoreFunctions(&m_glFunctions, &funcLoader, contextType.getAPI());
|
||||||
|
glu::initExtensionFunctions(&m_glFunctions, &funcLoader, contextType.getAPI());
|
||||||
|
}
|
||||||
|
|
||||||
|
EglRenderContext::~EglRenderContext(void)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (m_eglDisplay != EGL_NO_DISPLAY)
|
||||||
|
{
|
||||||
|
m_egl.makeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
|
||||||
|
if (m_eglContext != EGL_NO_CONTEXT)
|
||||||
|
m_egl.destroyContext(m_eglDisplay, m_eglContext);
|
||||||
|
|
||||||
|
if (m_eglSurface != EGL_NO_SURFACE)
|
||||||
|
m_egl.destroySurface(m_eglDisplay, m_eglSurface);
|
||||||
|
|
||||||
|
if (m_sharedEglContext == EGL_NO_CONTEXT)
|
||||||
|
m_egl.terminate(m_eglDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
delete m_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EglRenderContext::makeCurrent(void)
|
||||||
|
{
|
||||||
|
EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EglRenderContext::postIterate(void)
|
||||||
|
{
|
||||||
|
m_glFunctions.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mobilegl
|
||||||
|
} // namespace tcu
|
||||||
|
|
||||||
|
tcu::Platform *createPlatform(void)
|
||||||
|
{
|
||||||
|
return new tcu::mobilegl::Platform();
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef _TCUMOBILEGLPLATFORM_HPP
|
||||||
|
#define _TCUMOBILEGLPLATFORM_HPP
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* dEQP platform port for MobileGL on Android
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*//*!
|
||||||
|
* \file
|
||||||
|
* \brief MobileGL platform - drives libMobileGL.so's own EGL from a bare
|
||||||
|
* Android process, with no Activity and no system EGL involved.
|
||||||
|
*//*--------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
#include "tcuDefs.hpp"
|
||||||
|
|
||||||
|
namespace tcu
|
||||||
|
{
|
||||||
|
class Platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
tcu::Platform *createPlatform(void);
|
||||||
|
|
||||||
|
#endif // _TCUMOBILEGLPLATFORM_HPP
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mgprobe
|
||||||
|
*.o
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/* mgprobe - preflight gate for running a GL conformance suite against MobileGL
|
||||||
|
* from a bare adb-shell process (no APK, no Activity).
|
||||||
|
*
|
||||||
|
* Verifies, for one backend and one surface type, that MobileGL can hand out a
|
||||||
|
* GL 3.3 core context and that pixels read back correctly - both from the
|
||||||
|
* default framebuffer and from a user FBO. Run this before burning hours on a
|
||||||
|
* CTS run; it catches a broken device/library pairing in about a second.
|
||||||
|
*
|
||||||
|
* mgprobe --backend DirectGLES|DirectVulkan --surface pbuffer|imagereader
|
||||||
|
* [--lib /path/to/libMobileGL.so]
|
||||||
|
*
|
||||||
|
* Exit status: 0 if a context came up and FBO readback is correct, non-zero
|
||||||
|
* otherwise. Default-framebuffer readback is reported but does NOT gate, because
|
||||||
|
* DirectVulkan is known to return zeros there while FBO readback is sound.
|
||||||
|
*
|
||||||
|
* Build (NDK, arm64):
|
||||||
|
* $NDK/toolchains/llvm/prebuilt/<host>/bin/aarch64-linux-android26-clang \
|
||||||
|
* -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk
|
||||||
|
*/
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <media/NdkImageReader.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
typedef void *EGLDisplay;
|
||||||
|
typedef void *EGLConfig;
|
||||||
|
typedef void *EGLSurface;
|
||||||
|
typedef void *EGLContext;
|
||||||
|
typedef int EGLint;
|
||||||
|
typedef unsigned int EGLBoolean;
|
||||||
|
typedef unsigned int EGLenum;
|
||||||
|
typedef void *EGLNativeDisplayType;
|
||||||
|
typedef void *EGLNativeWindowType;
|
||||||
|
|
||||||
|
#define EGL_DEFAULT_DISPLAY ((EGLNativeDisplayType)0)
|
||||||
|
#define EGL_NO_CONTEXT ((EGLContext)0)
|
||||||
|
#define EGL_NO_SURFACE ((EGLSurface)0)
|
||||||
|
#define EGL_NONE 0x3038
|
||||||
|
#define EGL_WIDTH 0x3057
|
||||||
|
#define EGL_HEIGHT 0x3056
|
||||||
|
#define EGL_RENDERABLE_TYPE 0x3040
|
||||||
|
#define EGL_SURFACE_TYPE 0x3033
|
||||||
|
#define EGL_WINDOW_BIT 0x0004
|
||||||
|
#define EGL_PBUFFER_BIT 0x0001
|
||||||
|
#define EGL_OPENGL_BIT 0x0008
|
||||||
|
#define EGL_OPENGL_API 0x30A2
|
||||||
|
#define EGL_RED_SIZE 0x3024
|
||||||
|
#define EGL_GREEN_SIZE 0x3023
|
||||||
|
#define EGL_BLUE_SIZE 0x3022
|
||||||
|
#define EGL_ALPHA_SIZE 0x3021
|
||||||
|
#define EGL_DEPTH_SIZE 0x3025
|
||||||
|
#define EGL_STENCIL_SIZE 0x3026
|
||||||
|
#define EGL_NATIVE_VISUAL_ID 0x302E
|
||||||
|
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
|
||||||
|
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||||
|
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||||
|
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||||
|
|
||||||
|
#define GL_VENDOR 0x1F00
|
||||||
|
#define GL_RENDERER 0x1F01
|
||||||
|
#define GL_VERSION 0x1F02
|
||||||
|
#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
|
||||||
|
#define GL_CONTEXT_PROFILE_MASK 0x9126
|
||||||
|
#define GL_MAJOR_VERSION 0x821B
|
||||||
|
#define GL_MINOR_VERSION 0x821C
|
||||||
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||||
|
#define GL_RGBA 0x1908
|
||||||
|
#define GL_RGBA8 0x8058
|
||||||
|
#define GL_UNSIGNED_BYTE 0x1401
|
||||||
|
#define GL_TEXTURE_2D 0x0DE1
|
||||||
|
#define GL_FRAMEBUFFER 0x8D40
|
||||||
|
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
||||||
|
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||||||
|
#define GL_TEXTURE_MIN_FILTER 0x2801
|
||||||
|
#define GL_TEXTURE_MAG_FILTER 0x2800
|
||||||
|
#define GL_NEAREST 0x2600
|
||||||
|
#define GL_RENDERBUFFER 0x8D41
|
||||||
|
|
||||||
|
typedef EGLDisplay (*P_getDisplay)(EGLNativeDisplayType);
|
||||||
|
typedef EGLBoolean (*P_initialize)(EGLDisplay, EGLint *, EGLint *);
|
||||||
|
typedef EGLBoolean (*P_bindAPI)(EGLenum);
|
||||||
|
typedef EGLBoolean (*P_chooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *);
|
||||||
|
typedef EGLBoolean (*P_getConfigAttrib)(EGLDisplay, EGLConfig, EGLint, EGLint *);
|
||||||
|
typedef EGLSurface (*P_createWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *);
|
||||||
|
typedef EGLSurface (*P_createPbufferSurface)(EGLDisplay, EGLConfig, const EGLint *);
|
||||||
|
typedef EGLContext (*P_createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *);
|
||||||
|
typedef EGLBoolean (*P_makeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext);
|
||||||
|
typedef EGLint (*P_getError)(void);
|
||||||
|
|
||||||
|
typedef const unsigned char *(*P_glGetString)(unsigned int);
|
||||||
|
typedef void (*P_glGetIntegerv)(unsigned int, int *);
|
||||||
|
typedef void (*P_glClearColor)(float, float, float, float);
|
||||||
|
typedef void (*P_glClear)(unsigned int);
|
||||||
|
typedef void (*P_glFinish)(void);
|
||||||
|
typedef void (*P_glReadPixels)(int, int, int, int, unsigned int, unsigned int, void *);
|
||||||
|
typedef unsigned int (*P_glGetError)(void);
|
||||||
|
typedef void (*P_glGenTextures)(int, unsigned int *);
|
||||||
|
typedef void (*P_glBindTexture)(unsigned int, unsigned int);
|
||||||
|
typedef void (*P_glTexImage2D)(unsigned int, int, int, int, int, int, unsigned int, unsigned int, const void *);
|
||||||
|
typedef void (*P_glTexParameteri)(unsigned int, unsigned int, int);
|
||||||
|
typedef void (*P_glGenFramebuffers)(int, unsigned int *);
|
||||||
|
typedef void (*P_glBindFramebuffer)(unsigned int, unsigned int);
|
||||||
|
typedef void (*P_glFramebufferTexture2D)(unsigned int, unsigned int, unsigned int, unsigned int, int);
|
||||||
|
typedef unsigned int (*P_glCheckFramebufferStatus)(unsigned int);
|
||||||
|
typedef void (*P_glViewport)(int, int, int, int);
|
||||||
|
typedef void (*P_glGenRenderbuffers)(int, unsigned int *);
|
||||||
|
typedef void (*P_glBindRenderbuffer)(unsigned int, unsigned int);
|
||||||
|
typedef void (*P_glRenderbufferStorage)(unsigned int, unsigned int, int, int);
|
||||||
|
typedef void (*P_glFramebufferRenderbuffer)(unsigned int, unsigned int, unsigned int, unsigned int);
|
||||||
|
|
||||||
|
static void *g_lib;
|
||||||
|
static void *S(const char *n) { return dlsym(g_lib, n); }
|
||||||
|
|
||||||
|
static void on_image(void *ctx, AImageReader *r) {
|
||||||
|
(void)ctx;
|
||||||
|
AImage *img = NULL;
|
||||||
|
/* Drain the queue, or the producer blocks once maxImages are in flight. */
|
||||||
|
if (AImageReader_acquireNextImage(r, &img) == AMEDIA_OK && img) AImage_delete(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DIM 256
|
||||||
|
|
||||||
|
static int near8(unsigned got, int want, int tol) {
|
||||||
|
int d = (int)got - want;
|
||||||
|
return d <= tol && d >= -tol;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *backend = "DirectGLES";
|
||||||
|
const char *surface = "pbuffer";
|
||||||
|
const char *libpath = "libMobileGL.so";
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (!strcmp(argv[i], "--backend") && i + 1 < argc) backend = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--surface") && i + 1 < argc) surface = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--lib") && i + 1 < argc) libpath = argv[++i];
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "usage: %s [--backend DirectGLES|DirectVulkan]"
|
||||||
|
" [--surface pbuffer|imagereader] [--lib path]\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setvbuf(stdout, NULL, _IONBF, 0);
|
||||||
|
|
||||||
|
/* MobileGL parses its config from an ELF constructor, so the backend must be
|
||||||
|
* selected before the library is mapped. */
|
||||||
|
setenv("MOBILEGL_BACKEND_TYPE", backend, 1);
|
||||||
|
printf("mgprobe backend=%s surface=%s lib=%s\n", backend, surface, libpath);
|
||||||
|
|
||||||
|
int useWindow = !strcmp(surface, "imagereader");
|
||||||
|
ANativeWindow *win = NULL;
|
||||||
|
AImageReader *reader = NULL;
|
||||||
|
if (useWindow) {
|
||||||
|
if (AImageReader_newWithUsage(DIM, DIM, AIMAGE_FORMAT_RGBA_8888,
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
|
||||||
|
AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
||||||
|
4, &reader) != AMEDIA_OK || !reader) {
|
||||||
|
printf("FAIL AImageReader_newWithUsage\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
AImageReader_ImageListener l = {NULL, on_image};
|
||||||
|
AImageReader_setImageListener(reader, &l);
|
||||||
|
if (AImageReader_getWindow(reader, &win) != AMEDIA_OK || !win) {
|
||||||
|
printf("FAIL AImageReader_getWindow\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_lib = dlopen(libpath, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!g_lib) {
|
||||||
|
printf("FAIL dlopen: %s\n", dlerror());
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
P_getDisplay eglGetDisplay_ = (P_getDisplay)S("eglGetDisplay");
|
||||||
|
P_initialize eglInitialize_ = (P_initialize)S("eglInitialize");
|
||||||
|
P_bindAPI eglBindAPI_ = (P_bindAPI)S("eglBindAPI");
|
||||||
|
P_chooseConfig eglChooseConfig_ = (P_chooseConfig)S("eglChooseConfig");
|
||||||
|
P_getConfigAttrib eglGetConfigAttrib_ = (P_getConfigAttrib)S("eglGetConfigAttrib");
|
||||||
|
P_createWindowSurface eglCreateWindowSurface_ = (P_createWindowSurface)S("eglCreateWindowSurface");
|
||||||
|
P_createPbufferSurface eglCreatePbufferSurface_ = (P_createPbufferSurface)S("eglCreatePbufferSurface");
|
||||||
|
P_createContext eglCreateContext_ = (P_createContext)S("eglCreateContext");
|
||||||
|
P_makeCurrent eglMakeCurrent_ = (P_makeCurrent)S("eglMakeCurrent");
|
||||||
|
P_getError eglGetError_ = (P_getError)S("eglGetError");
|
||||||
|
|
||||||
|
if (!eglGetDisplay_ || !eglInitialize_ || !eglChooseConfig_ || !eglCreateContext_ || !eglMakeCurrent_) {
|
||||||
|
printf("FAIL missing core EGL exports\n");
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLDisplay dpy = eglGetDisplay_(EGL_DEFAULT_DISPLAY);
|
||||||
|
EGLint vmaj = 0, vmin = 0;
|
||||||
|
if (!eglInitialize_(dpy, &vmaj, &vmin)) {
|
||||||
|
printf("FAIL eglInitialize err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
if (eglBindAPI_ && !eglBindAPI_(EGL_OPENGL_API)) {
|
||||||
|
printf("FAIL eglBindAPI(EGL_OPENGL_API) err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint cfgAttribs[] = {
|
||||||
|
EGL_SURFACE_TYPE, useWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||||
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8,
|
||||||
|
EGL_NONE};
|
||||||
|
EGLConfig cfg = 0;
|
||||||
|
EGLint ncfg = 0;
|
||||||
|
if (!eglChooseConfig_(dpy, cfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
|
printf("FAIL eglChooseConfig n=%d err=0x%x\n", ncfg, eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLSurface surf;
|
||||||
|
if (useWindow) {
|
||||||
|
EGLint vis = 0;
|
||||||
|
if (eglGetConfigAttrib_ && eglGetConfigAttrib_(dpy, cfg, EGL_NATIVE_VISUAL_ID, &vis) && vis)
|
||||||
|
ANativeWindow_setBuffersGeometry(win, DIM, DIM, vis);
|
||||||
|
surf = eglCreateWindowSurface_(dpy, cfg, (EGLNativeWindowType)win, NULL);
|
||||||
|
} else {
|
||||||
|
const EGLint sa[] = {EGL_WIDTH, DIM, EGL_HEIGHT, DIM, EGL_NONE};
|
||||||
|
surf = eglCreatePbufferSurface_(dpy, cfg, sa);
|
||||||
|
}
|
||||||
|
if (surf == EGL_NO_SURFACE) {
|
||||||
|
printf("FAIL create%sSurface err=0x%x\n", useWindow ? "Window" : "Pbuffer",
|
||||||
|
eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint ctxAttribs[] = {
|
||||||
|
EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||||
|
EGLContext ctx = eglCreateContext_(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
|
||||||
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
|
printf("FAIL eglCreateContext(3.3 core) err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
/* MobileGL requires draw == read and rejects EGL_NO_SURFACE. */
|
||||||
|
if (!eglMakeCurrent_(dpy, surf, surf, ctx)) {
|
||||||
|
printf("FAIL eglMakeCurrent err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
P_glGetString glGetString_ = (P_glGetString)S("glGetString");
|
||||||
|
P_glGetIntegerv glGetIntegerv_ = (P_glGetIntegerv)S("glGetIntegerv");
|
||||||
|
P_glClearColor glClearColor_ = (P_glClearColor)S("glClearColor");
|
||||||
|
P_glClear glClear_ = (P_glClear)S("glClear");
|
||||||
|
P_glFinish glFinish_ = (P_glFinish)S("glFinish");
|
||||||
|
P_glReadPixels glReadPixels_ = (P_glReadPixels)S("glReadPixels");
|
||||||
|
P_glGetError glGetError_ = (P_glGetError)S("glGetError");
|
||||||
|
P_glGenTextures glGenTextures_ = (P_glGenTextures)S("glGenTextures");
|
||||||
|
P_glBindTexture glBindTexture_ = (P_glBindTexture)S("glBindTexture");
|
||||||
|
P_glTexImage2D glTexImage2D_ = (P_glTexImage2D)S("glTexImage2D");
|
||||||
|
P_glTexParameteri glTexParameteri_ = (P_glTexParameteri)S("glTexParameteri");
|
||||||
|
P_glGenFramebuffers glGenFramebuffers_ = (P_glGenFramebuffers)S("glGenFramebuffers");
|
||||||
|
P_glBindFramebuffer glBindFramebuffer_ = (P_glBindFramebuffer)S("glBindFramebuffer");
|
||||||
|
P_glFramebufferTexture2D glFramebufferTexture2D_ = (P_glFramebufferTexture2D)S("glFramebufferTexture2D");
|
||||||
|
P_glCheckFramebufferStatus glCheckFramebufferStatus_ = (P_glCheckFramebufferStatus)S("glCheckFramebufferStatus");
|
||||||
|
P_glViewport glViewport_ = (P_glViewport)S("glViewport");
|
||||||
|
P_glGenRenderbuffers glGenRenderbuffers_ = (P_glGenRenderbuffers)S("glGenRenderbuffers");
|
||||||
|
P_glBindRenderbuffer glBindRenderbuffer_ = (P_glBindRenderbuffer)S("glBindRenderbuffer");
|
||||||
|
P_glRenderbufferStorage glRenderbufferStorage_ = (P_glRenderbufferStorage)S("glRenderbufferStorage");
|
||||||
|
P_glFramebufferRenderbuffer glFramebufferRenderbuffer_ = (P_glFramebufferRenderbuffer)S("glFramebufferRenderbuffer");
|
||||||
|
|
||||||
|
int major = -1, minor = -1, profile = -1;
|
||||||
|
glGetIntegerv_(GL_MAJOR_VERSION, &major);
|
||||||
|
glGetIntegerv_(GL_MINOR_VERSION, &minor);
|
||||||
|
glGetIntegerv_(GL_CONTEXT_PROFILE_MASK, &profile);
|
||||||
|
printf(" GL_VENDOR %s\n", (const char *)glGetString_(GL_VENDOR));
|
||||||
|
printf(" GL_RENDERER %s\n", (const char *)glGetString_(GL_RENDERER));
|
||||||
|
printf(" GL_VERSION %s\n", (const char *)glGetString_(GL_VERSION));
|
||||||
|
printf(" GLSL %s\n", (const char *)glGetString_(GL_SHADING_LANGUAGE_VERSION));
|
||||||
|
printf(" version %d.%d profile_mask 0x%x %s\n", major, minor, profile,
|
||||||
|
(profile & 1) ? "(core)" : "(NOT CORE)");
|
||||||
|
|
||||||
|
unsigned char px[4];
|
||||||
|
|
||||||
|
/* Default framebuffer. */
|
||||||
|
glClearColor_(0.25f, 0.5f, 0.75f, 1.0f);
|
||||||
|
glClear_(GL_COLOR_BUFFER_BIT);
|
||||||
|
if (glFinish_) glFinish_();
|
||||||
|
memset(px, 0, sizeof px);
|
||||||
|
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
||||||
|
int defOk = near8(px[0], 64, 10) && near8(px[1], 128, 10) && near8(px[2], 191, 10);
|
||||||
|
printf(" default-FB readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
|
||||||
|
defOk ? "ok" : "BROKEN");
|
||||||
|
|
||||||
|
/* User FBO - this is what dEQP uses with --deqp-surface-type=fbo. */
|
||||||
|
unsigned int tex = 0, fbo = 0;
|
||||||
|
glGenTextures_(1, &tex);
|
||||||
|
glBindTexture_(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D_(GL_TEXTURE_2D, 0, GL_RGBA8, DIM, DIM, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||||
|
glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glGenFramebuffers_(1, &fbo);
|
||||||
|
glBindFramebuffer_(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||||
|
unsigned int fbst = glCheckFramebufferStatus_(GL_FRAMEBUFFER);
|
||||||
|
int fboOk = 0;
|
||||||
|
if (fbst == GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
glViewport_(0, 0, DIM, DIM);
|
||||||
|
glClearColor_(0.9f, 0.2f, 0.4f, 1.0f);
|
||||||
|
glClear_(GL_COLOR_BUFFER_BIT);
|
||||||
|
if (glFinish_) glFinish_();
|
||||||
|
memset(px, 0, sizeof px);
|
||||||
|
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
||||||
|
fboOk = near8(px[0], 230, 10) && near8(px[1], 51, 10) && near8(px[2], 102, 10);
|
||||||
|
printf(" user-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
|
||||||
|
fboOk ? "ok" : "BROKEN");
|
||||||
|
} else {
|
||||||
|
printf(" user-FBO incomplete status=0x%x\n", fbst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FBO with a RENDERBUFFER colour attachment. This is what dEQP's
|
||||||
|
* FboRenderContext allocates for --deqp-surface-type=fbo, so it is the path
|
||||||
|
* that actually decides a conformance run - a texture-attached FBO working
|
||||||
|
* says nothing about it. */
|
||||||
|
unsigned int rbo = 0, rfbo = 0;
|
||||||
|
int rboOk = 0;
|
||||||
|
if (glGenRenderbuffers_ && glBindRenderbuffer_ && glRenderbufferStorage_ && glFramebufferRenderbuffer_) {
|
||||||
|
glGenRenderbuffers_(1, &rbo);
|
||||||
|
glBindRenderbuffer_(GL_RENDERBUFFER, rbo);
|
||||||
|
glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA8, DIM, DIM);
|
||||||
|
glBindRenderbuffer_(GL_RENDERBUFFER, 0);
|
||||||
|
glGenFramebuffers_(1, &rfbo);
|
||||||
|
glBindFramebuffer_(GL_FRAMEBUFFER, rfbo);
|
||||||
|
glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
||||||
|
unsigned int rst = glCheckFramebufferStatus_(GL_FRAMEBUFFER);
|
||||||
|
if (rst == GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
glViewport_(0, 0, DIM, DIM);
|
||||||
|
glClearColor_(0.1f, 0.7f, 0.3f, 1.0f);
|
||||||
|
glClear_(GL_COLOR_BUFFER_BIT);
|
||||||
|
if (glFinish_) glFinish_();
|
||||||
|
memset(px, 0, sizeof px);
|
||||||
|
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
||||||
|
rboOk = near8(px[0], 26, 10) && near8(px[1], 179, 10) && near8(px[2], 77, 10);
|
||||||
|
printf(" rbo-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
|
||||||
|
rboOk ? "ok" : "BROKEN");
|
||||||
|
} else {
|
||||||
|
printf(" rbo-FBO incomplete status=0x%x\n", rst);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf(" rbo-FBO skipped (renderbuffer entry points unavailable)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned glerr = glGetError_ ? glGetError_() : 0;
|
||||||
|
int ok = fboOk && rboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0;
|
||||||
|
printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s rbo_fbo=%s glerr=0x%x\n",
|
||||||
|
ok ? "PASS" : "FAIL", backend, surface, defOk ? "ok" : "broken",
|
||||||
|
fboOk ? "ok" : "broken", rboOk ? "ok" : "broken", glerr);
|
||||||
|
|
||||||
|
fflush(stdout);
|
||||||
|
/* MobileGL aborts in static teardown; leave before that runs. */
|
||||||
|
_exit(ok ? 0 : 1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Summarise dEQP/glcts .qpa logs into a conformance pass rate.
|
||||||
|
|
||||||
|
Handles the two ways a case can end in a .qpa: a normal
|
||||||
|
``#beginTestCaseResult``/``#endTestCaseResult`` pair carrying a
|
||||||
|
``<Result StatusCode="...">`` element, and ``#terminateTestCaseResult <reason>``,
|
||||||
|
which is what the log contains when the process died partway through a case.
|
||||||
|
Cases that were started but never terminated (the run was killed) are reported
|
||||||
|
separately so a truncated chunk is never silently scored as a pass.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python qpa_report.py <file-or-dir> [<file-or-dir> ...] [--json out.json] [--top N]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
|
# Khronos conformance treats these as non-failures: the test either passed or
|
||||||
|
# the implementation legitimately does not expose the feature under test.
|
||||||
|
NON_FAILURE = {
|
||||||
|
"Pass",
|
||||||
|
"NotSupported",
|
||||||
|
"QualityWarning",
|
||||||
|
"CompatibilityWarning",
|
||||||
|
"Waiver",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Statuses that indicate the case did not merely fail but destabilised the run.
|
||||||
|
HARD = {"Crash", "Timeout", "InternalError", "ResourceError", "DeviceHang"}
|
||||||
|
|
||||||
|
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
|
||||||
|
CASE_END = re.compile(r"^#endTestCaseResult")
|
||||||
|
CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)")
|
||||||
|
RESULT = re.compile(r'<Result\s+StatusCode="([^"]+)"')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_qpa(path):
|
||||||
|
"""Yield (case_name, status) for every case recorded in one .qpa file."""
|
||||||
|
current = None
|
||||||
|
status = None
|
||||||
|
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
|
for line in fh:
|
||||||
|
m = CASE_START.match(line)
|
||||||
|
if m:
|
||||||
|
if current is not None:
|
||||||
|
# A new case started before the previous one closed.
|
||||||
|
yield current, status or "Incomplete"
|
||||||
|
current, status = m.group(1), None
|
||||||
|
continue
|
||||||
|
if current is None:
|
||||||
|
continue
|
||||||
|
m = RESULT.search(line)
|
||||||
|
if m:
|
||||||
|
status = m.group(1)
|
||||||
|
continue
|
||||||
|
m = CASE_TERM.match(line)
|
||||||
|
if m:
|
||||||
|
reason = m.group(1).strip() or "Terminated"
|
||||||
|
# dEQP writes e.g. "Crash" / "Timeout" here.
|
||||||
|
yield current, reason if reason in HARD else "Crash"
|
||||||
|
current, status = None, None
|
||||||
|
continue
|
||||||
|
if CASE_END.match(line):
|
||||||
|
yield current, status or "Incomplete"
|
||||||
|
current, status = None, None
|
||||||
|
if current is not None:
|
||||||
|
# File ended mid-case: the runner was killed.
|
||||||
|
yield current, "Incomplete"
|
||||||
|
|
||||||
|
|
||||||
|
def collect(paths):
|
||||||
|
files = []
|
||||||
|
for p in paths:
|
||||||
|
if os.path.isdir(p):
|
||||||
|
for root, _dirs, names in os.walk(p):
|
||||||
|
files.extend(os.path.join(root, n) for n in sorted(names) if n.endswith(".qpa"))
|
||||||
|
else:
|
||||||
|
files.append(p)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def group_of(case):
|
||||||
|
"""The case's parent group, e.g. KHR-GL33.shaders.arrays for ...arrays.foo."""
|
||||||
|
parts = case.split(".")
|
||||||
|
return ".".join(parts[:-1]) if len(parts) > 1 else case
|
||||||
|
|
||||||
|
|
||||||
|
def load_sidecar(paths, name):
|
||||||
|
"""Case names run_cts.py recorded in one of its sidecar lists."""
|
||||||
|
out = set()
|
||||||
|
for p in paths:
|
||||||
|
d = p if os.path.isdir(p) else os.path.dirname(p)
|
||||||
|
f = os.path.join(d, name)
|
||||||
|
if os.path.isfile(f):
|
||||||
|
with open(f, "r", encoding="utf-8") as fh:
|
||||||
|
out.update(l.strip() for l in fh if l.strip() and not l.strip().startswith("#"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("paths", nargs="+")
|
||||||
|
ap.add_argument("--json", dest="json_out")
|
||||||
|
ap.add_argument("--top", type=int, default=25)
|
||||||
|
ap.add_argument("--label", default="")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
files = collect(args.paths)
|
||||||
|
if not files:
|
||||||
|
print("no .qpa files found", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# Later chunks may re-run a case; last result wins.
|
||||||
|
results = {}
|
||||||
|
for f in files:
|
||||||
|
for case, status in parse_qpa(f):
|
||||||
|
results[case] = status
|
||||||
|
|
||||||
|
# A case the runner saw take the process down is a Crash, not merely an
|
||||||
|
# unterminated log entry - but a real result from a later retry wins.
|
||||||
|
for case in load_sidecar(args.paths, "crashed.txt"):
|
||||||
|
if results.get(case, "Incomplete") == "Incomplete":
|
||||||
|
results[case] = "Crash"
|
||||||
|
# Worse than a crash: these rebooted the device.
|
||||||
|
for case in load_sidecar(args.paths, "hung.txt"):
|
||||||
|
if results.get(case, "Incomplete") in ("Incomplete", "Crash"):
|
||||||
|
results[case] = "DeviceHang"
|
||||||
|
|
||||||
|
# Cases excluded up front, and cases the run never reached, are not results.
|
||||||
|
# Report them separately so a partial run is never read as a complete one.
|
||||||
|
skipped = load_sidecar(args.paths, "skipped.txt")
|
||||||
|
unrun = load_sidecar(args.paths, "unrun.txt") - set(results)
|
||||||
|
|
||||||
|
counts = Counter(results.values())
|
||||||
|
total = len(results)
|
||||||
|
non_fail = sum(counts[s] for s in NON_FAILURE)
|
||||||
|
strict_pass = counts["Pass"]
|
||||||
|
failures = total - non_fail
|
||||||
|
|
||||||
|
by_group_fail = defaultdict(int)
|
||||||
|
by_group_total = defaultdict(int)
|
||||||
|
for case, status in results.items():
|
||||||
|
g = group_of(case)
|
||||||
|
by_group_total[g] += 1
|
||||||
|
if status not in NON_FAILURE:
|
||||||
|
by_group_fail[g] += 1
|
||||||
|
|
||||||
|
label = f" [{args.label}]" if args.label else ""
|
||||||
|
print(f"=== glcts conformance summary{label} ===")
|
||||||
|
print(f"files parsed : {len(files)}")
|
||||||
|
print(f"cases with result : {total}")
|
||||||
|
print()
|
||||||
|
for status, n in counts.most_common():
|
||||||
|
mark = " " if status in NON_FAILURE else " ! "
|
||||||
|
print(f"{mark}{status:<22} {n:>7} {100.0 * n / total:6.2f}%")
|
||||||
|
print()
|
||||||
|
if total:
|
||||||
|
print(f"conformance pass rate (Pass+NotSupported+warnings) : {100.0 * non_fail / total:6.2f}% ({non_fail}/{total})")
|
||||||
|
print(f"strict pass rate (Pass only) : {100.0 * strict_pass / total:6.2f}% ({strict_pass}/{total})")
|
||||||
|
print(f"failures : {failures}")
|
||||||
|
|
||||||
|
if skipped or unrun:
|
||||||
|
print("\n--- NOT MEASURED (excluded from the rates above) ---")
|
||||||
|
if skipped:
|
||||||
|
print(f" quarantined up front : {len(skipped)}")
|
||||||
|
if unrun:
|
||||||
|
print(f" never reached : {len(unrun)}")
|
||||||
|
print(" The rates above cover only cases that produced a result.")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
print(f"\n--- worst groups (of {len(by_group_total)}) ---")
|
||||||
|
worst = sorted(by_group_fail.items(), key=lambda kv: -kv[1])[: args.top]
|
||||||
|
for g, nf in worst:
|
||||||
|
nt = by_group_total[g]
|
||||||
|
print(f" {g:<52} {nf:>6}/{nt:<6} fail ({100.0 * nf / nt:5.1f}%)")
|
||||||
|
|
||||||
|
if args.json_out:
|
||||||
|
with open(args.json_out, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"label": args.label,
|
||||||
|
"files": len(files),
|
||||||
|
"total": total,
|
||||||
|
"counts": dict(counts),
|
||||||
|
"non_failure": non_fail,
|
||||||
|
"strict_pass": strict_pass,
|
||||||
|
"failures": failures,
|
||||||
|
"pass_rate": (non_fail / total) if total else 0.0,
|
||||||
|
"strict_pass_rate": (strict_pass / total) if total else 0.0,
|
||||||
|
"results": results,
|
||||||
|
},
|
||||||
|
fh,
|
||||||
|
indent=1,
|
||||||
|
)
|
||||||
|
print(f"\nwrote {args.json_out}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Drive a glcts run on a device, resuming across crashes.
|
||||||
|
|
||||||
|
MobileGL crashes on some cases, and glcts takes the whole process down with it.
|
||||||
|
A single invocation would therefore stop at the first crash and leave most of
|
||||||
|
the suite unmeasured. This runner re-invokes glcts with only the cases that have
|
||||||
|
not produced a result yet, records each crashed case as "Crash", and repeats
|
||||||
|
until the list is exhausted, so one bad case costs one case rather than the run.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python run_cts.py --serial <adb-serial> --backend DirectGLES|DirectVulkan \\
|
||||||
|
--caselist <host-path-to-mustpass.txt> --outdir <host-dir> [--device-dir /data/local/tmp/mgcts]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
|
||||||
|
CASE_END = re.compile(r"^#endTestCaseResult")
|
||||||
|
CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)")
|
||||||
|
|
||||||
|
|
||||||
|
def adb(serial, *args, timeout=None):
|
||||||
|
try:
|
||||||
|
return subprocess.run(["adb", "-s", serial, *args], capture_output=True, text=True, timeout=timeout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return subprocess.CompletedProcess(args, returncode=124, stdout="", stderr="adb timeout")
|
||||||
|
|
||||||
|
|
||||||
|
def device_alive(serial, timeout=30):
|
||||||
|
"""True only if the device answers a trivial shell command.
|
||||||
|
|
||||||
|
Distinguishes "glcts crashed" from "the device fell over". Without this a
|
||||||
|
dead device looks like every remaining case crashing, which silently turns a
|
||||||
|
broken run into a plausible-looking conformance number.
|
||||||
|
"""
|
||||||
|
r = adb(serial, "shell", "echo alive", timeout=timeout)
|
||||||
|
return r.returncode == 0 and "alive" in (r.stdout or "")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_device(serial, attempts=20, delay=15):
|
||||||
|
for i in range(attempts):
|
||||||
|
if device_alive(serial):
|
||||||
|
return True
|
||||||
|
print(f"[run_cts] device {serial} unresponsive, waiting ({i + 1}/{attempts})")
|
||||||
|
time.sleep(delay)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def mem_available_kb(serial):
|
||||||
|
r = adb(serial, "shell", "grep MemAvailable /proc/meminfo", timeout=30)
|
||||||
|
m = re.search(r"(\d+)", r.stdout or "")
|
||||||
|
return int(m.group(1)) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def completed_cases(qpa_path):
|
||||||
|
"""Return (finished_case_names, last_started_case_or_None).
|
||||||
|
|
||||||
|
A case that was started but never closed is the one the process died in.
|
||||||
|
"""
|
||||||
|
finished = []
|
||||||
|
current = None
|
||||||
|
if not os.path.exists(qpa_path):
|
||||||
|
return finished, None
|
||||||
|
with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
|
for line in fh:
|
||||||
|
m = CASE_START.match(line)
|
||||||
|
if m:
|
||||||
|
current = m.group(1)
|
||||||
|
continue
|
||||||
|
if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)):
|
||||||
|
finished.append(current)
|
||||||
|
current = None
|
||||||
|
return finished, current
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--serial", required=True)
|
||||||
|
ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"])
|
||||||
|
ap.add_argument("--caselist", required=True)
|
||||||
|
ap.add_argument("--outdir", required=True)
|
||||||
|
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
||||||
|
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
||||||
|
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||||
|
ap.add_argument("--max-empty-streak", type=int, default=8,
|
||||||
|
help="abort after this many consecutive chunks that produce no log at all")
|
||||||
|
ap.add_argument("--min-mem-kb", type=int, default=400000,
|
||||||
|
help="pause when the device drops below this much available memory")
|
||||||
|
ap.add_argument("--chunk-timeout", type=int, default=900,
|
||||||
|
help="seconds before giving up on one glcts invocation (a GPU hang never returns)")
|
||||||
|
ap.add_argument("--skip-file", default=None,
|
||||||
|
help="file of case names to exclude, e.g. cases known to hang the device")
|
||||||
|
ap.add_argument("--env", action="append", default=[], metavar="K=V",
|
||||||
|
help="extra environment variable for glcts (repeatable)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(args.outdir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(args.caselist, "r", encoding="utf-8") as fh:
|
||||||
|
remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
|
||||||
|
|
||||||
|
skipped = []
|
||||||
|
if args.skip_file and os.path.isfile(args.skip_file):
|
||||||
|
with open(args.skip_file, "r", encoding="utf-8") as fh:
|
||||||
|
skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")}
|
||||||
|
skipped = [c for c in remaining if c in skip]
|
||||||
|
remaining = [c for c in remaining if c not in skip]
|
||||||
|
print(f"[run_cts] skipping {len(skipped)} case(s) from {args.skip_file}")
|
||||||
|
|
||||||
|
total = len(remaining)
|
||||||
|
print(f"[run_cts] {args.backend} on {args.serial}: {total} cases")
|
||||||
|
|
||||||
|
crashed = []
|
||||||
|
hung = []
|
||||||
|
done = set()
|
||||||
|
chunk = 0
|
||||||
|
started = time.time()
|
||||||
|
empty_streak = 0
|
||||||
|
|
||||||
|
if not wait_for_device(args.serial):
|
||||||
|
print("[run_cts] device not responding before start; aborting", file=sys.stderr)
|
||||||
|
return 3
|
||||||
|
|
||||||
|
while remaining and chunk < args.max_rounds:
|
||||||
|
listfile = os.path.join(args.outdir, "remaining.txt")
|
||||||
|
with open(listfile, "w", encoding="utf-8", newline="\n") as fh:
|
||||||
|
fh.write("\n".join(remaining) + "\n")
|
||||||
|
|
||||||
|
# Repeated process launches plus crash tombstones can drive the device
|
||||||
|
# into memory pressure; give it room rather than pushing it over.
|
||||||
|
mem = mem_available_kb(args.serial)
|
||||||
|
if mem is not None and mem < args.min_mem_kb:
|
||||||
|
print(f"[run_cts] low memory ({mem} kB available); pausing 30 s")
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
dev_list = f"{args.device_dir}/remaining.txt"
|
||||||
|
dev_qpa = f"{args.device_dir}/chunk.qpa"
|
||||||
|
push = adb(args.serial, "push", listfile, dev_list, timeout=120)
|
||||||
|
if push.returncode != 0:
|
||||||
|
print(f"[run_cts] push failed ({push.stderr.strip()}); treating as device trouble",
|
||||||
|
file=sys.stderr)
|
||||||
|
if not wait_for_device(args.serial):
|
||||||
|
print("[run_cts] ABORTING: device unreachable.", file=sys.stderr)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60)
|
||||||
|
|
||||||
|
extra_env = "".join(f"{kv} " for kv in args.env)
|
||||||
|
cmd = (
|
||||||
|
f"cd {args.device_dir} && "
|
||||||
|
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
|
||||||
|
f"./glcts --deqp-caselist-file={dev_list} "
|
||||||
|
f"--deqp-surface-type={args.surface} "
|
||||||
|
f"--deqp-terminate-on-device-lost=disable "
|
||||||
|
f"--deqp-log-images=disable --deqp-log-shader-sources=disable "
|
||||||
|
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?"
|
||||||
|
)
|
||||||
|
run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout)
|
||||||
|
if run.returncode == 124:
|
||||||
|
print(f"[run_cts] chunk {chunk:04d} timed out after {args.chunk_timeout}s "
|
||||||
|
f"(likely a GPU hang)", file=sys.stderr)
|
||||||
|
|
||||||
|
# Some cases hang the GPU hard enough to reboot the device. The log on
|
||||||
|
# /data/local/tmp survives that, so wait for the device to come back and
|
||||||
|
# pull it anyway rather than losing the whole chunk.
|
||||||
|
rebooted = False
|
||||||
|
if not device_alive(args.serial, timeout=30):
|
||||||
|
print(f"[run_cts] device went away during chunk {chunk:04d}; waiting for it",
|
||||||
|
file=sys.stderr)
|
||||||
|
if not wait_for_device(args.serial, attempts=40, delay=15):
|
||||||
|
print("[run_cts] ABORTING: device never came back. Results are incomplete; "
|
||||||
|
"do NOT treat the remaining cases as failures.", file=sys.stderr)
|
||||||
|
break
|
||||||
|
rebooted = True
|
||||||
|
print("[run_cts] device is back")
|
||||||
|
|
||||||
|
local_qpa = os.path.join(args.outdir, f"chunk{chunk:04d}.qpa")
|
||||||
|
pull = adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300)
|
||||||
|
if pull.returncode != 0 and rebooted:
|
||||||
|
time.sleep(10)
|
||||||
|
adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300)
|
||||||
|
|
||||||
|
finished, in_flight = completed_cases(local_qpa)
|
||||||
|
for c in finished:
|
||||||
|
done.add(c)
|
||||||
|
|
||||||
|
progressed = len(finished)
|
||||||
|
if progressed > 0:
|
||||||
|
empty_streak = 0
|
||||||
|
if in_flight is not None:
|
||||||
|
# The case that was open when the process (or the device) died.
|
||||||
|
if rebooted:
|
||||||
|
# It took the whole device down: quarantine it, or the next
|
||||||
|
# invocation walks straight back into it.
|
||||||
|
print(f"[run_cts] DEVICE HANG in {in_flight} - quarantining it")
|
||||||
|
hung.append(in_flight)
|
||||||
|
else:
|
||||||
|
crashed.append(in_flight)
|
||||||
|
done.add(in_flight)
|
||||||
|
progressed += 1
|
||||||
|
elif progressed == 0:
|
||||||
|
# Nothing at all came back. Either the first remaining case takes
|
||||||
|
# the process down before the log is flushed, or the device died.
|
||||||
|
# Those look identical from here, so confirm the device is alive
|
||||||
|
# before blaming the test.
|
||||||
|
if not device_alive(args.serial):
|
||||||
|
print(f"[run_cts] device went away during chunk {chunk:04d}", file=sys.stderr)
|
||||||
|
if not wait_for_device(args.serial):
|
||||||
|
print("[run_cts] ABORTING: device never came back. Results are "
|
||||||
|
"incomplete; do NOT treat the remaining cases as crashes.", file=sys.stderr)
|
||||||
|
break
|
||||||
|
print("[run_cts] device recovered; retrying the same chunk")
|
||||||
|
continue
|
||||||
|
|
||||||
|
empty_streak += 1
|
||||||
|
if empty_streak >= args.max_empty_streak:
|
||||||
|
print(f"[run_cts] ABORTING: {empty_streak} consecutive chunks produced no output "
|
||||||
|
f"while the device stayed reachable. Something systemic is wrong; refusing "
|
||||||
|
f"to label the rest of the suite as crashes.", file=sys.stderr)
|
||||||
|
break
|
||||||
|
|
||||||
|
victim = remaining[0]
|
||||||
|
print(f"[run_cts] no output at all; recording {victim} as Crash")
|
||||||
|
crashed.append(victim)
|
||||||
|
done.add(victim)
|
||||||
|
progressed = 1
|
||||||
|
|
||||||
|
remaining = [c for c in remaining if c not in done]
|
||||||
|
elapsed = time.time() - started
|
||||||
|
print(
|
||||||
|
f"[run_cts] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, "
|
||||||
|
f"crashes {len(crashed)}, {elapsed / 60:.1f} min)"
|
||||||
|
)
|
||||||
|
chunk += 1
|
||||||
|
|
||||||
|
with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||||
|
fh.write("\n".join(crashed) + ("\n" if crashed else ""))
|
||||||
|
|
||||||
|
# Cases that rebooted the device. Feed this back in via --skip-file to avoid
|
||||||
|
# paying for the same reboot on the next run.
|
||||||
|
with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||||
|
fh.write("\n".join(hung) + ("\n" if hung else ""))
|
||||||
|
if hung:
|
||||||
|
print(f"[run_cts] {len(hung)} case(s) hung the device (see hung.txt):")
|
||||||
|
for c in hung:
|
||||||
|
print(f" {c}")
|
||||||
|
|
||||||
|
# Anything still in `remaining` was never measured. Record it so the report
|
||||||
|
# cannot quietly present a partial run as a complete one.
|
||||||
|
with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||||
|
fh.write("\n".join(remaining) + ("\n" if remaining else ""))
|
||||||
|
if skipped:
|
||||||
|
with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh:
|
||||||
|
fh.write("\n".join(skipped) + "\n")
|
||||||
|
|
||||||
|
if remaining:
|
||||||
|
print(f"[run_cts] WARNING: {len(remaining)} cases were never run (see unrun.txt)", file=sys.stderr)
|
||||||
|
print(f"[run_cts] finished: {len(done)}/{total} cases, {len(crashed)} crashes, {chunk} invocations")
|
||||||
|
print(f"[run_cts] qpa chunks in {args.outdir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Copy the MobileGL dEQP platform port into a VK-GL-CTS checkout.
|
||||||
|
|
||||||
|
The port is version-controlled here, in the MobileGL repo, so it survives a
|
||||||
|
throwaway CTS clone. This drops it into the places VK-GL-CTS expects:
|
||||||
|
|
||||||
|
framework/platform/mobilegl/ <- platform sources
|
||||||
|
targets/mobilegl/mobilegl.cmake <- target definition (-DDEQP_TARGET=mobilegl)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python sync_to_cts.py <path-to-VK-GL-CTS>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
CTS_TOOLS = os.path.dirname(HERE)
|
||||||
|
|
||||||
|
COPIES = [
|
||||||
|
(os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None),
|
||||||
|
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(__doc__)
|
||||||
|
return 2
|
||||||
|
cts = sys.argv[1]
|
||||||
|
if not os.path.isfile(os.path.join(cts, "CMakeLists.txt")):
|
||||||
|
print(f"error: {cts} does not look like a VK-GL-CTS checkout", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for src, reldst, only in COPIES:
|
||||||
|
dst = os.path.join(cts, reldst)
|
||||||
|
os.makedirs(dst, exist_ok=True)
|
||||||
|
for name in sorted(os.listdir(src)):
|
||||||
|
if only is not None and name not in only:
|
||||||
|
continue
|
||||||
|
s = os.path.join(src, name)
|
||||||
|
if not os.path.isfile(s):
|
||||||
|
continue
|
||||||
|
shutil.copy2(s, os.path.join(dst, name))
|
||||||
|
print(f" {reldst}/{name}")
|
||||||
|
|
||||||
|
print("\nsynced. configure with -DDEQP_TARGET=mobilegl")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# MobileGL conformance-suite skills
|
||||||
|
|
||||||
|
Task-focused skills for running Khronos conformance suites against MobileGL.
|
||||||
|
Each skill is a self-contained package, matching the layout used by
|
||||||
|
`tools/trace_replay/skills/`:
|
||||||
|
|
||||||
|
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
|
||||||
|
directory name equals the frontmatter `name`.
|
||||||
|
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
|
||||||
|
`short_description`, `default_prompt`).
|
||||||
|
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
|
||||||
|
skill has them.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
| Skill | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
name: gl-cts-on-mobilegl
|
||||||
|
description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts, KHR-GL33) against MobileGL on an Android device and compute a per-backend conformance rate. Use when measuring OpenGL 3.3 core conformance for DirectGLES or DirectVulkan, building glcts for Android arm64, porting a dEQP tcu::Platform onto MobileGL, or triaging CTS failures, crashes, and cases that hang the device.
|
||||||
|
---
|
||||||
|
|
||||||
|
# OpenGL CTS on MobileGL (Android)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`glcts` from VK-GL-CTS is built as a **standalone arm64 executable** and run from
|
||||||
|
`adb shell`. It reaches OpenGL only through `libMobileGL.so`, which supplies both
|
||||||
|
EGL and desktop GL, so a result is unambiguously MobileGL's and never the system
|
||||||
|
GL stack's. No APK and no Activity are involved.
|
||||||
|
|
||||||
|
The port lives in this repository under `MobileGL/tools/cts/` and is copied into
|
||||||
|
a VK-GL-CTS checkout by `scripts/sync_to_cts.py`, so it survives a throwaway CTS
|
||||||
|
clone.
|
||||||
|
|
||||||
|
Set up paths first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export MG=<path-to-MobileGL-worktree> # do builds in a worktree, not the shared tree
|
||||||
|
export CTS=<path-to-VK-GL-CTS-checkout>
|
||||||
|
export NDK="$ANDROID_HOME/ndk/27.3.13750724"
|
||||||
|
export SERIAL=<adb-device-serial>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Android NDK r27 (the repo builds MobileGL with 27.3.13750724), CMake, Ninja, Python 3.
|
||||||
|
- A rooted-or-not Android device with `adb`; ~600 MB free under `/data/local/tmp`.
|
||||||
|
- **A device you can physically power-cycle.** Some cases hang the GPU hard
|
||||||
|
enough to reboot it — see "Cases that take the device down".
|
||||||
|
- On Windows, invoke `python`, not `python3`: the latter resolves to the
|
||||||
|
Microsoft Store alias stub and exits 49.
|
||||||
|
|
||||||
|
## Step 1 — build libMobileGL.so
|
||||||
|
|
||||||
|
Build in a git worktree (other agents share the main tree). A fresh worktree is
|
||||||
|
missing glslang's bundled SPIR-V Tools, which is a hard configure blocker
|
||||||
|
because `ENABLE_OPT` is forced on:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp -r <main-tree>/3rdparty/glslang/External/* "$MG/3rdparty/glslang/External/"
|
||||||
|
./gradlew -p "$MG/android-plugin" :app:assembleTraceRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
The stripped library lands in
|
||||||
|
`android-plugin/app/build/intermediates/stripped_native_libs/traceRelease/.../arm64-v8a/libMobileGL.so`.
|
||||||
|
|
||||||
|
## Step 2 — get VK-GL-CTS and its externals
|
||||||
|
|
||||||
|
Use a **release tag**, not `main`, so the mustpass list — and therefore the
|
||||||
|
reported rate — is citable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git -C "$CTS" checkout opengl-cts-4.6.8.1
|
||||||
|
cd "$CTS" && python external/fetch_sources.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — build glcts for Android arm64
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS"
|
||||||
|
|
||||||
|
cmake -S "$CTS" -B build-cts-a64 -G Ninja \
|
||||||
|
-DDEQP_TARGET=mobilegl -DDEQP_TARGET_TOOLCHAIN=ndk-modern \
|
||||||
|
-DANDROID_NDK_PATH="$NDK" -DDE_ANDROID_API=26 -DANDROID_ABI=arm64-v8a \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
ninja -C build-cts-a64 glcts
|
||||||
|
"$NDK"/toolchains/llvm/prebuilt/*/bin/llvm-strip build-cts-a64/external/openglcts/modules/glcts
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm the configure output says `DE_OS = DE_OS_ANDROID`, `DE_CPU =
|
||||||
|
DE_CPU_ARM_64` and `DEQP_ANDROID_BUILD = EXE`. Two things make that work and
|
||||||
|
both are easy to get wrong:
|
||||||
|
|
||||||
|
- `DEQP_TARGET_TOOLCHAIN=ndk-modern` is required. dEQP includes `Defs.cmake`
|
||||||
|
*before* the target file, so a target cannot set `DE_OS` itself. Without the
|
||||||
|
toolchain hook the build mis-detects as `DE_OS_UNIX`/`x86_64` and dies on
|
||||||
|
`__assert_fail` (bionic has `__assert2`).
|
||||||
|
- The target sets `DEQP_ANDROID_EXE ON`. Otherwise dEQP builds the modules into
|
||||||
|
the `libdeqp.so` an APK would load and no `glcts` executable exists.
|
||||||
|
|
||||||
|
`KHR-GL33` needs no ungating — the package registry registers it unconditionally;
|
||||||
|
only the `dEQP-*` packages are `#if DE_OS != DE_OS_ANDROID`.
|
||||||
|
|
||||||
|
## Step 4 — deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb -s $SERIAL shell mkdir -p /data/local/tmp/mgcts
|
||||||
|
adb -s $SERIAL push build-cts-a64/external/openglcts/modules/glcts /data/local/tmp/mgcts/
|
||||||
|
adb -s $SERIAL push build-cts-a64/external/openglcts/modules/gl_cts /data/local/tmp/mgcts/
|
||||||
|
adb -s $SERIAL push <libMobileGL.so> /data/local/tmp/mgcts/
|
||||||
|
adb -s $SERIAL shell chmod 755 /data/local/tmp/mgcts/glcts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5 — preflight
|
||||||
|
|
||||||
|
Never start a multi-hour run without this. It proves the device/library pair
|
||||||
|
yields a 3.3 core context and that FBO readback is correct, in about a second:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb -s $SERIAL shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \
|
||||||
|
--backend DirectVulkan --surface imagereader --lib ./libMobileGL.so'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `PASS ... user_fbo=ok`. `default_fb=broken` on DirectVulkan is expected
|
||||||
|
and does not gate — see below.
|
||||||
|
|
||||||
|
## Step 6 — run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python "$MG/tools/cts/scripts/run_cts.py" \
|
||||||
|
--serial $SERIAL --backend DirectGLES \
|
||||||
|
--caselist .../mustpass/gl/khronos_mustpass/main/gl33-main.txt \
|
||||||
|
--outdir runs/gles --skip-file runs/skip.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The runner re-invokes `glcts` with only the cases that have no result yet, so a
|
||||||
|
crash costs one case rather than the run. It distinguishes a crashed *case* from
|
||||||
|
a dead *device* by checking the device still answers a shell command — without
|
||||||
|
that check a dead device looks like every remaining case crashing, which yields
|
||||||
|
a completely bogus but plausible-looking conformance number. On a device reboot
|
||||||
|
it waits, re-pulls the partial `.qpa` (which survives on `/data/local/tmp`),
|
||||||
|
records the case that was open as `DeviceHang`, and quarantines it.
|
||||||
|
|
||||||
|
## Step 7 — report
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python "$MG/tools/cts/scripts/qpa_report.py" runs/gles --label DirectGLES
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass rate counts `Pass`, `NotSupported`, `QualityWarning`, `CompatibilityWarning`
|
||||||
|
and `Waiver` as non-failures, matching how Khronos scores a submission; the
|
||||||
|
strict rate counts only `Pass`. Quarantined and never-reached cases are reported
|
||||||
|
separately and excluded from the rates, so a partial run cannot read as a
|
||||||
|
complete one.
|
||||||
|
|
||||||
|
## Required flags, and why
|
||||||
|
|
||||||
|
| Flag | Why it is not optional |
|
||||||
|
| --- | --- |
|
||||||
|
| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. Use it for **both** backends so the two numbers stay comparable. |
|
||||||
|
| `MOBILEGL_CTS_FBO_COLOR_TEXTURE=1` | **`--deqp-surface-type=fbo` alone is not enough.** dEQP's `FboRenderContext` allocates a *renderbuffer* colour attachment, and DirectVulkan returns zeros from a renderbuffer-attached FBO too — only a *texture*-attached FBO reads back correctly. This env var (a patch to `framework/opengl/gluFboRenderContext.cpp`, off by default) switches the attachment to a texture and isolates that single defect. Measured effect: `KHR-GL33.shaders.loops.for_constant_iterations.*` goes 0/62 → 62/62, and the whole-suite DirectVulkan conformance rate goes 46.15% → 72.74%. DirectGLES is bit-identical either way (93.05%), which is the control proving the switch is neutral where readback works. |
|
||||||
|
| `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. |
|
||||||
|
|
||||||
|
## Cases that take the device down
|
||||||
|
|
||||||
|
Some cases hang the GPU hard enough that the device reboots or stops answering
|
||||||
|
adb entirely. Keep them in a `--skip-file`, and expect to find more:
|
||||||
|
|
||||||
|
- `KHR-GL33.clip_distance.functional` — wedged an Adreno 750 tablet; it rebooted
|
||||||
|
and then stopped responding to adb altogether.
|
||||||
|
- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test`
|
||||||
|
— rebooted an Adreno 830 phone after 862 cases, on DirectGLES.
|
||||||
|
- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test`
|
||||||
|
— same, on both backends (found and quarantined automatically by the runner).
|
||||||
|
- `KHR-GL33.texture_repeat_mode.rgb565_11x131_0_clamp_to_edge` — on DirectVulkan.
|
||||||
|
|
||||||
|
The whole `framebuffer_blit.multisampled_to_singlesampled_*` family is suspect;
|
||||||
|
treat a new variant as a device-hang candidate rather than a normal failure.
|
||||||
|
|
||||||
|
When a run dies, pull `/data/local/tmp/mgcts/chunk.qpa` — it survives the reboot,
|
||||||
|
and the last `#beginTestCaseResult` with no matching `#endTestCaseResult` names
|
||||||
|
the case that did it.
|
||||||
|
|
||||||
|
## Reference results
|
||||||
|
|
||||||
|
`opengl-cts-4.6.8.1`, KHR-GL33 mustpass (`gl33-main.txt`, 9886 cases), Adreno 830
|
||||||
|
/ Android 15, MobileGL `dev`@199164c2, 9884 measured / 0 unrun / 2 quarantined.
|
||||||
|
Conformance rate = Pass + NotSupported, as Khronos scores a submission.
|
||||||
|
|
||||||
|
| backend | conformance | strict Pass | Fail | Crash | InternalError | DeviceHang |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| DirectGLES | **93.05%** | 85.94% | 679 | 1 | 6 | 1 |
|
||||||
|
| DirectVulkan (texture FBO) | **72.74%** | 65.71% | 2394 | 294 | 5 | 1 |
|
||||||
|
| DirectVulkan (stock renderbuffer FBO) | 46.15% | 39.11% | 5024 | 292 | 5 | 2 |
|
||||||
|
|
||||||
|
The third row is what stock dEQP reports; the gap to the second row is entirely
|
||||||
|
the renderbuffer-FBO readback defect.
|
||||||
|
|
||||||
|
## MobileGL constraints the port works around
|
||||||
|
|
||||||
|
- **DirectVulkan cannot use an EGL pbuffer.** That path needs
|
||||||
|
`VK_EXT_headless_surface`, which Adreno's Android driver does not expose; it
|
||||||
|
fails inside `eglMakeCurrent`. The platform therefore gets a real
|
||||||
|
`ANativeWindow` from **`AImageReader`** — an ordinary BufferQueue producer that
|
||||||
|
`vkCreateAndroidSurfaceKHR` accepts, with no Activity. An `onImageAvailable`
|
||||||
|
listener must drain the queue or the producer blocks once `maxImages` buffers
|
||||||
|
are in flight and the next swap deadlocks.
|
||||||
|
- **`eglMakeCurrent` requires draw == read** and rejects `EGL_NO_SURFACE` with
|
||||||
|
`EGL_BAD_MATCH`, so dEQP's `surfaceless` platform cannot be used at all, and
|
||||||
|
`--deqp-surface-type=fbo` (which asks the platform for `SURFACETYPE_DONT_CARE`)
|
||||||
|
must still be given a real surface.
|
||||||
|
- **Every EGL call must go through the dynamically loaded library.** dEQP's
|
||||||
|
`surfaceless` platform mixes wrapper calls with globally linked `egl*` symbols;
|
||||||
|
copying that on Android silently reaches the system EGL and invalidates the
|
||||||
|
measurement. The `mobilegl` target links no `libEGL`/`libGLESv*` at all.
|
||||||
|
- **Desktop-GL configs need `EGL_OPENGL_BIT`.** The surfaceless port always asks
|
||||||
|
for an ES bit, which can never satisfy a GL 3.3 core context.
|
||||||
|
- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on
|
||||||
|
a destroyed mutex`) *after* the work is done; flush and `_exit()` in any small
|
||||||
|
tool, or its exit code and output are lost.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL
|
||||||
|
targets/mobilegl.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl)
|
||||||
|
targets/ndk-modern.cmake NDK toolchain hook (sets DE_OS/DE_CPU)
|
||||||
|
probe/mgprobe.c preflight gate
|
||||||
|
scripts/sync_to_cts.py inject the port into a CTS checkout
|
||||||
|
scripts/run_cts.py crash- and reboot-resuming runner
|
||||||
|
scripts/qpa_report.py .qpa -> conformance rate
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "OpenGL CTS on MobileGL (Android)"
|
||||||
|
short_description: "Build and run VK-GL-CTS KHR-GL33 against MobileGL and report per-backend conformance"
|
||||||
|
default_prompt: "Use $gl-cts-on-mobilegl to run the OpenGL 3.3 core CTS against MobileGL on my Android device and report the conformance rate for DirectGLES and DirectVulkan."
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#-------------------------------------------------------------------------
|
||||||
|
# VK-GL-CTS target: MobileGL on Android
|
||||||
|
#
|
||||||
|
# Builds a standalone arm64 ELF that reaches OpenGL exclusively through
|
||||||
|
# libMobileGL.so, loaded at runtime. Nothing here links libEGL or libGLESv*:
|
||||||
|
# the whole point is that the system GL stack must not be reachable, so that a
|
||||||
|
# conformance result is unambiguously MobileGL's.
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
message("*** Using MobileGL target")
|
||||||
|
|
||||||
|
set(DEQP_TARGET_NAME "MobileGL")
|
||||||
|
|
||||||
|
# Build the modules as standalone executables instead of the libdeqp.so an APK
|
||||||
|
# would load. The suite runs from adb shell, with no Activity.
|
||||||
|
set(DEQP_ANDROID_EXE ON)
|
||||||
|
|
||||||
|
# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support
|
||||||
|
# flag is on but no import library is supplied.
|
||||||
|
set(DEQP_SUPPORT_EGL ON)
|
||||||
|
set(DEQP_EGL_LIBRARIES)
|
||||||
|
set(DEQP_GLES2_LIBRARIES)
|
||||||
|
set(DEQP_GLES3_LIBRARIES)
|
||||||
|
|
||||||
|
set(TCUTIL_PLATFORM_SRCS
|
||||||
|
mobilegl/tcuMobileGLPlatform.cpp
|
||||||
|
mobilegl/tcuMobileGLPlatform.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(LOG_LIBRARY NAMES log)
|
||||||
|
find_library(ANDROID_LIBRARY NAMES android)
|
||||||
|
find_library(MEDIANDK_LIBRARY NAMES mediandk)
|
||||||
|
|
||||||
|
# libmediandk supplies AImageReader, which is how a process with no Activity
|
||||||
|
# gets a real ANativeWindow.
|
||||||
|
list(APPEND TCUTIL_PLATFORM_LIBS ${ANDROID_LIBRARY} ${MEDIANDK_LIBRARY} ${LOG_LIBRARY})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#-------------------------------------------------------------------------
|
||||||
|
# drawElements CMake utilities
|
||||||
|
# ----------------------------
|
||||||
|
#
|
||||||
|
# Copyright 2016 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Delegate most things to the NDK's cmake toolchain script
|
||||||
|
|
||||||
|
if (NOT DEFINED ANDROID_NDK_PATH)
|
||||||
|
message(FATAL_ERROR "Please provide ANDROID_NDK_PATH")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(ANDROID_PLATFORM "android-${DE_ANDROID_API}")
|
||||||
|
set(ANDROID_STL c++_static)
|
||||||
|
set(ANDROID_CPP_FEATURES "rtti exceptions")
|
||||||
|
|
||||||
|
include(${ANDROID_NDK_PATH}/build/cmake/android.toolchain.cmake)
|
||||||
|
|
||||||
|
# The try_compile() used to verify the C/C++ compilers are sane tries to
|
||||||
|
# generate an executable, but doesn't seem to use the right compiler/linker
|
||||||
|
# options when cross-compiling, so it fails even when building an actual
|
||||||
|
# shared library or executable succeeds.
|
||||||
|
#
|
||||||
|
# I don't know why this doesn't affect simpler projects that use the NDK
|
||||||
|
# toolchain.
|
||||||
|
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
|
||||||
|
|
||||||
|
# Set variables used by other parts of dEQP's build scripts
|
||||||
|
|
||||||
|
set(DE_OS "DE_OS_ANDROID")
|
||||||
|
|
||||||
|
if (NOT DEFINED DE_COMPILER)
|
||||||
|
set(DE_COMPILER "DE_COMPILER_CLANG")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (ANDROID_ABI STREQUAL "x86")
|
||||||
|
set(DE_CPU "DE_CPU_X86")
|
||||||
|
elseif (ANDROID_ABI STREQUAL "armeabi" OR
|
||||||
|
ANDROID_ABI STREQUAL "armeabi-v7a")
|
||||||
|
set(DE_CPU "DE_CPU_ARM")
|
||||||
|
elseif (ANDROID_ABI STREQUAL "arm64-v8a")
|
||||||
|
set(DE_CPU "DE_CPU_ARM_64")
|
||||||
|
elseif (ANDROID_ABI STREQUAL "x86_64")
|
||||||
|
set(DE_CPU "DE_CPU_X86_64")
|
||||||
|
else ()
|
||||||
|
message(FATAL_ERROR "Unknown ABI \"${ANDROID_ABI}\"")
|
||||||
|
endif ()
|
||||||
@@ -93,6 +93,15 @@ The bundled fixtures cover:
|
|||||||
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||||
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
|
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
|
||||||

|

|
||||||
|
- minecraft-1.21.4-fabric-iris-iterationrp-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||||
|
iterationRP after entering a singleplayer world, framing the iterationRP name overlay over a lake with far-shore
|
||||||
|
tree reflections. iterationRP's temporal auto-exposure makes a single-frame trim overexpose and drop the overlay,
|
||||||
|
so the fixture is a prefix trace (all calls up to the target frame) that replays the temporal state. The pack also
|
||||||
|
gates an NVIDIA-only shadow path (`subgroupPartitionNV`, `GL_NV_shader_subgroup_partitioned`) on the GL vendor
|
||||||
|
string, so the capture reports a masked vendor and the trace carries the portable `subgroupShuffleXor` path that
|
||||||
|
non-NVIDIA GPUs take.
|
||||||
|
The trace archive and golden are not committed yet (the repository's Git LFS quota rejects new objects with
|
||||||
|
`GH009`); the case stays registered and its fixture files are hydrated from the trace fixture mirror.
|
||||||
- minecraft-1.21.4-fabric-iris-photon-v1.1-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
- minecraft-1.21.4-fabric-iris-photon-v1.1-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||||
Photon v1.1 after entering a singleplayer world.
|
Photon v1.1 after entering a singleplayer world.
|
||||||

|

|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Capture RenderDoc Trace Frame"
|
|
||||||
short_description: "Capture exact Android Vulkan/GLES trace frames"
|
|
||||||
default_prompt: "Use $renderdoc-capture-trace-frame to capture and validate an exact Android retrace frame."
|
|
||||||
@@ -18,17 +18,16 @@ SUMMARY_HTML = "mobilegl-android-retrace-overview.html"
|
|||||||
DEFAULT_ANGLE_VARIANT = "ec889e6ea831"
|
DEFAULT_ANGLE_VARIANT = "ec889e6ea831"
|
||||||
BLISS_ANGLE_VARIANT = "90a62123d794"
|
BLISS_ANGLE_VARIANT = "90a62123d794"
|
||||||
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
|
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
|
||||||
|
TRACE_APK_DIR = ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "trace" / "debug"
|
||||||
|
|
||||||
BACKENDS = {
|
BACKENDS = {
|
||||||
"DirectGLES": {
|
"DirectGLES": {
|
||||||
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk",
|
"package": "top.mobilegl.plugin.trace",
|
||||||
"package": "top.mobilegl.plugin.espryt.trace",
|
"use_angle": False,
|
||||||
"use_angle": True,
|
|
||||||
"use_pbuffer": False,
|
"use_pbuffer": False,
|
||||||
},
|
},
|
||||||
"DirectVulkan": {
|
"DirectVulkan": {
|
||||||
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "magmaTrace" / "debug" / "MobileGL-MagmaTrace-debug.apk",
|
"package": "top.mobilegl.plugin.trace",
|
||||||
"package": "top.mobilegl.plugin.magma.trace",
|
|
||||||
"use_angle": False,
|
"use_angle": False,
|
||||||
"use_pbuffer": False,
|
"use_pbuffer": False,
|
||||||
},
|
},
|
||||||
@@ -45,6 +44,11 @@ def is_lfs_pointer(path):
|
|||||||
return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1")
|
return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1")
|
||||||
|
|
||||||
|
|
||||||
|
def find_trace_apk():
|
||||||
|
candidates = list(TRACE_APK_DIR.glob("MobileGL-plugin-trace-release-*.apk"))
|
||||||
|
return max(candidates, key=lambda path: path.stat().st_mtime) if candidates else None
|
||||||
|
|
||||||
|
|
||||||
def bash_path(path):
|
def bash_path(path):
|
||||||
path = Path(path).resolve()
|
path = Path(path).resolve()
|
||||||
drive = path.drive.rstrip(":").lower()
|
drive = path.drive.rstrip(":").lower()
|
||||||
@@ -117,9 +121,13 @@ def render_summary():
|
|||||||
|
|
||||||
def run_case(case, backend):
|
def run_case(case, backend):
|
||||||
backend_info = BACKENDS[backend]
|
backend_info = BACKENDS[backend]
|
||||||
|
apk = find_trace_apk()
|
||||||
trace_archive = FIXTURES / case["trace_archive"]
|
trace_archive = FIXTURES / case["trace_archive"]
|
||||||
golden = FIXTURES / case["golden"]
|
golden = FIXTURES / case["golden"]
|
||||||
alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None
|
alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None
|
||||||
|
if apk is None:
|
||||||
|
mark_skipped(case, backend, f"SKIPPED_MISSING_APK: no trace APK found under {TRACE_APK_DIR}")
|
||||||
|
return 2
|
||||||
if not trace_archive.exists() or is_lfs_pointer(trace_archive):
|
if not trace_archive.exists() or is_lfs_pointer(trace_archive):
|
||||||
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
|
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
|
||||||
copy_goldens(case, backend)
|
copy_goldens(case, backend)
|
||||||
@@ -134,7 +142,7 @@ def run_case(case, backend):
|
|||||||
"C:/Program Files/Git/bin/bash.exe",
|
"C:/Program Files/Git/bin/bash.exe",
|
||||||
"android-plugin/trace-replay-ci.sh",
|
"android-plugin/trace-replay-ci.sh",
|
||||||
"--apk-file",
|
"--apk-file",
|
||||||
bash_path(backend_info["apk"]),
|
bash_path(apk),
|
||||||
"--package",
|
"--package",
|
||||||
backend_info["package"],
|
backend_info["package"],
|
||||||
"--backend",
|
"--backend",
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ RESULT_ROOT = ROOT / ".trace-work" / "macos-window-retrace-result"
|
|||||||
WORK_ROOT = ROOT / ".trace-work" / "macos-window-retrace-work"
|
WORK_ROOT = ROOT / ".trace-work" / "macos-window-retrace-work"
|
||||||
SUMMARY_DIR = ROOT / ".trace-work" / "macos-window-retrace-summary"
|
SUMMARY_DIR = ROOT / ".trace-work" / "macos-window-retrace-summary"
|
||||||
SUMMARY_HTML = "mobilegl-macos-window-vulkan-retrace-overview.html"
|
SUMMARY_HTML = "mobilegl-macos-window-vulkan-retrace-overview.html"
|
||||||
DEFAULT_MIRROR_BASE = "https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
|
# Fixture mirrors, tried in order before Git LFS.
|
||||||
|
DEFAULT_MIRROR_BASES = [
|
||||||
|
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures",
|
||||||
|
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures",
|
||||||
|
]
|
||||||
|
DEFAULT_MIRROR_BASE = DEFAULT_MIRROR_BASES[0]
|
||||||
BACKENDS = ("DirectVulkan",)
|
BACKENDS = ("DirectVulkan",)
|
||||||
CASES = load_trace_cases()
|
CASES = load_trace_cases()
|
||||||
|
|
||||||
@@ -100,34 +105,35 @@ def default_vulkan_icd(mobilegl_library):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def download_fixture(path, mirror_base):
|
def download_fixture(path, mirror_bases):
|
||||||
|
"""Try each mirror in order; return on the first that serves a good file."""
|
||||||
|
if isinstance(mirror_bases, str):
|
||||||
|
mirror_bases = [mirror_bases]
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
url = f"{mirror_base.rstrip('/')}/{path.name}"
|
|
||||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
command = [
|
token = os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN", "")
|
||||||
"curl",
|
errors = []
|
||||||
"-L",
|
for mirror_base in mirror_bases:
|
||||||
"--fail",
|
url = f"{mirror_base.rstrip('/')}/{path.name}"
|
||||||
"--retry",
|
command = ["curl", "-L", "--fail", "--retry", "3", "--retry-delay", "2",
|
||||||
"3",
|
"--continue-at", "-"]
|
||||||
"--retry-delay",
|
if token:
|
||||||
"2",
|
command += ["--header", f"Authorization: token {token}"]
|
||||||
"--continue-at",
|
command += ["-o", str(tmp), url]
|
||||||
"-",
|
result = subprocess.run(command, text=True, capture_output=True)
|
||||||
"-o",
|
if result.returncode != 0:
|
||||||
str(tmp),
|
errors.append(f"{mirror_base}: {result.stderr.strip() or result.stdout.strip()}")
|
||||||
url,
|
tmp.unlink(missing_ok=True)
|
||||||
]
|
continue
|
||||||
result = subprocess.run(command, text=True, capture_output=True)
|
tmp.replace(path)
|
||||||
if result.returncode != 0:
|
if is_bad_fixture(path):
|
||||||
return path, False, result.stderr.strip() or result.stdout.strip()
|
errors.append(f"{mirror_base}: downloaded fixture is empty or still an LFS pointer")
|
||||||
tmp.replace(path)
|
continue
|
||||||
if is_bad_fixture(path):
|
return path, True, ""
|
||||||
return path, False, "downloaded fixture is empty or still an LFS pointer"
|
return path, False, "; ".join(errors)
|
||||||
return path, True, ""
|
|
||||||
|
|
||||||
|
|
||||||
def hydrate_fixtures(cases, fetch, mirror_base, download_jobs):
|
def hydrate_fixtures(cases, fetch, mirror_bases, download_jobs):
|
||||||
required = []
|
required = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for case in cases:
|
for case in cases:
|
||||||
@@ -149,7 +155,7 @@ def hydrate_fixtures(cases, fetch, mirror_base, download_jobs):
|
|||||||
print(f"fixtures: downloading {len(required)} file(s) with {download_jobs} worker(s)", flush=True)
|
print(f"fixtures: downloading {len(required)} file(s) with {download_jobs} worker(s)", flush=True)
|
||||||
failures = []
|
failures = []
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=download_jobs) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=download_jobs) as executor:
|
||||||
futures = [executor.submit(download_fixture, path, mirror_base) for path in required]
|
futures = [executor.submit(download_fixture, path, mirror_bases) for path in required]
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
path, ok, message = future.result()
|
path, ok, message = future.result()
|
||||||
if ok:
|
if ok:
|
||||||
@@ -434,7 +440,9 @@ def parse_args():
|
|||||||
parser.add_argument("--keep-results", action="store_true", help="Do not clear previous result/work roots.")
|
parser.add_argument("--keep-results", action="store_true", help="Do not clear previous result/work roots.")
|
||||||
parser.add_argument("--no-render", action="store_true", help="Do not render the HTML summary.")
|
parser.add_argument("--no-render", action="store_true", help="Do not render the HTML summary.")
|
||||||
parser.add_argument("--fetch-fixtures", action=argparse.BooleanOptionalAction, default=True, help="Hydrate missing fixtures before running.")
|
parser.add_argument("--fetch-fixtures", action=argparse.BooleanOptionalAction, default=True, help="Hydrate missing fixtures before running.")
|
||||||
parser.add_argument("--fixture-mirror-base", default=os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_BASE", DEFAULT_MIRROR_BASE))
|
parser.add_argument("--fixture-mirror-base", action="append", dest="fixture_mirror_bases",
|
||||||
|
help="Fixture mirror base URL; repeatable, tried in order. "
|
||||||
|
"Defaults to the hit.moe mirror then the miawa mirror.")
|
||||||
parser.add_argument("--download-jobs", type=int, default=4, help="Parallel fixture download count.")
|
parser.add_argument("--download-jobs", type=int, default=4, help="Parallel fixture download count.")
|
||||||
parser.add_argument("--continue-after-fatal", action="store_true", help="Continue launching later cases after a fatal native-window replay.")
|
parser.add_argument("--continue-after-fatal", action="store_true", help="Continue launching later cases after a fatal native-window replay.")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
@@ -463,7 +471,10 @@ def main():
|
|||||||
print(f"missing MobileGL library: {mobilegl_library}", file=sys.stderr)
|
print(f"missing MobileGL library: {mobilegl_library}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
if not hydrate_fixtures(selected_cases, args.fetch_fixtures, args.fixture_mirror_base, max(1, args.download_jobs)):
|
mirror_bases = args.fixture_mirror_bases or [
|
||||||
|
b for b in os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_BASES", "").split() or []
|
||||||
|
] or ([os.environ["MOBILEGL_TRACE_FIXTURE_MIRROR_BASE"]] if os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_BASE") else DEFAULT_MIRROR_BASES)
|
||||||
|
if not hydrate_fixtures(selected_cases, args.fetch_fixtures, mirror_bases, max(1, args.download_jobs)):
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
if not args.keep_results:
|
if not args.keep_results:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# MobileGL trace-replay skills
|
||||||
|
|
||||||
|
Task-focused skills for capturing, replaying, debugging, and authoring MobileGL
|
||||||
|
apitrace fixtures. Each skill is a self-contained package:
|
||||||
|
|
||||||
|
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
|
||||||
|
directory name equals the frontmatter `name`.
|
||||||
|
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
|
||||||
|
`short_description`, `default_prompt`).
|
||||||
|
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
|
||||||
|
skill has them.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
| Skill | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| [trace-fixture-authoring-on-android-fcl](trace-fixture-authoring-on-android-fcl/SKILL.md) | Capture an on-device Android apitrace from FCL's MobileGL renderers (DirectGLES / Magma / SimpleFPEWrapper), mark the defect frame, and pull `full.trace`. |
|
||||||
|
| [renderdoc-debug-on-trace-replay](renderdoc-debug-on-trace-replay/SKILL.md) | Capture and validate an exact frame from a MobileGL retrace on a connected Android device with RenderDoc / rdc-cli. |
|
||||||
|
| [mismatch-retrace-debugging](mismatch-retrace-debugging/SKILL.md) | Localize the first divergent render pass and draw call when a fixture replays correctly in a golden environment but renders differently on a target backend. |
|
||||||
|
| [trace-fixture-authoring](trace-fixture-authoring/SKILL.md) | Author a deterministic trace-replay fixture — trim, golden, package under the size budget, register in `trace_cases.json`, and validate on Linux and Android. |
|
||||||
+5
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: mismatch-retrace-debugging
|
||||||
|
description: Localize the first divergent render pass and draw call when a MobileGL apitrace fixture replays correctly in a golden environment but renders differently under mobilegl_trace_replay, Android trace replay, or another backend. Use to binary-search pass/draw endpoints, diff GL state around the first bad call, and classify the fault as a vertex/VS, fragment/FS, or framebuffer/composition mismatch.
|
||||||
|
---
|
||||||
|
|
||||||
# Mismatch retrace debugging
|
# Mismatch retrace debugging
|
||||||
|
|
||||||
Use this when an apitrace fixture replays correctly on one environment but
|
Use this when an apitrace fixture replays correctly on one environment but
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "MobileGL Mismatch Retrace Debugging"
|
||||||
|
short_description: "Localize the first divergent draw in a mismatching MobileGL retrace"
|
||||||
|
default_prompt: "Use $mismatch-retrace-debugging to find the first divergent render pass and draw call in a MobileGL retrace mismatch."
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: renderdoc-capture-trace-frame
|
name: renderdoc-debug-on-trace-replay
|
||||||
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
|
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,20 +19,20 @@ adb -s SERIAL shell pm path top.mobilegl.plugin.trace
|
|||||||
rdc doctor
|
rdc doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/capture_android_retrace.py`.
|
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py`.
|
||||||
|
|
||||||
## Capture
|
## Capture
|
||||||
|
|
||||||
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
|
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
|
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Change only the backend and output for GLES:
|
Change only the backend and output for GLES:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
|
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
|
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "RenderDoc Debug on Trace Replay"
|
||||||
|
short_description: "Capture and debug an exact MobileGL trace-replay frame in RenderDoc"
|
||||||
|
default_prompt: "Use $renderdoc-debug-on-trace-replay to capture and validate an exact Android trace-replay frame in RenderDoc."
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## TargetControl timing
|
## TargetControl timing
|
||||||
|
|
||||||
- Start `tools/trace_replay/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
|
- Start `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
|
||||||
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
|
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
|
||||||
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
|
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
|
||||||
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
|
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user