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.
Files changed (130) hide show
  1. bobframes/__init__.py +3 -0
  2. bobframes/_version.py +1 -0
  3. bobframes/catalog.py +154 -0
  4. bobframes/cli.py +266 -0
  5. bobframes/derive_post_merge.py +365 -0
  6. bobframes/derives/__init__.py +0 -0
  7. bobframes/derives/pass_class_breakdown.py +102 -0
  8. bobframes/derives/texture_usage.py +121 -0
  9. bobframes/discovery.py +132 -0
  10. bobframes/global_entities.py +99 -0
  11. bobframes/html/__init__.py +0 -0
  12. bobframes/html/template.py +1056 -0
  13. bobframes/lint.py +114 -0
  14. bobframes/manifest.py +127 -0
  15. bobframes/parquetize.py +282 -0
  16. bobframes/parsers/__init__.py +0 -0
  17. bobframes/parsers/derive_program_transitions.py +73 -0
  18. bobframes/parsers/parse_init_state.py +675 -0
  19. bobframes/paths.py +111 -0
  20. bobframes/probes/__init__.py +0 -0
  21. bobframes/probes/whatif.py +165 -0
  22. bobframes/qrd_harness.py +119 -0
  23. bobframes/query_examples.py +222 -0
  24. bobframes/rdcmd.py +72 -0
  25. bobframes/replay/__init__.py +26 -0
  26. bobframes/replay/replay_main.py +2305 -0
  27. bobframes/reports/__init__.py +0 -0
  28. bobframes/reports/_dashboard.py +425 -0
  29. bobframes/reports/ab.py +88 -0
  30. bobframes/reports/base.py +114 -0
  31. bobframes/reports/cache.py +147 -0
  32. bobframes/reports/chrome.py +1306 -0
  33. bobframes/reports/cli.py +99 -0
  34. bobframes/reports/delta.py +167 -0
  35. bobframes/reports/discovery.py +118 -0
  36. bobframes/reports/draws_by_class.py +165 -0
  37. bobframes/reports/formatters.py +122 -0
  38. bobframes/reports/instancing_opportunities.py +276 -0
  39. bobframes/reports/orchestrator.py +59 -0
  40. bobframes/reports/overdraw.py +293 -0
  41. bobframes/reports/pass_gpu.py +190 -0
  42. bobframes/reports/shader_hotlist.py +240 -0
  43. bobframes/reports/trend_table.py +444 -0
  44. bobframes/resource_labels.py +162 -0
  45. bobframes/run.py +480 -0
  46. bobframes/schemas.py +426 -0
  47. bobframes/stable_keys.py +83 -0
  48. bobframes/tests/__init__.py +0 -0
  49. bobframes/tests/_render_util.py +84 -0
  50. bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
  51. bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
  52. bobframes/tests/data/golden/_reports/index.html +264 -0
  53. bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
  54. bobframes/tests/data/golden/_reports/overdraw.html +275 -0
  55. bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
  56. bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
  57. bobframes/tests/data/golden/_reports/trend_table.html +390 -0
  58. bobframes/tests/data/golden/index.html +1175 -0
  59. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
  60. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  61. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  62. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  63. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  64. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  65. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  66. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  67. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  68. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  69. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  70. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  71. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  72. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  73. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  74. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  75. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  76. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  77. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  78. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  79. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  80. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  81. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  82. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  83. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  84. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  85. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  86. bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  87. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
  88. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  89. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  90. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  91. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  92. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  93. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  94. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  95. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  96. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  97. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  98. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  99. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  100. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  101. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  102. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  103. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  104. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  105. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  106. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  107. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  108. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  109. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  110. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  111. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  112. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  113. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  114. bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  115. bobframes/tests/make_synthetic.py +171 -0
  116. bobframes/tests/smoke.py +199 -0
  117. bobframes/tests/test_determinism.py +19 -0
  118. bobframes/tests/test_discovery.py +97 -0
  119. bobframes/tests/test_hardening.py +142 -0
  120. bobframes/tests/test_parity.py +22 -0
  121. bobframes/tests/test_perf.py +18 -0
  122. bobframes/tests/test_replay_drift.py +115 -0
  123. bobframes/tests/test_schemas.py +26 -0
  124. bobframes/tests/test_schemas_unit.py +55 -0
  125. bobframes/tests/test_stable_keys.py +61 -0
  126. bobframes-0.1.0.dist-info/METADATA +144 -0
  127. bobframes-0.1.0.dist-info/RECORD +130 -0
  128. bobframes-0.1.0.dist-info/WHEEL +4 -0
  129. bobframes-0.1.0.dist-info/entry_points.txt +2 -0
  130. bobframes-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,675 @@
