bobframes 0.1.0__tar.gz

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 (131) hide show
  1. bobframes-0.1.0/.gitignore +57 -0
  2. bobframes-0.1.0/CHANGELOG.md +45 -0
  3. bobframes-0.1.0/LICENSE +21 -0
  4. bobframes-0.1.0/PKG-INFO +144 -0
  5. bobframes-0.1.0/README.md +114 -0
  6. bobframes-0.1.0/bobframes/__init__.py +3 -0
  7. bobframes-0.1.0/bobframes/_version.py +1 -0
  8. bobframes-0.1.0/bobframes/catalog.py +154 -0
  9. bobframes-0.1.0/bobframes/cli.py +266 -0
  10. bobframes-0.1.0/bobframes/derive_post_merge.py +365 -0
  11. bobframes-0.1.0/bobframes/derives/__init__.py +0 -0
  12. bobframes-0.1.0/bobframes/derives/pass_class_breakdown.py +102 -0
  13. bobframes-0.1.0/bobframes/derives/texture_usage.py +121 -0
  14. bobframes-0.1.0/bobframes/discovery.py +132 -0
  15. bobframes-0.1.0/bobframes/global_entities.py +99 -0
  16. bobframes-0.1.0/bobframes/html/__init__.py +0 -0
  17. bobframes-0.1.0/bobframes/html/template.py +1056 -0
  18. bobframes-0.1.0/bobframes/lint.py +114 -0
  19. bobframes-0.1.0/bobframes/manifest.py +127 -0
  20. bobframes-0.1.0/bobframes/parquetize.py +282 -0
  21. bobframes-0.1.0/bobframes/parsers/__init__.py +0 -0
  22. bobframes-0.1.0/bobframes/parsers/derive_program_transitions.py +73 -0
  23. bobframes-0.1.0/bobframes/parsers/parse_init_state.py +675 -0
  24. bobframes-0.1.0/bobframes/paths.py +111 -0
  25. bobframes-0.1.0/bobframes/probes/__init__.py +0 -0
  26. bobframes-0.1.0/bobframes/probes/whatif.py +165 -0
  27. bobframes-0.1.0/bobframes/qrd_harness.py +119 -0
  28. bobframes-0.1.0/bobframes/query_examples.py +222 -0
  29. bobframes-0.1.0/bobframes/rdcmd.py +72 -0
  30. bobframes-0.1.0/bobframes/replay/__init__.py +26 -0
  31. bobframes-0.1.0/bobframes/replay/replay_main.py +2305 -0
  32. bobframes-0.1.0/bobframes/reports/__init__.py +0 -0
  33. bobframes-0.1.0/bobframes/reports/_dashboard.py +425 -0
  34. bobframes-0.1.0/bobframes/reports/ab.py +88 -0
  35. bobframes-0.1.0/bobframes/reports/base.py +114 -0
  36. bobframes-0.1.0/bobframes/reports/cache.py +147 -0
  37. bobframes-0.1.0/bobframes/reports/chrome.py +1306 -0
  38. bobframes-0.1.0/bobframes/reports/cli.py +99 -0
  39. bobframes-0.1.0/bobframes/reports/delta.py +167 -0
  40. bobframes-0.1.0/bobframes/reports/discovery.py +118 -0
  41. bobframes-0.1.0/bobframes/reports/draws_by_class.py +165 -0
  42. bobframes-0.1.0/bobframes/reports/formatters.py +122 -0
  43. bobframes-0.1.0/bobframes/reports/instancing_opportunities.py +276 -0
  44. bobframes-0.1.0/bobframes/reports/orchestrator.py +59 -0
  45. bobframes-0.1.0/bobframes/reports/overdraw.py +293 -0
  46. bobframes-0.1.0/bobframes/reports/pass_gpu.py +190 -0
  47. bobframes-0.1.0/bobframes/reports/shader_hotlist.py +240 -0
  48. bobframes-0.1.0/bobframes/reports/trend_table.py +444 -0
  49. bobframes-0.1.0/bobframes/resource_labels.py +162 -0
  50. bobframes-0.1.0/bobframes/run.py +480 -0
  51. bobframes-0.1.0/bobframes/schemas.py +426 -0
  52. bobframes-0.1.0/bobframes/stable_keys.py +83 -0
  53. bobframes-0.1.0/bobframes/tests/__init__.py +0 -0
  54. bobframes-0.1.0/bobframes/tests/_render_util.py +84 -0
  55. bobframes-0.1.0/bobframes/tests/data/golden/_reports/draws_by_class.html +323 -0
  56. bobframes-0.1.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +1560 -0
  57. bobframes-0.1.0/bobframes/tests/data/golden/_reports/index.html +264 -0
  58. bobframes-0.1.0/bobframes/tests/data/golden/_reports/instancing_opportunities.html +266 -0
  59. bobframes-0.1.0/bobframes/tests/data/golden/_reports/overdraw.html +275 -0
  60. bobframes-0.1.0/bobframes/tests/data/golden/_reports/pass_gpu.html +277 -0
  61. bobframes-0.1.0/bobframes/tests/data/golden/_reports/shader_hotlist.html +265 -0
  62. bobframes-0.1.0/bobframes/tests/data/golden/_reports/trend_table.html +390 -0
  63. bobframes-0.1.0/bobframes/tests/data/golden/index.html +1175 -0
  64. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +51 -0
  65. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  66. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  67. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  68. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  69. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  70. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  71. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  72. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  73. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  74. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  75. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  76. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  77. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  78. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  79. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  80. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  81. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  82. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  83. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  84. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  85. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  86. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  87. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  88. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  89. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  90. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  91. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  92. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +51 -0
  93. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  94. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  95. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  96. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  97. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  98. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  99. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  100. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  101. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  102. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  103. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  104. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  105. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  106. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  107. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  108. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  109. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  110. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  111. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  112. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  113. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  114. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  115. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  116. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  117. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  118. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  119. bobframes-0.1.0/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  120. bobframes-0.1.0/bobframes/tests/make_synthetic.py +171 -0
  121. bobframes-0.1.0/bobframes/tests/smoke.py +199 -0
  122. bobframes-0.1.0/bobframes/tests/test_determinism.py +19 -0
  123. bobframes-0.1.0/bobframes/tests/test_discovery.py +97 -0
  124. bobframes-0.1.0/bobframes/tests/test_hardening.py +142 -0
  125. bobframes-0.1.0/bobframes/tests/test_parity.py +22 -0
  126. bobframes-0.1.0/bobframes/tests/test_perf.py +18 -0
  127. bobframes-0.1.0/bobframes/tests/test_replay_drift.py +115 -0
  128. bobframes-0.1.0/bobframes/tests/test_schemas.py +26 -0
  129. bobframes-0.1.0/bobframes/tests/test_schemas_unit.py +55 -0
  130. bobframes-0.1.0/bobframes/tests/test_stable_keys.py +61 -0
  131. bobframes-0.1.0/pyproject.toml +56 -0
