Files
alvinshi 5e2a01e379 fix(renderdoc): compute PSO, REPL capture path cache, drop unused imports
- pipeline: use GetComputePipelineObject for ShaderStage.Compute in disasm, cbuffer, reflection export, dump/diff

- cli: invalidate cached CaptureHandle when capture_path changes (REPL -c)

- capture: remove unused struct/json/Tuple imports

Made-with: Cursor
2026-03-25 20:59:28 +08:00

924 lines
31 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
RenderDoc CLI - Command-line interface for RenderDoc graphics debugger.
Provides headless access to RenderDoc capture analysis:
- Inspect capture metadata and sections
- List and search draw calls / actions
- Inspect pipeline state at any event
- List, inspect, and export textures
- Read buffer and mesh data
- Query GPU performance counters
- Pick pixel values
Usage:
renderdoc-cli [OPTIONS] COMMAND [ARGS]...
All commands support --json for machine-readable output.
"""
from __future__ import annotations
import json
import os
import sys
from typing import Optional
import click
# ---------------------------------------------------------------------------
# Lazy import helpers we don't want to import renderdoc at CLI parse time
# ---------------------------------------------------------------------------
_capture_handle = None # type: ignore
_capture_handle_path = None # type: ignore
_repl_mode = False
def _close_all_captures():
global _capture_handle, _capture_handle_b, _capture_handle_path, _capture_handle_b_path
if _capture_handle is not None:
_capture_handle.close()
_capture_handle = None
_capture_handle_path = None
if _capture_handle_b is not None:
_capture_handle_b.close()
_capture_handle_b = None
_capture_handle_b_path = None
def _get_export_dir(ctx: click.Context, subfolder: str = "") -> str:
"""Return the default export directory for the current capture.
Layout: <capture_dir>/<stem>_exported/<subfolder>/
e.g. tests/pc_exported/shaders/
"""
capture_path = ctx.obj.get("capture_path", "capture")
capture_dir = os.path.dirname(os.path.abspath(capture_path))
stem = os.path.splitext(os.path.basename(capture_path))[0]
export_dir = os.path.join(capture_dir, "%s_exported" % stem)
if subfolder:
export_dir = os.path.join(export_dir, subfolder)
os.makedirs(export_dir, exist_ok=True)
return export_dir
def _get_handle(ctx: click.Context):
"""Return the active CaptureHandle, opening it if needed."""
global _capture_handle, _capture_handle_path
path = ctx.obj.get("capture_path")
if not path:
click.echo("Error: No capture file specified. Use --capture <path>", err=True)
ctx.exit(1)
path_abs = os.path.abspath(path)
if _capture_handle is not None:
if _capture_handle_path == path_abs:
return _capture_handle
_capture_handle.close()
_capture_handle = None
_capture_handle_path = None
from cli_anything.renderdoc.core.capture import CaptureHandle
from cli_anything.renderdoc.utils.errors import handle_error
try:
_capture_handle = CaptureHandle(path)
_capture_handle_path = path_abs
except Exception as e:
debug = ctx.obj.get("debug", False)
err = handle_error(e, debug=debug)
if ctx.obj.get("json_mode"):
from cli_anything.renderdoc.utils.output import output_json
output_json(err)
ctx.exit(1)
else:
msg = "Failed to open capture: %s" % err["error"]
if debug and "traceback" in err:
msg += "\n" + err["traceback"]
raise click.ClickException(msg)
return _capture_handle
def _output(ctx: click.Context, data, human_fn=None):
"""Output data as JSON or human-readable."""
if ctx.obj.get("json_mode"):
from cli_anything.renderdoc.utils.output import output_json
output_json(data)
elif human_fn:
human_fn(data)
else:
from cli_anything.renderdoc.utils.output import output_json
output_json(data)
# ===========================================================================
# Root group
# ===========================================================================
@click.group(invoke_without_command=True)
@click.option(
"--capture", "-c",
type=click.Path(exists=False),
envvar="RENDERDOC_CAPTURE",
help="Path to .rdc capture file.",
)
@click.option("--json", "json_mode", is_flag=True, help="Output in JSON format.")
@click.option("--debug", is_flag=True, help="Show debug tracebacks on errors.")
@click.version_option(package_name="cli-anything-renderdoc")
@click.pass_context
def cli(ctx, capture, json_mode, debug):
"""RenderDoc CLI headless capture analysis tool.
Run without a subcommand to enter interactive REPL mode.
"""
ctx.ensure_object(dict)
# Preserve REPL session state: nested `cli.main(...)` omits global options, so
# only overwrite capture when the user passed `-c` on that invocation.
if capture is not None:
ctx.obj["capture_path"] = capture
ctx.obj["json_mode"] = json_mode
ctx.obj["debug"] = debug
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
# ===========================================================================
# capture commands
# ===========================================================================
@cli.group("capture")
def capture_group():
"""Capture file operations."""
pass
@capture_group.command("info")
@click.pass_context
def capture_info(ctx):
"""Show capture file metadata and sections."""
handle = _get_handle(ctx)
meta = handle.metadata()
meta["sections"] = handle.list_sections()
def _human(data):
click.echo(f"Capture: {data['path']}")
click.echo(f"API: {data['api']}")
click.echo(f"Replay: {'yes' if data['replay_supported'] else 'no'}")
click.echo(f"\nSections ({len(data['sections'])}):")
for s in data["sections"]:
click.echo(f" [{s['index']}] {s['name']} ({s['type']}) - {s['uncompressed_size']} bytes")
_output(ctx, meta, _human)
@capture_group.command("thumb")
@click.option("--output", "-o", required=True, type=click.Path(), help="Output image path.")
@click.option("--max-dim", default=0, type=int, help="Max thumbnail dimension (0 = original).")
@click.pass_context
def capture_thumb(ctx, output, max_dim):
"""Extract capture thumbnail to an image file."""
handle = _get_handle(ctx)
result = handle.thumbnail(output, max_dim)
_output(ctx, result)
@capture_group.command("convert")
@click.option("--output", "-o", required=True, type=click.Path(), help="Output file path.")
@click.option("--format", "fmt", default="", help="Target format (default: rdc).")
@click.pass_context
def capture_convert(ctx, output, fmt):
"""Convert capture to a different format."""
handle = _get_handle(ctx)
result = handle.convert(output, fmt)
_output(ctx, result)
# ===========================================================================
# action commands
# ===========================================================================
@cli.group("actions")
def actions_group():
"""Draw call / action inspection."""
pass
@actions_group.command("list")
@click.option("--flat/--no-flat", default=True, help="Flat list vs root-only.")
@click.option("--draws-only", is_flag=True, help="Only show actual draw calls.")
@click.pass_context
def actions_list(ctx, flat, draws_only):
"""List all actions (draw calls, clears, etc.) in the capture."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.actions import list_actions, get_drawcalls_only
if draws_only:
data = get_drawcalls_only(handle.controller)
else:
data = list_actions(handle.controller, flat=flat)
def _human(actions):
click.echo(f"Total actions: {len(actions)}")
for a in actions:
indent = " " * a.get("depth", 0)
flags = ",".join(a["flags"]) if a["flags"] else ""
click.echo(f"{indent}[{a['eventId']:>5}] {a['name']:<50} {flags}")
_output(ctx, data, _human)
@actions_group.command("summary")
@click.pass_context
def actions_summary(ctx):
"""Show action count summary by type."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.actions import action_summary
data = action_summary(handle.controller)
def _human(d):
click.echo("Action Summary:")
for k, v in d.items():
click.echo(f" {k}: {v}")
_output(ctx, data, _human)
@actions_group.command("find")
@click.argument("pattern")
@click.pass_context
def actions_find(ctx, pattern):
"""Find actions by name pattern (case-insensitive)."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.actions import find_actions_by_name
data = find_actions_by_name(handle.controller, pattern)
_output(ctx, data)
@actions_group.command("get")
@click.argument("event_id", type=int)
@click.pass_context
def actions_get(ctx, event_id):
"""Get details of a single action by eventId."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.actions import find_action_by_event
data = find_action_by_event(handle.controller, event_id)
if data is None:
data = {"error": f"No action found with eventId={event_id}"}
_output(ctx, data)
# ===========================================================================
# texture commands
# ===========================================================================
@cli.group("textures")
def textures_group():
"""Texture inspection and export."""
pass
@textures_group.command("list")
@click.pass_context
def textures_list(ctx):
"""List all textures in the capture."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.textures import list_textures
data = list_textures(handle.controller)
def _human(textures):
click.echo(f"Total textures: {len(textures)}")
for t in textures:
click.echo(
f" [{t['resourceId']}] {t['width']}x{t['height']} "
f"mips={t['mips']} fmt={t['format']}"
)
_output(ctx, data, _human)
@textures_group.command("get")
@click.argument("resource_id")
@click.pass_context
def textures_get(ctx, resource_id):
"""Get details of a single texture by resource ID."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.textures import get_texture
data = get_texture(handle.controller, resource_id)
if data is None:
data = {"error": f"Texture {resource_id} not found"}
_output(ctx, data)
@textures_group.command("save")
@click.argument("resource_id")
@click.option("--output", "-o", required=True, type=click.Path(), help="Output file path.")
@click.option("--format", "fmt", default="png", help="Image format: png, jpg, bmp, tga, hdr, exr, dds.")
@click.option("--mip", default=0, type=int, help="Mip level (-1 for all, DDS only).")
@click.option("--slice", "slice_idx", default=0, type=int, help="Array slice (-1 for all, DDS only).")
@click.option("--alpha", default="preserve", help="Alpha: preserve, discard, blend_checkerboard.")
@click.pass_context
def textures_save(ctx, resource_id, output, fmt, mip, slice_idx, alpha):
"""Save a texture to an image file."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.textures import save_texture
data = save_texture(handle.controller, resource_id, output, fmt, mip, slice_idx, alpha)
_output(ctx, data)
@textures_group.command("save-outputs")
@click.argument("event_id", type=int)
@click.option("--output-dir", "-o", required=True, type=click.Path(), help="Output directory.")
@click.option("--format", "fmt", default="png", help="Image format.")
@click.pass_context
def textures_save_outputs(ctx, event_id, output_dir, fmt):
"""Save all render target outputs at a specific event."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.textures import save_action_outputs
data = save_action_outputs(handle.controller, event_id, output_dir, fmt)
_output(ctx, data)
@textures_group.command("pick")
@click.argument("resource_id")
@click.argument("x", type=int)
@click.argument("y", type=int)
@click.option("--mip", default=0, type=int)
@click.option("--slice", "slice_idx", default=0, type=int)
@click.pass_context
def textures_pick(ctx, resource_id, x, y, mip, slice_idx):
"""Pick a pixel value from a texture."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.textures import pick_pixel
data = pick_pixel(handle.controller, resource_id, x, y, mip, slice_idx)
_output(ctx, data)
# ===========================================================================
# pipeline commands
# ===========================================================================
@cli.group("pipeline")
def pipeline_group():
"""Pipeline state inspection."""
pass
@pipeline_group.command("state")
@click.argument("event_id", type=int)
@click.pass_context
def pipeline_state(ctx, event_id):
"""Show full pipeline state at a specific event."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.pipeline import get_pipeline_state
data = get_pipeline_state(handle.controller, event_id)
_output(ctx, data)
@pipeline_group.command("shader-export")
@click.argument("event_id", type=int)
@click.option("--stage", default="Fragment", help="Shader stage: Vertex, Fragment, Compute, etc.")
@click.option("-o", "--output", "output_dir", default=None,
help="Output directory. Default: <capture>_exported/shaders/")
@click.pass_context
def pipeline_shader_export(ctx, event_id, stage, output_dir):
"""Export shader source in human-readable form.
For text shaders (GLSL, HLSL, Slang) the raw bytes are already
readable — they are saved directly.
For binary shaders (DXBC, SPIR-V, DXIL) the tool tries, in order:
\b
1. Embedded debug source (HLSL/GLSL compiled with /Zi)
2. RenderDoc disassembly (bytecode asm)
The raw binary is always saved alongside for completeness.
\b
Default output: <capture>_exported/shaders/
"""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.pipeline import export_shader
if output_dir is None:
output_dir = _get_export_dir(ctx, "shaders")
data = export_shader(handle.controller, event_id, stage, output_dir=output_dir)
def _human(d):
if "error" in d:
click.echo("Error: %s" % d["error"])
return
click.echo(" Encoding: %s" % d["encoding"])
click.echo(" Raw: %s" % d["raw_path"])
rp = d.get("readable_path")
if rp and rp != d["raw_path"]:
label = "Source" if d.get("readable_kind") == "source" else "Disassembly"
click.echo(" %s: %s" % (label.ljust(12), rp))
_output(ctx, data, _human)
@pipeline_group.command("dump-shader-reflection", hidden=True)
@click.argument("event_id", type=int)
@click.option("--stage", default="Fragment", help="Shader stage: Vertex, Fragment, Compute, etc.")
@click.option("-o", "--output", "output_dir", default=None, help="Output directory path.")
@click.pass_context
def pipeline_dump_shader_reflection(ctx, event_id, stage, output_dir):
"""Export complete ShaderReflection for a shader stage to a folder.
Creates a directory containing:
\b
reflection.json Full ShaderReflection (signatures, cbuffer layouts,
resource declarations, debug info with source)
bindings.json Runtime GPU bindings (bound resource IDs, offsets)
cbuffer_values.json Runtime constant buffer variable values
shader_raw.* Raw shader bytes (e.g. .dxbc, .glsl)
sources/ Debug source files (if compiled with debug info)
\b
Default output: <capture>_exported/shaders/<shader>_reflection/
"""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.pipeline import export_shader_reflection
if output_dir is None:
# Build default output_dir under the capture's export directory.
# We need the resourceId to name the folder, so do a quick probe first.
import renderdoc as rd
from cli_anything.renderdoc.core.pipeline import STAGE_MAP
stage_enum = STAGE_MAP.get(stage.lower())
if stage_enum is None:
_output(ctx, {"error": "Unknown stage: %s" % stage})
return
handle.controller.SetFrameEvent(event_id, True)
pipe = handle.controller.GetPipelineState()
refl = pipe.GetShaderReflection(stage_enum)
if refl is None:
_output(ctx, {"error": "No shader bound at stage %s for event %d" % (stage, event_id)})
return
rid_str = str(refl.resourceId).replace("::", "_")
shader_dir = _get_export_dir(ctx, "shaders")
output_dir = os.path.join(
shader_dir,
"shader_%s_%s_eid%d_reflection" % (rid_str, stage, event_id),
)
data = export_shader_reflection(
handle.controller, event_id, stage,
output_dir=output_dir,
)
def _human(d):
if "error" in d:
click.echo("Error: %s" % d["error"])
return
click.echo("Exported: %s" % d["output_dir"])
click.echo(" Stage: %s" % d["stage"])
click.echo(" ResourceId: %s" % d["resourceId"])
click.echo(" EntryPoint: %s" % d["entryPoint"])
click.echo(" Encoding: %s" % d["encoding"])
click.echo("")
click.echo(" Files:")
for f in d.get("files", []):
click.echo(" %s" % f)
src_files = d.get("source_files", [])
if src_files:
click.echo("")
click.echo(" Debug sources: %d files" % len(src_files))
for sf in src_files:
click.echo(" %s (%d bytes)" % (sf["original_path"], sf["size"]))
click.echo("")
click.echo(" CBuffers: %d, ReadOnly: %d, ReadWrite: %d, Samplers: %d" % (
d["constantBlocks_count"],
d["readOnlyResources_count"],
d["readWriteResources_count"],
d["samplers_count"],
))
_output(ctx, data, _human)
@pipeline_group.command("dump", hidden=True)
@click.argument("event_id", type=int)
@click.option("-o", "--output", "output_path", default=None, help="Output JSON file path.")
@click.pass_context
def pipeline_dump(ctx, event_id, output_path):
"""Dump full PipelineState + ShaderReflection at EVENT_ID to JSON.
Exports the complete pipeline state, shader reflection metadata for all
bound stages, and GPU runtime bindings. Intended for human debugging.
\b
Default output: <capture>_exported/pipeline_eid<EID>_dump.json
"""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.pipeline import dump_pipeline
data = dump_pipeline(handle.controller, event_id)
if output_path is None:
export_dir = _get_export_dir(ctx)
output_path = os.path.join(export_dir, "pipeline_eid%d_dump.json" % event_id)
output_path = os.path.abspath(output_path)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False, default=str)
if ctx.obj.get("json_mode"):
_output(ctx, {"path": output_path})
else:
click.echo(output_path)
@pipeline_group.command("cbuffer")
@click.argument("event_id", type=int)
@click.option("--stage", default="Fragment", help="Shader stage.")
@click.option("--index", "cbuffer_index", default=0, type=int, help="CBuffer index.")
@click.pass_context
def pipeline_cbuffer(ctx, event_id, stage, cbuffer_index):
"""Get constant buffer contents at a specific event."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.pipeline import get_cbuffer_contents
data = get_cbuffer_contents(handle.controller, event_id, stage, cbuffer_index)
_output(ctx, data)
# ===========================================================================
# resource commands
# ===========================================================================
@cli.group("resources")
def resources_group():
"""Resource (buffer/texture) listing and data reading."""
pass
@resources_group.command("list")
@click.pass_context
def resources_list(ctx):
"""List all resources in the capture."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.resources import list_resources
data = list_resources(handle.controller)
def _human(resources):
click.echo(f"Total resources: {len(resources)}")
for r in resources:
click.echo(f" [{r['resourceId']}] {r['type']}: {r['name']}")
_output(ctx, data, _human)
@resources_group.command("buffers")
@click.pass_context
def resources_buffers(ctx):
"""List all buffer resources."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.resources import list_buffers
data = list_buffers(handle.controller)
_output(ctx, data)
@resources_group.command("read-buffer")
@click.argument("resource_id")
@click.option("--offset", default=0, type=int, help="Byte offset.")
@click.option("--length", default=256, type=int, help="Number of bytes to read.")
@click.option("--format", "fmt", default="hex", help="Output format: hex, float32, uint32, raw.")
@click.pass_context
def resources_read_buffer(ctx, resource_id, offset, length, fmt):
"""Read raw buffer data."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.resources import get_buffer_data
data = get_buffer_data(handle.controller, resource_id, offset, length, fmt)
_output(ctx, data)
# ===========================================================================
# mesh commands
# ===========================================================================
@cli.group("mesh")
def mesh_group():
"""Mesh data (vertex inputs/outputs) inspection."""
pass
@mesh_group.command("inputs")
@click.argument("event_id", type=int)
@click.option("--max-vertices", default=100, type=int, help="Max vertices to decode.")
@click.pass_context
def mesh_inputs(ctx, event_id, max_vertices):
"""Get vertex shader inputs at a draw call."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.mesh import get_mesh_inputs
data = get_mesh_inputs(handle.controller, event_id, max_vertices)
_output(ctx, data)
@mesh_group.command("outputs")
@click.argument("event_id", type=int)
@click.option("--max-vertices", default=100, type=int, help="Max vertices to decode.")
@click.pass_context
def mesh_outputs(ctx, event_id, max_vertices):
"""Get post-vertex-shader outputs at a draw call."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.mesh import get_mesh_outputs
data = get_mesh_outputs(handle.controller, event_id, max_vertices)
_output(ctx, data)
# ===========================================================================
# counter commands
# ===========================================================================
@cli.group("counters")
def counters_group():
"""GPU performance counters."""
pass
@counters_group.command("list")
@click.pass_context
def counters_list(ctx):
"""List all available GPU counters."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.counters import list_counters
data = list_counters(handle.controller)
def _human(counters):
click.echo(f"Available GPU counters: {len(counters)}")
for c in counters:
click.echo(f" [{c['counter']}] {c['name']}: {c['description']}")
_output(ctx, data, _human)
@counters_group.command("fetch")
@click.option("--ids", default=None, help="Comma-separated counter IDs (default: SamplesPassed).")
@click.pass_context
def counters_fetch(ctx, ids):
"""Fetch GPU counter results."""
handle = _get_handle(ctx)
from cli_anything.renderdoc.core.counters import fetch_counters
counter_ids = None
if ids:
counter_ids = [int(i.strip()) for i in ids.split(",")]
data = fetch_counters(handle.controller, counter_ids)
_output(ctx, data)
# ===========================================================================
# pipeline diff (compare two events)
# ===========================================================================
# Secondary capture handle for diff B-side
_capture_handle_b = None # type: ignore
_capture_handle_b_path = None # type: ignore
def _get_handle_b(ctx: click.Context, path: str):
"""Open a second capture file for the B-side of a diff."""
global _capture_handle_b, _capture_handle_b_path
path_abs = os.path.abspath(path)
if _capture_handle_b is not None:
if _capture_handle_b_path == path_abs:
return _capture_handle_b
_capture_handle_b.close()
_capture_handle_b = None
_capture_handle_b_path = None
from cli_anything.renderdoc.core.capture import CaptureHandle
from cli_anything.renderdoc.utils.errors import handle_error
try:
_capture_handle_b = CaptureHandle(path)
_capture_handle_b_path = path_abs
except Exception as e:
debug = ctx.obj.get("debug", False)
err = handle_error(e, debug=debug)
if ctx.obj.get("json_mode"):
from cli_anything.renderdoc.utils.output import output_json
output_json(err)
ctx.exit(1)
else:
msg = "Failed to open capture-b: %s" % err["error"]
if debug and "traceback" in err:
msg += "\n" + err["traceback"]
raise click.ClickException(msg)
return _capture_handle_b
@pipeline_group.command("diff")
@click.argument("event_a", type=int)
@click.argument("event_b", type=int)
@click.option(
"--capture-b", "-b",
type=click.Path(exists=False),
default=None,
help="Path to second .rdc capture (default: same as --capture).",
)
@click.option(
"--compact/--no-compact",
default=True,
help="Omit identical sections (default: compact).",
)
@click.option(
"--output", "-o",
type=click.Path(),
default=None,
help="Output JSON path. Default: auto-generated next to capture.",
)
@click.pass_context
def pipeline_diff_cmd(ctx, event_a, event_b, capture_b, compact, output):
"""Compare pipeline state at EVENT_A vs EVENT_B.
By default both events come from the same capture (--capture).
Use --capture-b / -b to specify a second capture file.
Results are written to a JSON file; only the path is printed to stdout.
\b
Examples:
# Two events in different captures
cli-anything-renderdoc -c a.rdc pipeline diff 100 200 -b b.rdc
# Two events in the same capture
cli-anything-renderdoc -c frame.rdc pipeline diff 100 200
# Custom output path
cli-anything-renderdoc -c a.rdc pipeline diff 100 200 -b b.rdc -o result.json
"""
handle_a = _get_handle(ctx)
if capture_b:
handle_b = _get_handle_b(ctx, capture_b)
else:
handle_b = handle_a
from cli_anything.renderdoc.core.diff import diff_pipeline
data = diff_pipeline(
handle_a.controller, event_a,
handle_b.controller, event_b,
)
if compact:
def _prune_same(obj):
"""Recursively remove 'SAME' markers and empty containers."""
if isinstance(obj, dict):
pruned = {}
for k, v in obj.items():
if v == "SAME":
continue
cleaned = _prune_same(v)
if cleaned is not None:
pruned[k] = cleaned
return pruned if pruned else None
if isinstance(obj, list):
pruned = [_prune_same(item) for item in obj if item != "SAME"]
pruned = [item for item in pruned if item is not None]
return pruned if pruned else None
return obj
data = _prune_same(data) or {}
# Determine output file path
if output is None:
capture_a_path = ctx.obj.get("capture_path", "capture")
base_dir = os.path.dirname(os.path.abspath(capture_a_path))
stem_a = os.path.splitext(os.path.basename(capture_a_path))[0]
if capture_b:
stem_b = os.path.splitext(os.path.basename(capture_b))[0]
output = os.path.join(
base_dir,
"diff_%s_eid%d_vs_%s_eid%d.json" % (stem_a, event_a, stem_b, event_b),
)
else:
output = os.path.join(
base_dir,
"diff_%s_eid%d_vs_eid%d.json" % (stem_a, event_a, event_b),
)
output = os.path.abspath(output)
with open(output, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False, default=str)
if ctx.obj.get("json_mode"):
_output(ctx, {"path": output})
else:
click.echo(output)
# Cleanup hook
# ===========================================================================
@cli.result_callback()
@click.pass_context
def cleanup(ctx, *args, **kwargs):
global _repl_mode
# REPL invokes cli.main() per line; keep captures open until repl() exits.
if _repl_mode:
return
_close_all_captures()
# ===========================================================================
# REPL
# ===========================================================================
@cli.command()
@click.pass_context
def repl(ctx):
"""Start interactive REPL session."""
from cli_anything.renderdoc.utils.repl_skin import ReplSkin
global _repl_mode
_repl_mode = True
skin = ReplSkin("renderdoc", version="0.1.0")
skin.print_banner()
pt_session = skin.create_prompt_session()
_repl_commands = {
"capture": "info|thumb|convert",
"actions": "list|summary|find|get",
"textures": "list|get|save|save-outputs|pick",
"pipeline": "state|shader-export|cbuffer|diff",
"resources": "list|buffers|read-buffer",
"mesh": "inputs|outputs",
"counters": "list|fetch",
"help": "Show this help",
"quit": "Exit REPL",
}
capture_path = ctx.obj.get("capture_path", "")
context = os.path.basename(capture_path) if capture_path else ""
try:
while True:
try:
line = skin.get_input(pt_session, project_name=context, modified=False)
if not line:
continue
if line.lower() in ("quit", "exit", "q"):
skin.print_goodbye()
break
if line.lower() == "help":
skin.help(_repl_commands)
continue
args = line.split()
try:
cli.main(args, standalone_mode=False, obj=ctx.obj)
except SystemExit:
pass
except click.exceptions.UsageError as e:
skin.warning("Usage error: %s" % e)
except Exception as e:
skin.error("%s" % e)
except (EOFError, KeyboardInterrupt):
skin.print_goodbye()
break
finally:
_close_all_captures()
_repl_mode = False
# ===========================================================================
# Entry point
# ===========================================================================
def main():
cli(obj={})
if __name__ == "__main__":
main()