bobframes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bobframes/__init__.py +3 -0
- bobframes/_version.py +1 -0
- bobframes/catalog.py +154 -0
- bobframes/cli.py +266 -0
- bobframes/derive_post_merge.py +365 -0
- bobframes/derives/__init__.py +0 -0
- bobframes/derives/pass_class_breakdown.py +102 -0
- bobframes/derives/texture_usage.py +121 -0
- bobframes/discovery.py +132 -0
- bobframes/global_entities.py +99 -0
- bobframes/html/__init__.py +0 -0
- bobframes/html/template.py +1056 -0
- bobframes/lint.py +114 -0
- bobframes/manifest.py +127 -0
- bobframes/parquetize.py +282 -0
- bobframes/parsers/__init__.py +0 -0
- bobframes/parsers/derive_program_transitions.py +73 -0
- bobframes/parsers/parse_init_state.py +675 -0
- bobframes/paths.py +111 -0
- bobframes/probes/__init__.py +0 -0
- bobframes/probes/whatif.py +165 -0
- bobframes/qrd_harness.py +119 -0
- bobframes/query_examples.py +222 -0
- bobframes/rdcmd.py +72 -0
- bobframes/replay/__init__.py +26 -0
- bobframes/replay/replay_main.py +2305 -0
- bobframes/reports/__init__.py +0 -0
- bobframes/reports/_dashboard.py +425 -0
- bobframes/reports/ab.py +88 -0
- bobframes/reports/base.py +114 -0
- bobframes/reports/cache.py +147 -0
- bobframes/reports/chrome.py +1306 -0
- bobframes/reports/cli.py +99 -0
- bobframes/reports/delta.py +167 -0
- bobframes/reports/discovery.py +118 -0
- bobframes/reports/draws_by_class.py +165 -0
- bobframes/reports/formatters.py +122 -0
- bobframes/reports/instancing_opportunities.py +276 -0
- bobframes/reports/orchestrator.py +59 -0
- bobframes/reports/overdraw.py +293 -0
- bobframes/reports/pass_gpu.py +190 -0
- bobframes/reports/shader_hotlist.py +240 -0
- bobframes/reports/trend_table.py +444 -0
- bobframes/resource_labels.py +162 -0
- bobframes/run.py +480 -0
- bobframes/schemas.py +426 -0
- bobframes/stable_keys.py +83 -0
- bobframes/tests/__init__.py +0 -0
- bobframes/tests/_render_util.py +84 -0
- bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
- bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
- bobframes/tests/data/golden/_reports/index.html +264 -0
- bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
- bobframes/tests/data/golden/_reports/overdraw.html +275 -0
- bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
- bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
- bobframes/tests/data/golden/_reports/trend_table.html +390 -0
- bobframes/tests/data/golden/index.html +1175 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
- bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
- bobframes/tests/make_synthetic.py +171 -0
- bobframes/tests/smoke.py +199 -0
- bobframes/tests/test_determinism.py +19 -0
- bobframes/tests/test_discovery.py +97 -0
- bobframes/tests/test_hardening.py +142 -0
- bobframes/tests/test_parity.py +22 -0
- bobframes/tests/test_perf.py +18 -0
- bobframes/tests/test_replay_drift.py +115 -0
- bobframes/tests/test_schemas.py +26 -0
- bobframes/tests/test_schemas_unit.py +55 -0
- bobframes/tests/test_stable_keys.py +61 -0
- bobframes-0.1.0.dist-info/METADATA +144 -0
- bobframes-0.1.0.dist-info/RECORD +130 -0
- bobframes-0.1.0.dist-info/WHEEL +4 -0
- bobframes-0.1.0.dist-info/entry_points.txt +2 -0
- bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2305 @@
|
|
|
1
|
+
"""Main replay-time extraction. Runs INSIDE qrenderdoc's embedded Python 3.10.
|
|
2
|
+
|
|
3
|
+
Invoked via:
|
|
4
|
+
qrenderdoc.exe --python replay_main.py
|
|
5
|
+
|
|
6
|
+
Args via env var RDC_INSIDE_ARGS, separated by \\x1f:
|
|
7
|
+
drop_dir, capture, area, drop_date, drop_label, stage_root
|
|
8
|
+
|
|
9
|
+
Writes CSVs to <stage_root>/<capture>/<table>.csv (one row per record).
|
|
10
|
+
|
|
11
|
+
This script cannot import the host bobframes package reliably from inside
|
|
12
|
+
qrenderdoc, so column tuples are duplicated here. Host-side parquetize
|
|
13
|
+
verifies headers against schemas.py.
|
|
14
|
+
|
|
15
|
+
CRITICAL:
|
|
16
|
+
- End with os._exit(0); sys.exit() lets qrenderdoc start its GUI.
|
|
17
|
+
- stdout is swallowed; everything important must go to <capture>/_replay.log.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import csv
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
import traceback
|
|
28
|
+
|
|
29
|
+
_SEP = '\x1f'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- Schema constants (mirror schemas.py exactly) ---------------------------
|
|
33
|
+
#
|
|
34
|
+
# Duplicated by design (H-6): this script runs inside qrenderdoc's embedded Python and cannot import
|
|
35
|
+
# the host bobframes package, so these tuples mirror bobframes/schemas.py. Drift is guarded by
|
|
36
|
+
# bobframes/tests/test_replay_drift.py (CI) and re-checked at parquetize header-verify time.
|
|
37
|
+
# EVENTS/DRAWS/PASSES intentionally omit their *_norm + draw_class columns — those are derived
|
|
38
|
+
# host-side after replay (see derive_post_merge.py). Keep these in sync with schemas.py.
|
|
39
|
+
|
|
40
|
+
ID_COLS = ('area', 'drop_date', 'drop_label', 'capture')
|
|
41
|
+
|
|
42
|
+
EVENTS_COLS = ID_COLS + (
|
|
43
|
+
'event_id', 'parent_marker_path', 'chunk_name', 'depth',
|
|
44
|
+
'is_drawcall', 'is_dispatch', 'is_clear', 'is_copy', 'is_resolve',
|
|
45
|
+
'is_marker_push', 'is_marker_pop', 'is_set_state',
|
|
46
|
+
'num_indices', 'num_instances',
|
|
47
|
+
'dispatch_x', 'dispatch_y', 'dispatch_z',
|
|
48
|
+
'output_color_rt_id', 'output_depth_rt_id',
|
|
49
|
+
'copy_source_id', 'copy_destination_id',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
DRAWS_COLS = ID_COLS + (
|
|
53
|
+
'event_id', 'parent_pass_path', 'draw_name',
|
|
54
|
+
'num_indices', 'num_instances', 'base_vertex', 'vertex_offset', 'index_offset',
|
|
55
|
+
'topology',
|
|
56
|
+
'program_id', 'vs_shader_id', 'fs_shader_id',
|
|
57
|
+
'color_rt_ids', 'depth_rt_id',
|
|
58
|
+
'viewport_x', 'viewport_y', 'viewport_w', 'viewport_h',
|
|
59
|
+
'scissor_x', 'scissor_y', 'scissor_w', 'scissor_h',
|
|
60
|
+
'cull_mode', 'front_face',
|
|
61
|
+
'depth_test_enable', 'depth_write_enable', 'depth_func',
|
|
62
|
+
'stencil_enable',
|
|
63
|
+
'stencil_front_pass_op', 'stencil_front_fail_op', 'stencil_front_depth_fail_op',
|
|
64
|
+
'stencil_back_pass_op', 'stencil_back_fail_op', 'stencil_back_depth_fail_op',
|
|
65
|
+
'stencil_ref', 'stencil_read_mask', 'stencil_write_mask',
|
|
66
|
+
'blend_enable',
|
|
67
|
+
'blend_src_color', 'blend_dst_color', 'blend_op_color',
|
|
68
|
+
'blend_src_alpha', 'blend_dst_alpha', 'blend_op_alpha',
|
|
69
|
+
'color_write_mask',
|
|
70
|
+
'ibo_id', 'ibo_index_type',
|
|
71
|
+
'gpu_duration_s',
|
|
72
|
+
'post_vs_primitives', 'post_vs_vertices',
|
|
73
|
+
'mesh_hash',
|
|
74
|
+
'screen_min_x', 'screen_min_y', 'screen_max_x', 'screen_max_y',
|
|
75
|
+
'screen_coverage_px',
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
PASSES_COLS = ID_COLS + (
|
|
79
|
+
'marker_path', 'depth', 'first_event_id', 'last_event_id',
|
|
80
|
+
'num_draws', 'num_dispatches', 'num_clears', 'num_other_actions',
|
|
81
|
+
'num_primitives_pre_vs', 'num_primitives_post_vs',
|
|
82
|
+
'num_vertices_pre_vs', 'num_vertices_post_vs',
|
|
83
|
+
'gpu_duration_s',
|
|
84
|
+
'unique_programs', 'unique_shaders', 'unique_meshes', 'unique_materials',
|
|
85
|
+
'color_rt_id_first', 'depth_rt_id_first',
|
|
86
|
+
'draws_by_class_opaque', 'draws_by_class_prepass', 'draws_by_class_translucent',
|
|
87
|
+
'draws_by_class_decal', 'draws_by_class_shadow', 'draws_by_class_ui',
|
|
88
|
+
'draws_by_class_postprocess', 'draws_by_class_additive', 'draws_by_class_other',
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
RT_COLS = ID_COLS + (
|
|
92
|
+
'stable_key',
|
|
93
|
+
'rt_id', 'format', 'width', 'height', 'depth', 'mip_levels', 'sample_count',
|
|
94
|
+
'is_color', 'is_depth', 'is_stencil', 'is_swap_chain_target',
|
|
95
|
+
'first_write_event', 'last_write_event', 'first_read_event', 'last_read_event',
|
|
96
|
+
'num_write_events', 'num_read_events',
|
|
97
|
+
'attached_to_fbo_ids', 'sampled_by_shader_ids',
|
|
98
|
+
'min_value_r', 'min_value_g', 'min_value_b', 'min_value_a',
|
|
99
|
+
'max_value_r', 'max_value_g', 'max_value_b', 'max_value_a',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
RT_TIMELINE_COLS = ID_COLS + (
|
|
103
|
+
'rt_id', 'event_id', 'usage_code', 'usage_name', 'view_id',
|
|
104
|
+
'attachment_point_or_slot',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
COUNTERS_COLS = ID_COLS + (
|
|
108
|
+
'event_id', 'counter_name', 'counter_unit', 'value_double', 'value_uint64',
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
STATE_CHANGE_COLS = ID_COLS + (
|
|
112
|
+
'event_id', 'parent_marker_path', 'call_name',
|
|
113
|
+
'target_or_cap', 'arg_id', 'arg_int', 'arg_float', 'arg_extra_json',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
CLEARS_COLS = ID_COLS + (
|
|
117
|
+
'event_id', 'parent_marker_path', 'target',
|
|
118
|
+
'color_r', 'color_g', 'color_b', 'color_a',
|
|
119
|
+
'depth_value', 'stencil_value',
|
|
120
|
+
'buffer_mask', 'fbo_id',
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
DISPATCHES_COLS = ID_COLS + (
|
|
124
|
+
'event_id', 'parent_marker_path',
|
|
125
|
+
'program_id', 'cs_shader_id',
|
|
126
|
+
'group_count_x', 'group_count_y', 'group_count_z',
|
|
127
|
+
'work_group_size_x', 'work_group_size_y', 'work_group_size_z',
|
|
128
|
+
'total_threads',
|
|
129
|
+
'ssbo_bindings', 'image_bindings', 'atomic_counter_bindings',
|
|
130
|
+
'gpu_duration_s',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
DRAW_BINDINGS_COLS = ID_COLS + (
|
|
134
|
+
'event_id', 'slot_kind', 'slot_index', 'resource_id', 'sampler_id',
|
|
135
|
+
'offset', 'size', 'stride',
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
VERTEX_INPUTS_COLS = ID_COLS + (
|
|
139
|
+
'event_id', 'attribute_index', 'attribute_name', 'enabled',
|
|
140
|
+
'component_count', 'component_type', 'normalized', 'integer',
|
|
141
|
+
'stride_bytes', 'offset_bytes', 'buffer_id', 'vbo_slot',
|
|
142
|
+
'divisor',
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
DESCRIPTOR_ACCESS_COLS = ID_COLS + (
|
|
146
|
+
'event_id', 'descriptor_kind', 'slot_index', 'resource_id', 'view_id',
|
|
147
|
+
'byte_offset', 'byte_size', 'access_type',
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
INDIRECT_ARGS_COLS = ID_COLS + (
|
|
151
|
+
'event_id', 'call_name', 'indirect_buffer_id', 'offset',
|
|
152
|
+
'count', 'instance_count', 'first', 'base_vertex', 'base_instance',
|
|
153
|
+
'group_x', 'group_y', 'group_z', 'stride', 'draw_count',
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
VBO_SAMPLES_COLS = ID_COLS + (
|
|
157
|
+
'buffer_id', 'vertex_index', 'byte_offset',
|
|
158
|
+
'raw_hex', 'as_f32_0', 'as_f32_1', 'as_f32_2', 'as_f32_3',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
IBO_SAMPLES_COLS = ID_COLS + (
|
|
162
|
+
'buffer_id', 'index_position', 'index_value', 'index_type',
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
POST_VS_SAMPLES_COLS = ID_COLS + (
|
|
166
|
+
'event_id', 'vertex_index',
|
|
167
|
+
'position_x', 'position_y', 'position_z', 'position_w',
|
|
168
|
+
'clipped',
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
TEXTURE_SAMPLES_COLS = ID_COLS + (
|
|
172
|
+
'tex_id', 'row_index', 'col_index',
|
|
173
|
+
'raw_hex', 'as_unorm8_r', 'as_unorm8_g', 'as_unorm8_b', 'as_unorm8_a',
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
PIXEL_HISTORY_COLS = ID_COLS + (
|
|
177
|
+
'rt_id', 'sample_x', 'sample_y', 'mod_index',
|
|
178
|
+
'event_id', 'primitive_id',
|
|
179
|
+
'passed', 'backface_culled', 'depth_test_failed', 'stencil_test_failed',
|
|
180
|
+
'scissor_clipped', 'shader_discarded', 'sample_masked',
|
|
181
|
+
'depth_clipped', 'view_clipped',
|
|
182
|
+
'shader_out_r', 'shader_out_g', 'shader_out_b', 'shader_out_a',
|
|
183
|
+
'pre_mod_r', 'pre_mod_g', 'pre_mod_b', 'pre_mod_a',
|
|
184
|
+
'post_mod_r', 'post_mod_g', 'post_mod_b', 'post_mod_a',
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
FBOS_COLS = ID_COLS + (
|
|
188
|
+
'stable_key',
|
|
189
|
+
'fbo_id', 'attachment_point', 'kind', 'resource_id', 'format',
|
|
190
|
+
'width', 'height', 'sample_count',
|
|
191
|
+
'mip_level', 'layer', 'created_at_event', 'bound_at_events',
|
|
192
|
+
'num_clears', 'num_writes', 'num_reads', 'label',
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
FRAME_TOTALS_COLS = ID_COLS + (
|
|
196
|
+
'n_events', 'n_draws', 'n_dispatches', 'n_clears',
|
|
197
|
+
'total_primitives_pre_vs', 'total_vertices_pre_vs',
|
|
198
|
+
'total_primitives_post_vs', 'total_vertices_post_vs',
|
|
199
|
+
'total_gpu_duration_s',
|
|
200
|
+
'glUseProgram_count', 'glBindBuffer_count', 'glBindTexture_count', 'glActiveTexture_count',
|
|
201
|
+
'glBindFramebuffer_count', 'glBindBufferBase_count', 'glBindSampler_count',
|
|
202
|
+
'glDrawElements_count', 'glDrawArrays_count', 'glDrawElementsInstanced_count',
|
|
203
|
+
'glDispatchCompute_count', 'glClear_count', 'glClearBuffer_count',
|
|
204
|
+
'total_vbo_bytes_uploaded', 'total_ibo_bytes_uploaded', 'total_ubo_bytes_uploaded',
|
|
205
|
+
'total_texture_bytes_allocated', 'total_renderbuffer_bytes_allocated',
|
|
206
|
+
'unique_programs_used', 'unique_shaders_used', 'unique_meshes_drawn',
|
|
207
|
+
'unique_materials_drawn', 'unique_textures_bound',
|
|
208
|
+
'fbo_switches', 'program_switches', 'texture_unit_switches',
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# --- Helpers -----------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
def _parse_args() -> dict:
|
|
215
|
+
env = os.environ.get('RDC_INSIDE_ARGS', '')
|
|
216
|
+
parts = env.split(_SEP) if env else []
|
|
217
|
+
if len(parts) < 6:
|
|
218
|
+
raise RuntimeError(f'expected 6 args in RDC_INSIDE_ARGS, got {len(parts)}: {parts!r}')
|
|
219
|
+
return {
|
|
220
|
+
'drop_dir': parts[0],
|
|
221
|
+
'capture': parts[1],
|
|
222
|
+
'area': parts[2],
|
|
223
|
+
'drop_date': parts[3],
|
|
224
|
+
'drop_label': parts[4],
|
|
225
|
+
'stage_root': parts[5],
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _tee_setup(capture_stage: str):
|
|
230
|
+
log_path = os.path.join(capture_stage, '_replay.log')
|
|
231
|
+
os.makedirs(capture_stage, exist_ok=True)
|
|
232
|
+
log = open(log_path, 'w', encoding='utf-8', buffering=1)
|
|
233
|
+
|
|
234
|
+
class _Tee:
|
|
235
|
+
def __init__(self, *s): self.s = s
|
|
236
|
+
def write(self, d):
|
|
237
|
+
for o in self.s:
|
|
238
|
+
try: o.write(d); o.flush()
|
|
239
|
+
except Exception: pass
|
|
240
|
+
def flush(self):
|
|
241
|
+
for o in self.s:
|
|
242
|
+
try: o.flush()
|
|
243
|
+
except Exception: pass
|
|
244
|
+
|
|
245
|
+
sys.stdout = _Tee(sys.stdout, log)
|
|
246
|
+
sys.stderr = _Tee(sys.stderr, log)
|
|
247
|
+
return log
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _rid_int(rid) -> int:
|
|
251
|
+
"""Extract trailing integer from a ResourceId."""
|
|
252
|
+
if rid is None:
|
|
253
|
+
return 0
|
|
254
|
+
try:
|
|
255
|
+
s = str(rid)
|
|
256
|
+
if '::' in s:
|
|
257
|
+
return int(s.split('::', 1)[1])
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _enum_short(value) -> str:
|
|
264
|
+
"""Decode rd enum value to its short name (after final '.')."""
|
|
265
|
+
s = str(value)
|
|
266
|
+
return s.rsplit('.', 1)[-1] if '.' in s else s
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _open_capture(rdc_path: str):
|
|
270
|
+
import renderdoc as rd # type: ignore
|
|
271
|
+
cap = rd.OpenCaptureFile()
|
|
272
|
+
status = cap.OpenFile(rdc_path, '', None)
|
|
273
|
+
if status != rd.ResultCode.Succeeded:
|
|
274
|
+
raise RuntimeError(f'OpenFile failed: {status}')
|
|
275
|
+
result = cap.OpenCapture(rd.ReplayOptions(), None)
|
|
276
|
+
if isinstance(result, tuple):
|
|
277
|
+
rc, ctrl = result[0], result[1]
|
|
278
|
+
else:
|
|
279
|
+
rc, ctrl = result.result, result.controller
|
|
280
|
+
if rc != rd.ResultCode.Succeeded:
|
|
281
|
+
cap.Shutdown()
|
|
282
|
+
raise RuntimeError(f'OpenCapture failed: {rc}')
|
|
283
|
+
return cap, ctrl
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# --- Action tree walk (events + classification) ------------------------------
|
|
287
|
+
|
|
288
|
+
def _build_event_records(ctrl, sd, ctx):
|
|
289
|
+
"""Walk action tree once, build:
|
|
290
|
+
- events[]: list of dicts (one per action node)
|
|
291
|
+
- draw_events[]: subset where is_drawcall=1
|
|
292
|
+
- marker_pushes[]: list of (event_id, depth, marker_path) for PUSH
|
|
293
|
+
- marker_pops[]: list of (event_id, depth) for POP
|
|
294
|
+
- action_by_event: {event_id: action node} (used later for SetFrameEvent)
|
|
295
|
+
- parent_path_by_event: {event_id: marker_path string}
|
|
296
|
+
Returns dict.
|
|
297
|
+
"""
|
|
298
|
+
import renderdoc as rd # type: ignore
|
|
299
|
+
|
|
300
|
+
F = rd.ActionFlags
|
|
301
|
+
DRAW = int(F.Drawcall)
|
|
302
|
+
DISPATCH = int(F.Dispatch)
|
|
303
|
+
CLEAR = int(F.Clear)
|
|
304
|
+
COPY = int(F.Copy)
|
|
305
|
+
RESOLVE = int(F.Resolve)
|
|
306
|
+
PUSH = int(F.PushMarker)
|
|
307
|
+
POP = int(F.PopMarker)
|
|
308
|
+
SET_MARK = int(F.SetMarker) if hasattr(F, 'SetMarker') else 0
|
|
309
|
+
SET_STATE = 0 # rd has no SetState; treated as residual
|
|
310
|
+
|
|
311
|
+
events: list[dict] = []
|
|
312
|
+
draw_events: list[dict] = []
|
|
313
|
+
marker_pushes: list[tuple] = [] # (event_id, depth, marker_path)
|
|
314
|
+
marker_pops: list[tuple] = [] # (event_id, depth)
|
|
315
|
+
action_by_event: dict[int, object] = {}
|
|
316
|
+
parent_path_by_event: dict[int, str] = {}
|
|
317
|
+
chunk_name_by_event: dict[int, str] = {}
|
|
318
|
+
chunk_to_event_id: dict[int, int] = {}
|
|
319
|
+
|
|
320
|
+
null_rid = rd.ResourceId.Null()
|
|
321
|
+
|
|
322
|
+
def name_of(n):
|
|
323
|
+
if hasattr(n, 'GetName'):
|
|
324
|
+
try:
|
|
325
|
+
return n.GetName(sd) or ''
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
return getattr(n, 'customName', '') or ''
|
|
329
|
+
|
|
330
|
+
def walk(nodes, stack, depth):
|
|
331
|
+
for n in nodes:
|
|
332
|
+
flags = int(getattr(n, 'flags', 0))
|
|
333
|
+
nm = name_of(n)
|
|
334
|
+
this_stack = stack
|
|
335
|
+
ev = int(getattr(n, 'eventId', 0))
|
|
336
|
+
|
|
337
|
+
if flags & PUSH:
|
|
338
|
+
this_stack = stack + [nm or 'marker']
|
|
339
|
+
marker_pushes.append((ev, depth, '/'.join(this_stack)))
|
|
340
|
+
elif flags & POP:
|
|
341
|
+
marker_pops.append((ev, depth))
|
|
342
|
+
|
|
343
|
+
outputs = getattr(n, 'outputs', None) or ()
|
|
344
|
+
depth_out_rid = _rid_int(getattr(n, 'depthOut', None))
|
|
345
|
+
color_out_ids: list[int] = []
|
|
346
|
+
for o in outputs:
|
|
347
|
+
v = _rid_int(o)
|
|
348
|
+
if v:
|
|
349
|
+
color_out_ids.append(v)
|
|
350
|
+
color_first = color_out_ids[0] if color_out_ids else 0
|
|
351
|
+
color_join = ';'.join(str(x) for x in color_out_ids)
|
|
352
|
+
|
|
353
|
+
dim = getattr(n, 'dispatchDimension', None) or (0, 0, 0)
|
|
354
|
+
try:
|
|
355
|
+
dx, dy, dz = int(dim[0]), int(dim[1]), int(dim[2])
|
|
356
|
+
except Exception:
|
|
357
|
+
dx = dy = dz = 0
|
|
358
|
+
|
|
359
|
+
row = {
|
|
360
|
+
**ctx,
|
|
361
|
+
'event_id': ev,
|
|
362
|
+
'parent_marker_path': '/'.join(stack),
|
|
363
|
+
'chunk_name': nm,
|
|
364
|
+
'depth': depth,
|
|
365
|
+
'is_drawcall': int(bool(flags & DRAW)),
|
|
366
|
+
'is_dispatch': int(bool(flags & DISPATCH)),
|
|
367
|
+
'is_clear': int(bool(flags & CLEAR)),
|
|
368
|
+
'is_copy': int(bool(flags & COPY)),
|
|
369
|
+
'is_resolve': int(bool(flags & RESOLVE)),
|
|
370
|
+
'is_marker_push': int(bool(flags & PUSH)),
|
|
371
|
+
'is_marker_pop': int(bool(flags & POP)),
|
|
372
|
+
'is_set_state': int(bool(flags & SET_MARK)),
|
|
373
|
+
'num_indices': int(getattr(n, 'numIndices', 0) or 0),
|
|
374
|
+
'num_instances': int(getattr(n, 'numInstances', 0) or 0),
|
|
375
|
+
'dispatch_x': dx, 'dispatch_y': dy, 'dispatch_z': dz,
|
|
376
|
+
'output_color_rt_id': color_first,
|
|
377
|
+
'output_depth_rt_id': depth_out_rid,
|
|
378
|
+
'copy_source_id': _rid_int(getattr(n, 'copySource', None)),
|
|
379
|
+
'copy_destination_id': _rid_int(getattr(n, 'copyDestination', None)),
|
|
380
|
+
}
|
|
381
|
+
row['_color_ids_join'] = color_join
|
|
382
|
+
|
|
383
|
+
events.append(row)
|
|
384
|
+
action_by_event[ev] = n
|
|
385
|
+
parent_path_by_event[ev] = '/'.join(stack)
|
|
386
|
+
chunk_name_by_event[ev] = nm
|
|
387
|
+
|
|
388
|
+
# Build chunk_index -> event_id map from action's API events.
|
|
389
|
+
for api_ev in (getattr(n, 'events', None) or ()):
|
|
390
|
+
ci = getattr(api_ev, 'chunkIndex', -1)
|
|
391
|
+
if ci >= 0:
|
|
392
|
+
chunk_to_event_id[ci] = ev
|
|
393
|
+
|
|
394
|
+
if row['is_drawcall']:
|
|
395
|
+
draw_events.append(row)
|
|
396
|
+
|
|
397
|
+
children = getattr(n, 'children', None) or ()
|
|
398
|
+
if children:
|
|
399
|
+
walk(children, this_stack, depth + 1)
|
|
400
|
+
|
|
401
|
+
walk(ctrl.GetRootActions(), [], 0)
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
'events': events,
|
|
405
|
+
'draw_events': draw_events,
|
|
406
|
+
'marker_pushes': marker_pushes,
|
|
407
|
+
'marker_pops': marker_pops,
|
|
408
|
+
'action_by_event': action_by_event,
|
|
409
|
+
'parent_path_by_event': parent_path_by_event,
|
|
410
|
+
'chunk_name_by_event': chunk_name_by_event,
|
|
411
|
+
'chunk_to_event_id': chunk_to_event_id,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# --- Topology + primitive math ----------------------------------------------
|
|
416
|
+
|
|
417
|
+
def _topology_short(topology) -> str:
|
|
418
|
+
return _enum_short(topology)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _primitives_for(topology_short: str, ni: int, ic: int) -> int:
|
|
422
|
+
if ni == 0:
|
|
423
|
+
return 0
|
|
424
|
+
if topology_short == 'TriangleList':
|
|
425
|
+
return (ni // 3) * max(ic, 1)
|
|
426
|
+
if topology_short == 'TriangleStrip':
|
|
427
|
+
return max(0, ni - 2) * max(ic, 1)
|
|
428
|
+
if topology_short == 'LineList':
|
|
429
|
+
return (ni // 2) * max(ic, 1)
|
|
430
|
+
if topology_short == 'LineStrip':
|
|
431
|
+
return max(0, ni - 1) * max(ic, 1)
|
|
432
|
+
if topology_short == 'PointList':
|
|
433
|
+
return ni * max(ic, 1)
|
|
434
|
+
return (ni // 3) * max(ic, 1) # fallback triangle
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# --- Draw classification ----------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def _classify_draw(blend_enable: int, depth_write: int, marker_path: str,
|
|
440
|
+
blend_src_color: str, blend_dst_color: str) -> str:
|
|
441
|
+
mp = (marker_path or '').lower()
|
|
442
|
+
if 'shadow' in mp or 'shadowdepth' in mp:
|
|
443
|
+
return 'shadow'
|
|
444
|
+
if 'prepass' in mp or 'depthonly' in mp:
|
|
445
|
+
return 'prepass'
|
|
446
|
+
if 'slate' in mp or 'ui' in mp:
|
|
447
|
+
return 'ui'
|
|
448
|
+
if 'postprocess' in mp or 'tonemap' in mp or 'bloom' in mp or 'eyeadapt' in mp:
|
|
449
|
+
return 'postprocess'
|
|
450
|
+
if 'decal' in mp:
|
|
451
|
+
return 'decal'
|
|
452
|
+
if blend_enable:
|
|
453
|
+
bs = (blend_src_color or '').lower()
|
|
454
|
+
bd = (blend_dst_color or '').lower()
|
|
455
|
+
if bs == 'one' and bd == 'one':
|
|
456
|
+
return 'additive'
|
|
457
|
+
if 'src_alpha' in bs and 'one_minus_src_alpha' in bd:
|
|
458
|
+
return 'translucent'
|
|
459
|
+
return 'translucent'
|
|
460
|
+
if depth_write:
|
|
461
|
+
return 'opaque'
|
|
462
|
+
return 'other'
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# --- Pipeline state read per draw ------------------------------------------
|
|
466
|
+
|
|
467
|
+
def _read_draw_state(ctrl, ev_id: int, base_row: dict, parent_path: str,
|
|
468
|
+
event_durations: dict[int, float]) -> dict:
|
|
469
|
+
import renderdoc as rd # type: ignore
|
|
470
|
+
|
|
471
|
+
ctrl.SetFrameEvent(ev_id, False)
|
|
472
|
+
pipe = ctrl.GetPipelineState()
|
|
473
|
+
glp = ctrl.GetGLPipelineState()
|
|
474
|
+
|
|
475
|
+
topology = _topology_short(pipe.GetPrimitiveTopology())
|
|
476
|
+
|
|
477
|
+
# depth state
|
|
478
|
+
ds = glp.depthState
|
|
479
|
+
depth_test = int(bool(ds.depthEnable))
|
|
480
|
+
depth_write = int(bool(ds.depthWrites))
|
|
481
|
+
depth_func = _enum_short(rd.CompareFunction(int(ds.depthFunction)))
|
|
482
|
+
|
|
483
|
+
# stencil state
|
|
484
|
+
ss = glp.stencilState
|
|
485
|
+
stencil_enable = int(bool(ss.stencilEnable))
|
|
486
|
+
sf = ss.frontFace
|
|
487
|
+
sb = ss.backFace
|
|
488
|
+
s_fp = _enum_short(rd.StencilOperation(int(sf.passOperation)))
|
|
489
|
+
s_ff = _enum_short(rd.StencilOperation(int(sf.failOperation)))
|
|
490
|
+
s_fd = _enum_short(rd.StencilOperation(int(sf.depthFailOperation)))
|
|
491
|
+
s_bp = _enum_short(rd.StencilOperation(int(sb.passOperation)))
|
|
492
|
+
s_bf = _enum_short(rd.StencilOperation(int(sb.failOperation)))
|
|
493
|
+
s_bd = _enum_short(rd.StencilOperation(int(sb.depthFailOperation)))
|
|
494
|
+
sref = int(getattr(sf, 'reference', 0) or 0)
|
|
495
|
+
srd = int(getattr(sf, 'compareMask', 0) or 0)
|
|
496
|
+
swr = int(getattr(sf, 'writeMask', 0) or 0)
|
|
497
|
+
|
|
498
|
+
# rasterizer
|
|
499
|
+
rast = glp.rasterizer
|
|
500
|
+
rstate = rast.state
|
|
501
|
+
cull = _enum_short(rd.CullMode(int(rstate.cullMode)))
|
|
502
|
+
front_face = 'CW' if not bool(getattr(rstate, 'frontCCW', True)) else 'CCW'
|
|
503
|
+
# viewport (first)
|
|
504
|
+
vp = rast.viewports[0] if rast.viewports else None
|
|
505
|
+
vp_x = int(vp.x) if vp else 0
|
|
506
|
+
vp_y = int(vp.y) if vp else 0
|
|
507
|
+
vp_w = int(vp.width) if vp else 0
|
|
508
|
+
vp_h = int(vp.height) if vp else 0
|
|
509
|
+
sc = rast.scissors[0] if getattr(rast, 'scissors', None) else None
|
|
510
|
+
sc_x = int(sc.x) if sc else 0
|
|
511
|
+
sc_y = int(sc.y) if sc else 0
|
|
512
|
+
sc_w = int(sc.width) if sc else 0
|
|
513
|
+
sc_h = int(sc.height) if sc else 0
|
|
514
|
+
|
|
515
|
+
# blend
|
|
516
|
+
fb = glp.framebuffer
|
|
517
|
+
bs = fb.blendState
|
|
518
|
+
b0 = bs.blends[0] if len(bs.blends) else None
|
|
519
|
+
blend_enable = int(bool(b0.enabled)) if b0 else 0
|
|
520
|
+
if b0:
|
|
521
|
+
cb = b0.colorBlend
|
|
522
|
+
ab = b0.alphaBlend
|
|
523
|
+
blend_src_color = _enum_short(rd.BlendMultiplier(int(cb.source)))
|
|
524
|
+
blend_dst_color = _enum_short(rd.BlendMultiplier(int(cb.destination)))
|
|
525
|
+
blend_op_color = _enum_short(rd.BlendOperation(int(cb.operation)))
|
|
526
|
+
blend_src_alpha = _enum_short(rd.BlendMultiplier(int(ab.source)))
|
|
527
|
+
blend_dst_alpha = _enum_short(rd.BlendMultiplier(int(ab.destination)))
|
|
528
|
+
blend_op_alpha = _enum_short(rd.BlendOperation(int(ab.operation)))
|
|
529
|
+
color_write_mask = int(b0.writeMask)
|
|
530
|
+
else:
|
|
531
|
+
blend_src_color = blend_dst_color = blend_op_color = ''
|
|
532
|
+
blend_src_alpha = blend_dst_alpha = blend_op_alpha = ''
|
|
533
|
+
color_write_mask = 15
|
|
534
|
+
|
|
535
|
+
# render targets
|
|
536
|
+
dfb = fb.drawFBO
|
|
537
|
+
color_rt_ids: list[int] = []
|
|
538
|
+
for a in getattr(dfb, 'colorAttachments', []) or []:
|
|
539
|
+
rid = _rid_int(getattr(a, 'resource', None))
|
|
540
|
+
if rid:
|
|
541
|
+
color_rt_ids.append(rid)
|
|
542
|
+
depth_rt_id = _rid_int(getattr(dfb.depthAttachment, 'resource', None)) if getattr(dfb, 'depthAttachment', None) else 0
|
|
543
|
+
color_rt_join = ';'.join(str(x) for x in color_rt_ids)
|
|
544
|
+
|
|
545
|
+
# shaders
|
|
546
|
+
program_id = _rid_int(getattr(glp.vertexShader, 'programResourceId', None)) or \
|
|
547
|
+
_rid_int(getattr(glp.fragmentShader, 'programResourceId', None))
|
|
548
|
+
vs_id = _rid_int(getattr(glp.vertexShader, 'shaderResourceId', None))
|
|
549
|
+
fs_id = _rid_int(getattr(glp.fragmentShader, 'shaderResourceId', None))
|
|
550
|
+
|
|
551
|
+
# ibo
|
|
552
|
+
vi = glp.vertexInput
|
|
553
|
+
ibo_id = _rid_int(getattr(vi, 'indexBuffer', None))
|
|
554
|
+
ibo_type = ''
|
|
555
|
+
if hasattr(vi, 'indexByteStride'):
|
|
556
|
+
s = int(vi.indexByteStride)
|
|
557
|
+
ibo_type = {1: 'UNSIGNED_BYTE', 2: 'UNSIGNED_SHORT', 4: 'UNSIGNED_INT'}.get(s, str(s))
|
|
558
|
+
|
|
559
|
+
# GPU duration: from pre-fetched counters
|
|
560
|
+
gpu_s = event_durations.get(ev_id, 0.0)
|
|
561
|
+
|
|
562
|
+
# Post-VS data (count only; samples done later)
|
|
563
|
+
post_vs_prim = 0
|
|
564
|
+
post_vs_vert = 0
|
|
565
|
+
try:
|
|
566
|
+
mf = ctrl.GetPostVSData(0, 0, rd.MeshDataStage.VSOut)
|
|
567
|
+
if mf and getattr(mf, 'vertexResourceId', None) and mf.vertexResourceId != rd.ResourceId.Null():
|
|
568
|
+
post_vs_vert = int(mf.numIndices)
|
|
569
|
+
post_vs_prim = _primitives_for(_topology_short(mf.topology), post_vs_vert, 1)
|
|
570
|
+
except Exception:
|
|
571
|
+
pass
|
|
572
|
+
|
|
573
|
+
draw_name = base_row.get('chunk_name', '')
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
**{k: base_row[k] for k in ID_COLS},
|
|
577
|
+
'event_id': ev_id,
|
|
578
|
+
'parent_pass_path': parent_path,
|
|
579
|
+
'draw_name': draw_name,
|
|
580
|
+
'num_indices': base_row['num_indices'],
|
|
581
|
+
'num_instances': base_row['num_instances'],
|
|
582
|
+
'base_vertex': int(getattr(action_attr_get(ctrl, ev_id), 'baseVertex', 0) or 0),
|
|
583
|
+
'vertex_offset': int(getattr(action_attr_get(ctrl, ev_id), 'vertexOffset', 0) or 0),
|
|
584
|
+
'index_offset': int(getattr(action_attr_get(ctrl, ev_id), 'indexOffset', 0) or 0),
|
|
585
|
+
'topology': topology,
|
|
586
|
+
'program_id': program_id,
|
|
587
|
+
'vs_shader_id': vs_id,
|
|
588
|
+
'fs_shader_id': fs_id,
|
|
589
|
+
'color_rt_ids': color_rt_join,
|
|
590
|
+
'depth_rt_id': depth_rt_id,
|
|
591
|
+
'viewport_x': vp_x, 'viewport_y': vp_y, 'viewport_w': vp_w, 'viewport_h': vp_h,
|
|
592
|
+
'scissor_x': sc_x, 'scissor_y': sc_y, 'scissor_w': sc_w, 'scissor_h': sc_h,
|
|
593
|
+
'cull_mode': cull, 'front_face': front_face,
|
|
594
|
+
'depth_test_enable': depth_test, 'depth_write_enable': depth_write,
|
|
595
|
+
'depth_func': depth_func,
|
|
596
|
+
'stencil_enable': stencil_enable,
|
|
597
|
+
'stencil_front_pass_op': s_fp,
|
|
598
|
+
'stencil_front_fail_op': s_ff,
|
|
599
|
+
'stencil_front_depth_fail_op': s_fd,
|
|
600
|
+
'stencil_back_pass_op': s_bp,
|
|
601
|
+
'stencil_back_fail_op': s_bf,
|
|
602
|
+
'stencil_back_depth_fail_op': s_bd,
|
|
603
|
+
'stencil_ref': sref, 'stencil_read_mask': srd, 'stencil_write_mask': swr,
|
|
604
|
+
'blend_enable': blend_enable,
|
|
605
|
+
'blend_src_color': blend_src_color,
|
|
606
|
+
'blend_dst_color': blend_dst_color,
|
|
607
|
+
'blend_op_color': blend_op_color,
|
|
608
|
+
'blend_src_alpha': blend_src_alpha,
|
|
609
|
+
'blend_dst_alpha': blend_dst_alpha,
|
|
610
|
+
'blend_op_alpha': blend_op_alpha,
|
|
611
|
+
'color_write_mask': color_write_mask,
|
|
612
|
+
'ibo_id': ibo_id, 'ibo_index_type': ibo_type,
|
|
613
|
+
'gpu_duration_s': gpu_s,
|
|
614
|
+
'post_vs_primitives': post_vs_prim,
|
|
615
|
+
'post_vs_vertices': post_vs_vert,
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def action_attr_get(ctrl, ev_id: int):
|
|
620
|
+
"""Reach the current action for ev_id via ctrl. Used to read base_vertex etc."""
|
|
621
|
+
# Note: ctrl.SetFrameEvent already positioned us; pipe.GetMeshFormat is one path
|
|
622
|
+
# but reading via the cached action node is simpler. We pass _action_by_event in.
|
|
623
|
+
return _ACTION_CACHE.get(ev_id, _Empty())
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class _Empty: pass
|
|
627
|
+
|
|
628
|
+
_ACTION_CACHE: dict[int, object] = {}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# --- Per-draw aux extractors (bindings, vertex_inputs, descriptor_access, post_vs samples, fbo state) ---
|
|
632
|
+
|
|
633
|
+
def _extract_draw_aux(ctrl, ev_id: int, ctx: dict, draw_row: dict,
|
|
634
|
+
vi_rows: list, db_rows: list, da_rows: list, pvs_rows: list,
|
|
635
|
+
fbo_state_per_event: dict,
|
|
636
|
+
mesh_hash_cache: dict,
|
|
637
|
+
buffer_map: dict,
|
|
638
|
+
pvs_max_verts: int) -> None:
|
|
639
|
+
"""Extend writer lists with bindings/vertex_inputs/descriptor_access/post_vs
|
|
640
|
+
samples for the current draw event. Mutates draw_row to add mesh_hash and
|
|
641
|
+
screen_min_x/min_y/max_x/max_y/screen_coverage_px. SetFrameEvent already done.
|
|
642
|
+
"""
|
|
643
|
+
import renderdoc as rd # type: ignore
|
|
644
|
+
import struct
|
|
645
|
+
import hashlib
|
|
646
|
+
|
|
647
|
+
glp = ctrl.GetGLPipelineState()
|
|
648
|
+
|
|
649
|
+
# FBO state at this draw: drawFBO with its color/depth/stencil attachments
|
|
650
|
+
try:
|
|
651
|
+
dfb = glp.framebuffer.drawFBO
|
|
652
|
+
fbo_rid = _rid_int(getattr(dfb, 'resourceId', None))
|
|
653
|
+
if fbo_rid and fbo_rid not in fbo_state_per_event:
|
|
654
|
+
attachments: list[dict] = []
|
|
655
|
+
for i, a in enumerate(getattr(dfb, 'colorAttachments', None) or ()):
|
|
656
|
+
rid = _rid_int(getattr(a, 'resource', None))
|
|
657
|
+
if not rid:
|
|
658
|
+
continue
|
|
659
|
+
attachments.append({
|
|
660
|
+
'attachment_point': f'GL_COLOR_ATTACHMENT{i}',
|
|
661
|
+
'kind': 'tex',
|
|
662
|
+
'resource_id': rid,
|
|
663
|
+
'mip_level': int(getattr(a, 'firstMip', 0) or 0),
|
|
664
|
+
'layer': int(getattr(a, 'firstSlice', 0) or 0),
|
|
665
|
+
'first_event': ev_id,
|
|
666
|
+
})
|
|
667
|
+
for tag, attr_name in (('GL_DEPTH_ATTACHMENT', 'depthAttachment'),
|
|
668
|
+
('GL_STENCIL_ATTACHMENT', 'stencilAttachment')):
|
|
669
|
+
a = getattr(dfb, attr_name, None)
|
|
670
|
+
if a is None:
|
|
671
|
+
continue
|
|
672
|
+
rid = _rid_int(getattr(a, 'resource', None))
|
|
673
|
+
if not rid:
|
|
674
|
+
continue
|
|
675
|
+
attachments.append({
|
|
676
|
+
'attachment_point': tag,
|
|
677
|
+
'kind': 'tex',
|
|
678
|
+
'resource_id': rid,
|
|
679
|
+
'mip_level': int(getattr(a, 'firstMip', 0) or 0),
|
|
680
|
+
'layer': int(getattr(a, 'firstSlice', 0) or 0),
|
|
681
|
+
'first_event': ev_id,
|
|
682
|
+
})
|
|
683
|
+
fbo_state_per_event[fbo_rid] = attachments
|
|
684
|
+
except Exception:
|
|
685
|
+
pass
|
|
686
|
+
|
|
687
|
+
# Vertex inputs (per attribute slot)
|
|
688
|
+
vi = glp.vertexInput
|
|
689
|
+
attrs = getattr(vi, 'attributes', None) or []
|
|
690
|
+
vbuffers = getattr(vi, 'vertexBuffers', None) or []
|
|
691
|
+
for i, attr in enumerate(attrs):
|
|
692
|
+
slot = int(getattr(attr, 'vertexBufferSlot', 0) or 0)
|
|
693
|
+
buf = vbuffers[slot] if 0 <= slot < len(vbuffers) else None
|
|
694
|
+
bytes_offset = int(getattr(attr, 'byteOffset', 0) or 0)
|
|
695
|
+
comp_type = _enum_short(getattr(attr.format, 'compType', '')) if hasattr(attr, 'format') else ''
|
|
696
|
+
comp_count = int(getattr(attr.format, 'compCount', 0) or 0) if hasattr(attr, 'format') else 0
|
|
697
|
+
normalized = int(bool(getattr(attr.format, 'compType', None) and 'NORM' in _enum_short(attr.format.compType).upper())) if hasattr(attr, 'format') else 0
|
|
698
|
+
integer = int(bool(comp_type.startswith('UInt') or comp_type.startswith('SInt'))) if comp_type else 0
|
|
699
|
+
stride = int(getattr(buf, 'byteStride', 0) or 0) if buf else 0
|
|
700
|
+
buffer_id = _rid_int(getattr(buf, 'resourceId', None)) if buf else 0
|
|
701
|
+
divisor = int(getattr(buf, 'instanceDivisor', 0) or 0) if buf else 0
|
|
702
|
+
vi_rows.append({
|
|
703
|
+
**ctx,
|
|
704
|
+
'event_id': ev_id, 'attribute_index': i,
|
|
705
|
+
'attribute_name': getattr(attr, 'name', '') or '',
|
|
706
|
+
'enabled': int(bool(getattr(attr, 'enabled', True))),
|
|
707
|
+
'component_count': comp_count, 'component_type': comp_type,
|
|
708
|
+
'normalized': normalized, 'integer': integer,
|
|
709
|
+
'stride_bytes': stride, 'offset_bytes': bytes_offset,
|
|
710
|
+
'buffer_id': buffer_id, 'vbo_slot': slot,
|
|
711
|
+
'divisor': divisor,
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
# Draw bindings: textures + samplers + uniform buffers + ssbos + image units
|
|
715
|
+
textures = getattr(glp, 'textures', None) or []
|
|
716
|
+
for i, t in enumerate(textures):
|
|
717
|
+
rid = _rid_int(getattr(t, 'resourceId', None))
|
|
718
|
+
if not rid:
|
|
719
|
+
continue
|
|
720
|
+
db_rows.append({
|
|
721
|
+
**ctx, 'event_id': ev_id,
|
|
722
|
+
'slot_kind': 'texture', 'slot_index': i, 'resource_id': rid,
|
|
723
|
+
'sampler_id': 0, 'offset': 0, 'size': 0, 'stride': 0,
|
|
724
|
+
})
|
|
725
|
+
samplers = getattr(glp, 'samplers', None) or []
|
|
726
|
+
for i, s in enumerate(samplers):
|
|
727
|
+
rid = _rid_int(getattr(s, 'resourceId', None))
|
|
728
|
+
if not rid:
|
|
729
|
+
continue
|
|
730
|
+
db_rows.append({
|
|
731
|
+
**ctx, 'event_id': ev_id,
|
|
732
|
+
'slot_kind': 'sampler', 'slot_index': i, 'resource_id': 0,
|
|
733
|
+
'sampler_id': rid, 'offset': 0, 'size': 0, 'stride': 0,
|
|
734
|
+
})
|
|
735
|
+
ubos = getattr(glp, 'uniformBuffers', None) or []
|
|
736
|
+
for i, b in enumerate(ubos):
|
|
737
|
+
rid = _rid_int(getattr(b, 'resourceId', None))
|
|
738
|
+
if not rid:
|
|
739
|
+
continue
|
|
740
|
+
db_rows.append({
|
|
741
|
+
**ctx, 'event_id': ev_id,
|
|
742
|
+
'slot_kind': 'ubo', 'slot_index': i, 'resource_id': rid,
|
|
743
|
+
'sampler_id': 0,
|
|
744
|
+
'offset': int(getattr(b, 'byteOffset', 0) or 0),
|
|
745
|
+
'size': int(getattr(b, 'byteSize', 0) or 0),
|
|
746
|
+
'stride': 0,
|
|
747
|
+
})
|
|
748
|
+
ssbos = getattr(glp, 'shaderStorageBuffers', None) or []
|
|
749
|
+
for i, b in enumerate(ssbos):
|
|
750
|
+
rid = _rid_int(getattr(b, 'resourceId', None))
|
|
751
|
+
if not rid:
|
|
752
|
+
continue
|
|
753
|
+
db_rows.append({
|
|
754
|
+
**ctx, 'event_id': ev_id,
|
|
755
|
+
'slot_kind': 'ssbo', 'slot_index': i, 'resource_id': rid,
|
|
756
|
+
'sampler_id': 0,
|
|
757
|
+
'offset': int(getattr(b, 'byteOffset', 0) or 0),
|
|
758
|
+
'size': int(getattr(b, 'byteSize', 0) or 0),
|
|
759
|
+
'stride': 0,
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
# Descriptor access (resources actually used by bound shaders)
|
|
763
|
+
try:
|
|
764
|
+
accesses = ctrl.GetDescriptorAccess()
|
|
765
|
+
except Exception:
|
|
766
|
+
accesses = ()
|
|
767
|
+
for a in accesses or ():
|
|
768
|
+
try:
|
|
769
|
+
kind = _enum_short(getattr(a, 'type', ''))
|
|
770
|
+
slot = int(getattr(a, 'index', 0) or 0)
|
|
771
|
+
rid = _rid_int(getattr(a, 'descriptorStore', None))
|
|
772
|
+
view = _rid_int(getattr(a, 'view', None))
|
|
773
|
+
byte_offset = int(getattr(a, 'byteOffset', 0) or 0)
|
|
774
|
+
byte_size = int(getattr(a, 'byteSize', 0) or 0)
|
|
775
|
+
access = _enum_short(getattr(a, 'stage', ''))
|
|
776
|
+
da_rows.append({
|
|
777
|
+
**ctx, 'event_id': ev_id,
|
|
778
|
+
'descriptor_kind': kind, 'slot_index': slot,
|
|
779
|
+
'resource_id': rid, 'view_id': view,
|
|
780
|
+
'byte_offset': byte_offset, 'byte_size': byte_size,
|
|
781
|
+
'access_type': access,
|
|
782
|
+
})
|
|
783
|
+
except Exception:
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
# Post-VS samples + screen-space bbox derivation
|
|
787
|
+
vp_x = float(draw_row.get('viewport_x', 0) or 0)
|
|
788
|
+
vp_y = float(draw_row.get('viewport_y', 0) or 0)
|
|
789
|
+
vp_w = float(draw_row.get('viewport_w', 0) or 0)
|
|
790
|
+
vp_h = float(draw_row.get('viewport_h', 0) or 0)
|
|
791
|
+
min_x = min_y = float('inf')
|
|
792
|
+
max_x = max_y = float('-inf')
|
|
793
|
+
have_bbox = False
|
|
794
|
+
try:
|
|
795
|
+
mf = ctrl.GetPostVSData(0, 0, rd.MeshDataStage.VSOut)
|
|
796
|
+
if mf and mf.vertexResourceId and mf.vertexResourceId != rd.ResourceId.Null():
|
|
797
|
+
stride = int(getattr(mf, 'vertexByteStride', 0) or 0)
|
|
798
|
+
n_v = min(int(mf.numIndices), pvs_max_verts)
|
|
799
|
+
if stride >= 16 and n_v > 0:
|
|
800
|
+
raw = ctrl.GetBufferData(mf.vertexResourceId, 0, stride * n_v)
|
|
801
|
+
for vi_idx in range(n_v):
|
|
802
|
+
off = vi_idx * stride
|
|
803
|
+
try:
|
|
804
|
+
x, y, z, w = struct.unpack_from('<ffff', raw, off)
|
|
805
|
+
except struct.error:
|
|
806
|
+
break
|
|
807
|
+
clipped = 0
|
|
808
|
+
if w == 0.0 or abs(x) > abs(w) or abs(y) > abs(w) or abs(z) > abs(w):
|
|
809
|
+
clipped = 1
|
|
810
|
+
pvs_rows.append({
|
|
811
|
+
**ctx,
|
|
812
|
+
'event_id': ev_id, 'vertex_index': vi_idx,
|
|
813
|
+
'position_x': x, 'position_y': y, 'position_z': z, 'position_w': w,
|
|
814
|
+
'clipped': clipped,
|
|
815
|
+
})
|
|
816
|
+
# NDC -> viewport pixel space
|
|
817
|
+
if w != 0.0 and not clipped:
|
|
818
|
+
ndc_x = x / w
|
|
819
|
+
ndc_y = y / w
|
|
820
|
+
sx = vp_x + (ndc_x * 0.5 + 0.5) * vp_w
|
|
821
|
+
sy = vp_y + (ndc_y * 0.5 + 0.5) * vp_h
|
|
822
|
+
if sx < min_x: min_x = sx
|
|
823
|
+
if sy < min_y: min_y = sy
|
|
824
|
+
if sx > max_x: max_x = sx
|
|
825
|
+
if sy > max_y: max_y = sy
|
|
826
|
+
have_bbox = True
|
|
827
|
+
except Exception:
|
|
828
|
+
pass
|
|
829
|
+
|
|
830
|
+
if have_bbox:
|
|
831
|
+
# clamp to viewport
|
|
832
|
+
cx0 = max(min_x, vp_x); cy0 = max(min_y, vp_y)
|
|
833
|
+
cx1 = min(max_x, vp_x + vp_w); cy1 = min(max_y, vp_y + vp_h)
|
|
834
|
+
draw_row['screen_min_x'] = float(min_x)
|
|
835
|
+
draw_row['screen_min_y'] = float(min_y)
|
|
836
|
+
draw_row['screen_max_x'] = float(max_x)
|
|
837
|
+
draw_row['screen_max_y'] = float(max_y)
|
|
838
|
+
if cx1 > cx0 and cy1 > cy0:
|
|
839
|
+
draw_row['screen_coverage_px'] = int(round((cx1 - cx0) * (cy1 - cy0)))
|
|
840
|
+
else:
|
|
841
|
+
draw_row['screen_coverage_px'] = 0
|
|
842
|
+
else:
|
|
843
|
+
draw_row['screen_min_x'] = 0.0
|
|
844
|
+
draw_row['screen_min_y'] = 0.0
|
|
845
|
+
draw_row['screen_max_x'] = 0.0
|
|
846
|
+
draw_row['screen_max_y'] = 0.0
|
|
847
|
+
draw_row['screen_coverage_px'] = 0
|
|
848
|
+
|
|
849
|
+
# Mesh hash for instancing-candidate detection
|
|
850
|
+
ibo_id = int(draw_row.get('ibo_id', 0) or 0)
|
|
851
|
+
n_indices = int(draw_row.get('num_indices', 0) or 0)
|
|
852
|
+
base_vertex = int(draw_row.get('base_vertex', 0) or 0)
|
|
853
|
+
index_offset = int(draw_row.get('index_offset', 0) or 0)
|
|
854
|
+
vbo_slot_0_id = 0
|
|
855
|
+
vbuffers = getattr(vi, 'vertexBuffers', None) or []
|
|
856
|
+
if vbuffers:
|
|
857
|
+
vbo_slot_0_id = _rid_int(getattr(vbuffers[0], 'resourceId', None))
|
|
858
|
+
|
|
859
|
+
if ibo_id > 0:
|
|
860
|
+
identity = (ibo_id, base_vertex, index_offset, n_indices)
|
|
861
|
+
cached = mesh_hash_cache.get(identity)
|
|
862
|
+
if cached is not None:
|
|
863
|
+
draw_row['mesh_hash'] = cached
|
|
864
|
+
else:
|
|
865
|
+
ibo_obj = buffer_map.get(ibo_id)
|
|
866
|
+
data_bytes = b''
|
|
867
|
+
if ibo_obj is not None:
|
|
868
|
+
try:
|
|
869
|
+
# index_offset in draws is the offset within IBO (in bytes typically
|
|
870
|
+
# for glDrawElementsBaseVertex* path; we read 1KB from there)
|
|
871
|
+
data_bytes = bytes(ctrl.GetBufferData(ibo_obj.resourceId,
|
|
872
|
+
max(index_offset, 0), 1024))
|
|
873
|
+
except Exception:
|
|
874
|
+
data_bytes = b''
|
|
875
|
+
h = hashlib.sha256()
|
|
876
|
+
h.update(f'{ibo_id}|{base_vertex}|{index_offset}|{n_indices}|'.encode())
|
|
877
|
+
h.update(data_bytes)
|
|
878
|
+
digest = h.hexdigest()[:24]
|
|
879
|
+
mesh_hash_cache[identity] = digest
|
|
880
|
+
draw_row['mesh_hash'] = digest
|
|
881
|
+
else:
|
|
882
|
+
identity = ('noidx', vbo_slot_0_id, int(draw_row.get('vertex_offset', 0) or 0), n_indices)
|
|
883
|
+
cached = mesh_hash_cache.get(identity)
|
|
884
|
+
if cached is None:
|
|
885
|
+
h = hashlib.sha256(f'noidx|{vbo_slot_0_id}|{draw_row.get("vertex_offset",0)}|{n_indices}'.encode())
|
|
886
|
+
cached = h.hexdigest()[:24]
|
|
887
|
+
mesh_hash_cache[identity] = cached
|
|
888
|
+
draw_row['mesh_hash'] = cached
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
# --- Uniforms per pass -------------------------------------------------------
|
|
892
|
+
|
|
893
|
+
def _snapshot_uniforms(ctrl, ev_id: int, marker_path: str, ctx: dict) -> dict | None:
|
|
894
|
+
"""For the first draw under a unique pass marker, capture each bound
|
|
895
|
+
constant block: the binding metadata + the reflection layout (name,
|
|
896
|
+
type, byteOffset, byteSize per member) + a hex-encoded UBO data sample.
|
|
897
|
+
|
|
898
|
+
SetFrameEvent must have been called on ev_id. Returns dict or None.
|
|
899
|
+
"""
|
|
900
|
+
import renderdoc as rd # type: ignore
|
|
901
|
+
|
|
902
|
+
glp = ctrl.GetGLPipelineState()
|
|
903
|
+
blocks_out: list[dict] = []
|
|
904
|
+
|
|
905
|
+
stages = [
|
|
906
|
+
('vertex', getattr(glp, 'vertexShader', None)),
|
|
907
|
+
('fragment', getattr(glp, 'fragmentShader', None)),
|
|
908
|
+
('compute', getattr(glp, 'computeShader', None)),
|
|
909
|
+
]
|
|
910
|
+
seen_buffer_keys: set[tuple[int, int, int]] = set()
|
|
911
|
+
|
|
912
|
+
# First fetch raw UBO bytes (needed to decode member values inline).
|
|
913
|
+
raw_by_binding: dict[int, dict] = {}
|
|
914
|
+
raw_bytes_by_slot: dict[int, bytes] = {}
|
|
915
|
+
try:
|
|
916
|
+
accesses = ctrl.GetDescriptorAccess()
|
|
917
|
+
except Exception:
|
|
918
|
+
accesses = ()
|
|
919
|
+
for a in accesses or ():
|
|
920
|
+
try:
|
|
921
|
+
kind = _enum_short(getattr(a, 'type', ''))
|
|
922
|
+
if kind != 'ConstantBuffer':
|
|
923
|
+
continue
|
|
924
|
+
slot = int(getattr(a, 'index', 0) or 0)
|
|
925
|
+
buf_rid = getattr(a, 'descriptorStore', None)
|
|
926
|
+
if buf_rid is None:
|
|
927
|
+
continue
|
|
928
|
+
offset = int(getattr(a, 'byteOffset', 0) or 0)
|
|
929
|
+
size = int(getattr(a, 'byteSize', 0) or 0)
|
|
930
|
+
if not size:
|
|
931
|
+
continue
|
|
932
|
+
sample = min(size, 1024)
|
|
933
|
+
data = bytes(ctrl.GetBufferData(buf_rid, offset, sample))
|
|
934
|
+
raw_by_binding[slot] = {
|
|
935
|
+
'buffer_id': _rid_int(buf_rid),
|
|
936
|
+
'offset': offset, 'size': size,
|
|
937
|
+
'raw_hex': data.hex(),
|
|
938
|
+
}
|
|
939
|
+
raw_bytes_by_slot[slot] = data
|
|
940
|
+
except Exception:
|
|
941
|
+
continue
|
|
942
|
+
|
|
943
|
+
for stage_name, stage in stages:
|
|
944
|
+
if stage is None:
|
|
945
|
+
continue
|
|
946
|
+
refl = getattr(stage, 'reflection', None)
|
|
947
|
+
if refl is None:
|
|
948
|
+
continue
|
|
949
|
+
prog_rid = _rid_int(getattr(stage, 'programResourceId', None))
|
|
950
|
+
shader_rid = _rid_int(getattr(stage, 'shaderResourceId', None))
|
|
951
|
+
constant_blocks = getattr(refl, 'constantBlocks', None) or ()
|
|
952
|
+
for cb in constant_blocks:
|
|
953
|
+
binding_index = int(getattr(cb, 'bindPoint', 0) or 0)
|
|
954
|
+
members_out: list[dict] = []
|
|
955
|
+
ubo_data = raw_bytes_by_slot.get(binding_index, b'')
|
|
956
|
+
for var in getattr(cb, 'variables', None) or ():
|
|
957
|
+
vt = getattr(var, 'type', None)
|
|
958
|
+
byte_offset = int(getattr(var, 'byteOffset', 0) or 0)
|
|
959
|
+
base_type = _enum_short(getattr(vt, 'baseType', '')) if vt else ''
|
|
960
|
+
rows = int(getattr(vt, 'rows', 0) or 0) if vt else 0
|
|
961
|
+
cols = int(getattr(vt, 'columns', 0) or 0) if vt else 0
|
|
962
|
+
elements = int(getattr(vt, 'elements', 0) or 0) if vt else 0
|
|
963
|
+
m = {
|
|
964
|
+
'name': getattr(var, 'name', ''),
|
|
965
|
+
'byte_offset': byte_offset,
|
|
966
|
+
'type': base_type,
|
|
967
|
+
'rows': rows, 'columns': cols, 'elements': elements,
|
|
968
|
+
}
|
|
969
|
+
value, truncated = _decode_ubo_member(ubo_data, byte_offset,
|
|
970
|
+
base_type, rows, cols, elements,
|
|
971
|
+
array_cap=16)
|
|
972
|
+
if value is not None:
|
|
973
|
+
m['value'] = value
|
|
974
|
+
if truncated:
|
|
975
|
+
m['truncated'] = True
|
|
976
|
+
members_out.append(m)
|
|
977
|
+
byte_size = int(getattr(cb, 'byteSize', 0) or 0)
|
|
978
|
+
blocks_out.append({
|
|
979
|
+
'stage': stage_name,
|
|
980
|
+
'shader_id': shader_rid,
|
|
981
|
+
'program_id': prog_rid,
|
|
982
|
+
'block_name': getattr(cb, 'name', ''),
|
|
983
|
+
'binding_index': binding_index,
|
|
984
|
+
'byte_size': byte_size,
|
|
985
|
+
'members': members_out,
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
if not blocks_out:
|
|
989
|
+
return None
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
**ctx,
|
|
993
|
+
'event_id': ev_id,
|
|
994
|
+
'marker_path': marker_path,
|
|
995
|
+
'constant_blocks': blocks_out,
|
|
996
|
+
'raw_by_binding': raw_by_binding,
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _decode_ubo_member(raw: bytes, byte_offset: int, base_type: str,
|
|
1001
|
+
rows: int, cols: int, elements: int,
|
|
1002
|
+
array_cap: int = 16):
|
|
1003
|
+
"""Decode std140-laid-out scalars/vectors/matrices from raw UBO bytes.
|
|
1004
|
+
|
|
1005
|
+
Returns (value, truncated). value is None when decode not possible
|
|
1006
|
+
(out-of-range offset, unknown type, no bytes). Arrays beyond array_cap
|
|
1007
|
+
are truncated; truncated=True flagged.
|
|
1008
|
+
"""
|
|
1009
|
+
import struct
|
|
1010
|
+
if not raw or byte_offset < 0:
|
|
1011
|
+
return None, False
|
|
1012
|
+
n = len(raw)
|
|
1013
|
+
# Determine scalar size + struct fmt by base type.
|
|
1014
|
+
fmt = None
|
|
1015
|
+
elem_size = 0
|
|
1016
|
+
if base_type in ('Float',):
|
|
1017
|
+
fmt = '<f'; elem_size = 4
|
|
1018
|
+
elif base_type in ('SInt32', 'SInt', 'Int'):
|
|
1019
|
+
fmt = '<i'; elem_size = 4
|
|
1020
|
+
elif base_type in ('UInt32', 'UInt'):
|
|
1021
|
+
fmt = '<I'; elem_size = 4
|
|
1022
|
+
else:
|
|
1023
|
+
return None, False
|
|
1024
|
+
|
|
1025
|
+
def read_one(off, rows_, cols_):
|
|
1026
|
+
if rows_ <= 1 and cols_ <= 1:
|
|
1027
|
+
if off + elem_size > n:
|
|
1028
|
+
return None
|
|
1029
|
+
return struct.unpack_from(fmt, raw, off)[0]
|
|
1030
|
+
if rows_ == 1 and cols_ > 1:
|
|
1031
|
+
need = elem_size * cols_
|
|
1032
|
+
if off + need > n:
|
|
1033
|
+
return None
|
|
1034
|
+
return list(struct.unpack_from(fmt * cols_, raw, off))
|
|
1035
|
+
# Matrix: GL std140 layout = each column on 16-byte boundary, column-major
|
|
1036
|
+
out = []
|
|
1037
|
+
for c in range(cols_):
|
|
1038
|
+
row_off = off + c * 16
|
|
1039
|
+
if row_off + elem_size * rows_ > n:
|
|
1040
|
+
return None
|
|
1041
|
+
out.append(list(struct.unpack_from(fmt * rows_, raw, row_off)))
|
|
1042
|
+
return out
|
|
1043
|
+
|
|
1044
|
+
n_elements = max(1, elements)
|
|
1045
|
+
truncated = False
|
|
1046
|
+
if n_elements > array_cap:
|
|
1047
|
+
n_elements = array_cap
|
|
1048
|
+
truncated = True
|
|
1049
|
+
|
|
1050
|
+
if elements <= 1:
|
|
1051
|
+
return read_one(byte_offset, rows, cols), False
|
|
1052
|
+
|
|
1053
|
+
# std140 array stride: 16 bytes per scalar/vec; mat stride = cols * 16
|
|
1054
|
+
if rows <= 1:
|
|
1055
|
+
stride = 16
|
|
1056
|
+
else:
|
|
1057
|
+
stride = cols * 16
|
|
1058
|
+
out_list = []
|
|
1059
|
+
for ei in range(n_elements):
|
|
1060
|
+
v = read_one(byte_offset + ei * stride, rows, cols)
|
|
1061
|
+
if v is None:
|
|
1062
|
+
break
|
|
1063
|
+
out_list.append(v)
|
|
1064
|
+
return out_list, truncated
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
# --- Clears + dispatches extraction -----------------------------------------
|
|
1068
|
+
|
|
1069
|
+
_CLEAR_CHUNK_NAMES = {'glClear', 'glClearBufferfv', 'glClearBufferfi',
|
|
1070
|
+
'glClearBufferiv', 'glClearBufferuiv'}
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _decode_clear_chunk(chunk) -> dict:
|
|
1074
|
+
"""Decode a clear chunk's args. Probed on Arm RD v1.43.
|
|
1075
|
+
|
|
1076
|
+
Children for glClearBufferfv: framebuffer (ResourceId), buffer (enum string),
|
|
1077
|
+
drawbuffer (int), value (array of 4 with sub.name='$el' .AsFloat()).
|
|
1078
|
+
Children for glClearBufferfi: framebuffer, buffer (GL_DEPTH_STENCIL),
|
|
1079
|
+
drawbuffer, depth (AsFloat), stencil (AsInt).
|
|
1080
|
+
Children for glClear: mask (bitfield via AsInt).
|
|
1081
|
+
"""
|
|
1082
|
+
out = {
|
|
1083
|
+
'color_r': 0.0, 'color_g': 0.0, 'color_b': 0.0, 'color_a': 0.0,
|
|
1084
|
+
'depth_value': 0.0, 'stencil_value': 0,
|
|
1085
|
+
'buffer_mask': 0, 'target': '', 'fbo_id': 0,
|
|
1086
|
+
}
|
|
1087
|
+
name = chunk.name
|
|
1088
|
+
fb = chunk.FindChild('framebuffer')
|
|
1089
|
+
if fb is not None:
|
|
1090
|
+
try:
|
|
1091
|
+
out['fbo_id'] = _rid_int(fb.AsResourceId())
|
|
1092
|
+
except Exception:
|
|
1093
|
+
pass
|
|
1094
|
+
if name == 'glClear':
|
|
1095
|
+
m = chunk.FindChild('mask')
|
|
1096
|
+
if m is not None:
|
|
1097
|
+
try: out['buffer_mask'] = int(m.AsInt() or 0)
|
|
1098
|
+
except Exception: pass
|
|
1099
|
+
elif name in ('glClearBufferfv', 'glClearBufferiv', 'glClearBufferuiv'):
|
|
1100
|
+
b = chunk.FindChild('buffer')
|
|
1101
|
+
if b is not None:
|
|
1102
|
+
try: out['target'] = b.AsString() or ''
|
|
1103
|
+
except Exception: pass
|
|
1104
|
+
v = chunk.FindChild('value')
|
|
1105
|
+
if v is not None:
|
|
1106
|
+
try:
|
|
1107
|
+
n = v.NumChildren()
|
|
1108
|
+
keys = ('color_r', 'color_g', 'color_b', 'color_a')
|
|
1109
|
+
for i in range(min(n, 4)):
|
|
1110
|
+
child = v.GetChild(i)
|
|
1111
|
+
if child is None: continue
|
|
1112
|
+
if name == 'glClearBufferfv':
|
|
1113
|
+
out[keys[i]] = float(child.AsFloat() or 0.0)
|
|
1114
|
+
else:
|
|
1115
|
+
out[keys[i]] = float(child.AsInt() or 0)
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1118
|
+
elif name == 'glClearBufferfi':
|
|
1119
|
+
b = chunk.FindChild('buffer')
|
|
1120
|
+
if b is not None:
|
|
1121
|
+
try: out['target'] = b.AsString() or ''
|
|
1122
|
+
except Exception: pass
|
|
1123
|
+
d = chunk.FindChild('depth')
|
|
1124
|
+
if d is not None:
|
|
1125
|
+
try: out['depth_value'] = float(d.AsFloat() or 0.0)
|
|
1126
|
+
except Exception: pass
|
|
1127
|
+
s = chunk.FindChild('stencil')
|
|
1128
|
+
if s is not None:
|
|
1129
|
+
try: out['stencil_value'] = int(s.AsInt() or 0)
|
|
1130
|
+
except Exception: pass
|
|
1131
|
+
return out
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _extract_clears(sd, events: list[dict], action_by_event: dict,
|
|
1135
|
+
ctx: dict) -> list[dict]:
|
|
1136
|
+
"""One row per clear action with decoded color/depth/stencil/fbo_id."""
|
|
1137
|
+
rows: list[dict] = []
|
|
1138
|
+
chunks = sd.chunks
|
|
1139
|
+
n_chunks = len(chunks)
|
|
1140
|
+
for e in events:
|
|
1141
|
+
if not e['is_clear']:
|
|
1142
|
+
continue
|
|
1143
|
+
ev_id = e['event_id']
|
|
1144
|
+
action = action_by_event.get(ev_id)
|
|
1145
|
+
decoded = None
|
|
1146
|
+
if action is not None:
|
|
1147
|
+
api_evs = getattr(action, 'events', None) or ()
|
|
1148
|
+
for api_ev in api_evs:
|
|
1149
|
+
ci = getattr(api_ev, 'chunkIndex', -1)
|
|
1150
|
+
if 0 <= ci < n_chunks:
|
|
1151
|
+
c = chunks[ci]
|
|
1152
|
+
if c.name in _CLEAR_CHUNK_NAMES:
|
|
1153
|
+
try:
|
|
1154
|
+
decoded = _decode_clear_chunk(c)
|
|
1155
|
+
except Exception:
|
|
1156
|
+
decoded = None
|
|
1157
|
+
break
|
|
1158
|
+
if decoded is None:
|
|
1159
|
+
decoded = {
|
|
1160
|
+
'color_r': 0.0, 'color_g': 0.0, 'color_b': 0.0, 'color_a': 0.0,
|
|
1161
|
+
'depth_value': 0.0, 'stencil_value': 0,
|
|
1162
|
+
'buffer_mask': 0, 'target': '', 'fbo_id': 0,
|
|
1163
|
+
}
|
|
1164
|
+
rows.append({
|
|
1165
|
+
**ctx,
|
|
1166
|
+
'event_id': ev_id,
|
|
1167
|
+
'parent_marker_path': e['parent_marker_path'],
|
|
1168
|
+
'target': decoded['target'],
|
|
1169
|
+
'color_r': decoded['color_r'], 'color_g': decoded['color_g'],
|
|
1170
|
+
'color_b': decoded['color_b'], 'color_a': decoded['color_a'],
|
|
1171
|
+
'depth_value': decoded['depth_value'],
|
|
1172
|
+
'stencil_value': decoded['stencil_value'],
|
|
1173
|
+
'buffer_mask': decoded['buffer_mask'],
|
|
1174
|
+
'fbo_id': decoded['fbo_id'],
|
|
1175
|
+
})
|
|
1176
|
+
return rows
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _sample_vbos(ctrl, used_vbo_ids: set, ctx: dict, max_verts: int = 16) -> list[dict]:
|
|
1180
|
+
"""Sample first `max_verts` raw bytes of each used VBO. 64 bytes per row."""
|
|
1181
|
+
import renderdoc as rd # type: ignore
|
|
1182
|
+
|
|
1183
|
+
rows: list[dict] = []
|
|
1184
|
+
buffers = {_rid_int(b.resourceId): b for b in ctrl.GetBuffers()}
|
|
1185
|
+
for bid in sorted(used_vbo_ids):
|
|
1186
|
+
if not bid or bid not in buffers:
|
|
1187
|
+
continue
|
|
1188
|
+
b = buffers[bid]
|
|
1189
|
+
size = int(b.length) if hasattr(b, 'length') else 0
|
|
1190
|
+
if size <= 0:
|
|
1191
|
+
continue
|
|
1192
|
+
sample_bytes = min(64 * max_verts, size)
|
|
1193
|
+
try:
|
|
1194
|
+
data = ctrl.GetBufferData(b.resourceId, 0, sample_bytes)
|
|
1195
|
+
except Exception:
|
|
1196
|
+
continue
|
|
1197
|
+
stride = 64 # presumed vertex stride; tools can re-interpret per layout
|
|
1198
|
+
n_v = min(max_verts, len(data) // stride if stride else 0)
|
|
1199
|
+
for vi in range(n_v):
|
|
1200
|
+
off = vi * stride
|
|
1201
|
+
seg = data[off:off + stride]
|
|
1202
|
+
import struct
|
|
1203
|
+
try:
|
|
1204
|
+
f0, f1, f2, f3 = struct.unpack_from('<ffff', seg, 0)
|
|
1205
|
+
except struct.error:
|
|
1206
|
+
f0 = f1 = f2 = f3 = 0.0
|
|
1207
|
+
rows.append({
|
|
1208
|
+
**ctx,
|
|
1209
|
+
'buffer_id': bid,
|
|
1210
|
+
'vertex_index': vi,
|
|
1211
|
+
'byte_offset': off,
|
|
1212
|
+
'raw_hex': bytes(seg[:64]).hex(),
|
|
1213
|
+
'as_f32_0': float(f0), 'as_f32_1': float(f1),
|
|
1214
|
+
'as_f32_2': float(f2), 'as_f32_3': float(f3),
|
|
1215
|
+
})
|
|
1216
|
+
return rows
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def _sample_ibos(ctrl, used_ibo_ids: set, ctx: dict, max_indices: int = 32) -> list[dict]:
|
|
1220
|
+
import renderdoc as rd # type: ignore
|
|
1221
|
+
import struct
|
|
1222
|
+
|
|
1223
|
+
rows: list[dict] = []
|
|
1224
|
+
buffers = {_rid_int(b.resourceId): b for b in ctrl.GetBuffers()}
|
|
1225
|
+
for bid in sorted(used_ibo_ids):
|
|
1226
|
+
if not bid or bid not in buffers:
|
|
1227
|
+
continue
|
|
1228
|
+
b = buffers[bid]
|
|
1229
|
+
size = int(b.length) if hasattr(b, 'length') else 0
|
|
1230
|
+
if size <= 0:
|
|
1231
|
+
continue
|
|
1232
|
+
# try uint16 first (mobile typical)
|
|
1233
|
+
nb = min(max_indices * 2, size)
|
|
1234
|
+
try:
|
|
1235
|
+
data = ctrl.GetBufferData(b.resourceId, 0, nb)
|
|
1236
|
+
except Exception:
|
|
1237
|
+
continue
|
|
1238
|
+
n = min(max_indices, len(data) // 2)
|
|
1239
|
+
for i in range(n):
|
|
1240
|
+
try:
|
|
1241
|
+
v = struct.unpack_from('<H', data, i * 2)[0]
|
|
1242
|
+
except struct.error:
|
|
1243
|
+
v = 0
|
|
1244
|
+
rows.append({
|
|
1245
|
+
**ctx,
|
|
1246
|
+
'buffer_id': bid, 'index_position': i,
|
|
1247
|
+
'index_value': int(v), 'index_type': 'uint16',
|
|
1248
|
+
})
|
|
1249
|
+
return rows
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _sample_textures(ctrl, ctx: dict, max_dim: int = 256, max_cols: int = 16) -> list[dict]:
|
|
1253
|
+
import renderdoc as rd # type: ignore
|
|
1254
|
+
import struct
|
|
1255
|
+
|
|
1256
|
+
rows: list[dict] = []
|
|
1257
|
+
sub = rd.Subresource(0, 0, 0)
|
|
1258
|
+
for tex in ctrl.GetTextures():
|
|
1259
|
+
if tex.width <= 0 or tex.height <= 0:
|
|
1260
|
+
continue
|
|
1261
|
+
if tex.width > max_dim or tex.height > max_dim:
|
|
1262
|
+
continue
|
|
1263
|
+
try:
|
|
1264
|
+
data = ctrl.GetTextureData(tex.resourceId, sub)
|
|
1265
|
+
except Exception:
|
|
1266
|
+
continue
|
|
1267
|
+
if not data:
|
|
1268
|
+
continue
|
|
1269
|
+
# Heuristic: 4 bytes per pixel for unorm8 RGBA
|
|
1270
|
+
bpp = 4
|
|
1271
|
+
row_stride = tex.width * bpp
|
|
1272
|
+
if len(data) < row_stride:
|
|
1273
|
+
continue
|
|
1274
|
+
n_cols = min(max_cols, tex.width)
|
|
1275
|
+
for col in range(n_cols):
|
|
1276
|
+
off = col * bpp
|
|
1277
|
+
try:
|
|
1278
|
+
r, g, b, a = struct.unpack_from('BBBB', data, off)
|
|
1279
|
+
except struct.error:
|
|
1280
|
+
continue
|
|
1281
|
+
rows.append({
|
|
1282
|
+
**ctx,
|
|
1283
|
+
'tex_id': _rid_int(tex.resourceId),
|
|
1284
|
+
'row_index': 0, 'col_index': col,
|
|
1285
|
+
'raw_hex': bytes(data[off:off + 16]).hex(),
|
|
1286
|
+
'as_unorm8_r': r, 'as_unorm8_g': g,
|
|
1287
|
+
'as_unorm8_b': b, 'as_unorm8_a': a,
|
|
1288
|
+
})
|
|
1289
|
+
return rows
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def _extract_dispatches(ctrl, events: list[dict], event_durations: dict,
|
|
1293
|
+
ctx: dict) -> list[dict]:
|
|
1294
|
+
"""One row per compute dispatch with program + work group dimensions."""
|
|
1295
|
+
rows: list[dict] = []
|
|
1296
|
+
for e in events:
|
|
1297
|
+
if not e['is_dispatch']:
|
|
1298
|
+
continue
|
|
1299
|
+
ev = e['event_id']
|
|
1300
|
+
gpu = event_durations.get(ev, 0.0)
|
|
1301
|
+
program = 0; cs_shader = 0
|
|
1302
|
+
wg_x = wg_y = wg_z = 0
|
|
1303
|
+
try:
|
|
1304
|
+
ctrl.SetFrameEvent(ev, False)
|
|
1305
|
+
glp = ctrl.GetGLPipelineState()
|
|
1306
|
+
program = _rid_int(getattr(glp.computeShader, 'programResourceId', None))
|
|
1307
|
+
cs_shader = _rid_int(getattr(glp.computeShader, 'shaderResourceId', None))
|
|
1308
|
+
refl = getattr(glp.computeShader, 'reflection', None)
|
|
1309
|
+
if refl is not None:
|
|
1310
|
+
ds = getattr(refl, 'dispatchThreadsDimension', None) or (0, 0, 0)
|
|
1311
|
+
wg_x, wg_y, wg_z = int(ds[0]), int(ds[1]), int(ds[2])
|
|
1312
|
+
except Exception:
|
|
1313
|
+
pass
|
|
1314
|
+
gx, gy, gz = e['dispatch_x'], e['dispatch_y'], e['dispatch_z']
|
|
1315
|
+
rows.append({
|
|
1316
|
+
**ctx,
|
|
1317
|
+
'event_id': ev, 'parent_marker_path': e['parent_marker_path'],
|
|
1318
|
+
'program_id': program, 'cs_shader_id': cs_shader,
|
|
1319
|
+
'group_count_x': gx, 'group_count_y': gy, 'group_count_z': gz,
|
|
1320
|
+
'work_group_size_x': wg_x, 'work_group_size_y': wg_y, 'work_group_size_z': wg_z,
|
|
1321
|
+
'total_threads': gx * gy * gz * max(1, wg_x) * max(1, wg_y) * max(1, wg_z),
|
|
1322
|
+
'ssbo_bindings': '', 'image_bindings': '', 'atomic_counter_bindings': '',
|
|
1323
|
+
'gpu_duration_s': gpu,
|
|
1324
|
+
})
|
|
1325
|
+
return rows
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
# --- FBO finalizer ----------------------------------------------------------
|
|
1329
|
+
|
|
1330
|
+
def _build_fbo_rows(ctrl, fbo_state_per_event: dict[int, list[dict]],
|
|
1331
|
+
labels: dict[int, str], ctx: dict) -> list[dict]:
|
|
1332
|
+
"""Turn the (fbo_id -> attachments list) map into one row per
|
|
1333
|
+
(fbo_id, attachment_point). Cross-reference texture metadata for
|
|
1334
|
+
format/width/height.
|
|
1335
|
+
"""
|
|
1336
|
+
import renderdoc as rd # type: ignore
|
|
1337
|
+
|
|
1338
|
+
# Build texture metadata lookup
|
|
1339
|
+
tex_info: dict[int, dict] = {}
|
|
1340
|
+
for t in ctrl.GetTextures():
|
|
1341
|
+
rid = _rid_int(t.resourceId)
|
|
1342
|
+
if not rid:
|
|
1343
|
+
continue
|
|
1344
|
+
try:
|
|
1345
|
+
fmt = t.format.Name() if hasattr(t.format, 'Name') else str(t.format)
|
|
1346
|
+
except Exception:
|
|
1347
|
+
fmt = ''
|
|
1348
|
+
tex_info[rid] = {
|
|
1349
|
+
'format': fmt,
|
|
1350
|
+
'width': int(t.width), 'height': int(t.height),
|
|
1351
|
+
'sample_count': int(t.msSamp),
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
rows: list[dict] = []
|
|
1355
|
+
for fbo_id, attachments in fbo_state_per_event.items():
|
|
1356
|
+
for att in attachments:
|
|
1357
|
+
tinfo = tex_info.get(att['resource_id'], {'format': '', 'width': 0,
|
|
1358
|
+
'height': 0, 'sample_count': 0})
|
|
1359
|
+
rows.append({
|
|
1360
|
+
**ctx,
|
|
1361
|
+
'stable_key': '', # filled host-side from sorted attachments
|
|
1362
|
+
'fbo_id': fbo_id,
|
|
1363
|
+
'attachment_point': att['attachment_point'],
|
|
1364
|
+
'kind': att['kind'],
|
|
1365
|
+
'resource_id': att['resource_id'],
|
|
1366
|
+
'format': tinfo['format'],
|
|
1367
|
+
'width': tinfo['width'],
|
|
1368
|
+
'height': tinfo['height'],
|
|
1369
|
+
'sample_count': tinfo['sample_count'],
|
|
1370
|
+
'mip_level': att['mip_level'],
|
|
1371
|
+
'layer': att['layer'],
|
|
1372
|
+
'created_at_event': -1,
|
|
1373
|
+
'bound_at_events': str(att['first_event']),
|
|
1374
|
+
'num_clears': 0, 'num_writes': 0, 'num_reads': 0,
|
|
1375
|
+
'label': labels.get(fbo_id, ''),
|
|
1376
|
+
})
|
|
1377
|
+
return rows
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
# --- Indirect args -----------------------------------------------------------
|
|
1381
|
+
|
|
1382
|
+
_INDIRECT_CHUNK_NAMES = {
|
|
1383
|
+
'glDrawArraysIndirect',
|
|
1384
|
+
'glDrawElementsIndirect',
|
|
1385
|
+
'glMultiDrawArraysIndirect',
|
|
1386
|
+
'glMultiDrawElementsIndirect',
|
|
1387
|
+
'glDispatchComputeIndirect',
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def _extract_indirect_args(ctrl, events: list[dict], ctx: dict) -> list[dict]:
|
|
1392
|
+
"""For each indirect draw/dispatch event, read the indirect buffer at the
|
|
1393
|
+
bound offset and unpack the args. On Arm RD GL backend the bound indirect
|
|
1394
|
+
buffer is accessible via the structured chunk args; we use a best-effort
|
|
1395
|
+
approach via SetFrameEvent + GLPipelineState.vertexInput.indirectBuffer
|
|
1396
|
+
(when present). If we cannot recover the buffer reliably, skip silently.
|
|
1397
|
+
"""
|
|
1398
|
+
import renderdoc as rd # type: ignore
|
|
1399
|
+
import struct
|
|
1400
|
+
|
|
1401
|
+
rows: list[dict] = []
|
|
1402
|
+
for e in events:
|
|
1403
|
+
cn = e['chunk_name']
|
|
1404
|
+
if cn not in _INDIRECT_CHUNK_NAMES:
|
|
1405
|
+
continue
|
|
1406
|
+
ev = e['event_id']
|
|
1407
|
+
try:
|
|
1408
|
+
ctrl.SetFrameEvent(ev, False)
|
|
1409
|
+
glp = ctrl.GetGLPipelineState()
|
|
1410
|
+
ind_rid = None
|
|
1411
|
+
offset = 0
|
|
1412
|
+
# Try various attribute names where the bound indirect buffer may live.
|
|
1413
|
+
for attr_path in (('vertexInput', 'indirectBuffer'),
|
|
1414
|
+
('vertexInput', 'indirect'),
|
|
1415
|
+
('framebuffer', 'indirectBuffer')):
|
|
1416
|
+
node = glp
|
|
1417
|
+
ok = True
|
|
1418
|
+
for a in attr_path:
|
|
1419
|
+
node = getattr(node, a, None)
|
|
1420
|
+
if node is None:
|
|
1421
|
+
ok = False
|
|
1422
|
+
break
|
|
1423
|
+
if ok and node is not None:
|
|
1424
|
+
rid = _rid_int(getattr(node, 'resourceId', None) or node)
|
|
1425
|
+
if rid:
|
|
1426
|
+
ind_rid = (getattr(node, 'resourceId', None) or node)
|
|
1427
|
+
offset = int(getattr(node, 'byteOffset', 0) or 0)
|
|
1428
|
+
break
|
|
1429
|
+
if ind_rid is None:
|
|
1430
|
+
continue
|
|
1431
|
+
data = ctrl.GetBufferData(ind_rid, offset, 64)
|
|
1432
|
+
row = {**ctx, 'event_id': ev, 'call_name': cn,
|
|
1433
|
+
'indirect_buffer_id': _rid_int(ind_rid), 'offset': offset,
|
|
1434
|
+
'count': 0, 'instance_count': 0, 'first': 0,
|
|
1435
|
+
'base_vertex': 0, 'base_instance': 0,
|
|
1436
|
+
'group_x': 0, 'group_y': 0, 'group_z': 0,
|
|
1437
|
+
'stride': 0, 'draw_count': 0}
|
|
1438
|
+
if cn == 'glDrawArraysIndirect' and len(data) >= 16:
|
|
1439
|
+
count, instance_count, first, base_instance = struct.unpack_from('<IIII', data, 0)
|
|
1440
|
+
row.update(count=count, instance_count=instance_count,
|
|
1441
|
+
first=first, base_instance=base_instance)
|
|
1442
|
+
elif cn == 'glDrawElementsIndirect' and len(data) >= 20:
|
|
1443
|
+
count, instance_count, first, base_vertex, base_instance = struct.unpack_from('<IIIiI', data, 0)
|
|
1444
|
+
row.update(count=count, instance_count=instance_count,
|
|
1445
|
+
first=first, base_vertex=base_vertex, base_instance=base_instance)
|
|
1446
|
+
elif cn == 'glDispatchComputeIndirect' and len(data) >= 12:
|
|
1447
|
+
gx, gy, gz = struct.unpack_from('<III', data, 0)
|
|
1448
|
+
row.update(group_x=gx, group_y=gy, group_z=gz)
|
|
1449
|
+
rows.append(row)
|
|
1450
|
+
except Exception:
|
|
1451
|
+
continue
|
|
1452
|
+
return rows
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
# --- Pixel history ----------------------------------------------------------
|
|
1456
|
+
|
|
1457
|
+
def _extract_pixel_history(ctrl, ctx: dict, last_event_id: int,
|
|
1458
|
+
grid: int = 4, min_size: int = 256) -> list[dict]:
|
|
1459
|
+
"""Sample an N×N grid of pixels on each large color render target at
|
|
1460
|
+
last_event_id. Per-pixel PixelHistory() yields one row per modification.
|
|
1461
|
+
|
|
1462
|
+
Skips RTs smaller than min_size² (typical dummy/atlas textures).
|
|
1463
|
+
"""
|
|
1464
|
+
import renderdoc as rd # type: ignore
|
|
1465
|
+
|
|
1466
|
+
rows: list[dict] = []
|
|
1467
|
+
if grid <= 0:
|
|
1468
|
+
return rows
|
|
1469
|
+
sub = rd.Subresource(0, 0, 0)
|
|
1470
|
+
|
|
1471
|
+
for tex in ctrl.GetTextures():
|
|
1472
|
+
flags = int(tex.creationFlags)
|
|
1473
|
+
if not (flags & int(rd.TextureCategory.ColorTarget)):
|
|
1474
|
+
continue
|
|
1475
|
+
if tex.width < min_size or tex.height < min_size:
|
|
1476
|
+
continue
|
|
1477
|
+
rid_int = _rid_int(tex.resourceId)
|
|
1478
|
+
margin_x = max(2, int(0.05 * tex.width))
|
|
1479
|
+
margin_y = max(2, int(0.05 * tex.height))
|
|
1480
|
+
for gy in range(grid):
|
|
1481
|
+
for gx in range(grid):
|
|
1482
|
+
denom = max(grid - 1, 1)
|
|
1483
|
+
x = margin_x + (gx * (tex.width - 2 * margin_x)) // denom
|
|
1484
|
+
y = margin_y + (gy * (tex.height - 2 * margin_y)) // denom
|
|
1485
|
+
try:
|
|
1486
|
+
hist = ctrl.PixelHistory(tex.resourceId, x, y, sub, rd.CompType.Typeless)
|
|
1487
|
+
except Exception:
|
|
1488
|
+
continue
|
|
1489
|
+
for idx, mod in enumerate(hist or ()):
|
|
1490
|
+
passed = not (mod.backfaceCulled or mod.depthTestFailed or
|
|
1491
|
+
mod.stencilTestFailed or mod.scissorClipped or
|
|
1492
|
+
mod.shaderDiscarded or mod.sampleMasked or
|
|
1493
|
+
mod.depthClipped or mod.viewClipped)
|
|
1494
|
+
so = mod.shaderOut.col.floatValue
|
|
1495
|
+
pre = mod.preMod.col.floatValue
|
|
1496
|
+
post = mod.postMod.col.floatValue
|
|
1497
|
+
rows.append({
|
|
1498
|
+
**ctx,
|
|
1499
|
+
'rt_id': rid_int,
|
|
1500
|
+
'sample_x': x, 'sample_y': y, 'mod_index': idx,
|
|
1501
|
+
'event_id': int(mod.eventId),
|
|
1502
|
+
'primitive_id': int(getattr(mod, 'primitiveID', -1) or -1),
|
|
1503
|
+
'passed': int(bool(passed)),
|
|
1504
|
+
'backface_culled': int(bool(mod.backfaceCulled)),
|
|
1505
|
+
'depth_test_failed': int(bool(mod.depthTestFailed)),
|
|
1506
|
+
'stencil_test_failed': int(bool(mod.stencilTestFailed)),
|
|
1507
|
+
'scissor_clipped': int(bool(mod.scissorClipped)),
|
|
1508
|
+
'shader_discarded': int(bool(mod.shaderDiscarded)),
|
|
1509
|
+
'sample_masked': int(bool(mod.sampleMasked)),
|
|
1510
|
+
'depth_clipped': int(bool(mod.depthClipped)),
|
|
1511
|
+
'view_clipped': int(bool(mod.viewClipped)),
|
|
1512
|
+
'shader_out_r': float(so[0]), 'shader_out_g': float(so[1]),
|
|
1513
|
+
'shader_out_b': float(so[2]), 'shader_out_a': float(so[3]),
|
|
1514
|
+
'pre_mod_r': float(pre[0]), 'pre_mod_g': float(pre[1]),
|
|
1515
|
+
'pre_mod_b': float(pre[2]), 'pre_mod_a': float(pre[3]),
|
|
1516
|
+
'post_mod_r': float(post[0]), 'post_mod_g': float(post[1]),
|
|
1517
|
+
'post_mod_b': float(post[2]), 'post_mod_a': float(post[3]),
|
|
1518
|
+
})
|
|
1519
|
+
return rows
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
# --- Counters fetch ----------------------------------------------------------
|
|
1523
|
+
|
|
1524
|
+
def _fetch_counters_per_event(ctrl, ctx) -> tuple[list[dict], dict[int, float]]:
|
|
1525
|
+
"""Fetch ALL enumerable counters per event. Returns (rows, duration_by_event)."""
|
|
1526
|
+
import renderdoc as rd # type: ignore
|
|
1527
|
+
|
|
1528
|
+
rows: list[dict] = []
|
|
1529
|
+
duration_by_event: dict[int, float] = {}
|
|
1530
|
+
|
|
1531
|
+
counters = ctrl.EnumerateCounters()
|
|
1532
|
+
rt_double = (rd.CompType.Float, ) if False else ()
|
|
1533
|
+
# Use CounterResult.resultType-aware extraction. result_type attr may be int.
|
|
1534
|
+
try:
|
|
1535
|
+
FLOAT_T = int(rd.CompType.Float)
|
|
1536
|
+
DOUBLE_T = -1 # CompType has no Double; renderdoc uses Float for both internally
|
|
1537
|
+
except Exception:
|
|
1538
|
+
FLOAT_T = -1
|
|
1539
|
+
DOUBLE_T = -1
|
|
1540
|
+
|
|
1541
|
+
for counter in counters:
|
|
1542
|
+
d = ctrl.DescribeCounter(counter)
|
|
1543
|
+
try:
|
|
1544
|
+
results = ctrl.FetchCounters([counter])
|
|
1545
|
+
except Exception:
|
|
1546
|
+
continue
|
|
1547
|
+
unit = _enum_short(d.unit)
|
|
1548
|
+
name = d.name
|
|
1549
|
+
result_type = int(getattr(d, 'resultType', 0))
|
|
1550
|
+
# Heuristic: byte width 4 == 32-bit; width 8 + name says 'Duration' or
|
|
1551
|
+
# unit Seconds means double. Use d.resultByteWidth + result_type.
|
|
1552
|
+
rbw = int(getattr(d, 'resultByteWidth', 8))
|
|
1553
|
+
is_float_kind = ('Float' in _enum_short(getattr(d, 'resultType', 0))
|
|
1554
|
+
or unit in ('Seconds', 'Percentage', 'Hertz'))
|
|
1555
|
+
for r in results:
|
|
1556
|
+
ev = int(r.eventId)
|
|
1557
|
+
vd = 0.0
|
|
1558
|
+
vu = 0
|
|
1559
|
+
if is_float_kind:
|
|
1560
|
+
try: vd = float(r.value.d)
|
|
1561
|
+
except Exception: pass
|
|
1562
|
+
else:
|
|
1563
|
+
try: vu = int(r.value.u64)
|
|
1564
|
+
except Exception: pass
|
|
1565
|
+
rows.append({
|
|
1566
|
+
**ctx,
|
|
1567
|
+
'event_id': ev,
|
|
1568
|
+
'counter_name': name,
|
|
1569
|
+
'counter_unit': unit,
|
|
1570
|
+
'value_double': vd,
|
|
1571
|
+
'value_uint64': vu,
|
|
1572
|
+
})
|
|
1573
|
+
if counter == rd.GPUCounter.EventGPUDuration:
|
|
1574
|
+
duration_by_event[ev] = vd if is_float_kind else float(vu)
|
|
1575
|
+
|
|
1576
|
+
return rows, duration_by_event
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
# --- RT walk ----------------------------------------------------------------
|
|
1580
|
+
|
|
1581
|
+
def _walk_render_targets(ctrl, ctx) -> tuple[list[dict], list[dict]]:
|
|
1582
|
+
"""Build render_targets.csv rows + rt_event_timeline rows.
|
|
1583
|
+
|
|
1584
|
+
A render target is any texture that has the ColorTarget or DepthTarget
|
|
1585
|
+
creation flag. For each one, GetUsage() yields per-event read/write rows.
|
|
1586
|
+
"""
|
|
1587
|
+
import renderdoc as rd # type: ignore
|
|
1588
|
+
|
|
1589
|
+
rt_rows: list[dict] = []
|
|
1590
|
+
timeline_rows: list[dict] = []
|
|
1591
|
+
|
|
1592
|
+
null_rid = rd.ResourceId.Null()
|
|
1593
|
+
sub = rd.Subresource(0, 0, 0)
|
|
1594
|
+
|
|
1595
|
+
histogram_by_rt: dict[int, list[int]] = {}
|
|
1596
|
+
for tex in ctrl.GetTextures():
|
|
1597
|
+
flags = int(tex.creationFlags)
|
|
1598
|
+
is_color = bool(flags & int(rd.TextureCategory.ColorTarget))
|
|
1599
|
+
is_depth = bool(flags & int(rd.TextureCategory.DepthTarget))
|
|
1600
|
+
if not (is_color or is_depth):
|
|
1601
|
+
continue
|
|
1602
|
+
is_stencil = is_depth and tex.format.compType == rd.CompType.Depth
|
|
1603
|
+
is_swap = bool(flags & int(rd.TextureCategory.SwapBuffer))
|
|
1604
|
+
rid = _rid_int(tex.resourceId)
|
|
1605
|
+
if not rid:
|
|
1606
|
+
continue
|
|
1607
|
+
fmt = tex.format.Name() if hasattr(tex.format, 'Name') else str(tex.format)
|
|
1608
|
+
|
|
1609
|
+
# min/max for color RTs (depth often returns garbage on GLES replay)
|
|
1610
|
+
mn_r = mn_g = mn_b = mn_a = 0.0
|
|
1611
|
+
mx_r = mx_g = mx_b = mx_a = 0.0
|
|
1612
|
+
if is_color and tex.width > 1 and tex.height > 1:
|
|
1613
|
+
try:
|
|
1614
|
+
mn, mx = ctrl.GetMinMax(tex.resourceId, sub, rd.CompType.Typeless)
|
|
1615
|
+
mn_r, mn_g, mn_b, mn_a = float(mn.floatValue[0]), float(mn.floatValue[1]), float(mn.floatValue[2]), float(mn.floatValue[3])
|
|
1616
|
+
mx_r, mx_g, mx_b, mx_a = float(mx.floatValue[0]), float(mx.floatValue[1]), float(mx.floatValue[2]), float(mx.floatValue[3])
|
|
1617
|
+
except Exception:
|
|
1618
|
+
pass
|
|
1619
|
+
try:
|
|
1620
|
+
hist = ctrl.GetHistogram(tex.resourceId, sub, rd.CompType.Typeless,
|
|
1621
|
+
0.0, 1.0, (True, True, True, True))
|
|
1622
|
+
histogram_by_rt[rid] = list(int(x) for x in hist)
|
|
1623
|
+
except Exception:
|
|
1624
|
+
pass
|
|
1625
|
+
|
|
1626
|
+
# Walk usage for timeline
|
|
1627
|
+
first_write = -1; last_write = -1; first_read = -1; last_read = -1
|
|
1628
|
+
n_writes = 0; n_reads = 0
|
|
1629
|
+
try:
|
|
1630
|
+
for u in ctrl.GetUsage(tex.resourceId):
|
|
1631
|
+
code = int(u.usage)
|
|
1632
|
+
kind = _enum_short(rd.ResourceUsage(code))
|
|
1633
|
+
evid = int(u.eventId)
|
|
1634
|
+
view_rid = _rid_int(getattr(u, 'view', None))
|
|
1635
|
+
tl = {
|
|
1636
|
+
**ctx,
|
|
1637
|
+
'rt_id': rid, 'event_id': evid, 'usage_code': code,
|
|
1638
|
+
'usage_name': kind, 'view_id': view_rid,
|
|
1639
|
+
'attachment_point_or_slot': '',
|
|
1640
|
+
}
|
|
1641
|
+
timeline_rows.append(tl)
|
|
1642
|
+
lc = kind.lower()
|
|
1643
|
+
is_write_kind = ('target' in lc or 'write' in lc or 'clear' in lc or
|
|
1644
|
+
'copydst' in lc or 'resolvedst' in lc)
|
|
1645
|
+
if is_write_kind:
|
|
1646
|
+
n_writes += 1
|
|
1647
|
+
if first_write < 0: first_write = evid
|
|
1648
|
+
last_write = evid
|
|
1649
|
+
else:
|
|
1650
|
+
n_reads += 1
|
|
1651
|
+
if first_read < 0: first_read = evid
|
|
1652
|
+
last_read = evid
|
|
1653
|
+
except Exception:
|
|
1654
|
+
pass
|
|
1655
|
+
|
|
1656
|
+
rt_rows.append({
|
|
1657
|
+
**ctx,
|
|
1658
|
+
'stable_key': '', # filled host-side
|
|
1659
|
+
'rt_id': rid,
|
|
1660
|
+
'format': fmt,
|
|
1661
|
+
'width': int(tex.width), 'height': int(tex.height),
|
|
1662
|
+
'depth': int(tex.depth), 'mip_levels': int(tex.mips),
|
|
1663
|
+
'sample_count': int(tex.msSamp),
|
|
1664
|
+
'is_color': int(is_color),
|
|
1665
|
+
'is_depth': int(is_depth),
|
|
1666
|
+
'is_stencil': int(is_stencil),
|
|
1667
|
+
'is_swap_chain_target': int(is_swap),
|
|
1668
|
+
'first_write_event': first_write,
|
|
1669
|
+
'last_write_event': last_write,
|
|
1670
|
+
'first_read_event': first_read,
|
|
1671
|
+
'last_read_event': last_read,
|
|
1672
|
+
'num_write_events': n_writes,
|
|
1673
|
+
'num_read_events': n_reads,
|
|
1674
|
+
'attached_to_fbo_ids': '',
|
|
1675
|
+
'sampled_by_shader_ids': '',
|
|
1676
|
+
'min_value_r': mn_r, 'min_value_g': mn_g, 'min_value_b': mn_b, 'min_value_a': mn_a,
|
|
1677
|
+
'max_value_r': mx_r, 'max_value_g': mx_g, 'max_value_b': mx_b, 'max_value_a': mx_a,
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
return rt_rows, timeline_rows, histogram_by_rt
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
# --- State-change chunks -----------------------------------------------------
|
|
1684
|
+
|
|
1685
|
+
STATE_CHANGE_CHUNK_NAMES = {
|
|
1686
|
+
'glBindTexture', 'glActiveTexture', 'glUseProgram',
|
|
1687
|
+
'glEnable', 'glDisable',
|
|
1688
|
+
'glBindBuffer', 'glBindBufferBase', 'glBindBufferRange',
|
|
1689
|
+
'glBindFramebuffer', 'glBindSampler',
|
|
1690
|
+
'glStencilFunc', 'glStencilFuncSeparate',
|
|
1691
|
+
'glStencilOp', 'glStencilOpSeparate',
|
|
1692
|
+
'glStencilMask', 'glStencilMaskSeparate',
|
|
1693
|
+
'glDepthFunc', 'glDepthMask', 'glDepthRangef',
|
|
1694
|
+
'glCullFace', 'glFrontFace',
|
|
1695
|
+
'glBlendFunc', 'glBlendFuncSeparate', 'glBlendFunci', 'glBlendFuncSeparatei',
|
|
1696
|
+
'glBlendEquation', 'glBlendEquationSeparate',
|
|
1697
|
+
'glBlendEquationi', 'glBlendEquationSeparatei',
|
|
1698
|
+
'glColorMask', 'glColorMaski',
|
|
1699
|
+
'glViewport', 'glScissor', 'glPolygonOffset',
|
|
1700
|
+
'glPushGroupMarkerEXT', 'glPopGroupMarkerEXT',
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _build_state_change_rows(sd, parent_by_event, chunk_name_by_event,
|
|
1705
|
+
chunk_to_event_id, ctx) -> list[dict]:
|
|
1706
|
+
"""Walk sd.chunks once, emit one row per state-change chunk.
|
|
1707
|
+
|
|
1708
|
+
chunk_to_event_id maps each chunk index to the event_id of the action
|
|
1709
|
+
whose api-events list includes it. Built during action tree walk.
|
|
1710
|
+
"""
|
|
1711
|
+
rows: list[dict] = []
|
|
1712
|
+
|
|
1713
|
+
chunks = getattr(sd, 'chunks', None)
|
|
1714
|
+
if chunks is None:
|
|
1715
|
+
return rows
|
|
1716
|
+
|
|
1717
|
+
for ci, c in enumerate(chunks):
|
|
1718
|
+
name = c.name if hasattr(c, 'name') else ''
|
|
1719
|
+
if name not in STATE_CHANGE_CHUNK_NAMES:
|
|
1720
|
+
continue
|
|
1721
|
+
ev_id = chunk_to_event_id.get(ci, -1)
|
|
1722
|
+
parent = parent_by_event.get(ev_id, '') if ev_id >= 0 else ''
|
|
1723
|
+
# Args are too varied to fully decode; emit best-effort blob.
|
|
1724
|
+
arg_id = 0
|
|
1725
|
+
arg_int = 0
|
|
1726
|
+
arg_float = 0.0
|
|
1727
|
+
target_or_cap = ''
|
|
1728
|
+
args_repr: list = []
|
|
1729
|
+
try:
|
|
1730
|
+
for a in getattr(c, 'args', []) or []:
|
|
1731
|
+
args_repr.append(str(a))
|
|
1732
|
+
except Exception:
|
|
1733
|
+
pass
|
|
1734
|
+
rows.append({
|
|
1735
|
+
**ctx,
|
|
1736
|
+
'event_id': ev_id,
|
|
1737
|
+
'parent_marker_path': parent,
|
|
1738
|
+
'call_name': name,
|
|
1739
|
+
'target_or_cap': target_or_cap,
|
|
1740
|
+
'arg_id': arg_id,
|
|
1741
|
+
'arg_int': arg_int,
|
|
1742
|
+
'arg_float': arg_float,
|
|
1743
|
+
'arg_extra_json': json.dumps(args_repr) if args_repr else '',
|
|
1744
|
+
})
|
|
1745
|
+
return rows
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
# --- Passes (markers aggregation) -------------------------------------------
|
|
1749
|
+
|
|
1750
|
+
def _build_passes(events: list[dict], event_durations: dict[int, float],
|
|
1751
|
+
draw_classes: dict[int, str], ctx,
|
|
1752
|
+
draws_by_event: dict[int, dict] | None = None,
|
|
1753
|
+
bindings_by_event: dict[int, list] | None = None) -> list[dict]:
|
|
1754
|
+
"""Aggregate per-marker_path stats. Populates unique_programs/shaders/
|
|
1755
|
+
meshes/materials sets when draws_by_event + bindings_by_event are passed.
|
|
1756
|
+
"""
|
|
1757
|
+
draws_by_event = draws_by_event or {}
|
|
1758
|
+
bindings_by_event = bindings_by_event or {}
|
|
1759
|
+
passes: dict[tuple[int, str], dict] = {}
|
|
1760
|
+
|
|
1761
|
+
for e in events:
|
|
1762
|
+
if e['is_marker_push'] or e['is_marker_pop']:
|
|
1763
|
+
continue
|
|
1764
|
+
path = e['parent_marker_path']
|
|
1765
|
+
if not path:
|
|
1766
|
+
continue
|
|
1767
|
+
ev = e['event_id']
|
|
1768
|
+
gpu = event_durations.get(ev, 0.0)
|
|
1769
|
+
is_draw = e['is_drawcall']; is_disp = e['is_dispatch']; is_clr = e['is_clear']
|
|
1770
|
+
d_row = draws_by_event.get(ev) if is_draw else None
|
|
1771
|
+
|
|
1772
|
+
# contribute to every prefix path
|
|
1773
|
+
parts = path.split('/')
|
|
1774
|
+
for d in range(1, len(parts) + 1):
|
|
1775
|
+
sub = '/'.join(parts[:d])
|
|
1776
|
+
key = (d - 1, sub)
|
|
1777
|
+
p = passes.get(key)
|
|
1778
|
+
if p is None:
|
|
1779
|
+
p = {
|
|
1780
|
+
'marker_path': sub, 'depth': d - 1,
|
|
1781
|
+
'first_event_id': ev, 'last_event_id': ev,
|
|
1782
|
+
'num_draws': 0, 'num_dispatches': 0,
|
|
1783
|
+
'num_clears': 0, 'num_other_actions': 0,
|
|
1784
|
+
'num_primitives_pre_vs': 0, 'num_primitives_post_vs': 0,
|
|
1785
|
+
'num_vertices_pre_vs': 0, 'num_vertices_post_vs': 0,
|
|
1786
|
+
'gpu_duration_s': 0.0,
|
|
1787
|
+
'unique_programs': set(), 'unique_shaders': set(),
|
|
1788
|
+
'unique_meshes': set(), 'unique_materials': set(),
|
|
1789
|
+
'color_rt_id_first': 0, 'depth_rt_id_first': 0,
|
|
1790
|
+
'draws_by_class_opaque': 0, 'draws_by_class_prepass': 0,
|
|
1791
|
+
'draws_by_class_translucent': 0, 'draws_by_class_decal': 0,
|
|
1792
|
+
'draws_by_class_shadow': 0, 'draws_by_class_ui': 0,
|
|
1793
|
+
'draws_by_class_postprocess': 0, 'draws_by_class_additive': 0,
|
|
1794
|
+
'draws_by_class_other': 0,
|
|
1795
|
+
}
|
|
1796
|
+
if e.get('output_color_rt_id'):
|
|
1797
|
+
p['color_rt_id_first'] = e['output_color_rt_id']
|
|
1798
|
+
if e.get('output_depth_rt_id'):
|
|
1799
|
+
p['depth_rt_id_first'] = e['output_depth_rt_id']
|
|
1800
|
+
passes[key] = p
|
|
1801
|
+
p['last_event_id'] = ev
|
|
1802
|
+
p['gpu_duration_s'] += gpu
|
|
1803
|
+
|
|
1804
|
+
if is_draw:
|
|
1805
|
+
p['num_draws'] += 1
|
|
1806
|
+
ni = e['num_indices']; ic = e['num_instances'] or 1
|
|
1807
|
+
p['num_vertices_pre_vs'] += ni * ic
|
|
1808
|
+
p['num_primitives_pre_vs'] += (ni // 3) * ic
|
|
1809
|
+
cls = draw_classes.get(ev, 'other')
|
|
1810
|
+
col = f'draws_by_class_{cls}'
|
|
1811
|
+
if col in p:
|
|
1812
|
+
p[col] += 1
|
|
1813
|
+
if d_row is not None:
|
|
1814
|
+
pid = d_row.get('program_id') or 0
|
|
1815
|
+
vs = d_row.get('vs_shader_id') or 0
|
|
1816
|
+
fs = d_row.get('fs_shader_id') or 0
|
|
1817
|
+
mh = d_row.get('mesh_hash') or ''
|
|
1818
|
+
if pid: p['unique_programs'].add(pid)
|
|
1819
|
+
if vs: p['unique_shaders'].add(vs)
|
|
1820
|
+
if fs: p['unique_shaders'].add(fs)
|
|
1821
|
+
if mh: p['unique_meshes'].add(mh)
|
|
1822
|
+
# material identity = (program_id, sorted texture binding resource_ids)
|
|
1823
|
+
tex_ids = []
|
|
1824
|
+
for b in bindings_by_event.get(ev, ()):
|
|
1825
|
+
if b.get('slot_kind') == 'texture' and b.get('resource_id'):
|
|
1826
|
+
tex_ids.append(b['resource_id'])
|
|
1827
|
+
p['unique_materials'].add((pid, tuple(sorted(tex_ids))))
|
|
1828
|
+
elif is_disp:
|
|
1829
|
+
p['num_dispatches'] += 1
|
|
1830
|
+
elif is_clr:
|
|
1831
|
+
p['num_clears'] += 1
|
|
1832
|
+
else:
|
|
1833
|
+
p['num_other_actions'] += 1
|
|
1834
|
+
|
|
1835
|
+
# finalize: convert sets to ints
|
|
1836
|
+
rows: list[dict] = []
|
|
1837
|
+
for (_, _path), p in sorted(passes.items(), key=lambda kv: (kv[0][0], kv[0][1])):
|
|
1838
|
+
rows.append({
|
|
1839
|
+
**ctx,
|
|
1840
|
+
**{k: (len(v) if isinstance(v, set) else v) for k, v in p.items()},
|
|
1841
|
+
})
|
|
1842
|
+
return rows
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
# --- Frame totals -----------------------------------------------------------
|
|
1846
|
+
|
|
1847
|
+
def _build_frame_totals(events: list[dict], draws: list[dict],
|
|
1848
|
+
event_durations: dict[int, float],
|
|
1849
|
+
unique_programs_used: set, unique_shaders_used: set,
|
|
1850
|
+
unique_textures_bound: set, ctx,
|
|
1851
|
+
sc_rows: list[dict] | None = None,
|
|
1852
|
+
bindings_by_event: dict[int, list] | None = None,
|
|
1853
|
+
buffers_rows: list[dict] | None = None,
|
|
1854
|
+
textures_rows: list[dict] | None = None) -> dict:
|
|
1855
|
+
sc_rows = sc_rows or []
|
|
1856
|
+
bindings_by_event = bindings_by_event or {}
|
|
1857
|
+
buffers_rows = buffers_rows or []
|
|
1858
|
+
textures_rows = textures_rows or []
|
|
1859
|
+
|
|
1860
|
+
n_draws = sum(1 for e in events if e['is_drawcall'])
|
|
1861
|
+
n_disp = sum(1 for e in events if e['is_dispatch'])
|
|
1862
|
+
n_clr = sum(1 for e in events if e['is_clear'])
|
|
1863
|
+
|
|
1864
|
+
total_pre_vs_vertices = 0
|
|
1865
|
+
total_pre_vs_primitives = 0
|
|
1866
|
+
total_post_vs_vertices = 0
|
|
1867
|
+
total_post_vs_primitives = 0
|
|
1868
|
+
for d in draws:
|
|
1869
|
+
ic = d['num_instances'] or 1
|
|
1870
|
+
total_pre_vs_vertices += d['num_indices'] * ic
|
|
1871
|
+
total_pre_vs_primitives += _primitives_for(d['topology'], d['num_indices'], ic)
|
|
1872
|
+
total_post_vs_vertices += d['post_vs_vertices']
|
|
1873
|
+
total_post_vs_primitives += d['post_vs_primitives']
|
|
1874
|
+
|
|
1875
|
+
total_gpu = sum(event_durations.values())
|
|
1876
|
+
|
|
1877
|
+
# State change call counts
|
|
1878
|
+
call_counts: dict[str, int] = {}
|
|
1879
|
+
for r in sc_rows:
|
|
1880
|
+
nm = r.get('call_name', '')
|
|
1881
|
+
call_counts[nm] = call_counts.get(nm, 0) + 1
|
|
1882
|
+
# Add draw-call chunk names (action tree gives is_drawcall, but for counts
|
|
1883
|
+
# we tally by chunk_name).
|
|
1884
|
+
for e in events:
|
|
1885
|
+
cn = e.get('chunk_name', '')
|
|
1886
|
+
if cn in ('glDrawElements', 'glDrawArrays', 'glDrawElementsInstanced',
|
|
1887
|
+
'glDispatchCompute', 'glClear', 'glClearBufferfv',
|
|
1888
|
+
'glClearBufferfi', 'glClearBufferiv', 'glClearBufferuiv'):
|
|
1889
|
+
call_counts[cn] = call_counts.get(cn, 0) + 1
|
|
1890
|
+
gl_clear_buffer_count = (call_counts.get('glClearBufferfv', 0)
|
|
1891
|
+
+ call_counts.get('glClearBufferfi', 0)
|
|
1892
|
+
+ call_counts.get('glClearBufferiv', 0)
|
|
1893
|
+
+ call_counts.get('glClearBufferuiv', 0))
|
|
1894
|
+
|
|
1895
|
+
# Switches by walking draws/events in event_id order
|
|
1896
|
+
sorted_draws = sorted(draws, key=lambda d: d['event_id'])
|
|
1897
|
+
program_switches = 0
|
|
1898
|
+
prev_prog = None
|
|
1899
|
+
for d in sorted_draws:
|
|
1900
|
+
pid = d.get('program_id') or 0
|
|
1901
|
+
if prev_prog is not None and pid and pid != prev_prog:
|
|
1902
|
+
program_switches += 1
|
|
1903
|
+
if pid: prev_prog = pid
|
|
1904
|
+
fbo_switches = 0
|
|
1905
|
+
prev_rt = None
|
|
1906
|
+
for e in sorted(events, key=lambda x: x['event_id']):
|
|
1907
|
+
rt = e.get('output_color_rt_id') or 0
|
|
1908
|
+
if prev_rt is not None and rt and rt != prev_rt:
|
|
1909
|
+
fbo_switches += 1
|
|
1910
|
+
if rt: prev_rt = rt
|
|
1911
|
+
texture_unit_switches = call_counts.get('glActiveTexture', 0)
|
|
1912
|
+
|
|
1913
|
+
# Unique mesh + material from draws / bindings
|
|
1914
|
+
unique_meshes_drawn = len({d.get('mesh_hash') for d in draws if d.get('mesh_hash')})
|
|
1915
|
+
materials: set = set()
|
|
1916
|
+
for d in draws:
|
|
1917
|
+
pid = d.get('program_id') or 0
|
|
1918
|
+
ev = d['event_id']
|
|
1919
|
+
tex_ids = tuple(sorted(
|
|
1920
|
+
b['resource_id'] for b in bindings_by_event.get(ev, ())
|
|
1921
|
+
if b.get('slot_kind') == 'texture' and b.get('resource_id')
|
|
1922
|
+
))
|
|
1923
|
+
materials.add((pid, tex_ids))
|
|
1924
|
+
unique_materials_drawn = len(materials)
|
|
1925
|
+
|
|
1926
|
+
# Bytes totals from buffers + textures
|
|
1927
|
+
total_vbo = sum(r.get('allocated_size_bytes', 0) for r in buffers_rows if r.get('used_as_vbo'))
|
|
1928
|
+
total_ibo = sum(r.get('allocated_size_bytes', 0) for r in buffers_rows if r.get('used_as_ibo'))
|
|
1929
|
+
total_ubo = sum(r.get('allocated_size_bytes', 0) for r in buffers_rows if r.get('used_as_ubo'))
|
|
1930
|
+
total_tex_bytes = sum(r.get('est_bytes', 0) or 0 for r in textures_rows)
|
|
1931
|
+
|
|
1932
|
+
return {
|
|
1933
|
+
**ctx,
|
|
1934
|
+
'n_events': len(events),
|
|
1935
|
+
'n_draws': n_draws, 'n_dispatches': n_disp, 'n_clears': n_clr,
|
|
1936
|
+
'total_primitives_pre_vs': total_pre_vs_primitives,
|
|
1937
|
+
'total_vertices_pre_vs': total_pre_vs_vertices,
|
|
1938
|
+
'total_primitives_post_vs': total_post_vs_primitives,
|
|
1939
|
+
'total_vertices_post_vs': total_post_vs_vertices,
|
|
1940
|
+
'total_gpu_duration_s': total_gpu,
|
|
1941
|
+
'glUseProgram_count': call_counts.get('glUseProgram', 0),
|
|
1942
|
+
'glBindBuffer_count': call_counts.get('glBindBuffer', 0),
|
|
1943
|
+
'glBindTexture_count': call_counts.get('glBindTexture', 0),
|
|
1944
|
+
'glActiveTexture_count': call_counts.get('glActiveTexture', 0),
|
|
1945
|
+
'glBindFramebuffer_count': call_counts.get('glBindFramebuffer', 0),
|
|
1946
|
+
'glBindBufferBase_count': call_counts.get('glBindBufferBase', 0),
|
|
1947
|
+
'glBindSampler_count': call_counts.get('glBindSampler', 0),
|
|
1948
|
+
'glDrawElements_count': call_counts.get('glDrawElements', 0),
|
|
1949
|
+
'glDrawArrays_count': call_counts.get('glDrawArrays', 0),
|
|
1950
|
+
'glDrawElementsInstanced_count': call_counts.get('glDrawElementsInstanced', 0),
|
|
1951
|
+
'glDispatchCompute_count': call_counts.get('glDispatchCompute', 0),
|
|
1952
|
+
'glClear_count': call_counts.get('glClear', 0),
|
|
1953
|
+
'glClearBuffer_count': gl_clear_buffer_count,
|
|
1954
|
+
'total_vbo_bytes_uploaded': total_vbo,
|
|
1955
|
+
'total_ibo_bytes_uploaded': total_ibo,
|
|
1956
|
+
'total_ubo_bytes_uploaded': total_ubo,
|
|
1957
|
+
'total_texture_bytes_allocated': total_tex_bytes,
|
|
1958
|
+
'total_renderbuffer_bytes_allocated': 0,
|
|
1959
|
+
'unique_programs_used': len(unique_programs_used),
|
|
1960
|
+
'unique_shaders_used': len(unique_shaders_used),
|
|
1961
|
+
'unique_meshes_drawn': unique_meshes_drawn,
|
|
1962
|
+
'unique_materials_drawn': unique_materials_drawn,
|
|
1963
|
+
'unique_textures_bound': len(unique_textures_bound),
|
|
1964
|
+
'fbo_switches': fbo_switches,
|
|
1965
|
+
'program_switches': program_switches,
|
|
1966
|
+
'texture_unit_switches': texture_unit_switches,
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
|
|
1970
|
+
# --- Frame metadata ---------------------------------------------------------
|
|
1971
|
+
|
|
1972
|
+
def _frame_metadata(ctrl, ctx, rdc_path: str, sd=None) -> dict:
|
|
1973
|
+
md = dict(ctx)
|
|
1974
|
+
try:
|
|
1975
|
+
api = ctrl.GetAPIProperties()
|
|
1976
|
+
md['driver_name'] = _enum_short(api.driver) if hasattr(api, 'driver') else ''
|
|
1977
|
+
md['gpu_vendor'] = _enum_short(getattr(api, 'vendor', ''))
|
|
1978
|
+
md['gpu_description'] = getattr(api, 'description', '') or ''
|
|
1979
|
+
except Exception:
|
|
1980
|
+
pass
|
|
1981
|
+
try:
|
|
1982
|
+
fi = ctrl.GetFrameInfo()
|
|
1983
|
+
md['frame_number'] = int(getattr(fi, 'frameNumber', 0))
|
|
1984
|
+
md['capture_time_us'] = int(getattr(fi, 'captureTime', 0))
|
|
1985
|
+
md['file_size_bytes'] = int(getattr(fi, 'fileSize', 0))
|
|
1986
|
+
except Exception:
|
|
1987
|
+
pass
|
|
1988
|
+
md['rdc_file_bytes'] = os.path.getsize(rdc_path)
|
|
1989
|
+
# Probe Internal::Driver Initialisation Parameters chunk for GL_VERSION/RENDERER + format details
|
|
1990
|
+
md['gl_version_string'] = ''
|
|
1991
|
+
md['gl_renderer_string'] = ''
|
|
1992
|
+
md['init_color_bits'] = 0
|
|
1993
|
+
md['init_depth_bits'] = 0
|
|
1994
|
+
md['init_stencil_bits'] = 0
|
|
1995
|
+
md['init_multi_samples'] = 0
|
|
1996
|
+
md['init_width'] = 0
|
|
1997
|
+
md['init_height'] = 0
|
|
1998
|
+
md['init_is_srgb'] = 0
|
|
1999
|
+
if sd is not None:
|
|
2000
|
+
try:
|
|
2001
|
+
for c in sd.chunks:
|
|
2002
|
+
if c.name != 'Internal::Driver Initialisation Parameters':
|
|
2003
|
+
continue
|
|
2004
|
+
params = c.FindChild('InitParams')
|
|
2005
|
+
src = params if params is not None else c
|
|
2006
|
+
def _get_str(name):
|
|
2007
|
+
ch = src.FindChild(name)
|
|
2008
|
+
try: return ch.AsString() if ch is not None else ''
|
|
2009
|
+
except Exception: return ''
|
|
2010
|
+
def _get_int(name):
|
|
2011
|
+
ch = src.FindChild(name)
|
|
2012
|
+
try: return int(ch.AsInt()) if ch is not None else 0
|
|
2013
|
+
except Exception: return 0
|
|
2014
|
+
md['gl_renderer_string'] = _get_str('renderer')
|
|
2015
|
+
md['gl_version_string'] = _get_str('version')
|
|
2016
|
+
md['init_color_bits'] = _get_int('colorBits')
|
|
2017
|
+
md['init_depth_bits'] = _get_int('depthBits')
|
|
2018
|
+
md['init_stencil_bits'] = _get_int('stencilBits')
|
|
2019
|
+
md['init_multi_samples'] = _get_int('multiSamples')
|
|
2020
|
+
md['init_width'] = _get_int('width')
|
|
2021
|
+
md['init_height'] = _get_int('height')
|
|
2022
|
+
md['init_is_srgb'] = _get_int('isSRGB')
|
|
2023
|
+
break
|
|
2024
|
+
except Exception:
|
|
2025
|
+
pass
|
|
2026
|
+
return md
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
# --- Writers ----------------------------------------------------------------
|
|
2030
|
+
|
|
2031
|
+
def _write_rows(path: str, fields: tuple, rows: list[dict]) -> int:
|
|
2032
|
+
with open(path, 'w', encoding='utf-8', newline='') as f:
|
|
2033
|
+
w = csv.DictWriter(f, fieldnames=list(fields), extrasaction='ignore')
|
|
2034
|
+
w.writeheader()
|
|
2035
|
+
for r in rows:
|
|
2036
|
+
w.writerow(r)
|
|
2037
|
+
return len(rows)
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
# --- Main --------------------------------------------------------------------
|
|
2041
|
+
|
|
2042
|
+
def main() -> None:
|
|
2043
|
+
args = _parse_args()
|
|
2044
|
+
capture_stage = os.path.join(args['stage_root'], args['capture'])
|
|
2045
|
+
log = _tee_setup(capture_stage)
|
|
2046
|
+
|
|
2047
|
+
try:
|
|
2048
|
+
rdc_path = os.path.join(args['drop_dir'], f"{args['capture']}.rdc")
|
|
2049
|
+
if not os.path.exists(rdc_path):
|
|
2050
|
+
raise FileNotFoundError(rdc_path)
|
|
2051
|
+
print(f'opening {rdc_path}')
|
|
2052
|
+
ctx = {k: args[k] for k in ID_COLS}
|
|
2053
|
+
|
|
2054
|
+
cap, ctrl = _open_capture(rdc_path)
|
|
2055
|
+
try:
|
|
2056
|
+
sd = ctrl.GetStructuredFile()
|
|
2057
|
+
|
|
2058
|
+
t0 = time.monotonic()
|
|
2059
|
+
tree = _build_event_records(ctrl, sd, ctx)
|
|
2060
|
+
print(f' walked action tree: {len(tree["events"])} events, {len(tree["draw_events"])} draws ({time.monotonic()-t0:.1f}s)')
|
|
2061
|
+
|
|
2062
|
+
global _ACTION_CACHE
|
|
2063
|
+
_ACTION_CACHE = tree['action_by_event']
|
|
2064
|
+
|
|
2065
|
+
t0 = time.monotonic()
|
|
2066
|
+
counter_rows, event_durations = _fetch_counters_per_event(ctrl, ctx)
|
|
2067
|
+
print(f' fetched counters: {len(counter_rows)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2068
|
+
|
|
2069
|
+
t0 = time.monotonic()
|
|
2070
|
+
draws: list[dict] = []
|
|
2071
|
+
db_rows: list[dict] = []
|
|
2072
|
+
vi_rows: list[dict] = []
|
|
2073
|
+
da_rows: list[dict] = []
|
|
2074
|
+
pvs_rows: list[dict] = []
|
|
2075
|
+
fbo_state_per_event: dict[int, list[dict]] = {}
|
|
2076
|
+
uniforms_per_pass_rows: list[dict] = []
|
|
2077
|
+
seen_marker_paths: set = set()
|
|
2078
|
+
draw_classes: dict[int, str] = {}
|
|
2079
|
+
mesh_hash_cache: dict = {}
|
|
2080
|
+
buffer_map: dict = {_rid_int(b.resourceId): b for b in ctrl.GetBuffers()}
|
|
2081
|
+
unique_programs = set(); unique_shaders = set(); unique_textures = set()
|
|
2082
|
+
n_draws_total = len(tree['draw_events'])
|
|
2083
|
+
for i, e in enumerate(tree['draw_events']):
|
|
2084
|
+
ev_id = e['event_id']
|
|
2085
|
+
parent = tree['parent_path_by_event'].get(ev_id, '')
|
|
2086
|
+
try:
|
|
2087
|
+
row = _read_draw_state(ctrl, ev_id, e, parent, event_durations)
|
|
2088
|
+
except Exception as ex:
|
|
2089
|
+
print(f' draw {ev_id} failed: {ex}')
|
|
2090
|
+
continue
|
|
2091
|
+
draws.append(row)
|
|
2092
|
+
# aux per-draw extraction (re-uses the SetFrameEvent already done)
|
|
2093
|
+
try:
|
|
2094
|
+
_extract_draw_aux(ctrl, ev_id, ctx, row,
|
|
2095
|
+
vi_rows, db_rows, da_rows, pvs_rows,
|
|
2096
|
+
fbo_state_per_event, mesh_hash_cache, buffer_map,
|
|
2097
|
+
pvs_max_verts=32)
|
|
2098
|
+
except Exception as ex:
|
|
2099
|
+
print(f' draw {ev_id} aux failed: {ex}')
|
|
2100
|
+
# First-draw-per-marker uniforms snapshot
|
|
2101
|
+
if parent and parent not in seen_marker_paths:
|
|
2102
|
+
seen_marker_paths.add(parent)
|
|
2103
|
+
try:
|
|
2104
|
+
u_obj = _snapshot_uniforms(ctrl, ev_id, parent, ctx)
|
|
2105
|
+
if u_obj is not None:
|
|
2106
|
+
uniforms_per_pass_rows.append(u_obj)
|
|
2107
|
+
except Exception as ex:
|
|
2108
|
+
print(f' uniforms {ev_id} failed: {ex}')
|
|
2109
|
+
cls = _classify_draw(
|
|
2110
|
+
row['blend_enable'], row['depth_write_enable'], parent,
|
|
2111
|
+
row['blend_src_color'], row['blend_dst_color'],
|
|
2112
|
+
)
|
|
2113
|
+
draw_classes[ev_id] = cls
|
|
2114
|
+
if row['program_id']: unique_programs.add(row['program_id'])
|
|
2115
|
+
if row['vs_shader_id']: unique_shaders.add(row['vs_shader_id'])
|
|
2116
|
+
if row['fs_shader_id']: unique_shaders.add(row['fs_shader_id'])
|
|
2117
|
+
# Tally textures seen across draws
|
|
2118
|
+
for db in db_rows[-12:]: # cheap window; not exact but fast
|
|
2119
|
+
if db['slot_kind'] == 'texture' and db['resource_id']:
|
|
2120
|
+
unique_textures.add(db['resource_id'])
|
|
2121
|
+
if (i + 1) % 500 == 0:
|
|
2122
|
+
print(f' draws: {i+1}/{n_draws_total}')
|
|
2123
|
+
print(f' read draw states: {len(draws)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2124
|
+
print(f' per-draw aux: bindings={len(db_rows)} vertex_inputs={len(vi_rows)} descriptor_access={len(da_rows)} post_vs_samples={len(pvs_rows)}')
|
|
2125
|
+
|
|
2126
|
+
# On Arm RD's GLES backend the per-stage GetReadOnlyResources()
|
|
2127
|
+
# returns empty arrays — resources are reached via the descriptor
|
|
2128
|
+
# store, and only GetDescriptorAccess() yields the actually-used
|
|
2129
|
+
# bindings. To keep draw_bindings populated, project from
|
|
2130
|
+
# descriptor_access. The columns differ slightly but both describe
|
|
2131
|
+
# "what this draw touched."
|
|
2132
|
+
_KIND_TO_SLOT = {
|
|
2133
|
+
'ReadOnlyResource': 'texture',
|
|
2134
|
+
'ImageSampler': 'texture', # Arm RD GLES: combined tex+sampler
|
|
2135
|
+
'TypedBuffer': 'texture', # texture buffer object
|
|
2136
|
+
'ReadWriteResource': 'ssbo',
|
|
2137
|
+
'ReadWriteBuffer': 'ssbo',
|
|
2138
|
+
'Sampler': 'sampler',
|
|
2139
|
+
'ConstantBuffer': 'ubo',
|
|
2140
|
+
}
|
|
2141
|
+
for da in da_rows:
|
|
2142
|
+
slot_kind = _KIND_TO_SLOT.get(da['descriptor_kind'], da['descriptor_kind'].lower())
|
|
2143
|
+
db_rows.append({
|
|
2144
|
+
**{k: da[k] for k in ID_COLS},
|
|
2145
|
+
'event_id': da['event_id'],
|
|
2146
|
+
'slot_kind': slot_kind,
|
|
2147
|
+
'slot_index': da['slot_index'],
|
|
2148
|
+
'resource_id': da['resource_id'] if slot_kind != 'sampler' else 0,
|
|
2149
|
+
'sampler_id': da['resource_id'] if slot_kind == 'sampler' else 0,
|
|
2150
|
+
'offset': da['byte_offset'],
|
|
2151
|
+
'size': da['byte_size'],
|
|
2152
|
+
'stride': 0,
|
|
2153
|
+
})
|
|
2154
|
+
|
|
2155
|
+
t0 = time.monotonic()
|
|
2156
|
+
clears = _extract_clears(sd, tree['events'], tree['action_by_event'], ctx)
|
|
2157
|
+
dispatches = _extract_dispatches(ctrl, tree['events'], event_durations, ctx)
|
|
2158
|
+
print(f' clears={len(clears)} dispatches={len(dispatches)} ({time.monotonic()-t0:.1f}s)')
|
|
2159
|
+
|
|
2160
|
+
# Sample VBOs + IBOs that draws actually used
|
|
2161
|
+
used_vbo_ids: set = set()
|
|
2162
|
+
used_ibo_ids: set = set()
|
|
2163
|
+
for vi in vi_rows:
|
|
2164
|
+
if vi['buffer_id']:
|
|
2165
|
+
used_vbo_ids.add(vi['buffer_id'])
|
|
2166
|
+
for d in draws:
|
|
2167
|
+
if d['ibo_id']:
|
|
2168
|
+
used_ibo_ids.add(d['ibo_id'])
|
|
2169
|
+
t0 = time.monotonic()
|
|
2170
|
+
vbo_samples = _sample_vbos(ctrl, used_vbo_ids, ctx)
|
|
2171
|
+
ibo_samples = _sample_ibos(ctrl, used_ibo_ids, ctx)
|
|
2172
|
+
print(f' sampled VBOs={len(used_vbo_ids)}->{len(vbo_samples)} rows '
|
|
2173
|
+
f'IBOs={len(used_ibo_ids)}->{len(ibo_samples)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2174
|
+
|
|
2175
|
+
t0 = time.monotonic()
|
|
2176
|
+
texture_samples = _sample_textures(ctrl, ctx)
|
|
2177
|
+
print(f' sampled small textures: {len(texture_samples)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2178
|
+
|
|
2179
|
+
t0 = time.monotonic()
|
|
2180
|
+
rt_rows, timeline_rows, histogram_by_rt = _walk_render_targets(ctrl, ctx)
|
|
2181
|
+
print(f' walked RTs: {len(rt_rows)} rts, {len(timeline_rows)} timeline rows, {len(histogram_by_rt)} histograms ({time.monotonic()-t0:.1f}s)')
|
|
2182
|
+
|
|
2183
|
+
# Write histograms as sidecar JSON files
|
|
2184
|
+
hist_dir = os.path.join(capture_stage, 'histogram')
|
|
2185
|
+
os.makedirs(hist_dir, exist_ok=True)
|
|
2186
|
+
for rt_id, buckets in histogram_by_rt.items():
|
|
2187
|
+
with open(os.path.join(hist_dir, f'{rt_id}.json'), 'w', encoding='utf-8') as f:
|
|
2188
|
+
json.dump({'rt_id': rt_id, 'buckets': buckets}, f)
|
|
2189
|
+
|
|
2190
|
+
t0 = time.monotonic()
|
|
2191
|
+
sc_rows = _build_state_change_rows(sd, tree['parent_path_by_event'],
|
|
2192
|
+
tree['chunk_name_by_event'],
|
|
2193
|
+
tree['chunk_to_event_id'], ctx)
|
|
2194
|
+
print(f' state change chunks: {len(sc_rows)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2195
|
+
|
|
2196
|
+
# Indices for passes/frame_totals: per-event lookups
|
|
2197
|
+
draws_by_event = {d['event_id']: d for d in draws}
|
|
2198
|
+
bindings_by_event: dict[int, list] = {}
|
|
2199
|
+
for b in db_rows:
|
|
2200
|
+
bindings_by_event.setdefault(b['event_id'], []).append(b)
|
|
2201
|
+
|
|
2202
|
+
t0 = time.monotonic()
|
|
2203
|
+
passes = _build_passes(tree['events'], event_durations, draw_classes, ctx,
|
|
2204
|
+
draws_by_event=draws_by_event,
|
|
2205
|
+
bindings_by_event=bindings_by_event)
|
|
2206
|
+
print(f' passes: {len(passes)} ({time.monotonic()-t0:.1f}s)')
|
|
2207
|
+
|
|
2208
|
+
# FBO inventory from per-draw state
|
|
2209
|
+
labels: dict[int, str] = {}
|
|
2210
|
+
labels_path = os.path.join(capture_stage, 'labels.json')
|
|
2211
|
+
if os.path.exists(labels_path):
|
|
2212
|
+
try:
|
|
2213
|
+
with open(labels_path, 'r', encoding='utf-8') as f:
|
|
2214
|
+
labels = {int(k): v for k, v in json.load(f).items()}
|
|
2215
|
+
except Exception:
|
|
2216
|
+
pass
|
|
2217
|
+
t0 = time.monotonic()
|
|
2218
|
+
fbo_rows = _build_fbo_rows(ctrl, fbo_state_per_event, labels, ctx)
|
|
2219
|
+
print(f' fbos: {len(fbo_rows)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2220
|
+
|
|
2221
|
+
# Indirect args
|
|
2222
|
+
t0 = time.monotonic()
|
|
2223
|
+
indirect_rows = _extract_indirect_args(ctrl, tree['events'], ctx)
|
|
2224
|
+
print(f' indirect_args: {len(indirect_rows)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2225
|
+
|
|
2226
|
+
# Pixel history at last event
|
|
2227
|
+
last_ev = max((e['event_id'] for e in tree['events']), default=0)
|
|
2228
|
+
try:
|
|
2229
|
+
ctrl.SetFrameEvent(last_ev, False)
|
|
2230
|
+
except Exception:
|
|
2231
|
+
pass
|
|
2232
|
+
t0 = time.monotonic()
|
|
2233
|
+
pixel_history = _extract_pixel_history(ctrl, ctx, last_ev,
|
|
2234
|
+
grid=int(os.environ.get('RDC_PIXEL_GRID', '4')))
|
|
2235
|
+
print(f' pixel_history: {len(pixel_history)} rows ({time.monotonic()-t0:.1f}s)')
|
|
2236
|
+
|
|
2237
|
+
totals = _build_frame_totals(
|
|
2238
|
+
tree['events'], draws, event_durations,
|
|
2239
|
+
unique_programs, unique_shaders, unique_textures, ctx,
|
|
2240
|
+
sc_rows=sc_rows,
|
|
2241
|
+
bindings_by_event=bindings_by_event,
|
|
2242
|
+
buffers_rows=[], # parser owns buffers.csv; aggregates via derive_post_merge
|
|
2243
|
+
textures_rows=rt_rows, # RTs are textures with est_bytes after derive
|
|
2244
|
+
)
|
|
2245
|
+
|
|
2246
|
+
md = _frame_metadata(ctrl, ctx, rdc_path, sd=sd)
|
|
2247
|
+
|
|
2248
|
+
# --- write outputs ---
|
|
2249
|
+
print('writing CSVs')
|
|
2250
|
+
n_events = _write_rows(os.path.join(capture_stage, 'events.csv'), EVENTS_COLS, tree['events'])
|
|
2251
|
+
n_draws_w = _write_rows(os.path.join(capture_stage, 'draws.csv'), DRAWS_COLS, draws)
|
|
2252
|
+
n_db = _write_rows(os.path.join(capture_stage, 'draw_bindings.csv'), DRAW_BINDINGS_COLS, db_rows)
|
|
2253
|
+
n_vi = _write_rows(os.path.join(capture_stage, 'vertex_inputs.csv'), VERTEX_INPUTS_COLS, vi_rows)
|
|
2254
|
+
n_da = _write_rows(os.path.join(capture_stage, 'descriptor_access.csv'), DESCRIPTOR_ACCESS_COLS, da_rows)
|
|
2255
|
+
n_pvs = _write_rows(os.path.join(capture_stage, 'post_vs_samples.csv'), POST_VS_SAMPLES_COLS, pvs_rows)
|
|
2256
|
+
n_cl = _write_rows(os.path.join(capture_stage, 'clears.csv'), CLEARS_COLS, clears)
|
|
2257
|
+
n_disp = _write_rows(os.path.join(capture_stage, 'dispatches.csv'), DISPATCHES_COLS, dispatches)
|
|
2258
|
+
n_vbo = _write_rows(os.path.join(capture_stage, 'vbo_samples.csv'), VBO_SAMPLES_COLS, vbo_samples)
|
|
2259
|
+
n_ibo = _write_rows(os.path.join(capture_stage, 'ibo_samples.csv'), IBO_SAMPLES_COLS, ibo_samples)
|
|
2260
|
+
n_ts = _write_rows(os.path.join(capture_stage, 'texture_samples.csv'), TEXTURE_SAMPLES_COLS, texture_samples)
|
|
2261
|
+
n_rt = _write_rows(os.path.join(capture_stage, 'render_targets.csv'), RT_COLS, rt_rows)
|
|
2262
|
+
n_tl = _write_rows(os.path.join(capture_stage, 'rt_event_timeline.csv'), RT_TIMELINE_COLS, timeline_rows)
|
|
2263
|
+
n_ctr = _write_rows(os.path.join(capture_stage, 'counters_per_event.csv'), COUNTERS_COLS, counter_rows)
|
|
2264
|
+
n_sc = _write_rows(os.path.join(capture_stage, 'state_change_events.csv'), STATE_CHANGE_COLS, sc_rows)
|
|
2265
|
+
n_p = _write_rows(os.path.join(capture_stage, 'passes.csv'), PASSES_COLS, passes)
|
|
2266
|
+
n_fbo = _write_rows(os.path.join(capture_stage, 'fbos.csv'), FBOS_COLS, fbo_rows)
|
|
2267
|
+
n_ind = _write_rows(os.path.join(capture_stage, 'indirect_args.csv'), INDIRECT_ARGS_COLS, indirect_rows)
|
|
2268
|
+
n_ph = _write_rows(os.path.join(capture_stage, 'pixel_history.csv'), PIXEL_HISTORY_COLS, pixel_history)
|
|
2269
|
+
totals['unique_textures_bound'] = len(unique_textures)
|
|
2270
|
+
n_ft = _write_rows(os.path.join(capture_stage, 'frame_totals.csv'), FRAME_TOTALS_COLS, [totals])
|
|
2271
|
+
|
|
2272
|
+
with open(os.path.join(capture_stage, 'frame_metadata.json'), 'w', encoding='utf-8') as f:
|
|
2273
|
+
json.dump(md, f, indent=2)
|
|
2274
|
+
|
|
2275
|
+
with open(os.path.join(capture_stage, 'uniforms_per_pass.jsonl'), 'w', encoding='utf-8') as f:
|
|
2276
|
+
for u in uniforms_per_pass_rows:
|
|
2277
|
+
f.write(json.dumps(u))
|
|
2278
|
+
f.write('\n')
|
|
2279
|
+
|
|
2280
|
+
print(f'wrote events={n_events} draws={n_draws_w} bindings={n_db} vinputs={n_vi}'
|
|
2281
|
+
f' desc_access={n_da} post_vs={n_pvs} clears={n_cl} disp={n_disp}'
|
|
2282
|
+
f' rt={n_rt} timeline={n_tl} counters={n_ctr} state_change={n_sc}'
|
|
2283
|
+
f' passes={n_p} fbos={n_fbo} indirect={n_ind} pixel_history={n_ph}'
|
|
2284
|
+
f' uniforms_passes={len(uniforms_per_pass_rows)} totals={n_ft}')
|
|
2285
|
+
|
|
2286
|
+
finally:
|
|
2287
|
+
try: ctrl.Shutdown()
|
|
2288
|
+
except Exception: pass
|
|
2289
|
+
try: cap.Shutdown()
|
|
2290
|
+
except Exception: pass
|
|
2291
|
+
|
|
2292
|
+
print('done')
|
|
2293
|
+
except Exception:
|
|
2294
|
+
traceback.print_exc()
|
|
2295
|
+
try: log.flush(); log.close()
|
|
2296
|
+
except Exception: pass
|
|
2297
|
+
os._exit(1)
|
|
2298
|
+
|
|
2299
|
+
try: log.flush(); log.close()
|
|
2300
|
+
except Exception: pass
|
|
2301
|
+
os._exit(0)
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
if __name__ == '__main__':
|
|
2305
|
+
main()
|