mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 22:58:30 +09:00
Compare commits
4
Commits
itrp
...
aa2184e47a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2184e47a | ||
|
|
9152a4a4bc | ||
|
|
1963b427db | ||
|
|
f3def150e7 |
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "PipelineFactory.h"
|
#include "PipelineFactory.h"
|
||||||
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||||
switch (topology) {
|
switch (topology) {
|
||||||
@@ -108,6 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
"vkCreatePipelineCache");
|
"vkCreatePipelineCache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
|
||||||
|
s_suppressBlendedDepthWrite = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
PipelineFactory::~PipelineFactory() {
|
PipelineFactory::~PipelineFactory() {
|
||||||
DestroyAll();
|
DestroyAll();
|
||||||
if (m_pipelineCache != VK_NULL_HANDLE) {
|
if (m_pipelineCache != VK_NULL_HANDLE) {
|
||||||
@@ -258,6 +263,18 @@ 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 blended pipelines when the active driver cannot keep
|
||||||
|
// vertex positions invariant across the pipelines of a multi-pass depth-equality
|
||||||
|
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
|
||||||
|
// and the equality-dependent prepass pattern is exactly the case that breaks.
|
||||||
|
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
|
||||||
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
|
if (colorAttachments[i].blendEnable == VK_TRUE) {
|
||||||
|
depthStencil.depthWriteEnable = VK_FALSE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ 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 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.
|
||||||
|
// Set at renderer initialization based on the active driver.
|
||||||
|
static void SetSuppressBlendedDepthWrite(Bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||||
|
|
||||||
@@ -70,5 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
|
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -1697,6 +1697,22 @@ 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
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#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>
|
||||||
@@ -947,6 +950,7 @@ 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(),
|
||||||
@@ -1884,6 +1888,28 @@ 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 blended depth writes there; the env variable
|
||||||
|
// forces the quirk on ("0") or off ("1") on any driver.
|
||||||
|
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||||
|
Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm;
|
||||||
|
if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) {
|
||||||
|
if (env[0] == '0') {
|
||||||
|
suppressBlendedDepthWrite = true;
|
||||||
|
} else if (env[0] == '1') {
|
||||||
|
suppressBlendedDepthWrite = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (suppressBlendedDepthWrite) {
|
||||||
|
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
|
||||||
|
"(driver lacks cross-pipeline position invariance)");
|
||||||
|
}
|
||||||
|
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);
|
||||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "ShaderSourceProcessor.h"
|
#include "ShaderSourceProcessor.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 {
|
||||||
@@ -322,6 +323,75 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
||||||
spvc_compiler_options options;
|
spvc_compiler_options options;
|
||||||
session.CreateOptions(&options);
|
session.CreateOptions(&options);
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ 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);
|
||||||
static Result<String> DecompileShader(SpvcSession& session);
|
static Result<String> DecompileShader(SpvcSession& session);
|
||||||
};
|
};
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ The bundled fixtures cover:
|
|||||||
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
|
||||||
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
|
iterationT after entering a singleplayer world, with Iris' DSA path disabled.
|
||||||

|

|
||||||
|
- minecraft-1.21.4-fabric-iris-iterationrp-novidia-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.
|
||||||

|

|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Capture RenderDoc Trace Frame"
|
|
||||||
short_description: "Capture exact Android Vulkan/GLES trace frames"
|
|
||||||
default_prompt: "Use $renderdoc-capture-trace-frame to capture and validate an exact Android retrace frame."
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# MobileGL trace-replay skills
|
||||||
|
|
||||||
|
Task-focused skills for capturing, replaying, debugging, and authoring MobileGL
|
||||||
|
apitrace fixtures. Each skill is a self-contained package:
|
||||||
|
|
||||||
|
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
|
||||||
|
directory name equals the frontmatter `name`.
|
||||||
|
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
|
||||||
|
`short_description`, `default_prompt`).
|
||||||
|
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
|
||||||
|
skill has them.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
| Skill | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| [trace-fixture-authoring-on-android-fcl](trace-fixture-authoring-on-android-fcl/SKILL.md) | Capture an on-device Android apitrace from FCL's MobileGL renderers (DirectGLES / Magma / SimpleFPEWrapper), mark the defect frame, and pull `full.trace`. |
|
||||||
|
| [renderdoc-debug-on-trace-replay](renderdoc-debug-on-trace-replay/SKILL.md) | Capture and validate an exact frame from a MobileGL retrace on a connected Android device with RenderDoc / rdc-cli. |
|
||||||
|
| [mismatch-retrace-debugging](mismatch-retrace-debugging/SKILL.md) | Localize the first divergent render pass and draw call when a fixture replays correctly in a golden environment but renders differently on a target backend. |
|
||||||
|
| [trace-fixture-authoring](trace-fixture-authoring/SKILL.md) | Author a deterministic trace-replay fixture — trim, golden, package under the size budget, register in `trace_cases.json`, and validate on Linux and Android. |
|
||||||
+5
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: mismatch-retrace-debugging
|
||||||
|
description: Localize the first divergent render pass and draw call when a MobileGL apitrace fixture replays correctly in a golden environment but renders differently under mobilegl_trace_replay, Android trace replay, or another backend. Use to binary-search pass/draw endpoints, diff GL state around the first bad call, and classify the fault as a vertex/VS, fragment/FS, or framebuffer/composition mismatch.
|
||||||
|
---
|
||||||
|
|
||||||
# Mismatch retrace debugging
|
# Mismatch retrace debugging
|
||||||
|
|
||||||
Use this when an apitrace fixture replays correctly on one environment but
|
Use this when an apitrace fixture replays correctly on one environment but
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "MobileGL Mismatch Retrace Debugging"
|
||||||
|
short_description: "Localize the first divergent draw in a mismatching MobileGL retrace"
|
||||||
|
default_prompt: "Use $mismatch-retrace-debugging to find the first divergent render pass and draw call in a MobileGL retrace mismatch."
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: renderdoc-capture-trace-frame
|
name: renderdoc-debug-on-trace-replay
|
||||||
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
|
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,20 +19,20 @@ adb -s SERIAL shell pm path top.mobilegl.plugin.trace
|
|||||||
rdc doctor
|
rdc doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/capture_android_retrace.py`.
|
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py`.
|
||||||
|
|
||||||
## Capture
|
## Capture
|
||||||
|
|
||||||
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
|
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
|
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Change only the backend and output for GLES:
|
Change only the backend and output for GLES:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
|
python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
|
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "RenderDoc Debug on Trace Replay"
|
||||||
|
short_description: "Capture and debug an exact MobileGL trace-replay frame in RenderDoc"
|
||||||
|
default_prompt: "Use $renderdoc-debug-on-trace-replay to capture and validate an exact Android trace-replay frame in RenderDoc."
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## TargetControl timing
|
## TargetControl timing
|
||||||
|
|
||||||
- Start `tools/trace_replay/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
|
- Start `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
|
||||||
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
|
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
|
||||||
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
|
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
|
||||||
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
|
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Build output produced by build_android_egltrace.py (ABI-specific, regenerated).
|
||||||
|
out/
|
||||||
|
__pycache__/
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/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
@@ -0,0 +1,191 @@
|
|||||||
|
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
@@ -0,0 +1,234 @@
|
|||||||
|
#!/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
@@ -0,0 +1,168 @@
|
|||||||
|
#!/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()
|
||||||
+107
-4
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -75,10 +80,19 @@ 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), and a
|
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), a small
|
||||||
small `renderDistance` (3). Frame rate and render distance are the two main
|
`renderDistance` (3), and the capture resolution pinned to `WIDTH` x `HEIGHT`
|
||||||
levers on trace size; a ~35 s in-world session at 10 fps lands well under
|
(854x480) via `overrideWidth`/`overrideHeight` (or `--width`/`--height`).
|
||||||
the archive budget after repack.
|
These are the main levers on fixture size: a low frame rate keeps the full
|
||||||
|
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
|
||||||
@@ -111,6 +125,61 @@ 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`
|
||||||
@@ -172,12 +241,37 @@ 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 \
|
||||||
@@ -255,6 +349,15 @@ 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"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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,12 +276,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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
|
||||||
"coherent_as_flush": true
|
},
|
||||||
|
{
|
||||||
|
"name": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world",
|
||||||
|
"ci": false,
|
||||||
|
"trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.tgz",
|
||||||
|
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.0000202020.png",
|
||||||
|
"target_call": 202020,
|
||||||
|
"timeout_seconds": 1800
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user