@@ -0,0 +1,57 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .Python
6
+ *.egg-info/
7
+ .eggs/
8
+ *.egg
9
+ build/
10
+ dist/
11
+ wheels/
12
+ pip-wheel-metadata/
13
+
14
+ # Virtual env
15
+ .venv/
16
+ venv/
17
+ env/
18
+
19
+ # Editor
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ .DS_Store
24
+
25
+ # Test artifacts
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+
31
+ # Project-specific: never commit user data, manifests, or rendered output
32
+ **/_data/
33
+ **/_reports/
34
+ **/_stage/
35
+ **/_tmp/
36
+ **/index.html
37
+ **/*.parquet
38
+ **/*.rdc
39
+ **/*.zip.xml
40
+ **/_manifest.json
41
+ **/done.marker
42
+
43
+ # Exception: bundled test fixtures (ADR-8: synthetic _data + golden HTML are test code, not user
44
+ # data). Must override the broad _data/_reports/index.html/parquet/manifest ignores above. Git
45
+ # cannot un-ignore a file under an ignored directory, so re-include the directories first, then files.
46
+ !bobframes/tests/data/
47
+ !bobframes/tests/data/**/
48
+ !bobframes/tests/data/**
49
+
50
+ # Bobframes config
51
+ .bobframes.toml
52
+
53
+ # Generated docs (docs/plan/ IS tracked — only built output is ignored)
54
+ docs/_build/
55
+
56
+ # OS
57
+ Thumbs.db
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ Changes to this project are documented here, newest first.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-31
11
+
12
+ First standalone release. v1 is Windows-only (the replay stage drives `qrenderdoc`).
13
+
14
+ ### Added
15
+ - Single `bobframes` binary with subcommands `ingest`, `render`, `ab`, `report`, `catalog`, `lint`,
16
+ `check`, `serve`, `smoke`, and `version`. Positional `root` (default `.`) across verbs; long-flag
17
+ only; exit codes `0`/`1`/`2`/`3`/`4`. stdlib `logging` at INFO by default, `--verbose` for DEBUG,
18
+ `[HH:MM:SS]` line format (G-8).
19
+ - Replay script located via `importlib.resources` (`bobframes.replay.replay_script_path`) so replay
20
+ works from an installed wheel, not just an in-tree checkout.
21
+ - Reliability hardening: atomic writes for `_manifest.json`, Parquet+CSV pairs, and `done.marker`
22
+ (`.tmp` + `os.replace`, rollback on failure); process-tree kill (`taskkill /T /F`) when a
23
+ `qrenderdoc` replay times out; per-capture replay-failure isolation
24
+ (`capture_status='replay_failed'` instead of aborting the whole drop); subprocess stderr logged on
25
+ convert-timeout and on every parse; manifest provenance fields `tool_versions` and `host_info`.
26
+ - Test gates: golden-snapshot parity, schema regression, determinism, performance, and a replay
27
+ column-drift guard against `schemas.py` (H-6); mocked-subprocess tests for the hardening branches
28
+ the GPU-less runner cannot exercise; a data-driven `smoke` (render-only against a bundled synthetic
29
+ fixture by default); unit tests for stable keys, schemas, and drop discovery.
30
+ - GitHub Actions CI across a Windows / Python / pyarrow matrix, with a tag-gated PyPI publish job.
31
+
32
+ ### Changed
33
+ - **Stable-key format upgrade:** stable keys now carry a `KEY_VERSION = 1` version byte in the hash
34
+ input. Keys produced before this release are not comparable with `0.1.0` keys; rebuild affected
35
+ data with `bobframes ingest --force`. `KEY_VERSION` bumps on any future key-derivation rule change.
36
+ - Timestamps unified to a single UTC `now_iso()` helper (`bobframes.manifest.now_iso`); the
37
+ local-time variant in `reports/cli` was dropped.
38
+
39
+ ### Removed
40
+ - The project-embedded `_analysis` package. `python -m _analysis.run` and the other
41
+ `python -m _analysis.*` entry points no longer work; switch to the `bobframes` commands (see the
42
+ migration table in the README). This is a hard rename with no compatibility shim.
43
+
44
+ [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.1.0...HEAD
45
+ [0.1.0]: https://github.com/altpsyche/bobframes/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siva Subramanyam, Mayhem Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: bobframes
3
+ Version: 0.1.0
4
+ Summary: RenderDoc capture pipeline: ingest, analyze, render.
5
+ Project-URL: Homepage, https://github.com/altpsyche/bobframes
6
+ Project-URL: Issues, https://github.com/altpsyche/bobframes/issues
7
+ Project-URL: Changelog, https://github.com/altpsyche/bobframes/blob/main/CHANGELOG.md
8
+ Author-email: Siva Subramanyam <sivasubramanyam@mayhem-studios.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: gpu,parquet,profiling,renderdoc
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Multimedia :: Graphics
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Requires-Python: <3.15,>=3.10
23
+ Requires-Dist: pyarrow<22,>=17
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == 'dev'
26
+ Requires-Dist: hatchling; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: twine; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # BobFrames
32
+
33
+ RenderDoc capture pipeline: ingest, analyze, render. Point it at a folder of `.rdc` captures and it
34
+ produces `_data/` (Parquet tables) plus `_reports/` (static HTML you can browse). Windows-only in v1.
35
+
36
+ ## Requirements
37
+
38
+ - Windows 10 or later (the replay stage drives `qrenderdoc`, which is Windows-only in v1).
39
+ - Python 3.10 - 3.13.
40
+ - RenderDoc 1.x, or Arm Performance Studio, providing `renderdoccmd` and `qrenderdoc` on disk.
41
+
42
+ ## Install
43
+
44
+ ```
45
+ pipx install bobframes
46
+ bobframes check
47
+ ```
48
+
49
+ `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
50
+ either is missing, so you can confirm the toolchain before a long ingest.
51
+
52
+ ## Quickstart
53
+
54
+ ```
55
+ cd path\to\captures # a folder of <Area>\<YYYY-MM-DD[_label]>\*.rdc
56
+ bobframes ingest . # export, parse, replay, parquetize, derive, render
57
+ bobframes serve . # open a local static preview of the reports
58
+ ```
59
+
60
+ `ingest` writes Parquet under `_data/` and HTML under `_reports/`. Re-run `bobframes render .` any
61
+ time to rebuild the HTML from existing Parquet without re-replaying captures.
62
+
63
+ ## Commands
64
+
65
+ | Command | Purpose |
66
+ |---|---|
67
+ | `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
68
+ | `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
69
+ | `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
70
+ | `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
71
+ | `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
72
+ | `lint <file>...` | Check HTML or markdown against the banlist. |
73
+ | `check` | Print resolved tool paths; non-zero when a tool is missing. |
74
+ | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
75
+ | `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
76
+ | `version` | Print `bobframes`, schema, and pyarrow versions. |
77
+
78
+ `<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
79
+ `1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
80
+
81
+ ## External tools
82
+
83
+ The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
84
+ for both at a baked Arm Performance Studio install path and on `PATH`. A config file and a
85
+ tool-resolver with version globbing arrive in v0.2; until then, install RenderDoc where v1 expects it
86
+ or put the executables on `PATH`. Run `bobframes check` to see what was resolved.
87
+
88
+ ## Output layout
89
+
90
+ ```
91
+ <root>/
92
+ index.html root catalog view
93
+ <area>/<drop>/ raw RDC inputs (left untouched)
94
+ _data/ pipeline outputs
95
+ _catalog.parquet (+ .csv, .json)
96
+ _global_entities.parquet (+ .csv)
97
+ _query_examples.md
98
+ <area>/<drop>/ per-drop data (29 Parquet tables)
99
+ *.parquet (+ matching .csv)
100
+ _manifest.json, _resource_labels.json
101
+ shader_src/*.glsl, jsonl sidecars
102
+ done.marker
103
+ _reports/ rendered HTML
104
+ *.html (dashboard + reports)
105
+ ab/<pair>/*.html
106
+ drill/<area>/<drop>/index.html per-drop browser
107
+ _cache/
108
+ ```
109
+
110
+ The catalog stores each drop's path relative to `<root>` for portability.
111
+
112
+ ## Migrating from `_analysis`
113
+
114
+ v1 is a hard rename of the older project-embedded `_analysis` package, with no compatibility shim:
115
+ `python -m _analysis.*` stops working once `bobframes` is installed. Map old invocations to new ones:
116
+
117
+ | Old (`_analysis`) | New (`bobframes`) |
118
+ |---|---|
119
+ | `python -m _analysis.run --root . --area X --label Y` | `bobframes ingest . --area X --label Y` |
120
+ | `python -m _analysis.reports.ab --root . --baseline-label X --compare-label Y` | `bobframes ab . --baseline-label X --compare-label Y` |
121
+ | `python -m _analysis.lint <file>` | `bobframes lint <file>` |
122
+ | `python -m _analysis.tests.smoke` | `bobframes smoke` |
123
+
124
+ ## Troubleshooting
125
+
126
+ | Symptom | Resolution |
127
+ |---|---|
128
+ | `renderdoccmd not found` (exit 3) | Install RenderDoc or Arm Performance Studio, or put the executable on `PATH`; confirm with `bobframes check`. |
129
+ | `qrenderdoc` replay hangs | v1 kills the replay process tree on timeout and records `capture_status='replay_failed'` for that capture; the rest of the drop still completes. Re-run with `--force` to retry. |
130
+ | Lint failure during render | The emitted HTML contains a banned token. Run `bobframes lint <file>` to see the line and label. |
131
+ | `schema mismatch` (exit 1) | A drop's `_manifest.json` schema version differs from the installed schema. Rebuild it with `bobframes ingest --force` (the v1 schema-migration path; see G-3). |
132
+ | Permission denied on `_data` | Close any viewer holding a Parquet open, then re-run; writes are staged and renamed atomically. |
133
+
134
+ ## Advanced
135
+
136
+ - A/B reports: `bobframes ab . --baseline-label OLD --compare-label NEW` builds a side-by-side set.
137
+ - Programmatic use: import `bobframes.schemas`, `bobframes.discovery`, and `bobframes.paths` to drive
138
+ table lookups, drop discovery, and path resolution from your own scripts.
139
+ - `bobframes/probes/whatif.py` is a manual qrenderdoc-side probe and is not wired as a CLI command.
140
+ - A TOML config file, an externalized draw classifier, and design-token theming are planned for v0.2.
141
+
142
+ ## License
143
+
144
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,114 @@
1
+ # BobFrames
2
+
3
+ RenderDoc capture pipeline: ingest, analyze, render. Point it at a folder of `.rdc` captures and it
4
+ produces `_data/` (Parquet tables) plus `_reports/` (static HTML you can browse). Windows-only in v1.
5
+
6
+ ## Requirements
7
+
8
+ - Windows 10 or later (the replay stage drives `qrenderdoc`, which is Windows-only in v1).
9
+ - Python 3.10 - 3.13.
10
+ - RenderDoc 1.x, or Arm Performance Studio, providing `renderdoccmd` and `qrenderdoc` on disk.
11
+
12
+ ## Install
13
+
14
+ ```
15
+ pipx install bobframes
16
+ bobframes check
17
+ ```
18
+
19
+ `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
20
+ either is missing, so you can confirm the toolchain before a long ingest.
21
+
22
+ ## Quickstart
23
+
24
+ ```
25
+ cd path\to\captures # a folder of <Area>\<YYYY-MM-DD[_label]>\*.rdc
26
+ bobframes ingest . # export, parse, replay, parquetize, derive, render
27
+ bobframes serve . # open a local static preview of the reports
28
+ ```
29
+
30
+ `ingest` writes Parquet under `_data/` and HTML under `_reports/`. Re-run `bobframes render .` any
31
+ time to rebuild the HTML from existing Parquet without re-replaying captures.
32
+
33
+ ## Commands
34
+
35
+ | Command | Purpose |
36
+ |---|---|
37
+ | `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
38
+ | `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
39
+ | `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
40
+ | `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
41
+ | `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
42
+ | `lint <file>...` | Check HTML or markdown against the banlist. |
43
+ | `check` | Print resolved tool paths; non-zero when a tool is missing. |
44
+ | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
45
+ | `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
46
+ | `version` | Print `bobframes`, schema, and pyarrow versions. |
47
+
48
+ `<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
49
+ `1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
50
+
51
+ ## External tools
52
+
53
+ The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
54
+ for both at a baked Arm Performance Studio install path and on `PATH`. A config file and a
55
+ tool-resolver with version globbing arrive in v0.2; until then, install RenderDoc where v1 expects it
56
+ or put the executables on `PATH`. Run `bobframes check` to see what was resolved.
57
+
58
+ ## Output layout
59
+
60
+ ```
61
+ <root>/
62
+ index.html root catalog view
63
+ <area>/<drop>/ raw RDC inputs (left untouched)
64
+ _data/ pipeline outputs
65
+ _catalog.parquet (+ .csv, .json)
66
+ _global_entities.parquet (+ .csv)
67
+ _query_examples.md
68
+ <area>/<drop>/ per-drop data (29 Parquet tables)
69
+ *.parquet (+ matching .csv)
70
+ _manifest.json, _resource_labels.json
71
+ shader_src/*.glsl, jsonl sidecars
72
+ done.marker
73
+ _reports/ rendered HTML
74
+ *.html (dashboard + reports)
75
+ ab/<pair>/*.html
76
+ drill/<area>/<drop>/index.html per-drop browser
77
+ _cache/
78
+ ```
79
+
80
+ The catalog stores each drop's path relative to `<root>` for portability.
81
+
82
+ ## Migrating from `_analysis`
83
+
84
+ v1 is a hard rename of the older project-embedded `_analysis` package, with no compatibility shim:
85
+ `python -m _analysis.*` stops working once `bobframes` is installed. Map old invocations to new ones:
86
+
87
+ | Old (`_analysis`) | New (`bobframes`) |
88
+ |---|---|
89
+ | `python -m _analysis.run --root . --area X --label Y` | `bobframes ingest . --area X --label Y` |
90
+ | `python -m _analysis.reports.ab --root . --baseline-label X --compare-label Y` | `bobframes ab . --baseline-label X --compare-label Y` |
91
+ | `python -m _analysis.lint <file>` | `bobframes lint <file>` |
92
+ | `python -m _analysis.tests.smoke` | `bobframes smoke` |
93
+
94
+ ## Troubleshooting
95
+
96
+ | Symptom | Resolution |
97
+ |---|---|
98
+ | `renderdoccmd not found` (exit 3) | Install RenderDoc or Arm Performance Studio, or put the executable on `PATH`; confirm with `bobframes check`. |
99
+ | `qrenderdoc` replay hangs | v1 kills the replay process tree on timeout and records `capture_status='replay_failed'` for that capture; the rest of the drop still completes. Re-run with `--force` to retry. |
100
+ | Lint failure during render | The emitted HTML contains a banned token. Run `bobframes lint <file>` to see the line and label. |
101
+ | `schema mismatch` (exit 1) | A drop's `_manifest.json` schema version differs from the installed schema. Rebuild it with `bobframes ingest --force` (the v1 schema-migration path; see G-3). |
102
+ | Permission denied on `_data` | Close any viewer holding a Parquet open, then re-run; writes are staged and renamed atomically. |
103
+
104
+ ## Advanced
105
+
106
+ - A/B reports: `bobframes ab . --baseline-label OLD --compare-label NEW` builds a side-by-side set.
107
+ - Programmatic use: import `bobframes.schemas`, `bobframes.discovery`, and `bobframes.paths` to drive
108
+ table lookups, drop discovery, and path resolution from your own scripts.
109
+ - `bobframes/probes/whatif.py` is a manual qrenderdoc-side probe and is not wired as a CLI command.
110
+ - A TOML config file, an externalized draw classifier, and design-token theming are planned for v0.2.
111
+
112
+ ## License
113
+
114
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,3 @@
1
+ from ._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,154 @@
1
+ """Build <root>/_data/_catalog.parquet and _catalog.json.
2
+
3
+ One row per (area, drop_date, drop_label, capture). Per-capture row counts
4
+ are computed by reading each drop's parquets and grouping by the `capture`
5
+ column — that way the catalog reflects what actually landed for each
6
+ capture, not just drop-level totals.
7
+
8
+ Also tracks schema version, build timestamp, replay status, and the relative
9
+ path to the drop's data dir (`_data/<area>/<drop>`). Path is RELATIVE for
10
+ portability across machines.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import datetime as _dt
16
+ import json
17
+ import os
18
+ from collections import defaultdict
19
+
20
+ import pyarrow as pa
21
+ import pyarrow.csv as pacsv
22
+ import pyarrow.parquet as papq
23
+
24
+ from . import paths as _paths
25
+
26
+
27
+ _CATALOG_TABLE_KEYS = [
28
+ 'draws', 'events', 'shaders', 'textures',
29
+ 'render_targets', 'buffers', 'programs',
30
+ 'samplers', 'fbos', 'state_change_events',
31
+ 'counters_per_event', 'descriptor_access',
32
+ 'passes', 'frame_totals',
33
+ 'clears', 'dispatches', 'rt_event_timeline',
34
+ 'vertex_inputs', 'resource_creation',
35
+ 'draw_bindings', 'program_transitions',
36
+ 'pixel_history', 'vbo_samples', 'ibo_samples',
37
+ 'post_vs_samples', 'texture_samples', 'indirect_args',
38
+ 'pass_class_breakdown', 'texture_usage',
39
+ ]
40
+
41
+
42
+ def _find_manifests(root: str) -> list[tuple[str, str, dict]]:
43
+ """Walk _data/<area>/<drop>/_manifest.json. Returns [(data_dir, rel_path, manifest)]."""
44
+ out = []
45
+ data_root = _paths.data_root(root)
46
+ if not os.path.isdir(data_root):
47
+ return out
48
+ for area_entry in sorted(os.listdir(data_root)):
49
+ if area_entry.startswith(('_', '.')):
50
+ continue
51
+ area_dir = os.path.join(data_root, area_entry)
52
+ if not os.path.isdir(area_dir):
53
+ continue
54
+ for drop_entry in sorted(os.listdir(area_dir)):
55
+ drop_dir = os.path.join(area_dir, drop_entry)
56
+ if not os.path.isdir(drop_dir):
57
+ continue
58
+ mf = os.path.join(drop_dir, '_manifest.json')
59
+ if not os.path.exists(mf):
60
+ continue
61
+ try:
62
+ with open(mf, 'r', encoding='utf-8') as f:
63
+ m = json.load(f)
64
+ except Exception:
65
+ continue
66
+ rel = _paths.drop_dir_rel(area_entry, drop_entry)
67
+ out.append((drop_dir, rel, m))
68
+ return out
69
+
70
+
71
+ def _per_capture_row_counts(data_dir: str, captures: list[str]) -> dict[str, dict[str, int]]:
72
+ """Walk all parquets in data_dir; return {capture: {table: row_count}}."""
73
+ result: dict[str, dict[str, int]] = {c: defaultdict(int) for c in captures}
74
+ for table in _CATALOG_TABLE_KEYS:
75
+ pq = os.path.join(data_dir, f'{table}.parquet')
76
+ if not os.path.exists(pq):
77
+ continue
78
+ try:
79
+ t = papq.read_table(pq, columns=['capture'])
80
+ caps = t.column('capture').to_pylist()
81
+ except Exception:
82
+ continue
83
+ for c in caps:
84
+ if c in result:
85
+ result[c][table] += 1
86
+ return {c: dict(d) for c, d in result.items()}
87
+
88
+
89
+ def _capture_rows(data_dir: str, rel_path: str, manifest: dict) -> list[dict]:
90
+ captures = manifest.get('captures') or manifest.get('stems') or []
91
+ cap_status = manifest.get('capture_status') or manifest.get('stem_status') or {}
92
+
93
+ per_cap = _per_capture_row_counts(data_dir, captures) if captures else {}
94
+
95
+ rows: list[dict] = []
96
+ for cap in captures:
97
+ counts = per_cap.get(cap, {})
98
+ rows.append({
99
+ 'area': manifest['area'],
100
+ 'drop_date': manifest['drop_date'],
101
+ 'drop_label': manifest.get('drop_label', '') or '',
102
+ 'capture': cap,
103
+ 'schema_version': int(manifest.get('schema_version', 0)),
104
+ 'build_timestamp': manifest.get('build_timestamp', ''),
105
+ 'replay_status': cap_status.get(cap, 'unknown'),
106
+ **{f'row_count_{k}': int(counts.get(k, 0)) for k in _CATALOG_TABLE_KEYS},
107
+ 'analysis_out_path': rel_path,
108
+ })
109
+ return rows
110
+
111
+
112
+ def build_catalog(root: str) -> dict:
113
+ manifests = _find_manifests(root)
114
+ all_rows: list[dict] = []
115
+ for data_dir, rel_path, m in manifests:
116
+ all_rows.extend(_capture_rows(data_dir, rel_path, m))
117
+
118
+ cols = [
119
+ 'area', 'drop_date', 'drop_label', 'capture',
120
+ 'schema_version', 'build_timestamp', 'replay_status',
121
+ ] + [f'row_count_{k}' for k in _CATALOG_TABLE_KEYS] + ['analysis_out_path']
122
+
123
+ arrays: dict[str, pa.Array] = {}
124
+ for c in cols:
125
+ vs = [r.get(c, '' if not c.startswith('row_count_') and c != 'schema_version' else 0)
126
+ for r in all_rows]
127
+ if c.startswith('row_count_') or c == 'schema_version':
128
+ arrays[c] = pa.array(vs, type=pa.int64())
129
+ else:
130
+ arrays[c] = pa.array([str(v) for v in vs], type=pa.string())
131
+ table = pa.table(arrays)
132
+
133
+ os.makedirs(_paths.data_root(root), exist_ok=True)
134
+ papq.write_table(table, _paths.catalog_parquet(root), compression='snappy')
135
+ pacsv.write_csv(table, _paths.catalog_csv(root))
136
+
137
+ summary = {
138
+ 'schema_version': max((r['schema_version'] for r in all_rows), default=0),
139
+ 'build_timestamp': _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat(),
140
+ 'drop_count': len({(r['area'], r['drop_date'], r['drop_label']) for r in all_rows}),
141
+ 'capture_count': len(all_rows),
142
+ 'areas': sorted({r['area'] for r in all_rows}),
143
+ }
144
+ with open(_paths.catalog_json(root), 'w', encoding='utf-8') as f:
145
+ json.dump(summary, f, indent=2)
146
+
147
+ return summary
148
+
149
+
150
+ if __name__ == '__main__':
151
+ import sys
152
+ root = sys.argv[1] if len(sys.argv) > 1 else '.'
153
+ s = build_catalog(root)
154
+ print(json.dumps(s, indent=2))