1
+ """Stream a .zip.xml file and emit partial CSVs of init-state data.
2
+
3
+ Heavyweight text extraction that doesn't need replay state:
4
+ - glShaderSource source dumps (one .glsl file per shader id)
5
+ - shaders.csv partial rows (id, type, src_len, src_hash, complexity counts)
6
+ - programs.csv partial rows (id, linked flag, attached_shader_ids)
7
+ - textures.csv partial rows (id, format, w, h, mip_levels, sample_count)
8
+ - samplers.csv partial rows (id, filter/wrap/aniso parameters)
9
+ - buffers.csv partial rows (id, allocated_size_bytes, usage_hint)
10
+ - fbos.csv partial rows (id, attachments)
11
+ - resource_creation.csv rows for every glGen*/glCreate* chunk
12
+ - labels.json: {resource_id (int): label_string}
13
+ - chunk_index_init_max: max chunkIndex covered (so replay knows where the init region ends)
14
+
15
+ Replay-side (replay_main.py) fills the entity tables further at merge time
16
+ (used_by_draw_count, attached_to_fbo_ids from frame usage, etc.).
17
+
18
+ `created_at_event` is set to -1 ("init state") for everything emitted here
19
+ since these chunks are pre-action-tree. A merge-time pass overlays the real
20
+ event_id when one is associated.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import csv
26
+ import hashlib
27
+ import json
28
+ import os
29
+ import re
30
+ import sys
31
+ import time
32
+ from collections.abc import Iterator
33
+
34
+ # --- Streaming chunk reader --------------------------------------------------
35
+
36
+ _RE_CHUNK_START = re.compile(
37
+ r'<chunk\s+id="(\d+)"\s+chunkIndex="(\d+)"\s+name="([^"]+)"'
38
+ )
39
+
40
+
41
+ def iter_chunks(xml_path: str) -> Iterator[tuple[int, int, str, str]]:
42
+ """Yield (chunk_id, chunk_index, chunk_name, body) for every <chunk>.
43
+
44
+ Streams line by line; constant memory regardless of file size.
45
+ body excludes the opening <chunk ...> tag and the closing </chunk> tag.
46
+ """
47
+ in_chunk = False
48
+ cid = cidx = 0
49
+ cname = ''
50
+ buf: list[str] = []
51
+ with open(xml_path, 'r', encoding='utf-8', errors='replace') as f:
52
+ for line in f:
53
+ if not in_chunk:
54
+ m = _RE_CHUNK_START.search(line)
55
+ if m:
56
+ cid = int(m.group(1))
57
+ cidx = int(m.group(2))
58
+ cname = m.group(3)
59
+ in_chunk = True
60
+ buf = []
61
+ if '</chunk>' in line:
62
+ body = ''.join(buf)
63
+ yield cid, cidx, cname, body
64
+ in_chunk = False
65
+ buf = []
66
+ else:
67
+ buf.append(line)
68
+ if '</chunk>' in line:
69
+ body = ''.join(buf)
70
+ yield cid, cidx, cname, body
71
+ in_chunk = False
72
+ buf = []
73
+
74
+
75
+ # --- Body extractors ---------------------------------------------------------
76
+
77
+ _RE_RESID = re.compile(
78
+ r'<ResourceId\s+name="([^"]+)"[^>]*>(\d+)</ResourceId>'
79
+ )
80
+ _RE_INT = re.compile(
81
+ r'<int\s+name="([^"]+)"[^>]*>(-?\d+)</int>'
82
+ )
83
+ _RE_UINT = re.compile(
84
+ r'<u?int(?:64_t)?\s+name="([^"]+)"[^>]*>(\d+)</u?int(?:64_t)?>'
85
+ )
86
+ _RE_ENUM_WITH_STRING = re.compile(
87
+ r'<enum\s+name="([^"]+)"[^>]*string="([^"]+)"[^>]*>(\d+)</enum>'
88
+ )
89
+ _RE_STRING_PLAIN = re.compile(
90
+ r'<string\s+name="([^"]+)"[^>]*>([\s\S]*?)</string>'
91
+ )
92
+ _RE_SHADER_SRC = re.compile(
93
+ r'<array\s+name="sources"[^>]*>\s*<string[^>]*>([\s\S]*?)</string>'
94
+ )
95
+
96
+
97
+ class _CI(dict):
98
+ """Case-insensitive dict for chunk arg lookups."""
99
+ def get(self, key, default=None):
100
+ v = super().get(key, _MISS)
101
+ if v is not _MISS:
102
+ return v
103
+ kl = key.lower()
104
+ for k, v in self.items():
105
+ if k.lower() == kl:
106
+ return v
107
+ return default
108
+
109
+ def __contains__(self, key):
110
+ return self.get(key, _MISS) is not _MISS
111
+
112
+ _MISS = object()
113
+
114
+
115
+ def _resids(body: str) -> _CI:
116
+ d = _CI()
117
+ for m in _RE_RESID.finditer(body):
118
+ d[m.group(1)] = int(m.group(2))
119
+ return d
120
+
121
+
122
+ def _ints(body: str) -> _CI:
123
+ out = _CI()
124
+ for m in _RE_INT.finditer(body):
125
+ out[m.group(1)] = int(m.group(2))
126
+ for m in _RE_UINT.finditer(body):
127
+ if m.group(1) not in out:
128
+ out[m.group(1)] = int(m.group(2))
129
+ return out
130
+
131
+
132
+ def _enums(body: str) -> _CI:
133
+ out = _CI()
134
+ for m in _RE_ENUM_WITH_STRING.finditer(body):
135
+ out[m.group(1)] = (int(m.group(3)), m.group(2))
136
+ return out
137
+
138
+
139
+ def _strings(body: str) -> _CI:
140
+ out = _CI()
141
+ for m in _RE_STRING_PLAIN.finditer(body):
142
+ out[m.group(1)] = m.group(2)
143
+ return out
144
+
145
+
146
+ def _first_resid(body: str) -> int:
147
+ """Return the FIRST non-zero ResourceId in body, or 0 if none."""
148
+ for m in _RE_RESID.finditer(body):
149
+ v = int(m.group(2))
150
+ if v:
151
+ return v
152
+ return 0
153
+
154
+
155
+ # --- Shader source complexity heuristics (counts, not interpretation) --------
156
+
157
+ _RE_TEX_SAMPLE = re.compile(r'\btexture(?:\w*)\(')
158
+ _RE_BRANCH = re.compile(r'\b(if|else)\b')
159
+ _RE_LOOP = re.compile(r'\b(for|while)\b')
160
+ _RE_DISCARD = re.compile(r'\bdiscard\b')
161
+ _RE_DFDX = re.compile(r'\b(dFdx|dFdy|fwidth)\b')
162
+ _RE_MAT4_CTOR = re.compile(r'\bmat4\s*\(')
163
+ _RE_VARYING = re.compile(r'\b(in|out|varying)\s+(?:highp|mediump|lowp)?\s*\w+\s+\w+\s*;')
164
+ _RE_MEDIUMP = re.compile(r'\bmediump\b')
165
+ _RE_HIGHP = re.compile(r'\bhighp\b')
166
+ _RE_LOWP = re.compile(r'\blowp\b')
167
+ _RE_FB_FETCH = re.compile(r'\b(GL_EXT_shader_framebuffer_fetch|inout\s+\w+\s+gl_LastFragData)\b')
168
+ _RE_CUBEMAP = re.compile(r'\bsamplerCube\b')
169
+ _RE_TEX_GATHER = re.compile(r'\btextureGather\w*\(')
170
+ _RE_TEX_GRAD = re.compile(r'\btextureGrad\w*\(')
171
+
172
+
173
+ def shader_complexity(source: str) -> dict[str, int]:
174
+ return {
175
+ 'total_texture_samples': len(_RE_TEX_SAMPLE.findall(source)),
176
+ 'total_branches': len(_RE_BRANCH.findall(source)),
177
+ 'total_loops': len(_RE_LOOP.findall(source)),
178
+ 'total_discards': len(_RE_DISCARD.findall(source)),
179
+ 'total_dfdx_dfdy': len(_RE_DFDX.findall(source)),
180
+ 'total_mat4_constructors': len(_RE_MAT4_CTOR.findall(source)),
181
+ 'total_varyings': len(_RE_VARYING.findall(source)),
182
+ 'mediump_decls': len(_RE_MEDIUMP.findall(source)),
183
+ 'highp_decls': len(_RE_HIGHP.findall(source)),
184
+ 'lowp_decls': len(_RE_LOWP.findall(source)),
185
+ 'fb_fetch': 1 if _RE_FB_FETCH.search(source) else 0,
186
+ 'uses_cubemap': 1 if _RE_CUBEMAP.search(source) else 0,
187
+ 'uses_texture_gather': 1 if _RE_TEX_GATHER.search(source) else 0,
188
+ 'uses_texture_grad': 1 if _RE_TEX_GRAD.search(source) else 0,
189
+ }
190
+
191
+
192
+ # --- Shader type decode ------------------------------------------------------
193
+
194
+ _GL_SHADER_TYPE = {
195
+ 35633: 'vertex',
196
+ 35632: 'fragment',
197
+ 36313: 'geometry',
198
+ 36488: 'tess_control',
199
+ 36487: 'tess_evaluation',
200
+ 37305: 'compute',
201
+ }
202
+
203
+
204
+ # --- Resource kind mapping ---------------------------------------------------
205
+
206
+ _RESOURCE_GEN_CHUNKS = {
207
+ 'glGenBuffers': 'buffer',
208
+ 'glCreateBuffers': 'buffer',
209
+ 'glGenTextures': 'texture',
210
+ 'glCreateTextures': 'texture',
211
+ 'glGenFramebuffers': 'framebuffer',
212
+ 'glCreateFramebuffers': 'framebuffer',
213
+ 'glGenSamplers': 'sampler',
214
+ 'glCreateSamplers': 'sampler',
215
+ 'glGenRenderbuffers': 'renderbuffer',
216
+ 'glGenVertexArrays': 'vao',
217
+ 'glCreateVertexArrays': 'vao',
218
+ 'glGenQueries': 'query',
219
+ 'glFenceSync': 'sync',
220
+ }
221
+
222
+
223
+ # --- Parser ------------------------------------------------------------------
224
+
225
+ class _Acc:
226
+ """Accumulator across chunks during one parse pass."""
227
+
228
+ def __init__(self) -> None:
229
+ self.resource_creation: list[dict] = []
230
+ self.shaders: dict[int, dict] = {} # shader_id → row dict
231
+ self.programs: dict[int, dict] = {} # program_id → row
232
+ self.textures: dict[int, dict] = {} # texture_id → row
233
+ self.buffers: dict[int, dict] = {} # buffer_id → row
234
+ self.samplers: dict[int, dict] = {} # sampler_id → row
235
+ self.fbos: dict[int, dict] = {} # fbo_id → row
236
+ self.labels: dict[int, str] = {} # resource_id → label
237
+ self.max_chunk_index: int = -1
238
+ self.shader_sources: dict[int, str] = {} # shader_id → full source
239
+
240
+ def _ensure_shader(self, sid: int) -> dict:
241
+ return self.shaders.setdefault(sid, {
242
+ 'shader_id': sid,
243
+ 'shader_type': '',
244
+ 'src_len': 0,
245
+ 'src_hash': '',
246
+ 'linked_program_ids': '',
247
+ 'used_by_draw_count': 0,
248
+ 'total_texture_samples': 0, 'total_branches': 0, 'total_loops': 0,
249
+ 'total_discards': 0, 'total_dfdx_dfdy': 0,
250
+ 'total_mat4_constructors': 0, 'total_varyings': 0,
251
+ 'mediump_decls': 0, 'highp_decls': 0, 'lowp_decls': 0,
252
+ 'fb_fetch': 0, 'uses_cubemap': 0, 'uses_texture_gather': 0,
253
+ 'uses_texture_grad': 0,
254
+ 'src_file_path': '',
255
+ })
256
+
257
+ def _ensure_program(self, pid: int) -> dict:
258
+ return self.programs.setdefault(pid, {
259
+ 'program_id': pid, 'linked': 0,
260
+ 'num_attached_shaders': 0, 'attached_shader_ids': '',
261
+ 'vs_shader_id': 0, 'fs_shader_id': 0, 'cs_shader_id': 0,
262
+ 'gs_shader_id': 0, 'tcs_shader_id': 0, 'tes_shader_id': 0,
263
+ 'num_active_uniforms': 0, 'num_active_uniform_blocks': 0,
264
+ 'num_active_attributes': 0, 'used_by_draw_count': 0, 'label': '',
265
+ })
266
+
267
+ def _ensure_texture(self, tid: int) -> dict:
268
+ return self.textures.setdefault(tid, {
269
+ 'tex_id': tid, 'format': '', 'width': 0, 'height': 0, 'depth': 0,
270
+ 'mip_levels': 0, 'sample_count': 0, 'kind': '',
271
+ 'est_bytes': 0, 'is_rt': 0, 'is_swap_chain': 0, 'label': '',
272
+ 'created_at_event': -1, 'num_bind_events': 0, 'num_sample_events': 0,
273
+ 'sampled_by_shader_ids': '', 'attached_to_fbo_ids': '',
274
+ })
275
+
276
+ def _ensure_buffer(self, bid: int) -> dict:
277
+ return self.buffers.setdefault(bid, {
278
+ 'buffer_id': bid, 'allocated_size_bytes': 0, 'usage_hint': '',
279
+ 'target_history': '',
280
+ 'first_alloc_event': -1, 'last_alloc_event': -1,
281
+ 'first_bind_event': -1, 'last_bind_event': -1,
282
+ 'num_glBufferData': 0, 'num_glBufferSubData': 0,
283
+ 'num_glBindBuffer': 0, 'num_glBindBufferBase': 0, 'num_glBindBufferRange': 0,
284
+ 'used_by_draws': 0,
285
+ 'used_as_vbo': 0, 'used_as_ibo': 0, 'used_as_ubo': 0,
286
+ 'used_as_ssbo': 0, 'used_as_indirect': 0,
287
+ })
288
+
289
+ def _ensure_sampler(self, sid: int) -> dict:
290
+ return self.samplers.setdefault(sid, {
291
+ 'sampler_id': sid, 'min_filter': '', 'mag_filter': '',
292
+ 'wrap_s': '', 'wrap_t': '', 'wrap_r': '',
293
+ 'mip_min_lod': -1000.0, 'mip_max_lod': 1000.0, 'mip_lod_bias': 0.0,
294
+ 'max_anisotropy': 1, 'compare_mode': '', 'compare_func': '',
295
+ 'border_color_r': 0.0, 'border_color_g': 0.0,
296
+ 'border_color_b': 0.0, 'border_color_a': 0.0,
297
+ 'created_at_event': -1, 'bound_to_draw_count': 0, 'label': '',
298
+ })
299
+
300
+ def _ensure_fbo(self, fid: int) -> dict:
301
+ return self.fbos.setdefault(fid, {
302
+ 'fbo_id': fid, 'attachment_point': '', 'kind': '',
303
+ 'resource_id': 0, 'format': '',
304
+ 'width': 0, 'height': 0, 'sample_count': 0,
305
+ 'mip_level': 0, 'layer': 0, 'created_at_event': -1,
306
+ 'bound_at_events': '',
307
+ 'num_clears': 0, 'num_writes': 0, 'num_reads': 0, 'label': '',
308
+ })
309
+
310
+
311
+ def _handle_create_shader(acc: _Acc, body: str, cidx: int) -> None:
312
+ rids = _resids(body)
313
+ enums = _enums(body)
314
+ sid = rids.get('real') or rids.get('shader') or 0
315
+ if not sid:
316
+ return
317
+ type_enum = enums.get('type')
318
+ kind_full = type_enum[1] if type_enum else ''
319
+ type_int = type_enum[0] if type_enum else 0
320
+ short = _GL_SHADER_TYPE.get(type_int, '')
321
+ row = acc._ensure_shader(sid)
322
+ row['shader_type'] = short or kind_full
323
+ acc.resource_creation.append({
324
+ 'resource_id': sid, 'resource_kind': 'shader',
325
+ 'created_at_event': -1, 'creation_chunk': 'glCreateShader',
326
+ 'declared_label': '',
327
+ })
328
+
329
+
330
+ def _handle_shader_source(acc: _Acc, body: str, cidx: int) -> None:
331
+ rids = _resids(body)
332
+ sid = rids.get('shader') or rids.get('shaderId') or 0
333
+ if not sid:
334
+ return
335
+ m = _RE_SHADER_SRC.search(body)
336
+ source = m.group(1) if m else ''
337
+ if not source:
338
+ return
339
+ acc.shader_sources[sid] = source
340
+ row = acc._ensure_shader(sid)
341
+ row['src_len'] = len(source)
342
+ row['src_hash'] = hashlib.sha256(source.encode('utf-8')).hexdigest()[:16]
343
+ row.update(shader_complexity(source))
344
+
345
+
346
+ def _handle_create_program(acc: _Acc, body: str, cidx: int) -> None:
347
+ rids = _resids(body)
348
+ pid = rids.get('real') or rids.get('program') or 0
349
+ if not pid:
350
+ return
351
+ acc._ensure_program(pid)
352
+ acc.resource_creation.append({
353
+ 'resource_id': pid, 'resource_kind': 'program',
354
+ 'created_at_event': -1, 'creation_chunk': 'glCreateProgram',
355
+ 'declared_label': '',
356
+ })
357
+
358
+
359
+ def _handle_attach_shader(acc: _Acc, body: str, cidx: int) -> None:
360
+ rids = _resids(body)
361
+ pid = rids.get('program') or 0
362
+ sid = rids.get('shader') or 0
363
+ if not pid or not sid:
364
+ return
365
+ prog = acc._ensure_program(pid)
366
+ existing = [int(x) for x in prog['attached_shader_ids'].split(';') if x]
367
+ if sid not in existing:
368
+ existing.append(sid)
369
+ prog['attached_shader_ids'] = ';'.join(str(x) for x in existing)
370
+ prog['num_attached_shaders'] = len(existing)
371
+ sh = acc._ensure_shader(sid)
372
+ sh_type = sh.get('shader_type', '')
373
+ slot_map = {
374
+ 'vertex': 'vs_shader_id', 'fragment': 'fs_shader_id',
375
+ 'compute': 'cs_shader_id', 'geometry': 'gs_shader_id',
376
+ 'tess_control': 'tcs_shader_id', 'tess_evaluation': 'tes_shader_id',
377
+ }
378
+ if sh_type in slot_map:
379
+ prog[slot_map[sh_type]] = sid
380
+ linked = [int(x) for x in sh['linked_program_ids'].split(';') if x]
381
+ if pid not in linked:
382
+ linked.append(pid)
383
+ sh['linked_program_ids'] = ';'.join(str(x) for x in linked)
384
+
385
+
386
+ def _handle_link_program(acc: _Acc, body: str, cidx: int) -> None:
387
+ rids = _resids(body)
388
+ pid = rids.get('program') or 0
389
+ if pid:
390
+ acc._ensure_program(pid)['linked'] = 1
391
+
392
+
393
+ def _handle_gen_resources(acc: _Acc, body: str, cidx: int, chunk_name: str) -> None:
394
+ kind = _RESOURCE_GEN_CHUNKS[chunk_name]
395
+ for m in _RE_RESID.finditer(body):
396
+ rid = int(m.group(2))
397
+ if rid == 0:
398
+ continue
399
+ acc.resource_creation.append({
400
+ 'resource_id': rid, 'resource_kind': kind,
401
+ 'created_at_event': -1, 'creation_chunk': chunk_name,
402
+ 'declared_label': '',
403
+ })
404
+
405
+
406
+ def _handle_tex_storage(acc: _Acc, body: str, cidx: int, chunk_name: str) -> None:
407
+ rids = _resids(body)
408
+ tid = rids.get('textureId') or rids.get('texture') or rids.get('target') or 0
409
+ if not tid:
410
+ return
411
+ ints = _ints(body)
412
+ enums = _enums(body)
413
+ row = acc._ensure_texture(tid)
414
+ fmt = enums.get('internalformat')
415
+ if fmt:
416
+ row['format'] = fmt[1]
417
+ row['width'] = ints.get('width', row['width'])
418
+ row['height'] = ints.get('height', row['height'])
419
+ row['depth'] = ints.get('depth', row['depth'])
420
+ row['mip_levels'] = ints.get('levels', row['mip_levels']) or 1
421
+ row['sample_count'] = ints.get('samples', row['sample_count']) or 1
422
+ target = enums.get('target')
423
+ if target:
424
+ row['kind'] = target[1].replace('GL_TEXTURE_', 'tex_').lower()
425
+ if chunk_name.startswith('glTexStorage'):
426
+ row['kind'] = row['kind'] or 'tex_2d'
427
+
428
+
429
+ def _handle_buffer_data(acc: _Acc, body: str, cidx: int) -> None:
430
+ rids = _resids(body)
431
+ bid = rids.get('bufferId') or rids.get('buffer') or 0
432
+ if not bid:
433
+ return
434
+ row = acc._ensure_buffer(bid)
435
+ ints = _ints(body)
436
+ enums = _enums(body)
437
+ sz = ints.get('bytesize') or ints.get('size') or 0
438
+ if sz > row['allocated_size_bytes']:
439
+ row['allocated_size_bytes'] = sz
440
+ usage = enums.get('usage')
441
+ if usage:
442
+ row['usage_hint'] = usage[1]
443
+ target = enums.get('target')
444
+ if target:
445
+ th = [t for t in row['target_history'].split(';') if t]
446
+ if target[1] not in th:
447
+ th.append(target[1])
448
+ row['target_history'] = ';'.join(th)
449
+ row['num_glBufferData'] += 1
450
+
451
+
452
+ def _handle_sampler_parameter(acc: _Acc, body: str, cidx: int, chunk_name: str) -> None:
453
+ rids = _resids(body)
454
+ sid = rids.get('sampler') or 0
455
+ if not sid:
456
+ return
457
+ row = acc._ensure_sampler(sid)
458
+ enums = _enums(body)
459
+ pname = enums.get('pname')
460
+ if not pname:
461
+ return
462
+ pn = pname[1]
463
+ param_enum = enums.get('param')
464
+ ints = _ints(body)
465
+ pval_int = ints.get('param')
466
+ pval_float = None
467
+ if 'param' in body and 'float' in body:
468
+ m = re.search(r'<float\s+name="param"[^>]*>(-?\d*\.?\d+(?:[eE][+-]?\d+)?)</float>', body)
469
+ if m:
470
+ pval_float = float(m.group(1))
471
+ pstr = param_enum[1] if param_enum else (str(pval_int) if pval_int is not None else '')
472
+ if pn == 'GL_TEXTURE_MIN_FILTER': row['min_filter'] = pstr
473
+ elif pn == 'GL_TEXTURE_MAG_FILTER': row['mag_filter'] = pstr
474
+ elif pn == 'GL_TEXTURE_WRAP_S': row['wrap_s'] = pstr
475
+ elif pn == 'GL_TEXTURE_WRAP_T': row['wrap_t'] = pstr
476
+ elif pn == 'GL_TEXTURE_WRAP_R': row['wrap_r'] = pstr
477
+ elif pn == 'GL_TEXTURE_MIN_LOD': row['mip_min_lod'] = pval_float if pval_float is not None else (pval_int or 0)
478
+ elif pn == 'GL_TEXTURE_MAX_LOD': row['mip_max_lod'] = pval_float if pval_float is not None else (pval_int or 0)
479
+ elif pn == 'GL_TEXTURE_LOD_BIAS': row['mip_lod_bias'] = pval_float if pval_float is not None else (pval_int or 0)
480
+ elif pn == 'GL_TEXTURE_MAX_ANISOTROPY' or pn == 'GL_TEXTURE_MAX_ANISOTROPY_EXT':
481
+ row['max_anisotropy'] = int(pval_float if pval_float is not None else (pval_int or 1))
482
+ elif pn == 'GL_TEXTURE_COMPARE_MODE': row['compare_mode'] = pstr
483
+ elif pn == 'GL_TEXTURE_COMPARE_FUNC': row['compare_func'] = pstr
484
+
485
+
486
+ def _handle_label(acc: _Acc, body: str) -> None:
487
+ rids = _resids(body)
488
+ rid = rids.get('Resource') or rids.get('resource') or 0
489
+ if not rid:
490
+ return
491
+ strs = _strings(body)
492
+ label = strs.get('Label') or strs.get('label') or ''
493
+ if label:
494
+ acc.labels[rid] = label
495
+
496
+
497
+ # --- Driver ------------------------------------------------------------------
498
+
499
+ _CHUNK_HANDLERS = {
500
+ 'glCreateShader': _handle_create_shader,
501
+ 'glShaderSource': _handle_shader_source,
502
+ 'glCreateProgram': _handle_create_program,
503
+ 'glAttachShader': _handle_attach_shader,
504
+ 'glLinkProgram': _handle_link_program,
505
+ 'glBufferData': _handle_buffer_data,
506
+ 'glBufferStorageEXT': _handle_buffer_data,
507
+ 'glLabelObjectEXT': lambda acc, body, cidx: _handle_label(acc, body),
508
+ 'glObjectLabel': lambda acc, body, cidx: _handle_label(acc, body),
509
+ }
510
+
511
+
512
+ def parse(xml_path: str) -> _Acc:
513
+ acc = _Acc()
514
+ for cid, cidx, cname, body in iter_chunks(xml_path):
515
+ if cidx > acc.max_chunk_index:
516
+ acc.max_chunk_index = cidx
517
+ if cname in _RESOURCE_GEN_CHUNKS:
518
+ _handle_gen_resources(acc, body, cidx, cname)
519
+ elif cname.startswith('glTexStorage') or cname.startswith('glTexImage'):
520
+ _handle_tex_storage(acc, body, cidx, cname)
521
+ elif cname.startswith('glSamplerParameter'):
522
+ _handle_sampler_parameter(acc, body, cidx, cname)
523
+ elif cname in _CHUNK_HANDLERS:
524
+ _CHUNK_HANDLERS[cname](acc, body, cidx)
525
+ # apply labels to entity rows
526
+ for rid, label in acc.labels.items():
527
+ if rid in acc.textures:
528
+ acc.textures[rid]['label'] = label
529
+ if rid in acc.buffers:
530
+ acc.buffers[rid]['target_history'] = acc.buffers[rid]['target_history'] # noop
531
+ if rid in acc.programs:
532
+ acc.programs[rid]['label'] = label
533
+ if rid in acc.samplers:
534
+ acc.samplers[rid]['label'] = label
535
+ if rid in acc.fbos:
536
+ acc.fbos[rid]['label'] = label
537
+ return acc
538
+
539
+
540
+ # --- CSV writers (stage layout) ---------------------------------------------
541
+
542
+ def _open_csv(path: str, fieldnames: list[str]):
543
+ f = open(path, 'w', encoding='utf-8', newline='')
544
+ w = csv.DictWriter(f, fieldnames=fieldnames)
545
+ w.writeheader()
546
+ return f, w
547
+
548
+
549
+ def _id_cols(ctx: dict) -> dict:
550
+ return {
551
+ 'area': ctx['area'], 'drop_date': ctx['drop_date'],
552
+ 'drop_label': ctx['drop_label'], 'capture': ctx['capture'],
553
+ }
554
+
555
+
556
+ def write_outputs(acc: _Acc, ctx: dict, capture_stage: str) -> dict[str, int]:
557
+ """Write CSVs + shader source files under capture_stage. Returns row counts."""
558
+ os.makedirs(capture_stage, exist_ok=True)
559
+ shader_src_dir = os.path.join(capture_stage, 'shader_src')
560
+ os.makedirs(shader_src_dir, exist_ok=True)
561
+
562
+ rc_path = os.path.join(capture_stage, 'resource_creation.csv')
563
+ sh_path = os.path.join(capture_stage, 'shaders.csv')
564
+ pr_path = os.path.join(capture_stage, 'programs.csv')
565
+ tx_path = os.path.join(capture_stage, 'textures.csv')
566
+ bf_path = os.path.join(capture_stage, 'buffers.csv')
567
+ sm_path = os.path.join(capture_stage, 'samplers.csv')
568
+ fb_path = os.path.join(capture_stage, 'fbos.csv')
569
+
570
+ counts: dict[str, int] = {}
571
+
572
+ # write shader source files first so we can put paths in shaders.csv
573
+ src_path_by_id: dict[int, str] = {}
574
+ for sid, source in acc.shader_sources.items():
575
+ rel = os.path.join('shader_src', f'{sid}.glsl')
576
+ full = os.path.join(capture_stage, rel)
577
+ with open(full, 'w', encoding='utf-8') as f:
578
+ f.write(source)
579
+ src_path_by_id[sid] = rel
580
+
581
+ id_cols = _id_cols(ctx)
582
+
583
+ from .. import schemas
584
+ RESOURCE_CREATION_FIELDS = list(schemas.RESOURCE_CREATION_COLS)
585
+ SHADERS_FIELDS = list(schemas.SHADERS_COLS)
586
+ PROGRAMS_FIELDS = list(schemas.PROGRAMS_COLS)
587
+ TEXTURES_FIELDS = list(schemas.TEXTURES_COLS)
588
+ BUFFERS_FIELDS = list(schemas.BUFFERS_COLS)
589
+ SAMPLERS_FIELDS = list(schemas.SAMPLERS_COLS)
590
+ FBOS_FIELDS = list(schemas.FBOS_COLS)
591
+
592
+ # resource_creation
593
+ f, w = _open_csv(rc_path, RESOURCE_CREATION_FIELDS)
594
+ n = 0
595
+ for r in acc.resource_creation:
596
+ row = {**id_cols, **r}
597
+ row['declared_label'] = acc.labels.get(r['resource_id'], '')
598
+ w.writerow(row); n += 1
599
+ f.close(); counts['resource_creation'] = n
600
+
601
+ # shaders
602
+ f, w = _open_csv(sh_path, SHADERS_FIELDS)
603
+ n = 0
604
+ for sid, r in sorted(acc.shaders.items()):
605
+ r = dict(r)
606
+ r['src_file_path'] = src_path_by_id.get(sid, '')
607
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
608
+ f.close(); counts['shaders'] = n
609
+
610
+ # programs
611
+ f, w = _open_csv(pr_path, PROGRAMS_FIELDS)
612
+ n = 0
613
+ for pid, r in sorted(acc.programs.items()):
614
+ r = dict(r)
615
+ r['label'] = acc.labels.get(pid, '')
616
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
617
+ f.close(); counts['programs'] = n
618
+
619
+ # textures
620
+ f, w = _open_csv(tx_path, TEXTURES_FIELDS)
621
+ n = 0
622
+ for tid, r in sorted(acc.textures.items()):
623
+ r = dict(r)
624
+ r['label'] = acc.labels.get(tid, '')
625
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
626
+ f.close(); counts['textures'] = n
627
+
628
+ # buffers
629
+ f, w = _open_csv(bf_path, BUFFERS_FIELDS)
630
+ n = 0
631
+ for bid, r in sorted(acc.buffers.items()):
632
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
633
+ f.close(); counts['buffers'] = n
634
+
635
+ # samplers
636
+ f, w = _open_csv(sm_path, SAMPLERS_FIELDS)
637
+ n = 0
638
+ for sid, r in sorted(acc.samplers.items()):
639
+ r = dict(r)
640
+ r['label'] = acc.labels.get(sid, '')
641
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
642
+ f.close(); counts['samplers'] = n
643
+
644
+ # fbos (only ones we accumulated; mostly populated by replay_main)
645
+ f, w = _open_csv(fb_path, FBOS_FIELDS)
646
+ n = 0
647
+ for fid, r in sorted(acc.fbos.items()):
648
+ w.writerow({**id_cols, 'stable_key': '', **r}); n += 1
649
+ f.close(); counts['fbos'] = n
650
+
651
+ # labels.json as a sidecar so replay_main can pick up labels for entities
652
+ # it discovers (e.g. RTs not seen as raw textures at parse time).
653
+ with open(os.path.join(capture_stage, 'labels.json'), 'w', encoding='utf-8') as f:
654
+ json.dump({str(k): v for k, v in acc.labels.items()}, f)
655
+
656
+ return counts
657
+
658
+
659
+ def main(argv: list[str]) -> int:
660
+ if len(argv) < 6:
661
+ print('usage: parse_init_state.py <zip_xml_path> <capture_stage> <area> <drop_date> <drop_label> <capture>',
662
+ file=sys.stderr)
663
+ return 2
664
+ xml_path, capture_stage, area, drop_date, drop_label, capture = argv[:6]
665
+ ctx = {'area': area, 'drop_date': drop_date, 'drop_label': drop_label, 'capture': capture}
666
+ t0 = time.monotonic()
667
+ acc = parse(xml_path)
668
+ counts = write_outputs(acc, ctx, capture_stage)
669
+ elapsed = time.monotonic() - t0
670
+ print(f'parse_init_state: {os.path.basename(xml_path)} -> {sum(counts.values())} rows in {elapsed:.1f}s; per-table {counts}')
671
+ return 0
672
+
673
+
674
+ if __name__ == '__main__':
675
+ sys.exit(main(sys.argv[1:]))