mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
2
Commits
56124d931c
..
itrp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44cf9ab26a | ||
|
|
77366d51ed |
@@ -9,23 +9,7 @@ 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}"
|
||||||
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
|
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
|
||||||
# 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}"
|
||||||
|
|
||||||
@@ -46,8 +30,7 @@ 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}")"
|
||||||
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
|
mapfile -t files <<< "${fixture_list}"
|
||||||
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
|
||||||
@@ -123,7 +106,6 @@ 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}"
|
||||||
@@ -154,11 +136,7 @@ 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
|
||||||
|
|
||||||
curl_auth=()
|
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
|
||||||
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
|
||||||
@@ -206,19 +184,10 @@ 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}")"
|
||||||
for base in "${mirror_bases[@]}"; do
|
url="${mirror_base%/}/${name}"
|
||||||
url="${base%/}/${name}"
|
|
||||||
echo "Fetching trace fixture from mirror: ${url}"
|
echo "Fetching trace fixture from mirror: ${url}"
|
||||||
if fetch_file_from_mirror "${file}" "${url}"; then
|
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
|
||||||
@@ -227,7 +196,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 "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
|
echo "Mirror fetch 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,15 +455,6 @@ 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
|
||||||
|
|||||||
@@ -20,15 +20,6 @@ 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"
|
||||||
@@ -76,21 +67,6 @@ 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,17 +86,6 @@ 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()) {
|
||||||
@@ -134,10 +123,6 @@ 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,21 +230,6 @@ 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,
|
||||||
@@ -306,7 +291,6 @@ 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_clear_texture, E_GL_ARB_direct_state_access,
|
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,
|
||||||
@@ -1035,32 +1035,6 @@ 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 {
|
||||||
|
|||||||
@@ -794,32 +794,5 @@ 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,7 +8,6 @@
|
|||||||
|
|
||||||
#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) {
|
||||||
@@ -109,81 +108,6 @@ 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 {
|
|
||||||
// Order-independent accumulation blending: the write order of overlapping fragments
|
|
||||||
// does not change the result, which is what lets multi-pass chains re-rasterize the
|
|
||||||
// same geometry and combine per-pass contributions (MC 26.3 OIT: GL_MAX depth
|
|
||||||
// bounds, additive ONE+ONE transmittance/accumulate). Sorted-transparency "over"
|
|
||||||
// compositing (SRC_ALPHA-style factors) is order-dependent, drawn once per surface,
|
|
||||||
// and relies on its depth writes for occlusion - it must not be treated as hazardous.
|
|
||||||
// MIN/MAX ignore blend factors entirely per the Vulkan spec.
|
|
||||||
//
|
|
||||||
// Deliberately color-channel only. A separate-alpha accumulation
|
|
||||||
// (glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX)) whose color channel is an ordinary
|
|
||||||
// over-blend is not treated as hazardous: no known content pairs that shape with a
|
|
||||||
// depth-equality chain, and widening the test would re-capture sorted transparency.
|
|
||||||
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
|
|
||||||
if (attachment.colorBlendOp == VK_BLEND_OP_MIN || attachment.colorBlendOp == VK_BLEND_OP_MAX) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return attachment.colorBlendOp == VK_BLEND_OP_ADD &&
|
|
||||||
attachment.srcColorBlendFactor == VK_BLEND_FACTOR_ONE &&
|
|
||||||
attachment.dstColorBlendFactor == VK_BLEND_FACTOR_ONE;
|
|
||||||
}
|
|
||||||
} // 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) {
|
||||||
@@ -228,8 +152,6 @@ 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,
|
||||||
@@ -336,17 +258,6 @@ 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,9 +49,6 @@ 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;
|
||||||
@@ -65,27 +62,6 @@ 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 order-independent accumulation blends (MIN/MAX, additive ONE+ONE) are
|
|
||||||
// stripped - that is the signature of such equality chains (MC 26.3 OIT) - while
|
|
||||||
// sorted-transparency "over" compositing (e.g. vanilla MC water, SRC_ALPHA factors),
|
|
||||||
// which draws each surface once and depends on its depth writes to occlude later
|
|
||||||
// passes, keeps them. 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;
|
||||||
|
|
||||||
@@ -94,6 +70,5 @@ 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
|
||||||
|
|||||||
@@ -1218,22 +1218,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
} // 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case ShaderStage::Vertex:
|
case ShaderStage::Vertex:
|
||||||
@@ -1528,7 +1512,6 @@ 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) {
|
||||||
@@ -1550,8 +1533,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
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,
|
||||||
@@ -1827,7 +1808,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -1882,22 +1862,6 @@ 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 {
|
|
||||||
MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; "
|
|
||||||
"keeping the original module",
|
|
||||||
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
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
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{};
|
||||||
@@ -78,10 +75,6 @@ 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;
|
||||||
|
|
||||||
@@ -106,7 +99,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::move(other.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;
|
||||||
@@ -115,18 +107,15 @@ 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) {
|
||||||
@@ -150,7 +139,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::move(other.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;
|
||||||
@@ -159,18 +147,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +203,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
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);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
|
|||||||
@@ -298,23 +298,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// descriptor valid.
|
// descriptor valid.
|
||||||
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
|
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
|
||||||
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
numericDomain == SamplerNumericDomain::UnsignedInteger;
|
||||||
SamplerResolveMemo* viewFormatMemo =
|
const VkFormat sampledViewFormat =
|
||||||
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);
|
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) {
|
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||||
"textureId=%d imageFormat=%d numericDomain=%d",
|
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||||
@@ -322,12 +307,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||||
return false;
|
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 =
|
const VkImageView sampledImageView =
|
||||||
sampledViewFormat == resource->format
|
m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||||
? resource->sampledView
|
|
||||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
|
||||||
if (sampledImageView == VK_NULL_HANDLE) {
|
if (sampledImageView == VK_NULL_HANDLE) {
|
||||||
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||||
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||||
|
|||||||
@@ -176,13 +176,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint16 textureParamsVersion = 0;
|
Uint16 textureParamsVersion = 0;
|
||||||
Bool forceNearestFiltering = false;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -126,13 +126,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||||
attr.Type == DataType::Uint2101010Rev;
|
attr.Type == DataType::Uint2101010Rev;
|
||||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
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 &&
|
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
|
||||||
((sourceStride % requiredAlignment) != 0 ||
|
((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) {
|
||||||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
|
|
||||||
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
|
// 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
|
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||||
// attribute into a tightly packed transient stream without changing its format.
|
// attribute into a tightly packed transient stream without changing its format.
|
||||||
|
|||||||
@@ -1140,24 +1140,6 @@ 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,
|
||||||
@@ -1341,8 +1323,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||||
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
if (supportsStorageImage && IsMutableStorageImageFormat(format)) {
|
||||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
|
||||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,27 +1391,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.samples = resolvedSampleCount;
|
imageInfo.samples = resolvedSampleCount;
|
||||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
VkImageFormatProperties imageFormatProperties{};
|
VkImageFormatProperties imageFormatProperties{};
|
||||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
const 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 ||
|
||||||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||||
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||||
@@ -1958,15 +1921,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
|
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
|
||||||
SamplerNumericDomain numericDomain) {
|
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 ||
|
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
|
||||||
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
|
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
|
||||||
return imageFormat;
|
return imageFormat;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
#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;
|
||||||
@@ -285,11 +284,6 @@ 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 VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||||
@@ -385,9 +379,6 @@ 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;
|
||||||
|
|||||||
@@ -27,9 +27,6 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vulkan/utility/vk_format_utils.h>
|
#include <vulkan/utility/vk_format_utils.h>
|
||||||
#include <vulkan/vulkan_core.h>
|
#include <vulkan/vulkan_core.h>
|
||||||
#ifdef __ANDROID__
|
|
||||||
#include <sys/system_properties.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
@@ -1034,7 +1031,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
|
||||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||||
Int maxDimension = std::max<Int>(
|
Int maxDimension = std::max<Int>(
|
||||||
baseTexelSize.x(),
|
baseTexelSize.x(),
|
||||||
@@ -1622,63 +1618,6 @@ void main() {
|
|||||||
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
||||||
Memcpy(rgba, source, sizeof(Float) * 4);
|
Memcpy(rgba, source, sizeof(Float) * 4);
|
||||||
return true;
|
return true;
|
||||||
// Single- and dual-channel formats the reinterpretation feature makes common
|
|
||||||
// as readback sources (iterationRP custom images are R32F/R32UI-class).
|
|
||||||
// Missing channels take GL's defaults: 0 for GB, 1 for alpha.
|
|
||||||
case VK_FORMAT_R32_SFLOAT: {
|
|
||||||
Float value = 0.0f;
|
|
||||||
Memcpy(&value, source, sizeof(value));
|
|
||||||
rgba[0] = value;
|
|
||||||
rgba[1] = 0.0f;
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_R32G32_SFLOAT: {
|
|
||||||
Float values[2] = {0.0f, 0.0f};
|
|
||||||
Memcpy(values, source, sizeof(values));
|
|
||||||
rgba[0] = values[0];
|
|
||||||
rgba[1] = values[1];
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_R32_UINT: {
|
|
||||||
Uint32 value = 0;
|
|
||||||
Memcpy(&value, source, sizeof(value));
|
|
||||||
rgba[0] = static_cast<Float>(value);
|
|
||||||
rgba[1] = 0.0f;
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_R32_SINT: {
|
|
||||||
Int32 value = 0;
|
|
||||||
Memcpy(&value, source, sizeof(value));
|
|
||||||
rgba[0] = static_cast<Float>(value);
|
|
||||||
rgba[1] = 0.0f;
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_R16_SFLOAT: {
|
|
||||||
Uint16 value = 0;
|
|
||||||
Memcpy(&value, source, sizeof(value));
|
|
||||||
rgba[0] = MG_Util::DecodeHalfBitsToFloat(value);
|
|
||||||
rgba[1] = 0.0f;
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_R16G16_SFLOAT:
|
|
||||||
for (SizeT component = 0; component < 2; ++component) {
|
|
||||||
Uint16 value = 0;
|
|
||||||
Memcpy(&value, source + component * sizeof(value), sizeof(value));
|
|
||||||
rgba[component] = MG_Util::DecodeHalfBitsToFloat(value);
|
|
||||||
}
|
|
||||||
rgba[2] = 0.0f;
|
|
||||||
rgba[3] = 1.0f;
|
|
||||||
return true;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2107,26 +2046,6 @@ void main() {
|
|||||||
|
|
||||||
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
|
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
|
||||||
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
|
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
|
||||||
{
|
|
||||||
// Qualcomm's pipeline compiler does not keep vertex positions invariant across
|
|
||||||
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
|
|
||||||
// Invariant decoration), so a blended depth-writing prepass makes later
|
|
||||||
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
|
|
||||||
// clouds flicker black). Suppress depth writes on accumulation-blended pipelines
|
|
||||||
// there (see PipelineFactory::ShouldSuppressDepthWrite for the exact scope);
|
|
||||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any
|
|
||||||
// driver.
|
|
||||||
const MG_Config::QuirkOverride quirkOverride =
|
|
||||||
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
|
|
||||||
const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(
|
|
||||||
quirkOverride, m_physicalDevice.properties.vendorID);
|
|
||||||
if (suppressBlendedDepthWrite) {
|
|
||||||
MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines "
|
|
||||||
"(driver lacks cross-pipeline position invariance)%s",
|
|
||||||
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
|
||||||
}
|
|
||||||
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
|
||||||
}
|
|
||||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||||
m_shaderDrawParametersFeatureEnabled,
|
m_shaderDrawParametersFeatureEnabled,
|
||||||
m_unformattedFloatStorageImagesEnabled);
|
m_unformattedFloatStorageImagesEnabled);
|
||||||
@@ -2281,94 +2200,10 @@ void main() {
|
|||||||
MGLOG_I("VulkanRenderer shut down completed");
|
MGLOG_I("VulkanRenderer shut down completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scans the draw's index range from host-visible index bytes and returns the largest
|
|
||||||
// fetchable vertex index. Usable only when the draw's range is exactly its
|
|
||||||
// IndexBufferView (drawParams.indexRangeIsExactView). The view's byte offset is either
|
|
||||||
// an offset into the bound element-array buffer or, with no bound buffer, a raw client
|
|
||||||
// pointer. Primitive-restart sentinels are skipped so they cannot inflate the bound.
|
|
||||||
static Bool TryComputeMaxIndexFromHostBytes(const MG_State::GLState::VertexArrayObject& vao,
|
|
||||||
const IndexBufferView& indexView, Uint32& outMaxIndex) {
|
|
||||||
SizeT indexSize = 0;
|
|
||||||
switch (indexView.indexType) {
|
|
||||||
case GL_UNSIGNED_BYTE: indexSize = 1; break;
|
|
||||||
case GL_UNSIGNED_SHORT: indexSize = 2; break;
|
|
||||||
case GL_UNSIGNED_INT: indexSize = 4; break;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Uint8* indexBytes = nullptr;
|
|
||||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
|
||||||
if (indexBufferShared != nullptr) {
|
|
||||||
const SizeT bufferSize = indexBufferShared->GetSize();
|
|
||||||
if (indexBufferShared->MappedData() == nullptr || indexView.indexByteOffset > bufferSize ||
|
|
||||||
indexView.indexByteSize > bufferSize - indexView.indexByteOffset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
indexBufferShared->SyncPersistentMappedRange();
|
|
||||||
indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset;
|
|
||||||
} else {
|
|
||||||
indexBytes = reinterpret_cast<const Uint8*>(indexView.indexByteOffset);
|
|
||||||
if (indexBytes == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SizeT indexCount = indexView.indexByteSize / indexSize;
|
|
||||||
// The all-ones sentinel is only a restart marker when primitive restart is enabled;
|
|
||||||
// with restart off it is a legitimate index and excluding it would truncate the
|
|
||||||
// converted stream by exactly that vertex.
|
|
||||||
const Bool primitiveRestartActive =
|
|
||||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
|
||||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
|
||||||
const Uint32 restartSentinel = indexSize == 1 ? 0xFFu : indexSize == 2 ? 0xFFFFu : 0xFFFFFFFFu;
|
|
||||||
Uint32 maxIndex = 0;
|
|
||||||
Bool sawIndex = false;
|
|
||||||
for (SizeT i = 0; i < indexCount; ++i) {
|
|
||||||
Uint32 index = 0;
|
|
||||||
switch (indexSize) {
|
|
||||||
case 1: index = indexBytes[i]; break;
|
|
||||||
case 2: index = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
|
|
||||||
default: index = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
|
|
||||||
}
|
|
||||||
if (primitiveRestartActive && index == restartSentinel) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
maxIndex = std::max(maxIndex, index);
|
|
||||||
sawIndex = true;
|
|
||||||
}
|
|
||||||
if (!sawIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
outMaxIndex = maxIndex;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool VulkanRenderer::UploadAndBindVertexBuffers(
|
Bool VulkanRenderer::UploadAndBindVertexBuffers(
|
||||||
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams,
|
const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView) {
|
Bool indexedDraw) {
|
||||||
const Bool indexedDraw = pIndexBufferView != nullptr;
|
|
||||||
// Exclusive upper bound on the vertex-stream elements this draw can fetch through
|
|
||||||
// vertex-rate bindings, or 0 when unbounded (indirect/multi draws). Computed lazily
|
|
||||||
// because the index scan is only worth doing when a conversion actually needs it.
|
|
||||||
SizeT drawElementBound = 0;
|
|
||||||
Bool drawElementBoundComputed = false;
|
|
||||||
auto resolveDrawElementBound = [&]() -> SizeT {
|
|
||||||
if (drawElementBoundComputed) {
|
|
||||||
return drawElementBound;
|
|
||||||
}
|
|
||||||
drawElementBoundComputed = true;
|
|
||||||
if (!indexedDraw) {
|
|
||||||
drawElementBound = static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount;
|
|
||||||
} else if (drawParams.indexRangeIsExactView) {
|
|
||||||
Uint32 maxIndex = 0;
|
|
||||||
if (TryComputeMaxIndexFromHostBytes(vao, *pIndexBufferView, maxIndex)) {
|
|
||||||
drawElementBound = static_cast<SizeT>(maxIndex) + 1 +
|
|
||||||
static_cast<SizeT>(std::max(drawParams.baseVertex, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return drawElementBound;
|
|
||||||
};
|
|
||||||
// programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat
|
// programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat
|
||||||
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
|
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
|
||||||
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
||||||
@@ -2454,35 +2289,33 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client arrays have no queryable size, so bound the upload by the draw's
|
if (conversion != VertexInputStateFactory::VertexStreamConversion::None && indexedDraw) {
|
||||||
// real fetch range. For indexed draws that means scanning the index bytes:
|
// The current indexed setup only carries indexCount, not the maximum effective
|
||||||
// the guessed vertexCount (indexCount + baseVertex) can both truncate draws
|
// index. Guessing a client-memory range here can truncate the converted stream.
|
||||||
// whose max index exceeds their index count and over-read below it.
|
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
|
||||||
const SizeT clientElementBound = resolveDrawElementBound();
|
"location=%u requires an indexed vertex range", location);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (conversion != VertexInputStateFactory::VertexStreamConversion::None &&
|
||||||
|
drawParams.vertexCount == 0) {
|
||||||
|
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
|
||||||
|
"location=%u has an unknown vertex range", location);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint32 lastVertex = drawParams.vertexCount > 0
|
||||||
|
? drawParams.firstVertex + drawParams.vertexCount - 1
|
||||||
|
: drawParams.firstVertex;
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
Bool uploaded = false;
|
Bool uploaded = false;
|
||||||
if (conversion == VertexInputStateFactory::VertexStreamConversion::None) {
|
if (conversion == VertexInputStateFactory::VertexStreamConversion::None) {
|
||||||
const SizeT lastVertex =
|
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
|
||||||
clientElementBound > 0
|
|
||||||
? clientElementBound - 1
|
|
||||||
: (drawParams.vertexCount > 0
|
|
||||||
? static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount - 1
|
|
||||||
: static_cast<SizeT>(drawParams.firstVertex));
|
|
||||||
const SizeT uploadSize = lastVertex * stride + elementSize;
|
|
||||||
uploaded = m_bufferManager.UploadTransient(
|
uploaded = m_bufferManager.UploadTransient(
|
||||||
BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData,
|
BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData,
|
||||||
static_cast<VkDeviceSize>(uploadSize), 16, slice);
|
static_cast<VkDeviceSize>(uploadSize), 16, slice);
|
||||||
} else {
|
} else {
|
||||||
if (clientElementBound == 0) {
|
|
||||||
// Indirect/multi indexed draws have no CPU-visible index range and a
|
|
||||||
// client array has no size to fall back to; a guessed range could
|
|
||||||
// truncate the converted stream, so skip the draw loudly.
|
|
||||||
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
|
|
||||||
"location=%u has no computable vertex range", location);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize,
|
uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize,
|
||||||
clientElementBound, slice);
|
static_cast<SizeT>(lastVertex) + 1, slice);
|
||||||
}
|
}
|
||||||
if (!uploaded) {
|
if (!uploaded) {
|
||||||
MOBILEGL_ASSERT(false,
|
MOBILEGL_ASSERT(false,
|
||||||
@@ -2529,23 +2362,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceBufferShared->SyncPersistentMappedRange();
|
sourceBufferShared->SyncPersistentMappedRange();
|
||||||
const SizeT availableElementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
const SizeT elementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
||||||
const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped();
|
|
||||||
// Convert only what this draw can fetch instead of the whole buffer tail.
|
|
||||||
// Instance-rate bindings index by instance, not the vertex range, so they
|
|
||||||
// keep the tail. Indexed draws from cacheable buffers also keep the tail: a
|
|
||||||
// single cached whole-range conversion per frame is cheaper than a per-draw
|
|
||||||
// index scan. Persistent-mapped buffers are uncacheable and reconvert every
|
|
||||||
// draw, so for them the scan plus bounded conversion is the cheaper trade.
|
|
||||||
const Bool vertexRateBinding =
|
|
||||||
vertexInputState.bindings[binding].inputRate == VK_VERTEX_INPUT_RATE_VERTEX;
|
|
||||||
SizeT elementCount = availableElementCount;
|
|
||||||
if (vertexRateBinding && (!indexedDraw || !cacheable)) {
|
|
||||||
const SizeT elementBound = resolveDrawElementBound();
|
|
||||||
if (elementBound > 0) {
|
|
||||||
elementCount = std::min(elementCount, elementBound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ConvertedVertexStreamKey cacheKey{
|
const ConvertedVertexStreamKey cacheKey{
|
||||||
.buffer = sourceBufferShared.get(),
|
.buffer = sourceBufferShared.get(),
|
||||||
.changeSerial = sourceBufferShared->GetChangeSerial(),
|
.changeSerial = sourceBufferShared->GetChangeSerial(),
|
||||||
@@ -2558,18 +2375,12 @@ void main() {
|
|||||||
.conversion = conversion,
|
.conversion = conversion,
|
||||||
};
|
};
|
||||||
|
|
||||||
Bool reusedCachedStream = false;
|
const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped();
|
||||||
if (cacheable) {
|
auto cached = cacheable ? m_convertedVertexStreams.find(cacheKey)
|
||||||
const auto cached = m_convertedVertexStreams.find(cacheKey);
|
: m_convertedVertexStreams.end();
|
||||||
// A cached conversion covering at least this draw's range is a strict
|
if (cached != m_convertedVertexStreams.end()) {
|
||||||
// prefix match: converted streams are tightly packed from element 0.
|
slice = cached->second;
|
||||||
if (cached != m_convertedVertexStreams.end() &&
|
} else {
|
||||||
cached->second.elementCount >= elementCount) {
|
|
||||||
slice = cached->second.slice;
|
|
||||||
reusedCachedStream = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!reusedCachedStream) {
|
|
||||||
const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset;
|
const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset;
|
||||||
if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride,
|
if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride,
|
||||||
elementSize, elementCount, slice)) {
|
elementSize, elementCount, slice)) {
|
||||||
@@ -2578,8 +2389,7 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
m_convertedVertexStreams[cacheKey] =
|
m_convertedVertexStreams.emplace(cacheKey, slice);
|
||||||
ConvertedVertexStream{slice, elementCount, sourceBufferShared};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vkBuffers[binding] = slice.buffer;
|
vkBuffers[binding] = slice.buffer;
|
||||||
@@ -3464,7 +3274,6 @@ void main() {
|
|||||||
.backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp),
|
.backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp),
|
||||||
.backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp),
|
.backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp),
|
||||||
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
||||||
.fragmentReplacesDepth = programObj.fragmentReplacesDepth,
|
|
||||||
.stages = &programObj.stages,
|
.stages = &programObj.stages,
|
||||||
.vertexInputState = pipelineVertexInputState
|
.vertexInputState = pipelineVertexInputState
|
||||||
};
|
};
|
||||||
@@ -3669,16 +3478,6 @@ void main() {
|
|||||||
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
|
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
|
||||||
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
|
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
|
||||||
program.GetExternalIndex());
|
program.GetExternalIndex());
|
||||||
if (PipelineFactory::IsSuppressBlendedDepthWriteEnabled()) {
|
|
||||||
// With blending force-disabled the blended depth-write quirk can
|
|
||||||
// never fire for pipelines on this format, so a depth-equality
|
|
||||||
// chain that accumulates into it (MC 26.3 OIT depth_bounds on
|
|
||||||
// RGBA32F) keeps its depth writes and may flicker on this driver.
|
|
||||||
MGLOG_W("GetOrCreatePipeline: format=%d is not blendable, so the blended "
|
|
||||||
"depth-write quirk cannot apply to it; depth-equality chains "
|
|
||||||
"accumulating into this format may flicker",
|
|
||||||
static_cast<Int>(colorAttachmentFormat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!blendSupportIt->second) {
|
if (!blendSupportIt->second) {
|
||||||
@@ -3727,9 +3526,6 @@ void main() {
|
|||||||
VkCommandBuffer commandBuffer,
|
VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj) {
|
const ProgramFactory::VkProgramObject& programObj) {
|
||||||
if (!programObj.hasStorageImages) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
auto& storageTextures = m_storageImageTexturesScratch;
|
auto& storageTextures = m_storageImageTexturesScratch;
|
||||||
if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) {
|
if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) {
|
||||||
MGLOG_E("%s: failed to collect storage images for program=%u",
|
MGLOG_E("%s: failed to collect storage images for program=%u",
|
||||||
@@ -3740,24 +3536,6 @@ void main() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steady-state fast path: when every collected texture is already resident in GENERAL
|
|
||||||
// with no pending clear and no dirty content, the loop below has nothing to record, so
|
|
||||||
// keep the render pass alive instead of splitting it on every storage-image draw (on
|
|
||||||
// tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store
|
|
||||||
// coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here.
|
|
||||||
Bool anyNeedsPreparation = false;
|
|
||||||
for (auto* texture : storageTextures) {
|
|
||||||
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
|
|
||||||
if (m_textureManager->NeedsStorageImagePreparation(*texture) ||
|
|
||||||
m_clearManager->HasPendingClear(texture)) {
|
|
||||||
anyNeedsPreparation = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!anyNeedsPreparation) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
|
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
|
||||||
// a classic render pass. Do this before sampler preparation as well: a texture used by
|
// a classic render pass. Do this before sampler preparation as well: a texture used by
|
||||||
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
|
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
|
||||||
@@ -3767,6 +3545,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto* texture : storageTextures) {
|
for (auto* texture : storageTextures) {
|
||||||
|
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
|
||||||
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
|
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
|
||||||
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
|
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
|
||||||
__func__, texture->GetExternalIndex());
|
__func__, texture->GetExternalIndex());
|
||||||
@@ -3984,7 +3763,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto vtxUploadOk = UploadAndBindVertexBuffers(
|
auto vtxUploadOk = UploadAndBindVertexBuffers(
|
||||||
frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView);
|
frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView != nullptr);
|
||||||
if (!vtxUploadOk) {
|
if (!vtxUploadOk) {
|
||||||
MGLOG_E("SetupDraw skipped: failed to upload vertex buffers");
|
MGLOG_E("SetupDraw skipped: failed to upload vertex buffers");
|
||||||
return false;
|
return false;
|
||||||
@@ -6198,10 +5977,6 @@ void main() {
|
|||||||
vertexRange.instanceCount = payload.params.instanceCount;
|
vertexRange.instanceCount = payload.params.instanceCount;
|
||||||
vertexRange.firstVertex = 0;
|
vertexRange.firstVertex = 0;
|
||||||
vertexRange.firstInstance = static_cast<Uint32>(payload.params.firstInstance);
|
vertexRange.firstInstance = static_cast<Uint32>(payload.params.firstInstance);
|
||||||
vertexRange.baseVertex = payload.params.vertexOffset;
|
|
||||||
// Direct DrawElements fetches exactly the indices in its view, so vertex-stream
|
|
||||||
// conversion may bound its work by scanning them.
|
|
||||||
vertexRange.indexRangeIsExactView = true;
|
|
||||||
|
|
||||||
if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, vertexRange,
|
if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, vertexRange,
|
||||||
&payload.indexBufferView)) {
|
&payload.indexBufferView)) {
|
||||||
@@ -7232,10 +7007,7 @@ void main() {
|
|||||||
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
|
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
|
||||||
// out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight
|
// out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight
|
||||||
// repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules.
|
// repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules.
|
||||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS leaves it off to measure or dodge its GPU cost.
|
deviceFeatures.robustBufferAccess = supportedDeviceFeatures.robustBufferAccess;
|
||||||
deviceFeatures.robustBufferAccess = MG_Config::Features.DisableRobustBufferAccess
|
|
||||||
? VK_FALSE
|
|
||||||
: supportedDeviceFeatures.robustBufferAccess;
|
|
||||||
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
||||||
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
|
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
|
||||||
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
|
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
|
||||||
@@ -7265,15 +7037,6 @@ void main() {
|
|||||||
if (m_unformattedFloatStorageImagesEnabled) {
|
if (m_unformattedFloatStorageImagesEnabled) {
|
||||||
deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE;
|
deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE;
|
||||||
deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE;
|
deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE;
|
||||||
} else {
|
|
||||||
// Surface the degradation instead of failing silently: shader packs that bind a
|
|
||||||
// float storage image with a format different from its declaration (e.g.
|
|
||||||
// iterationRP) will render incorrectly on this device.
|
|
||||||
MGLOG_W("CreateLogicalDeviceAndQueues: shaderStorageImage*WithoutFormat unavailable "
|
|
||||||
"(read=%d write=%d); float storage-image format reinterpretation is disabled "
|
|
||||||
"and packs relying on it may misrender",
|
|
||||||
supportedDeviceFeatures.shaderStorageImageReadWithoutFormat,
|
|
||||||
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat);
|
|
||||||
}
|
}
|
||||||
deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance;
|
deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance;
|
||||||
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
|
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
|
||||||
|
|||||||
@@ -51,12 +51,6 @@ 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 {
|
||||||
@@ -494,18 +488,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ConvertedVertexStream {
|
UnorderedMap<ConvertedVertexStreamKey, BufferSlice, ConvertedVertexStreamKeyHash>
|
||||||
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;
|
m_convertedVertexStreams;
|
||||||
|
|
||||||
void CreateInstance();
|
void CreateInstance();
|
||||||
@@ -536,8 +519,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
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, Bool indexedDraw);
|
||||||
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);
|
||||||
|
|||||||
@@ -493,15 +493,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
|
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
|
||||||
if (level < 0) {
|
if (level < 0 || static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
|
||||||
RecordClearTextureError(caller, ErrorCode::InvalidValue,
|
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));
|
std::format("Texture level {} is not defined.", level));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -543,12 +536,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
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,
|
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
|
||||||
TextureUploadTarget uploadTarget, GLint level,
|
TextureUploadTarget uploadTarget, GLint level,
|
||||||
GLint xoffset, GLint yoffset, GLint zoffset,
|
GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
@@ -1670,21 +1657,6 @@ 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
|
||||||
@@ -4053,21 +4025,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
GLsizei width, GLsizei height) {
|
GLsizei width, GLsizei height) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
|
||||||
// GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
|
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ add_subdirectory(Texture)
|
|||||||
add_subdirectory(VertexArray)
|
add_subdirectory(VertexArray)
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
add_subdirectory(Query)
|
add_subdirectory(Query)
|
||||||
add_subdirectory(Pipeline)
|
|
||||||
if (ENABLE_INTEGRATION_TESTS)
|
if (ENABLE_INTEGRATION_TESTS)
|
||||||
add_subdirectory(Backend/DirectVulkan)
|
add_subdirectory(Backend/DirectVulkan)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
// 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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, AdditiveOnePlusOneIsStripped) {
|
|
||||||
// MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth.
|
|
||||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
|
||||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
|
||||||
EXPECT_TRUE(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.
|
|
||||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
|
||||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 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 hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates
|
|
||||||
// into a 2-target MRT.
|
|
||||||
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_ADD, kFullColorWriteMask);
|
|
||||||
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) {
|
|
||||||
// SRC_ALPHA,ONE additive is order-independent in the color channel but is the classic
|
|
||||||
// *sorted* particle/glow blend, not an OIT accumulation pass. Pins the src==ONE clause:
|
|
||||||
// without it this state would be stripped.
|
|
||||||
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 MIN/MAX and ONE+ONE ADD carry the equality-chain
|
|
||||||
// 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_ADD, 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));
|
|
||||||
}
|
|
||||||
@@ -1623,26 +1623,4 @@ TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTem
|
|||||||
ASSERT_NE(consumerEnd, String::npos);
|
ASSERT_NE(consumerEnd, String::npos);
|
||||||
nestedScan.insert(consumerEnd + 1, "\n }");
|
nestedScan.insert(consumerEnd + 1, "\n }");
|
||||||
expectUnchanged(std::move(nestedScan));
|
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));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -794,33 +794,6 @@ TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
|||||||
EXPECT_FLOAT_EQ(rgba16fFloatResult[3], 1.0f);
|
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;
|
||||||
|
|
||||||
@@ -1026,27 +999,9 @@ TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChan
|
|||||||
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger),
|
VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger),
|
||||||
VK_FORMAT_UNDEFINED);
|
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(
|
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
|
||||||
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
|
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
|
||||||
VK_FORMAT_D32_SFLOAT);
|
VK_FORMAT_UNDEFINED);
|
||||||
|
|
||||||
EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible(
|
EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible(
|
||||||
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
|
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
|
||||||
|
|||||||
@@ -318,49 +318,6 @@ TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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) {
|
||||||
@@ -1427,69 +1384,6 @@ TEST_F(TextureTest, TextureStorage1DAndSubImageModifyNamedObjectOnly) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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);
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ 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];
|
||||||
@@ -211,7 +210,6 @@ 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];
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ 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;
|
||||||
|
|||||||
@@ -255,37 +255,6 @@ 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;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
#include <MG_Backend/BackendObjects.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 {
|
||||||
@@ -345,75 +344,6 @@ 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) {
|
|
||||||
static constexpr Uint32 kHeaderWords = 5;
|
|
||||||
static constexpr Uint32 kOpDecorate = 71;
|
|
||||||
static constexpr Uint32 kOpMemberDecorate = 72;
|
|
||||||
static constexpr Uint32 kDecorationInvariant = 18;
|
|
||||||
static constexpr Uint32 kDecorationBuiltIn = 11;
|
|
||||||
static constexpr Uint32 kBuiltInPosition = 0;
|
|
||||||
if (inputBinary.size() < kHeaderWords) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First pass: find targets that already carry Invariant so we never duplicate.
|
|
||||||
struct MemberKey {
|
|
||||||
Uint32 id;
|
|
||||||
Uint32 member;
|
|
||||||
bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; }
|
|
||||||
};
|
|
||||||
Vector<Uint32> invariantIds;
|
|
||||||
Vector<MemberKey> invariantMembers;
|
|
||||||
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
|
|
||||||
const Uint32 word0 = inputBinary[i];
|
|
||||||
const Uint32 opcode = word0 & 0xFFFFu;
|
|
||||||
const Uint32 length = word0 >> 16;
|
|
||||||
if (length == 0 || i + length > inputBinary.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) {
|
|
||||||
invariantIds.push_back(inputBinary[i + 1]);
|
|
||||||
} else if (opcode == kOpMemberDecorate && length >= 4 &&
|
|
||||||
inputBinary[i + 3] == kDecorationInvariant) {
|
|
||||||
invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]});
|
|
||||||
}
|
|
||||||
i += length;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputBinary.clear();
|
|
||||||
outputBinary.reserve(inputBinary.size() + 8);
|
|
||||||
outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords);
|
|
||||||
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
|
|
||||||
const Uint32 word0 = inputBinary[i];
|
|
||||||
const Uint32 opcode = word0 & 0xFFFFu;
|
|
||||||
const Uint32 length = word0 >> 16;
|
|
||||||
outputBinary.insert(outputBinary.end(), inputBinary.begin() + i,
|
|
||||||
inputBinary.begin() + i + length);
|
|
||||||
if (opcode == kOpDecorate && length == 4 &&
|
|
||||||
inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) {
|
|
||||||
const Uint32 target = inputBinary[i + 1];
|
|
||||||
if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) {
|
|
||||||
outputBinary.push_back((3u << 16) | kOpDecorate);
|
|
||||||
outputBinary.push_back(target);
|
|
||||||
outputBinary.push_back(kDecorationInvariant);
|
|
||||||
}
|
|
||||||
} else if (opcode == kOpMemberDecorate && length == 5 &&
|
|
||||||
inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) {
|
|
||||||
const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]};
|
|
||||||
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) ==
|
|
||||||
invariantMembers.end()) {
|
|
||||||
outputBinary.push_back((4u << 16) | kOpMemberDecorate);
|
|
||||||
outputBinary.push_back(key.id);
|
|
||||||
outputBinary.push_back(key.member);
|
|
||||||
outputBinary.push_back(kDecorationInvariant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += length;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
|
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
|
||||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||||
|
|||||||
@@ -39,13 +39,6 @@ namespace MobileGL {
|
|||||||
// 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
|
// Replaces the declared format of float storage images with Unknown and adds the
|
||||||
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
|
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
|
||||||
// shaderStorageImage*WithoutFormat features are enabled, allowing the
|
// shaderStorageImage*WithoutFormat features are enabled, allowing the
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <Config.h>
|
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -366,21 +365,7 @@ namespace {
|
|||||||
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
|
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
|
||||||
HasIdentifierWithPrefixOutsideAllowed(
|
HasIdentifierWithPrefixOutsideAllowed(
|
||||||
tokens, "gl_Subgroup",
|
tokens, "gl_Subgroup",
|
||||||
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"}) ||
|
{"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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,14 +967,6 @@ namespace MobileGL {
|
|||||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
LinearPrefixScanMatch match;
|
LinearPrefixScanMatch match;
|
||||||
if (!ParseLinearPrefixScanTemplate(tokens, 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,76 +979,6 @@ namespace MobileGL {
|
|||||||
return true;
|
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.
|
||||||
@@ -1128,7 +1035,12 @@ namespace MobileGL {
|
|||||||
ModernizeLegacyGLSL(stage, source);
|
ModernizeLegacyGLSL(stage, source);
|
||||||
InjectDepthRangeBuiltinShim(stage, source);
|
InjectDepthRangeBuiltinShim(stage, source);
|
||||||
|
|
||||||
ApplyShaderSourceQuirks(stage, source);
|
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
||||||
|
if (stage == ShaderStage::Compute && activeBackend &&
|
||||||
|
activeBackend->GetBackendType() == BackendType::DirectVulkan) {
|
||||||
|
RewriteLinearSubgroupPrefixScanForVulkan(stage, activeBackend->GetDynamicParameters().SubgroupSize,
|
||||||
|
source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ namespace MobileGL {
|
|||||||
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
|
// 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
|
// 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
|
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
|
||||||
// template was recognized and rewritten. PreprocessShaderSource reaches this through
|
// template was recognized and rewritten. DirectVulkan calls this through
|
||||||
// its device-quirk registry: by default only on detected Qualcomm Vulkan devices,
|
// PreprocessShaderSource; the explicit entry point exists for deterministic tests.
|
||||||
// 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);
|
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
|
||||||
|
|||||||
@@ -93,15 +93,6 @@ 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.
|
||||||

|

|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: renderdoc-debug-on-trace-replay
|
name: renderdoc-capture-trace-frame
|
||||||
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/skills/renderdoc-debug-on-trace-replay/scripts/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/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/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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Change only the backend and output for GLES:
|
Change only the backend and output for GLES:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
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: "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."
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## TargetControl timing
|
## TargetControl timing
|
||||||
|
|
||||||
- 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.
|
- Start `tools/trace_replay/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.
|
||||||
@@ -19,12 +19,7 @@ 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"
|
||||||
# Fixture mirrors, tried in order before Git LFS.
|
DEFAULT_MIRROR_BASE = "https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
|
||||||
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()
|
||||||
|
|
||||||
@@ -105,35 +100,34 @@ def default_vulkan_icd(mobilegl_library):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def download_fixture(path, mirror_bases):
|
def download_fixture(path, mirror_base):
|
||||||
"""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)
|
||||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
||||||
token = os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN", "")
|
|
||||||
errors = []
|
|
||||||
for mirror_base in mirror_bases:
|
|
||||||
url = f"{mirror_base.rstrip('/')}/{path.name}"
|
url = f"{mirror_base.rstrip('/')}/{path.name}"
|
||||||
command = ["curl", "-L", "--fail", "--retry", "3", "--retry-delay", "2",
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
"--continue-at", "-"]
|
command = [
|
||||||
if token:
|
"curl",
|
||||||
command += ["--header", f"Authorization: token {token}"]
|
"-L",
|
||||||
command += ["-o", str(tmp), url]
|
"--fail",
|
||||||
|
"--retry",
|
||||||
|
"3",
|
||||||
|
"--retry-delay",
|
||||||
|
"2",
|
||||||
|
"--continue-at",
|
||||||
|
"-",
|
||||||
|
"-o",
|
||||||
|
str(tmp),
|
||||||
|
url,
|
||||||
|
]
|
||||||
result = subprocess.run(command, text=True, capture_output=True)
|
result = subprocess.run(command, text=True, capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
errors.append(f"{mirror_base}: {result.stderr.strip() or result.stdout.strip()}")
|
return path, False, result.stderr.strip() or result.stdout.strip()
|
||||||
tmp.unlink(missing_ok=True)
|
|
||||||
continue
|
|
||||||
tmp.replace(path)
|
tmp.replace(path)
|
||||||
if is_bad_fixture(path):
|
if is_bad_fixture(path):
|
||||||
errors.append(f"{mirror_base}: downloaded fixture is empty or still an LFS pointer")
|
return path, False, "downloaded fixture is empty or still an LFS pointer"
|
||||||
continue
|
|
||||||
return path, True, ""
|
return path, True, ""
|
||||||
return path, False, "; ".join(errors)
|
|
||||||
|
|
||||||
|
|
||||||
def hydrate_fixtures(cases, fetch, mirror_bases, download_jobs):
|
def hydrate_fixtures(cases, fetch, mirror_base, download_jobs):
|
||||||
required = []
|
required = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for case in cases:
|
for case in cases:
|
||||||
@@ -155,7 +149,7 @@ def hydrate_fixtures(cases, fetch, mirror_bases, 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_bases) for path in required]
|
futures = [executor.submit(download_fixture, path, mirror_base) 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:
|
||||||
@@ -440,9 +434,7 @@ 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", action="append", dest="fixture_mirror_bases",
|
parser.add_argument("--fixture-mirror-base", default=os.environ.get("MOBILEGL_TRACE_FIXTURE_MIRROR_BASE", DEFAULT_MIRROR_BASE))
|
||||||
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()
|
||||||
@@ -471,10 +463,7 @@ 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
|
||||||
|
|
||||||
mirror_bases = args.fixture_mirror_bases or [
|
if not hydrate_fixtures(selected_cases, args.fetch_fixtures, args.fixture_mirror_base, max(1, args.download_jobs)):
|
||||||
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:
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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,8 +1,3 @@
|
|||||||
---
|
|
||||||
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
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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."
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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,137 +0,0 @@
|
|||||||
---
|
|
||||||
name: trace-fixture-authoring-on-android-fcl
|
|
||||||
description: Capture an on-device Android apitrace from FCL's MobileGL renderers. Use when preparing a reproducible MobileGL DirectGLES, Magma (DirectVulkan), or SimpleFPEWrapper rendering trace, marking the frame of a visual defect, pulling the resulting full.trace, or turning a device capture into a replay fixture.
|
|
||||||
---
|
|
||||||
|
|
||||||
# MobileGL Android trace capture
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Use FCL's Android `egltrace.so` wrapper, not Perfetto. When enabled before
|
|
||||||
launch, it records the complete EGL/GL call stream to `full.trace`. The game's
|
|
||||||
**MobileGL Trace → Capture** control marks the next swap frame in
|
|
||||||
`capture-result.json`; it does not start or stop recording and does not produce
|
|
||||||
a one-frame trace by itself.
|
|
||||||
|
|
||||||
Run commands from the FoldCraftLauncher repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
export REPO="$PWD"
|
|
||||||
export CAPTURE="$REPO/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts"
|
|
||||||
export SERIAL=<adb-device-serial> # omit --serial only if exactly one device is attached
|
|
||||||
```
|
|
||||||
|
|
||||||
The capture scripts are bundled inside this skill under `scripts/`; they
|
|
||||||
auto-detect the FoldCraftLauncher repository root from their own location, so
|
|
||||||
`--repo` only needs to be passed for a non-standard checkout layout.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Use an FCL build containing `MobileGLTraceCapture` and the in-game Capture
|
|
||||||
menu entry.
|
|
||||||
- Select one of these renderers: MobileGL (DirectGLES), MobileGL Magma
|
|
||||||
(DirectVulkan), or SimpleFPEWrapper. MobileGlues is not supported by this
|
|
||||||
capture wrapper.
|
|
||||||
- Install `adb` and make it available on `PATH`; authorize USB debugging.
|
|
||||||
- Build the wrapper with Android NDK, CMake, Ninja, Python 3, and the checked
|
|
||||||
out in-tree `MobileGL/3rdparty/apitrace` submodule.
|
|
||||||
|
|
||||||
Confirm the attached device and ABI before building. The wrapper ABI must match
|
|
||||||
the device process ABI.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
adb devices -l
|
|
||||||
adb -s "$SERIAL" shell getprop ro.product.cpu.abi
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `arm64-v8a` for the usual `arm64-v8a` result; use the matching NDK ABI for
|
|
||||||
other devices.
|
|
||||||
|
|
||||||
## Build and install the wrapper
|
|
||||||
|
|
||||||
Build once per ABI or after changing apitrace/wrapper sources:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 "$CAPTURE/build_android_egltrace.py" --abi arm64-v8a
|
|
||||||
```
|
|
||||||
|
|
||||||
This generates `egltrace.so` under the skill's `scripts/out/` directory. Push
|
|
||||||
it and write FCL's enable sentinel before launching the game:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" install-wrapper
|
|
||||||
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" enable
|
|
||||||
```
|
|
||||||
|
|
||||||
The device-side control directory is `/sdcard/FCL/mobilegl-trace`. FCL copies
|
|
||||||
the shared `egltrace.so` into its private files directory at launch, replaces
|
|
||||||
the renderer's EGL library with it, and forwards to the real MobileGL library.
|
|
||||||
|
|
||||||
## Capture a reproduction
|
|
||||||
|
|
||||||
1. Start FCL after the wrapper and enable sentinel are in place. Select the
|
|
||||||
intended supported MobileGL renderer and launch the game.
|
|
||||||
2. Trace mode forces the game to `854x480`; account for that when reproducing
|
|
||||||
and comparing output.
|
|
||||||
3. Reproduce the issue. Start close to the target scene because tracing starts
|
|
||||||
when the game launches and trace files can grow rapidly.
|
|
||||||
4. At the desired visual state, open FCL's right-side game menu and press
|
|
||||||
**MobileGL Trace → Capture**. Let at least one frame present afterward.
|
|
||||||
5. Exit the game cleanly, then pull the latest session:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" pull-latest
|
|
||||||
```
|
|
||||||
|
|
||||||
The default local result is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
.trace-work/pulled-mobilegl-captures/capture-YYYYMMDD-HHMMSS-<renderer>/
|
|
||||||
full.trace
|
|
||||||
capture-status.json
|
|
||||||
capture-result.json
|
|
||||||
```
|
|
||||||
|
|
||||||
`capture-result.json` must show `"status": "captured"`. Its `targetFrame`
|
|
||||||
is the one-based swap count used by `gltrim`; `zeroBasedFrame` is included for
|
|
||||||
tools that use zero-based indexing.
|
|
||||||
|
|
||||||
## Diagnose setup failures
|
|
||||||
|
|
||||||
Inspect the active device session directly:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/latest-session.txt
|
|
||||||
adb -s "$SERIAL" shell ls -lh /sdcard/FCL/mobilegl-trace
|
|
||||||
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-status.json
|
|
||||||
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-result.json
|
|
||||||
```
|
|
||||||
|
|
||||||
If `capture-status.json` reports a missing `egltrace.so`, rebuild/push the
|
|
||||||
correct ABI and relaunch. If `capture-result.json` is absent, Capture was
|
|
||||||
pressed without an active trace session, or no subsequent `eglSwapBuffers`
|
|
||||||
occurred. The menu button itself only writes `capture-once.request`.
|
|
||||||
|
|
||||||
Disable tracing when finished; otherwise the next supported MobileGL launch
|
|
||||||
will trace again:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" disable
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a replay fixture (optional)
|
|
||||||
|
|
||||||
Keep the raw `full.trace` until replay validation succeeds. To frame-trim and
|
|
||||||
package the marked frame for MobileGL trace replay, use the existing helper:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 "$CAPTURE/package_capture_fixture.py" \
|
|
||||||
--serial "$SERIAL" \
|
|
||||||
--case <case-name> \
|
|
||||||
--apitrace <path-to-in-tree-apitrace>
|
|
||||||
```
|
|
||||||
|
|
||||||
It pulls the latest capture if necessary, uses `capture-result.json` to select
|
|
||||||
the frame, runs `apitrace gltrim`, creates a golden image, and enforces the
|
|
||||||
fixture archive-size limit. Follow `../trace-fixture-authoring/SKILL.md` for
|
|
||||||
deterministic scene setup, verification, and registry changes.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Trace Fixture Authoring on Android (FCL)"
|
|
||||||
short_description: "Capture and package a MobileGL trace fixture on Android FCL"
|
|
||||||
default_prompt: "Use $trace-fixture-authoring-on-android-fcl to capture a MobileGL trace on my Android device and package it into a replay fixture."
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Build output produced by build_android_egltrace.py (ABI-specific, regenerated).
|
|
||||||
out/
|
|
||||||
__pycache__/
|
|
||||||
-65
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
REMOTE_ROOT = "/sdcard/FCL/mobilegl-trace"
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
||||||
DEFAULT_WRAPPER = SCRIPT_DIR / "out" / "egltrace.so"
|
|
||||||
|
|
||||||
|
|
||||||
def adb(serial, args):
|
|
||||||
cmd = ["adb"]
|
|
||||||
if serial:
|
|
||||||
cmd += ["-s", serial]
|
|
||||||
cmd += args
|
|
||||||
print("+", " ".join(cmd), flush=True)
|
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--serial")
|
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
install = sub.add_parser("install-wrapper")
|
|
||||||
install.add_argument("--wrapper", default=str(DEFAULT_WRAPPER))
|
|
||||||
|
|
||||||
sub.add_parser("enable")
|
|
||||||
sub.add_parser("disable")
|
|
||||||
sub.add_parser("capture-once")
|
|
||||||
|
|
||||||
pull = sub.add_parser("pull-latest")
|
|
||||||
pull.add_argument("--output", default=".trace-work/pulled-mobilegl-captures")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
serial = args.serial
|
|
||||||
|
|
||||||
if args.command == "install-wrapper":
|
|
||||||
wrapper = Path(args.wrapper)
|
|
||||||
if not wrapper.exists():
|
|
||||||
raise SystemExit(f"missing wrapper: {wrapper}")
|
|
||||||
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
|
|
||||||
adb(serial, ["push", str(wrapper), f"{REMOTE_ROOT}/egltrace.so"])
|
|
||||||
elif args.command == "enable":
|
|
||||||
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
|
|
||||||
adb(serial, ["shell", f"printf enabled > {REMOTE_ROOT}/enable"])
|
|
||||||
elif args.command == "disable":
|
|
||||||
adb(serial, ["shell", "rm", "-f", f"{REMOTE_ROOT}/enable"])
|
|
||||||
elif args.command == "capture-once":
|
|
||||||
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
|
|
||||||
adb(serial, ["shell", f"date +%s%3N > {REMOTE_ROOT}/capture-once.request"])
|
|
||||||
elif args.command == "pull-latest":
|
|
||||||
tmp = subprocess.check_output((["adb"] + (["-s", serial] if serial else []) +
|
|
||||||
["shell", "cat", f"{REMOTE_ROOT}/latest-session.txt"]),
|
|
||||||
text=True, encoding="utf-8", errors="replace").strip()
|
|
||||||
if not tmp:
|
|
||||||
raise SystemExit("no latest-session.txt on device")
|
|
||||||
output = Path(args.output)
|
|
||||||
output.mkdir(parents=True, exist_ok=True)
|
|
||||||
adb(serial, ["pull", tmp, str(output / Path(tmp).name)])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
-191
@@ -1,191 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.22.1)
|
|
||||||
|
|
||||||
project(mobilegl_android_egltrace)
|
|
||||||
|
|
||||||
set(APITRACE_ROOT "" CACHE PATH "Path to apitrace source tree")
|
|
||||||
set(PATCHED_EGLTRACE_CPP "" CACHE FILEPATH "Generated and patched egltrace.cpp")
|
|
||||||
set(PATCHED_GLPROC_EGL_CPP "" CACHE FILEPATH "Patched glproc_egl.cpp")
|
|
||||||
if(NOT EXISTS "${APITRACE_ROOT}/wrappers")
|
|
||||||
message(FATAL_ERROR "APITRACE_ROOT must point to apitrace")
|
|
||||||
endif()
|
|
||||||
if(NOT EXISTS "${PATCHED_EGLTRACE_CPP}")
|
|
||||||
message(FATAL_ERROR "PATCHED_EGLTRACE_CPP is required")
|
|
||||||
endif()
|
|
||||||
if(NOT EXISTS "${PATCHED_GLPROC_EGL_CPP}")
|
|
||||||
message(FATAL_ERROR "PATCHED_GLPROC_EGL_CPP is required")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(APITRACE_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/apitrace")
|
|
||||||
set(APITRACE_VERSION "mobilegl-capture")
|
|
||||||
|
|
||||||
find_package(Python3 REQUIRED)
|
|
||||||
find_package(Threads REQUIRED)
|
|
||||||
|
|
||||||
include("${APITRACE_ROOT}/cmake/ConvenienceLibrary.cmake")
|
|
||||||
|
|
||||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
|
||||||
set(ENABLE_STATIC_SNAPPY ON CACHE BOOL "" FORCE)
|
|
||||||
set(DOC_INSTALL_DIR "doc" CACHE PATH "" FORCE)
|
|
||||||
set(HAVE_X86 OFF CACHE BOOL "" FORCE)
|
|
||||||
set(ZLIB_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(PNG_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(Snappy_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(BROTLIDEC_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(BROTLIENC_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(ZSTD_FOUND OFF CACHE BOOL "" FORCE)
|
|
||||||
set(CMAKE_EXECUTABLE_FORMAT "MobileGLAndroid" CACHE INTERNAL "" FORCE)
|
|
||||||
|
|
||||||
add_custom_target(check)
|
|
||||||
add_subdirectory("${APITRACE_ROOT}/thirdparty" "${APITRACE_BINARY_DIR}/thirdparty")
|
|
||||||
|
|
||||||
set(APITRACE_GENERATED_DIR "${APITRACE_BINARY_DIR}/generated")
|
|
||||||
file(MAKE_DIRECTORY "${APITRACE_GENERATED_DIR}")
|
|
||||||
configure_file("${APITRACE_ROOT}/version.h.in" "${APITRACE_GENERATED_DIR}/version.h" @ONLY)
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT
|
|
||||||
"${APITRACE_GENERATED_DIR}/glproc.hpp"
|
|
||||||
"${APITRACE_GENERATED_DIR}/glproc.cpp"
|
|
||||||
COMMAND ${Python3_EXECUTABLE}
|
|
||||||
"${APITRACE_ROOT}/dispatch/glproc.py"
|
|
||||||
"${APITRACE_GENERATED_DIR}/glproc.hpp"
|
|
||||||
"${APITRACE_GENERATED_DIR}/glproc.cpp"
|
|
||||||
DEPENDS
|
|
||||||
"${APITRACE_ROOT}/dispatch/glproc.py"
|
|
||||||
"${APITRACE_ROOT}/dispatch/dispatch.py"
|
|
||||||
"${APITRACE_ROOT}/specs/wglapi.py"
|
|
||||||
"${APITRACE_ROOT}/specs/glxapi.py"
|
|
||||||
"${APITRACE_ROOT}/specs/cglapi.py"
|
|
||||||
"${APITRACE_ROOT}/specs/eglapi.py"
|
|
||||||
"${APITRACE_ROOT}/specs/glapi.py"
|
|
||||||
"${APITRACE_ROOT}/specs/gltypes.py"
|
|
||||||
"${APITRACE_ROOT}/specs/stdapi.py")
|
|
||||||
|
|
||||||
add_library(apitrace_os STATIC
|
|
||||||
"${APITRACE_ROOT}/lib/os/os_backtrace.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/os/os_crtdbg.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/os/os_posix.cpp")
|
|
||||||
target_include_directories(apitrace_os PUBLIC
|
|
||||||
"${APITRACE_ROOT}/compat"
|
|
||||||
"${APITRACE_ROOT}/thirdparty"
|
|
||||||
"${APITRACE_ROOT}/lib/os"
|
|
||||||
"${APITRACE_ROOT}/lib/trace")
|
|
||||||
target_link_libraries(apitrace_os PUBLIC Threads::Threads)
|
|
||||||
|
|
||||||
add_library(glproc STATIC
|
|
||||||
"${APITRACE_GENERATED_DIR}/glproc.cpp"
|
|
||||||
"${PATCHED_GLPROC_EGL_CPP}")
|
|
||||||
target_include_directories(glproc PUBLIC
|
|
||||||
"${APITRACE_GENERATED_DIR}"
|
|
||||||
"${APITRACE_ROOT}/wrappers"
|
|
||||||
"${APITRACE_ROOT}/dispatch"
|
|
||||||
"${APITRACE_ROOT}/lib/os"
|
|
||||||
"${APITRACE_ROOT}/thirdparty/khronos")
|
|
||||||
target_link_libraries(glproc PUBLIC apitrace_os dl)
|
|
||||||
|
|
||||||
add_library(highlight STATIC "${APITRACE_ROOT}/lib/highlight/highlight.cpp")
|
|
||||||
target_include_directories(highlight PUBLIC "${APITRACE_ROOT}/lib/highlight")
|
|
||||||
|
|
||||||
add_library(guids STATIC "${APITRACE_ROOT}/lib/guids/guids.cpp")
|
|
||||||
target_include_directories(guids PUBLIC
|
|
||||||
"${APITRACE_ROOT}/lib/guids"
|
|
||||||
"${APITRACE_ROOT}/lib/os")
|
|
||||||
|
|
||||||
add_library(common STATIC
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_callset.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_dump.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_fast_callset.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_read.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_zlib.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_brotli.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_snappy.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_zstd.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_file_zstd_seekable.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_model.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_option.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_ostream_snappy.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_ostream_zlib.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_ostream_zstd.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_parser.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_parser_flags.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_parser_loop.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_profiler.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_writer.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_writer_local.cpp"
|
|
||||||
"${APITRACE_ROOT}/lib/trace/trace_writer_model.cpp")
|
|
||||||
target_include_directories(common PUBLIC
|
|
||||||
"${APITRACE_ROOT}/compat"
|
|
||||||
"${APITRACE_ROOT}/thirdparty"
|
|
||||||
"${APITRACE_ROOT}/lib/guids"
|
|
||||||
"${APITRACE_ROOT}/lib/highlight"
|
|
||||||
"${APITRACE_ROOT}/lib/os"
|
|
||||||
"${APITRACE_ROOT}/lib/trace"
|
|
||||||
"${APITRACE_ROOT}/lib/ubjson")
|
|
||||||
target_link_libraries(common PUBLIC
|
|
||||||
guids
|
|
||||||
highlight
|
|
||||||
apitrace_os
|
|
||||||
Snappy::snappy
|
|
||||||
ZLIB::ZLIB
|
|
||||||
PkgConfig::BROTLIDEC
|
|
||||||
PkgConfig::ZSTD
|
|
||||||
zstd_seekable)
|
|
||||||
|
|
||||||
add_convenience_library(trace
|
|
||||||
"${APITRACE_ROOT}/wrappers/memtrace.hpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/memtrace.cpp")
|
|
||||||
target_include_directories(trace PUBLIC
|
|
||||||
"${APITRACE_ROOT}/thirdparty/crc32c")
|
|
||||||
target_link_libraries(trace
|
|
||||||
common
|
|
||||||
guids
|
|
||||||
crc32c)
|
|
||||||
|
|
||||||
add_library(glhelpers STATIC
|
|
||||||
"${APITRACE_ROOT}/helpers/glfeatures.cpp"
|
|
||||||
"${APITRACE_ROOT}/helpers/eglsize.cpp")
|
|
||||||
target_include_directories(glhelpers PUBLIC
|
|
||||||
"${APITRACE_GENERATED_DIR}"
|
|
||||||
"${APITRACE_ROOT}/dispatch"
|
|
||||||
"${APITRACE_ROOT}/helpers"
|
|
||||||
"${APITRACE_ROOT}/lib/os"
|
|
||||||
"${APITRACE_ROOT}/thirdparty/khronos")
|
|
||||||
target_link_libraries(glhelpers PUBLIC glproc apitrace_os)
|
|
||||||
|
|
||||||
add_convenience_library(gltrace_common
|
|
||||||
"${APITRACE_ROOT}/wrappers/glcaps.cpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/config.cpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/gltrace_arrays.cpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/gltrace_state.cpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/glmemshadow.hpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/glmemshadow.cpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.hpp"
|
|
||||||
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.cpp")
|
|
||||||
add_dependencies(gltrace_common glproc)
|
|
||||||
target_include_directories(gltrace_common PUBLIC
|
|
||||||
"${APITRACE_ROOT}/wrappers")
|
|
||||||
target_link_libraries(gltrace_common
|
|
||||||
glhelpers
|
|
||||||
trace)
|
|
||||||
|
|
||||||
add_library(egltrace SHARED
|
|
||||||
"${PATCHED_EGLTRACE_CPP}"
|
|
||||||
"${APITRACE_ROOT}/wrappers/dlsym.cpp"
|
|
||||||
"${PATCHED_GLPROC_EGL_CPP}")
|
|
||||||
add_dependencies(egltrace glproc)
|
|
||||||
set_target_properties(egltrace PROPERTIES PREFIX "")
|
|
||||||
target_compile_definitions(egltrace PRIVATE -DEGLTRACE=1)
|
|
||||||
target_include_directories(egltrace PRIVATE
|
|
||||||
"${APITRACE_ROOT}/wrappers"
|
|
||||||
"${APITRACE_GENERATED_DIR}"
|
|
||||||
"${APITRACE_ROOT}/helpers"
|
|
||||||
"${APITRACE_ROOT}/dispatch"
|
|
||||||
"${APITRACE_ROOT}/lib/os"
|
|
||||||
"${APITRACE_ROOT}/lib/trace"
|
|
||||||
"${APITRACE_ROOT}/thirdparty/khronos")
|
|
||||||
target_link_libraries(egltrace
|
|
||||||
gltrace_common
|
|
||||||
glproc
|
|
||||||
Threads::Threads
|
|
||||||
dl)
|
|
||||||
-234
@@ -1,234 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# This script is bundled inside the trace-fixture-authoring-on-android-fcl skill at
|
|
||||||
# <FCL>/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
||||||
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
|
|
||||||
DEFAULT_OUTPUT = SCRIPT_DIR / "out" / "egltrace.so"
|
|
||||||
|
|
||||||
|
|
||||||
def run(cmd, cwd=None):
|
|
||||||
print("+", " ".join(str(part) for part in cmd), flush=True)
|
|
||||||
subprocess.run(cmd, cwd=cwd, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def find_ndk(repo):
|
|
||||||
for key in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT"):
|
|
||||||
value = os.environ.get(key)
|
|
||||||
if value:
|
|
||||||
return Path(value)
|
|
||||||
for key in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
|
|
||||||
value = os.environ.get(key)
|
|
||||||
if value:
|
|
||||||
ndk_root = Path(value) / "ndk"
|
|
||||||
if ndk_root.exists():
|
|
||||||
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
|
|
||||||
if versions:
|
|
||||||
return versions[-1]
|
|
||||||
local = repo / "local.properties"
|
|
||||||
if local.exists():
|
|
||||||
sdk = None
|
|
||||||
ndk = None
|
|
||||||
for line in local.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
||||||
if line.startswith("sdk.dir="):
|
|
||||||
sdk = Path(line.split("=", 1)[1].replace("\\:", ":"))
|
|
||||||
if line.startswith("ndk.dir="):
|
|
||||||
ndk = Path(line.split("=", 1)[1].replace("\\:", ":"))
|
|
||||||
if ndk:
|
|
||||||
return ndk
|
|
||||||
if sdk:
|
|
||||||
ndk_root = sdk / "ndk"
|
|
||||||
if ndk_root.exists():
|
|
||||||
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
|
|
||||||
if versions:
|
|
||||||
return versions[-1]
|
|
||||||
raise SystemExit("Android NDK not found; set ANDROID_NDK_HOME or local.properties sdk.dir/ndk.dir")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_and_patch(repo, build_dir):
|
|
||||||
wrapper_dir = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers"
|
|
||||||
generated = build_dir / "patched" / "egltrace.cpp"
|
|
||||||
generated.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with generated.open("w", encoding="utf-8", newline="\n") as out:
|
|
||||||
subprocess.run([sys.executable, str(wrapper_dir / "egltrace.py")], cwd=wrapper_dir, stdout=out, check=True)
|
|
||||||
|
|
||||||
text = generated.read_text(encoding="utf-8")
|
|
||||||
helper = r'''
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
static unsigned long long mobilegl_capture_swap_count = 0;
|
|
||||||
|
|
||||||
static long long mobilegl_capture_time_ms(void) {
|
|
||||||
struct timespec ts;
|
|
||||||
clock_gettime(CLOCK_REALTIME, &ts);
|
|
||||||
return (long long) ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int mobilegl_capture_exists(const char *path) {
|
|
||||||
return path != NULL && path[0] != '\0' && access(path, F_OK) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void mobilegl_capture_record_request(void) {
|
|
||||||
++mobilegl_capture_swap_count;
|
|
||||||
const char *request = getenv("MOBILEGL_TRACE_CAPTURE_REQUEST_FILE");
|
|
||||||
const char *output = getenv("MOBILEGL_TRACE_CAPTURE_FRAME_FILE");
|
|
||||||
if (!mobilegl_capture_exists(request) || output == NULL || output[0] == '\0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unlink(request);
|
|
||||||
FILE *file = fopen(output, "w");
|
|
||||||
if (file == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const char *trace_file = getenv("TRACE_FILE");
|
|
||||||
fprintf(file,
|
|
||||||
"{\n"
|
|
||||||
" \"status\": \"captured\",\n"
|
|
||||||
" \"swapCount\": %llu,\n"
|
|
||||||
" \"targetFrame\": %llu,\n"
|
|
||||||
" \"zeroBasedFrame\": %llu,\n"
|
|
||||||
" \"capturedAtMs\": %lld,\n"
|
|
||||||
" \"traceFile\": \"%s\"\n"
|
|
||||||
"}\n",
|
|
||||||
mobilegl_capture_swap_count,
|
|
||||||
mobilegl_capture_swap_count,
|
|
||||||
mobilegl_capture_swap_count == 0 ? 0 : mobilegl_capture_swap_count - 1,
|
|
||||||
mobilegl_capture_time_ms(),
|
|
||||||
trace_file == NULL ? "" : trace_file);
|
|
||||||
fclose(file);
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
insert_at = text.find("#include")
|
|
||||||
if insert_at < 0:
|
|
||||||
raise SystemExit("generated egltrace.cpp has no include block")
|
|
||||||
next_block = text.find("\n\n", insert_at)
|
|
||||||
text = text[:next_block] + "\n" + helper + text[next_block:]
|
|
||||||
|
|
||||||
needle = "EGLBoolean EGLAPIENTRY eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)"
|
|
||||||
start = text.find(needle)
|
|
||||||
if start < 0:
|
|
||||||
raise SystemExit("generated egltrace.cpp has no eglSwapBuffers wrapper to patch")
|
|
||||||
brace = text.find("{", start)
|
|
||||||
if brace < 0:
|
|
||||||
raise SystemExit("eglSwapBuffers wrapper has no function body")
|
|
||||||
text = text[:brace + 1] + "\n mobilegl_capture_record_request();" + text[brace + 1:]
|
|
||||||
generated.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
return generated
|
|
||||||
|
|
||||||
|
|
||||||
def patch_glproc_egl(repo, build_dir):
|
|
||||||
source = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers" / "glproc_egl.cpp"
|
|
||||||
patched = build_dir / "patched" / "glproc_egl.cpp"
|
|
||||||
patched.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
text = source.read_text(encoding="utf-8")
|
|
||||||
text = text.replace('#include "dlopen.hpp"\n', '#include "dlopen.hpp"\n#include <stdlib.h>\n')
|
|
||||||
needle = """void *
|
|
||||||
_getPublicProcAddress(const char *procName)
|
|
||||||
{
|
|
||||||
void *proc;
|
|
||||||
|
|
||||||
"""
|
|
||||||
replacement = """void *
|
|
||||||
_getPublicProcAddress(const char *procName)
|
|
||||||
{
|
|
||||||
void *proc;
|
|
||||||
|
|
||||||
static void *traceLibGL = NULL;
|
|
||||||
static bool triedTraceLibGL = false;
|
|
||||||
if (!triedTraceLibGL) {
|
|
||||||
triedTraceLibGL = true;
|
|
||||||
const char *traceLibGLName = getenv("TRACE_LIBGL");
|
|
||||||
if (traceLibGLName && traceLibGLName[0]) {
|
|
||||||
traceLibGL = _dlopen(traceLibGLName, RTLD_GLOBAL | RTLD_LAZY | RTLD_DEEPBIND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (traceLibGL) {
|
|
||||||
proc = dlsym(traceLibGL, procName);
|
|
||||||
if (proc) {
|
|
||||||
return proc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
if needle not in text:
|
|
||||||
raise SystemExit("glproc_egl.cpp patch point not found")
|
|
||||||
text = text.replace(needle, replacement, 1)
|
|
||||||
patched.write_text(text, encoding="utf-8", newline="\n")
|
|
||||||
return patched
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
|
|
||||||
parser.add_argument("--abi", default="arm64-v8a")
|
|
||||||
parser.add_argument("--android-platform", default="android-23")
|
|
||||||
parser.add_argument("--build-dir", default=".trace-work/build-android-egltrace")
|
|
||||||
parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
repo = Path(args.repo).resolve()
|
|
||||||
ndk = find_ndk(repo)
|
|
||||||
build_dir = (repo / args.build_dir / args.abi).resolve()
|
|
||||||
apitrace = repo / "MobileGL" / "3rdparty" / "apitrace"
|
|
||||||
source_dir = SCRIPT_DIR / "android_egltrace"
|
|
||||||
toolchain = ndk / "build" / "cmake" / "android.toolchain.cmake"
|
|
||||||
if not apitrace.exists():
|
|
||||||
raise SystemExit(f"missing apitrace checkout: {apitrace}")
|
|
||||||
if not source_dir.exists():
|
|
||||||
raise SystemExit(f"missing wrapper CMake project: {source_dir}")
|
|
||||||
if not toolchain.exists():
|
|
||||||
raise SystemExit(f"missing Android toolchain: {toolchain}")
|
|
||||||
cache = build_dir / "CMakeCache.txt"
|
|
||||||
if cache.exists() and "CMAKE_GENERATOR:INTERNAL=Ninja" not in cache.read_text(encoding="utf-8", errors="ignore"):
|
|
||||||
shutil.rmtree(build_dir)
|
|
||||||
elif cache.exists() and str(source_dir).replace("\\", "/") not in cache.read_text(encoding="utf-8", errors="ignore").replace("\\", "/"):
|
|
||||||
shutil.rmtree(build_dir)
|
|
||||||
|
|
||||||
ninja = shutil.which("ninja")
|
|
||||||
if ninja is None:
|
|
||||||
cmake_ninjas = sorted((Path(os.environ.get("ANDROID_HOME", "")) / "cmake").glob("*/bin/ninja.exe"))
|
|
||||||
ninja = str(cmake_ninjas[-1]) if cmake_ninjas else None
|
|
||||||
if ninja is None:
|
|
||||||
raise SystemExit("ninja not found; install Ninja or Android SDK CMake")
|
|
||||||
|
|
||||||
patched_egltrace = generate_and_patch(repo, build_dir)
|
|
||||||
patched_glproc_egl = patch_glproc_egl(repo, build_dir)
|
|
||||||
|
|
||||||
run([
|
|
||||||
"cmake", "-G", "Ninja", "-S", str(source_dir), "-B", str(build_dir),
|
|
||||||
"-DCMAKE_BUILD_TYPE=Release",
|
|
||||||
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
|
|
||||||
f"-DCMAKE_MAKE_PROGRAM={ninja}",
|
|
||||||
f"-DANDROID_ABI={args.abi}",
|
|
||||||
f"-DANDROID_PLATFORM={args.android_platform}",
|
|
||||||
f"-DAPITRACE_ROOT={apitrace}",
|
|
||||||
f"-DPATCHED_EGLTRACE_CPP={patched_egltrace}",
|
|
||||||
f"-DPATCHED_GLPROC_EGL_CPP={patched_glproc_egl}",
|
|
||||||
])
|
|
||||||
run(["cmake", "--build", str(build_dir), "--target", "egltrace", "--parallel"])
|
|
||||||
|
|
||||||
output = Path(args.output)
|
|
||||||
if not output.is_absolute():
|
|
||||||
output = repo / output
|
|
||||||
output = output.resolve()
|
|
||||||
output.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
candidates = list(build_dir.rglob("egltrace.so"))
|
|
||||||
if not candidates:
|
|
||||||
raise SystemExit("egltrace.so was not produced")
|
|
||||||
shutil.copy2(candidates[0], output)
|
|
||||||
print(output)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
-168
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tarfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
DEFAULT_MAX_ARCHIVE_BYTES = 20 * 1024 * 1024
|
|
||||||
# Bundled under the skill at .../skills/trace-fixture-authoring-on-android-fcl/scripts/.
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
||||||
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
|
|
||||||
|
|
||||||
|
|
||||||
def run(cmd, cwd=None, capture=False):
|
|
||||||
print("+", " ".join(str(part) for part in cmd), flush=True)
|
|
||||||
if capture:
|
|
||||||
return subprocess.check_output(cmd, cwd=cwd, text=True, encoding="utf-8", errors="replace")
|
|
||||||
subprocess.run(cmd, cwd=cwd, check=True)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def adb(args, serial=None):
|
|
||||||
cmd = ["adb"]
|
|
||||||
if serial:
|
|
||||||
cmd += ["-s", serial]
|
|
||||||
cmd += args
|
|
||||||
return run(cmd, capture=True)
|
|
||||||
|
|
||||||
|
|
||||||
def pull_latest(serial, dest):
|
|
||||||
latest = adb(["shell", "cat", "/sdcard/FCL/mobilegl-trace/latest-session.txt"], serial).strip()
|
|
||||||
if not latest:
|
|
||||||
raise SystemExit("device has no /sdcard/FCL/mobilegl-trace/latest-session.txt")
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
local = dest / Path(latest).name
|
|
||||||
if local.exists():
|
|
||||||
shutil.rmtree(local)
|
|
||||||
adb(["pull", latest, str(local)], serial)
|
|
||||||
return local
|
|
||||||
|
|
||||||
|
|
||||||
def choose_target_frame(capture_dir, explicit_frame):
|
|
||||||
if explicit_frame is not None:
|
|
||||||
return explicit_frame
|
|
||||||
result = capture_dir / "capture-result.json"
|
|
||||||
if not result.exists():
|
|
||||||
raise SystemExit(f"missing {result}; press the FCL capture button or create capture-once.request first")
|
|
||||||
data = json.loads(result.read_text(encoding="utf-8"))
|
|
||||||
if "targetFrame" not in data:
|
|
||||||
raise SystemExit(f"{result} has no targetFrame")
|
|
||||||
return int(data["targetFrame"])
|
|
||||||
|
|
||||||
|
|
||||||
def choose_snapshot(golden_dir):
|
|
||||||
pngs = sorted(golden_dir.glob("*.png"))
|
|
||||||
if not pngs:
|
|
||||||
raise SystemExit(f"no snapshots produced in {golden_dir}")
|
|
||||||
def call_no(path):
|
|
||||||
match = re.search(r"\.(\d+)\.png$", path.name)
|
|
||||||
return int(match.group(1)) if match else -1
|
|
||||||
return max(pngs, key=call_no)
|
|
||||||
|
|
||||||
|
|
||||||
def choose_target_call(explicit_call, golden):
|
|
||||||
if explicit_call is not None:
|
|
||||||
return explicit_call
|
|
||||||
if golden is None:
|
|
||||||
return None
|
|
||||||
match = re.search(r"\.(\d+)\.png$", golden.name)
|
|
||||||
return int(match.group(1)) if match else None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
|
|
||||||
parser.add_argument("--serial", help="adb serial; when set, pull latest capture from device")
|
|
||||||
parser.add_argument("--capture-dir", help="local capture directory; defaults to pulled latest")
|
|
||||||
parser.add_argument("--pull-root", default=".trace-work/pulled-mobilegl-captures")
|
|
||||||
parser.add_argument("--case", required=True)
|
|
||||||
parser.add_argument("--target-frame", type=int)
|
|
||||||
parser.add_argument("--target-call", type=int)
|
|
||||||
parser.add_argument("--golden", help="existing golden PNG, normally produced by Android replay")
|
|
||||||
parser.add_argument("--skip-desktop-golden", action="store_true",
|
|
||||||
help="skip apitrace replay --headless; requires --golden and --target-call")
|
|
||||||
parser.add_argument("--apitrace", default="apitrace")
|
|
||||||
parser.add_argument("--fixtures-dir", default="MobileGL/tools/trace_replay/fixtures")
|
|
||||||
parser.add_argument("--width", type=int, default=854)
|
|
||||||
parser.add_argument("--height", type=int, default=480)
|
|
||||||
parser.add_argument("--ssim-threshold", default="0.99")
|
|
||||||
parser.add_argument("--max-archive-bytes", type=int, default=DEFAULT_MAX_ARCHIVE_BYTES)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
repo = Path(args.repo).resolve()
|
|
||||||
if args.capture_dir:
|
|
||||||
capture_dir = Path(args.capture_dir).resolve()
|
|
||||||
elif args.serial:
|
|
||||||
capture_dir = pull_latest(args.serial, repo / args.pull_root)
|
|
||||||
else:
|
|
||||||
raise SystemExit("pass --capture-dir or --serial")
|
|
||||||
|
|
||||||
full_trace = capture_dir / "full.trace"
|
|
||||||
if not full_trace.exists():
|
|
||||||
raise SystemExit(f"missing trace: {full_trace}")
|
|
||||||
target_frame = choose_target_frame(capture_dir, args.target_frame)
|
|
||||||
work = capture_dir / "fixture-work"
|
|
||||||
if work.exists():
|
|
||||||
shutil.rmtree(work)
|
|
||||||
work.mkdir(parents=True)
|
|
||||||
|
|
||||||
frames_txt = work / "frames.txt"
|
|
||||||
frames_txt.write_text(run([args.apitrace, "dump", "--calls=frame", str(full_trace)], capture=True), encoding="utf-8")
|
|
||||||
|
|
||||||
trimmed = work / "trace.trace"
|
|
||||||
run([args.apitrace, "gltrim", "-f", str(target_frame), "--output", str(trimmed), str(full_trace)])
|
|
||||||
|
|
||||||
supplied_golden = Path(args.golden).resolve() if args.golden else None
|
|
||||||
target_call = choose_target_call(args.target_call, supplied_golden)
|
|
||||||
if args.skip_desktop_golden:
|
|
||||||
if supplied_golden is None or target_call is None:
|
|
||||||
raise SystemExit("--skip-desktop-golden requires --golden and --target-call")
|
|
||||||
golden = supplied_golden
|
|
||||||
else:
|
|
||||||
golden_dir = work / "golden"
|
|
||||||
golden_dir.mkdir()
|
|
||||||
prefix = golden_dir / f"{args.case}."
|
|
||||||
run([args.apitrace, "replay", "--headless", "--snapshot-prefix", str(prefix), "--call-nos", str(trimmed)])
|
|
||||||
golden = choose_snapshot(golden_dir)
|
|
||||||
target_call = choose_target_call(args.target_call, golden)
|
|
||||||
if target_call is None:
|
|
||||||
raise SystemExit(f"cannot infer target call from {golden}")
|
|
||||||
|
|
||||||
fixtures = (repo / args.fixtures_dir).resolve()
|
|
||||||
fixtures.mkdir(parents=True, exist_ok=True)
|
|
||||||
archive_root = work / "archive"
|
|
||||||
archive_root.mkdir()
|
|
||||||
shutil.copy2(trimmed, archive_root / "trace.trace")
|
|
||||||
tgz = fixtures / f"{args.case}.tgz"
|
|
||||||
with tarfile.open(tgz, "w:gz") as tar:
|
|
||||||
tar.add(archive_root / "trace.trace", arcname="trace.trace")
|
|
||||||
archive_size = tgz.stat().st_size
|
|
||||||
if archive_size > args.max_archive_bytes:
|
|
||||||
raise SystemExit(
|
|
||||||
f"{tgz} is {archive_size} bytes, over the {args.max_archive_bytes} byte fixture limit; "
|
|
||||||
"choose an earlier/smaller frame and re-run gltrim"
|
|
||||||
)
|
|
||||||
golden_out = fixtures / f"{args.case}.{target_call:010d}.png"
|
|
||||||
shutil.copy2(golden, golden_out)
|
|
||||||
|
|
||||||
manifest = {
|
|
||||||
"name": args.case,
|
|
||||||
"trace_archive": tgz.name,
|
|
||||||
"trace_file": "trace.trace",
|
|
||||||
"golden": golden_out.name,
|
|
||||||
"target_call": target_call,
|
|
||||||
"width": args.width,
|
|
||||||
"height": args.height,
|
|
||||||
"ssim_threshold": float(args.ssim_threshold),
|
|
||||||
"archive_size": archive_size,
|
|
||||||
}
|
|
||||||
manifest_path = capture_dir / f"{args.case}.fixture.json"
|
|
||||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
||||||
print(json.dumps(manifest, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+4
-107
@@ -1,8 +1,3 @@
|
|||||||
---
|
|
||||||
name: trace-fixture-authoring
|
|
||||||
description: Author a deterministic MobileGL trace-replay fixture from a captured apitrace - build the in-tree apitrace fork, capture a reproducible scene, frame-trim with gltrim, generate and verify a golden image, package under the archive-size budget, register the case in trace_cases.json, and validate on Linux and Android. Use when adding or re-trimming a trace_replay regression fixture.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Trace fixture authoring
|
# Trace fixture authoring
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
@@ -80,19 +75,10 @@ Minecraft specifics that keep the capture deterministic and small:
|
|||||||
`doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player
|
`doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player
|
||||||
`Rotation` that frames the intended subject. The camera snaps to the saved
|
`Rotation` that frames the intended subject. The camera snaps to the saved
|
||||||
rotation on world join, so composition is edited in the save, not in-game.
|
rotation on world join, so composition is edited in the save, not in-game.
|
||||||
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), a small
|
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), and a
|
||||||
`renderDistance` (3), and the capture resolution pinned to `WIDTH` x `HEIGHT`
|
small `renderDistance` (3). Frame rate and render distance are the two main
|
||||||
(854x480) via `overrideWidth`/`overrideHeight` (or `--width`/`--height`).
|
levers on trace size; a ~35 s in-world session at 10 fps lands well under
|
||||||
These are the main levers on fixture size: a low frame rate keeps the full
|
the archive budget after repack.
|
||||||
trace short, a small render distance keeps per-frame geometry down, and the
|
|
||||||
854x480 resolution keeps every render target the frame references small (a
|
|
||||||
trimmed frame's framebuffer/attachment textures scale with resolution
|
|
||||||
squared). A ~35 s in-world session at 10 fps and 854x480 lands well under the
|
|
||||||
archive budget after repack.
|
|
||||||
- `maxFps` has a practical floor: Minecraft ignores values below ~10 and falls
|
|
||||||
back to unlimited/vsync (a `maxFps:1` capture rendered ~60 fps and ballooned
|
|
||||||
the trace). 10 is as low as this lever goes, so do not count on a lower frame
|
|
||||||
rate to shrink the frame count further.
|
|
||||||
- Enter the world non-interactively with `--quickPlaySingleplayer <world>` so
|
- Enter the world non-interactively with `--quickPlaySingleplayer <world>` so
|
||||||
every capture takes the same path from boot to gameplay.
|
every capture takes the same path from boot to gameplay.
|
||||||
- Keep the game window UNFOCUSED for the whole capture (focus the desktop
|
- Keep the game window UNFOCUSED for the whole capture (focus the desktop
|
||||||
@@ -125,61 +111,6 @@ For Java:
|
|||||||
An `@argfile` with the full JVM+game command line keeps the invocation
|
An `@argfile` with the full JVM+game command line keeps the invocation
|
||||||
reproducible across recaptures.
|
reproducible across recaptures.
|
||||||
|
|
||||||
NEVER put a real credential on the traced command line. apitrace records the
|
|
||||||
traced process's argv into the trace as a `process.commandLine` property, so
|
|
||||||
anything passed there - `--accessToken`, session tokens, API keys - is embedded
|
|
||||||
in the trace and ships inside the committed fixture. Minecraft never validates
|
|
||||||
`--accessToken` for singleplayer, so pass a placeholder (`--accessToken 0`);
|
|
||||||
`--username`/`--uuid` are public and may stay real. Before packaging, grep the
|
|
||||||
UNCOMPRESSED trace for the secret to confirm it is absent:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
"$APITRACE" repack "$WORK/$CASE/trace.trace" /tmp/plain.trace # decompress
|
|
||||||
grep -ac "<secret-prefix>" /tmp/plain.trace # must be 0
|
|
||||||
```
|
|
||||||
|
|
||||||
If a secret has already been captured, it can be scrubbed in place instead of
|
|
||||||
recapturing: apitrace's snappy container is `[length][raw snappy]` chunks with
|
|
||||||
no checksum, and a high-entropy secret is stored as literal bytes, so replacing
|
|
||||||
those bytes with an EQUAL-LENGTH filler keeps the container valid and leaves the
|
|
||||||
GL call stream byte-identical. Blank every maximal run of the secret (it splits
|
|
||||||
across chunks), then verify: frame count unchanged, the decompressed trace no
|
|
||||||
longer contains the secret, and the replayed target frame still matches the
|
|
||||||
golden. Treat any already-pushed trace as leaked regardless - rotate the
|
|
||||||
credential, since a force-push does not purge the LFS object from the remote.
|
|
||||||
|
|
||||||
Watch for vendor-gated shader paths. Shader packs branch on the GL vendor that
|
|
||||||
Iris injects (`MC_GL_VENDOR_NVIDIA` / `_AMD` / ...) and compile a
|
|
||||||
vendor-exclusive path, so capturing on an NVIDIA card can bake NVIDIA-only GLSL
|
|
||||||
into the fixture (iterationRP selects `subgroupPartitionNV` /
|
|
||||||
`GL_NV_shader_subgroup_partitioned` instead of the portable
|
|
||||||
`subgroupShuffleXor`). Iris resolves the `#ifdef` before `glShaderSource`, so
|
|
||||||
only the taken branch is in the trace and the fixture cannot replay on the
|
|
||||||
mobile GPUs MobileGL targets. Rather than hunting for a second GPU (the Windows
|
|
||||||
per-app GPU preference does NOT change which OpenGL ICD is loaded), mask the
|
|
||||||
vendor at capture time with apitrace's own config - point `GLTRACE_CONF` at a
|
|
||||||
file containing:
|
|
||||||
|
|
||||||
```
|
|
||||||
GL_VENDOR = "NoVIDIA (MobileGL spoof)"
|
|
||||||
GL_RENDERER = "NoVIDIA (MobileGL spoof)"
|
|
||||||
```
|
|
||||||
|
|
||||||
The wrapper then returns that from `glGetString`, so the pack compiles the
|
|
||||||
portable path while still running on the fast driver. Pick a string that does
|
|
||||||
NOT contain the real vendor name as a substring (Iris matches by substring, so
|
|
||||||
"Not NVIDIA ..." would still match) and that is self-describing, so nobody later
|
|
||||||
mistakes the trace for a capture on different hardware. Afterwards, grep the
|
|
||||||
decoded trace to confirm the vendor-exclusive symbols are gone:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
"$APITRACE" dump "$WORK/$CASE/full.trace" | grep -c subgroupPartitionNV # must be 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Software rasterisers are not a substitute here: llvmpipe exposes no
|
|
||||||
`GL_KHR_shader_subgroup` at all, and packs that use subgroup ops unguarded
|
|
||||||
cannot run on it in any vendor configuration.
|
|
||||||
|
|
||||||
Keep `full.trace` until both backends are validated.
|
Keep `full.trace` until both backends are validated.
|
||||||
|
|
||||||
Persistent-mapped buffers: apps may legally write a `GL_MAP_PERSISTENT_BIT`
|
Persistent-mapped buffers: apps may legally write a `GL_MAP_PERSISTENT_BIT`
|
||||||
@@ -241,37 +172,12 @@ name"-style retrace warnings. If content is missing from the trimmed trace
|
|||||||
but present in the full trace, the fix belongs in `3rdparty/apitrace`'s
|
but present in the full trace, the fix belongs in `3rdparty/apitrace`'s
|
||||||
frametrim, not in the fixture.
|
frametrim, not in the fixture.
|
||||||
|
|
||||||
Temporal shaders (auto-exposure / eye adaptation, TAA, temporal reflections -
|
|
||||||
e.g. the iterationRP shader pack) break a single-frame `gltrim -f`: the target
|
|
||||||
frame reads its predecessors' feedback buffers, which the isolated frame no
|
|
||||||
longer contains, so a mid-sequence frame replays overexposed to white (and any
|
|
||||||
timed name/version overlay the pack draws in its first seconds silently drops).
|
|
||||||
The symptom is a trimmed frame that looks blown-out or washed while the same
|
|
||||||
frame of `full.trace` renders correctly, and it gets worse the later the frame.
|
|
||||||
When a single-frame trim of such a pack cannot be made to render correctly, keep
|
|
||||||
the temporal history instead of the dependency slice: select an early in-world
|
|
||||||
target frame and trim a PREFIX with `apitrace trim --calls=0-<target-swap-call>`
|
|
||||||
(it preserves call numbers, so `target_call` is just that swap call). The prefix
|
|
||||||
replays every frame up to the target, so its temporal buffers are correct.
|
|
||||||
Prefer the earliest frame that already shows the intended subject - fewer lead-in
|
|
||||||
frames means a smaller archive and a faster CI replay. This deviates from the
|
|
||||||
single-frame rule deliberately; note it in the README entry.
|
|
||||||
|
|
||||||
## Generate golden
|
## Generate golden
|
||||||
|
|
||||||
Generate frame snapshots from the trimmed trace, then choose the snapshot that
|
Generate frame snapshots from the trimmed trace, then choose the snapshot that
|
||||||
matches the selected frame. The target call used by replay registration must
|
matches the selected frame. The target call used by replay registration must
|
||||||
come from the trimmed trace, not from a call-filtered full-trace selection.
|
come from the trimmed trace, not from a call-filtered full-trace selection.
|
||||||
|
|
||||||
Generate the golden with the same GL stack the scene was captured on. A headless
|
|
||||||
software renderer (llvmpipe) is fine for vanilla and light packs, but heavy
|
|
||||||
ray-traced shader packs (compute-driven atmosphere LUTs, screen-space tracing -
|
|
||||||
e.g. iterationRP) render as solid black or blown-out white under llvmpipe. Drive
|
|
||||||
the golden from a real GPU instead: on Windows a stock `glretrace.exe` (an
|
|
||||||
upstream apitrace release works for replay even on an in-tree-fork trace) replays
|
|
||||||
the trace on the discrete GPU and snapshots the target call. Read the resulting
|
|
||||||
PNG back and confirm the subject actually rendered before trusting it as golden.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir -p "$WORK/$CASE/golden"
|
mkdir -p "$WORK/$CASE/golden"
|
||||||
"$APITRACE" replay --headless \
|
"$APITRACE" replay --headless \
|
||||||
@@ -349,15 +255,6 @@ Check the final archive size. The committed fixture archive should be less than
|
|||||||
with a shorter run or a lower frame rate / render distance instead of adding
|
with a shorter run or a lower frame rate / render distance instead of adding
|
||||||
call-based filtering.
|
call-based filtering.
|
||||||
|
|
||||||
Some packs have an irreducible size floor: a large static lookup table baked
|
|
||||||
into the pack (iterationRP ships a ~17 MiB half-float atmosphere LUT that the
|
|
||||||
target frame samples) lands in the trace once and does not compress, so every
|
|
||||||
variant - single frame, prefix, or full - sits near the same size regardless of
|
|
||||||
frame count. When the floor alone exceeds the budget, neither a lower frame rate
|
|
||||||
nor fewer frames helps; confirm the fixture is worth the exception and record the
|
|
||||||
measured size in the case's README entry rather than chasing an unreachable
|
|
||||||
target.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
du -h "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
|
du -h "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
|
||||||
tar -tzf "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
|
tar -tzf "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "MobileGL Trace Fixture Authoring"
|
|
||||||
short_description: "Author and register a MobileGL trace-replay fixture"
|
|
||||||
default_prompt: "Use $trace-fixture-authoring to author and register a MobileGL trace replay fixture."
|
|
||||||
@@ -276,11 +276,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "improved-transparency-minecraft-26.3",
|
"name": "improved-transparency-minecraft-26.3",
|
||||||
|
"ci": false,
|
||||||
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
|
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
|
||||||
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
|
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
|
||||||
"target_call": 2667619,
|
"target_call": 2667619,
|
||||||
"timeout_seconds": 1800,
|
"timeout_seconds": 1800,
|
||||||
"ssim_threshold": 0.995
|
"coherent_as_flush": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world",
|
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world",
|
||||||
|
|||||||
Reference in New Issue
Block a user