renderdoc 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +209 -0
- data/Rakefile +8 -0
- data/lib/renderdoc/application.rb +191 -0
- data/lib/renderdoc/capture.rb +5 -0
- data/lib/renderdoc/configuration.rb +26 -0
- data/lib/renderdoc/errors.rb +21 -0
- data/lib/renderdoc/frame_trigger.rb +40 -0
- data/lib/renderdoc/native/generated.rb +153 -0
- data/lib/renderdoc/native.rb +130 -0
- data/lib/renderdoc/rspec.rb +8 -0
- data/lib/renderdoc/rspec_integration.rb +60 -0
- data/lib/renderdoc/settings.rb +61 -0
- data/lib/renderdoc/version.rb +5 -0
- data/lib/renderdoc.rb +57 -0
- data/script/generate_native_api.rb +155 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aab8b86504640671b46abb7746f8fd15a8663b70dc4bcc51e5c5ffa113f7a3ae
|
|
4
|
+
data.tar.gz: e86076c7a2d76ffefea4c798f2c97ddd4d6e548ec6a9f2639672cba44b869afc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7b5fc719ae37e729bb4c2a0380468c6a091b893ab1304cebc95e8c33b73f42292259ff1b2391b1ef6e76d9cac7b0582e568ba4132839830657c8da6c10916d6c
|
|
7
|
+
data.tar.gz: 1e316d373258f1d24542d90eeecd206e43eac527f6077e1003c17f966770867c2bce902f81ab1edfc95e91b07cd5ddedca840fbc29fe0f927d049f99d022aad7
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# RenderDoc for Ruby
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ydah/renderdoc/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
`renderdoc` provides Ruby bindings for [RenderDoc](https://renderdoc.org/)'s in-application API. Use it to capture a specific block of GPU work, schedule frame captures, annotate captures, and open the replay UI from a Ruby application.
|
|
6
|
+
|
|
7
|
+
The gem binds to RenderDoc but does not bundle, load, or inject RenderDoc itself.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby 3.2 or newer
|
|
12
|
+
- RenderDoc with in-application API 1.6.0 or newer
|
|
13
|
+
- Linux or Windows
|
|
14
|
+
|
|
15
|
+
The only runtime gem dependency is [`ffi`](https://github.com/ffi/ffi), which Bundler installs automatically. RenderDoc does not officially support macOS, so `RenderDoc.available?` always returns `false` there. Use Xcode GPU Frame Capture for Metal applications.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add the gem to your `Gemfile`:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "renderdoc"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Until the first RubyGems release, install it directly from GitHub:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem "renderdoc", github: "ydah/renderdoc"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Loading RenderDoc
|
|
38
|
+
|
|
39
|
+
`ffi` lets Ruby call native functions; it does not place `librenderdoc.so` or `renderdoc.dll` in the process. The RenderDoc library must already be loaded before this gem can use its API.
|
|
40
|
+
|
|
41
|
+
| Platform | How to load RenderDoc |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Linux | Launch the application from RenderDoc, or use `LD_PRELOAD` |
|
|
44
|
+
| Windows | Launch the application from RenderDoc so `renderdoc.dll` is injected |
|
|
45
|
+
| macOS | Unsupported by RenderDoc |
|
|
46
|
+
|
|
47
|
+
For a direct Linux launch:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
LD_PRELOAD=librenderdoc.so bundle exec ruby app.rb
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use an absolute library path if `librenderdoc.so` is not on the dynamic loader's search path:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
LD_PRELOAD=/path/to/librenderdoc.so bundle exec ruby app.rb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When the application is launched from the RenderDoc UI, do not also set `LD_PRELOAD`. Requiring this gem without RenderDoc attached is safe: `RenderDoc.available?` returns `false`.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require "renderdoc"
|
|
65
|
+
|
|
66
|
+
if RenderDoc.available?
|
|
67
|
+
RenderDoc.capture_path_template = "captures/my_app"
|
|
68
|
+
|
|
69
|
+
RenderDoc.capture(title: "Bloom pass") do
|
|
70
|
+
renderer.render(scene, camera)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`RenderDoc.capture` returns the block's result. If the block raises an exception, the gem discards the capture and re-raises the original exception. Nested and overlapping captures raise `RenderDoc::CaptureError`.
|
|
76
|
+
|
|
77
|
+
## Capturing frames
|
|
78
|
+
|
|
79
|
+
### Capture a block
|
|
80
|
+
|
|
81
|
+
By default, RenderDoc uses wildcard device and window pointers. Supply native pointers when a particular device or window must be selected:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
RenderDoc.capture(device: device_pointer, window: window_pointer) do
|
|
85
|
+
renderer.render(scene, camera)
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For Vulkan, `device` must be the dispatch-table pointer described by `RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE`, not the `VkInstance` handle itself.
|
|
90
|
+
|
|
91
|
+
### Trigger upcoming frames
|
|
92
|
+
|
|
93
|
+
Schedule one or more captures without wrapping render code:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
RenderDoc.trigger_capture
|
|
97
|
+
RenderDoc.trigger_capture(frames: 3)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Captures and replay UI
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
RenderDoc.api_version
|
|
104
|
+
# => [1, 6, 0] or a newer compatible version
|
|
105
|
+
|
|
106
|
+
RenderDoc.captures.each do |capture|
|
|
107
|
+
puts "#{capture.path} at #{capture.timestamp.iso8601}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
RenderDoc.comments = "scene=atrium commit=abc123"
|
|
111
|
+
RenderDoc.set_capture_comments("regression case", path: "/tmp/frame.rdc")
|
|
112
|
+
|
|
113
|
+
pid = RenderDoc.launch_replay_ui(connect: true)
|
|
114
|
+
RenderDoc.show_replay_ui if RenderDoc.target_control_connected?
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`Capture#timestamp` is a UTC `Time`. Paths are UTF-8 strings reported by RenderDoc; a listed capture may have been deleted after it was recorded.
|
|
118
|
+
|
|
119
|
+
## Options, overlay, and keys
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
RenderDoc.overlay = false
|
|
123
|
+
RenderDoc.capture_keys = [:f12]
|
|
124
|
+
RenderDoc.focus_toggle_keys = [:f11]
|
|
125
|
+
|
|
126
|
+
RenderDoc.option(:api_validation, true)
|
|
127
|
+
RenderDoc.option(:delay_for_debugger, 3)
|
|
128
|
+
RenderDoc.option_value(:api_validation)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Option names from the API 1.6.0 header are accepted in snake case. Input buttons include function keys, letters, digits, navigation keys, `:print_screen`, and `:pause`. Unknown names and invalid values raise `ArgumentError` before entering FFI.
|
|
132
|
+
|
|
133
|
+
## Running without RenderDoc
|
|
134
|
+
|
|
135
|
+
By default, API calls other than `available?` raise `RenderDoc::NotAttachedError` when RenderDoc is absent. Applications where capture support is entirely optional can enable no-op behavior:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
RenderDoc.configure(unavailable: :noop)
|
|
139
|
+
|
|
140
|
+
RenderDoc.capture { renderer.render(scene, camera) } # still renders
|
|
141
|
+
RenderDoc.trigger_capture # => false
|
|
142
|
+
RenderDoc.captures # => []
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
| Error | Meaning |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `RenderDoc::NotAttachedError` | RenderDoc is not loaded in the process |
|
|
148
|
+
| `RenderDoc::VersionError` | The loaded RenderDoc cannot provide API 1.6.0 |
|
|
149
|
+
| `RenderDoc::CaptureError` | A capture failed or another capture is in progress |
|
|
150
|
+
|
|
151
|
+
## Frame-loop integration
|
|
152
|
+
|
|
153
|
+
`RenderDoc::FrameTrigger` parses `STAGECRAFT_RENDERDOC=frame:N` and triggers exactly once. A frame loop such as stagecraft's can soft-require the gem and continue working when it is not installed:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
begin
|
|
157
|
+
require "renderdoc"
|
|
158
|
+
renderdoc_trigger = RenderDoc::FrameTrigger.from_environment
|
|
159
|
+
rescue LoadError
|
|
160
|
+
renderdoc_trigger = nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
frame_number = 0
|
|
164
|
+
loop do
|
|
165
|
+
renderdoc_trigger&.tick(frame_number)
|
|
166
|
+
renderer.render
|
|
167
|
+
frame_number += 1
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For example, set `STAGECRAFT_RENDERDOC=frame:120` before launching the application through RenderDoc to capture frame 120.
|
|
172
|
+
|
|
173
|
+
## RSpec failure captures
|
|
174
|
+
|
|
175
|
+
Require the optional integration from `spec_helper.rb`:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
require "renderdoc/rspec"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Tag examples whose render work can safely run a second time:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
it "renders the bloom pass", :renderdoc_on_failure do
|
|
185
|
+
render_and_compare
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
When a tagged example fails and RenderDoc is attached, the integration runs the example body once more inside `RenderDoc.capture`, preserves the original failure, and reports the resulting `.rdc` path. Do not use the tag for examples with non-repeatable external side effects.
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
Run the unit suite:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
bundle exec rake
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The Linux integration spec is tagged `:renderdoc` and excluded from the default suite. CI runs it under `renderdoccmd`, Xvfb, SDL2, and Mesa software OpenGL, then verifies the generated `.rdc` file through the in-application API.
|
|
200
|
+
|
|
201
|
+
The ABI constants in `lib/renderdoc/native/generated.rb` are generated from RenderDoc v1.25's `renderdoc_app.h`. After reviewing an upstream header change, regenerate them with:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
ruby script/generate_native_api.rb path/to/renderdoc_app.h lib/renderdoc/native/generated.rb
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
The gem is available under the [MIT License](LICENSE.txt). RenderDoc is a separate project and is not distributed with this gem.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
require_relative "capture"
|
|
7
|
+
require_relative "configuration"
|
|
8
|
+
require_relative "native"
|
|
9
|
+
require_relative "settings"
|
|
10
|
+
|
|
11
|
+
module RenderDoc
|
|
12
|
+
class Application
|
|
13
|
+
include Settings
|
|
14
|
+
|
|
15
|
+
UINT32_MAX = (2**32) - 1
|
|
16
|
+
NULL_POINTER = FFI::Pointer::NULL
|
|
17
|
+
|
|
18
|
+
def initialize(loader: Native::Loader.new, configuration: Configuration.new)
|
|
19
|
+
@loader = loader
|
|
20
|
+
@configuration = configuration
|
|
21
|
+
@api_mutex = Mutex.new
|
|
22
|
+
@capture_mutex = Mutex.new
|
|
23
|
+
@capturing = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def available?
|
|
27
|
+
!load_api.nil?
|
|
28
|
+
rescue VersionError
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def api_version
|
|
33
|
+
with_api do |api|
|
|
34
|
+
version = Array.new(3) { FFI::MemoryPointer.new(:int) }
|
|
35
|
+
api.call(:GetAPIVersion, *version)
|
|
36
|
+
version.map(&:read_int)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def capture(device: nil, window: nil, title: nil)
|
|
41
|
+
raise ArgumentError, "capture requires a block" unless block_given?
|
|
42
|
+
|
|
43
|
+
api = api_or_nil
|
|
44
|
+
return yield unless api
|
|
45
|
+
|
|
46
|
+
begin_capture(api)
|
|
47
|
+
begin
|
|
48
|
+
api.call(:StartFrameCapture, device, window)
|
|
49
|
+
api.call(:SetCaptureTitle, String(title)) if title
|
|
50
|
+
result = yield
|
|
51
|
+
rescue Exception # rubocop:disable Lint/RescueException -- native capture must always be discarded
|
|
52
|
+
discard_capture(api, device, window)
|
|
53
|
+
raise
|
|
54
|
+
else
|
|
55
|
+
ended = api.call(:EndFrameCapture, device, window)
|
|
56
|
+
raise CaptureError, "RenderDoc failed to save the frame capture" if ended.zero?
|
|
57
|
+
|
|
58
|
+
result
|
|
59
|
+
ensure
|
|
60
|
+
finish_capture
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def trigger_capture(frames: 1)
|
|
65
|
+
unless frames.is_a?(Integer) && frames.positive? && frames <= UINT32_MAX
|
|
66
|
+
raise ArgumentError, "frames must be an integer between 1 and #{UINT32_MAX}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
with_api(default: false) do |api|
|
|
70
|
+
function = frames == 1 ? :TriggerCapture : :TriggerMultiFrameCapture
|
|
71
|
+
arguments = frames == 1 ? [] : [frames]
|
|
72
|
+
api.call(function, *arguments)
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def capture_path_template
|
|
78
|
+
with_api do |api|
|
|
79
|
+
pointer = api.call(:GetCaptureFilePathTemplate)
|
|
80
|
+
pointer.null? ? nil : pointer.read_string
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def capture_path_template=(path)
|
|
85
|
+
with_api(default: false) do |api|
|
|
86
|
+
api.call(:SetCaptureFilePathTemplate, String(path))
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def captures
|
|
92
|
+
with_api(default: []) do |api|
|
|
93
|
+
api.call(:GetNumCaptures).times.map { |index| read_capture(api, index) }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def launch_replay_ui(connect: true, command_line: nil)
|
|
98
|
+
with_api(default: 0) do |api|
|
|
99
|
+
api.call(:LaunchReplayUI, connect ? 1 : 0, command_line&.to_s)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def target_control_connected?
|
|
104
|
+
with_api(default: false) { |api| !api.call(:IsTargetControlConnected).zero? }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def show_replay_ui
|
|
108
|
+
with_api(default: false) { |api| !api.call(:ShowReplayUI).zero? }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def comments=(comments)
|
|
112
|
+
with_api(default: false) do |api|
|
|
113
|
+
api.call(:SetCaptureFileComments, nil, String(comments))
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def set_capture_comments(comments, path: nil)
|
|
119
|
+
with_api(default: false) do |api|
|
|
120
|
+
api.call(:SetCaptureFileComments, path&.to_s, String(comments))
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def load_api
|
|
128
|
+
return @api if @api
|
|
129
|
+
raise @version_error if @version_error
|
|
130
|
+
|
|
131
|
+
@api_mutex.synchronize do
|
|
132
|
+
return @api if @api
|
|
133
|
+
raise @version_error if @version_error
|
|
134
|
+
|
|
135
|
+
@api = @loader.load
|
|
136
|
+
rescue VersionError => error
|
|
137
|
+
@version_error = error
|
|
138
|
+
raise
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def api_or_nil
|
|
143
|
+
api = load_api
|
|
144
|
+
return api if api
|
|
145
|
+
return if @configuration.noop_when_unavailable?
|
|
146
|
+
|
|
147
|
+
raise NotAttachedError
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def with_api(default: nil)
|
|
151
|
+
api = api_or_nil
|
|
152
|
+
return default unless api
|
|
153
|
+
|
|
154
|
+
yield api
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def begin_capture(api)
|
|
158
|
+
@capture_mutex.synchronize do
|
|
159
|
+
if @capturing || !api.call(:IsFrameCapturing).zero?
|
|
160
|
+
raise CaptureError, "a RenderDoc frame capture is already in progress"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@capturing = true
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def finish_capture
|
|
168
|
+
@capture_mutex.synchronize { @capturing = false }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def discard_capture(api, device, window)
|
|
172
|
+
api.call(:DiscardFrameCapture, device, window)
|
|
173
|
+
rescue StandardError
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read_capture(api, index)
|
|
178
|
+
path_length = FFI::MemoryPointer.new(:uint32)
|
|
179
|
+
timestamp = FFI::MemoryPointer.new(:uint64)
|
|
180
|
+
found = api.call(:GetCapture, index, NULL_POINTER, path_length, timestamp)
|
|
181
|
+
raise CaptureError, "RenderDoc capture #{index} disappeared while enumerating" if found.zero?
|
|
182
|
+
|
|
183
|
+
buffer = FFI::MemoryPointer.new(:char, [path_length.read_uint32 + 1, 2].max)
|
|
184
|
+
path_length.write_uint32(buffer.size)
|
|
185
|
+
found = api.call(:GetCapture, index, buffer, path_length, timestamp)
|
|
186
|
+
raise CaptureError, "RenderDoc capture #{index} disappeared while reading its path" if found.zero?
|
|
187
|
+
|
|
188
|
+
Capture.new(path: buffer.read_string, timestamp: Time.at(timestamp.read_uint64).utc)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RenderDoc
|
|
4
|
+
class Configuration
|
|
5
|
+
UNAVAILABLE_BEHAVIORS = %i[raise noop].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :unavailable_behavior
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@unavailable_behavior = :raise
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def unavailable_behavior=(behavior)
|
|
14
|
+
behavior = behavior.to_sym
|
|
15
|
+
unless UNAVAILABLE_BEHAVIORS.include?(behavior)
|
|
16
|
+
raise ArgumentError, "unavailable behavior must be :raise or :noop"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@unavailable_behavior = behavior
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def noop_when_unavailable?
|
|
23
|
+
unavailable_behavior == :noop
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RenderDoc
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class NotAttachedError < Error
|
|
7
|
+
def initialize(message = nil)
|
|
8
|
+
super(message || self.class.default_message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.default_message
|
|
12
|
+
return "RenderDoc does not support macOS. Use Xcode GPU Frame Capture for Metal applications." if RUBY_PLATFORM.include?("darwin")
|
|
13
|
+
return "Launch the application from the RenderDoc UI so renderdoc.dll is already loaded." if Gem.win_platform?
|
|
14
|
+
|
|
15
|
+
"Launch via the RenderDoc UI, or run with LD_PRELOAD=librenderdoc.so ruby app.rb."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class VersionError < Error; end
|
|
20
|
+
class CaptureError < Error; end
|
|
21
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RenderDoc
|
|
4
|
+
class FrameTrigger
|
|
5
|
+
ENVIRONMENT_VARIABLE = "STAGECRAFT_RENDERDOC"
|
|
6
|
+
FRAME_PATTERN = /\Aframe:(\d+)\z/
|
|
7
|
+
|
|
8
|
+
attr_reader :frame
|
|
9
|
+
|
|
10
|
+
def self.from_environment(environment: ENV, renderdoc: RenderDoc)
|
|
11
|
+
specification = environment[ENVIRONMENT_VARIABLE]
|
|
12
|
+
return unless specification
|
|
13
|
+
|
|
14
|
+
match = FRAME_PATTERN.match(specification)
|
|
15
|
+
unless match
|
|
16
|
+
raise ArgumentError, "#{ENVIRONMENT_VARIABLE} must use the form frame:N"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
new(frame: Integer(match[1], 10), renderdoc: renderdoc)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(frame:, renderdoc: RenderDoc)
|
|
23
|
+
raise ArgumentError, "frame must be a non-negative Integer" unless frame.is_a?(Integer) && frame >= 0
|
|
24
|
+
|
|
25
|
+
@frame = frame
|
|
26
|
+
@renderdoc = renderdoc
|
|
27
|
+
@triggered = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(current_frame)
|
|
31
|
+
return false if @triggered || current_frame != frame
|
|
32
|
+
|
|
33
|
+
@renderdoc.trigger_capture
|
|
34
|
+
@triggered = true
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias tick call
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated from RenderDoc's renderdoc_app.h by script/generate_native_api.rb.
|
|
4
|
+
# Source: RenderDoc v1.25, RENDERDOC_API_1_6_0. Do not edit by hand.
|
|
5
|
+
module RenderDoc
|
|
6
|
+
module Native
|
|
7
|
+
API_VERSION = 10_600
|
|
8
|
+
|
|
9
|
+
API_TABLE_FIELDS = %i[
|
|
10
|
+
GetAPIVersion
|
|
11
|
+
SetCaptureOptionU32
|
|
12
|
+
SetCaptureOptionF32
|
|
13
|
+
GetCaptureOptionU32
|
|
14
|
+
GetCaptureOptionF32
|
|
15
|
+
SetFocusToggleKeys
|
|
16
|
+
SetCaptureKeys
|
|
17
|
+
GetOverlayBits
|
|
18
|
+
MaskOverlayBits
|
|
19
|
+
RemoveHooks
|
|
20
|
+
UnloadCrashHandler
|
|
21
|
+
SetCaptureFilePathTemplate
|
|
22
|
+
GetCaptureFilePathTemplate
|
|
23
|
+
GetNumCaptures
|
|
24
|
+
GetCapture
|
|
25
|
+
TriggerCapture
|
|
26
|
+
IsTargetControlConnected
|
|
27
|
+
LaunchReplayUI
|
|
28
|
+
SetActiveWindow
|
|
29
|
+
StartFrameCapture
|
|
30
|
+
IsFrameCapturing
|
|
31
|
+
EndFrameCapture
|
|
32
|
+
TriggerMultiFrameCapture
|
|
33
|
+
SetCaptureFileComments
|
|
34
|
+
DiscardFrameCapture
|
|
35
|
+
ShowReplayUI
|
|
36
|
+
SetCaptureTitle
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
FUNCTION_SIGNATURES = {
|
|
40
|
+
GetAPIVersion: [:void, %i[pointer pointer pointer]],
|
|
41
|
+
SetCaptureOptionU32: [:int, %i[int uint32]],
|
|
42
|
+
SetCaptureOptionF32: [:int, %i[int float]],
|
|
43
|
+
GetCaptureOptionU32: [:uint32, %i[int]],
|
|
44
|
+
GetCaptureOptionF32: [:float, %i[int]],
|
|
45
|
+
SetFocusToggleKeys: [:void, %i[pointer int]],
|
|
46
|
+
SetCaptureKeys: [:void, %i[pointer int]],
|
|
47
|
+
GetOverlayBits: [:uint32, []],
|
|
48
|
+
MaskOverlayBits: [:void, %i[uint32 uint32]],
|
|
49
|
+
RemoveHooks: [:void, []],
|
|
50
|
+
UnloadCrashHandler: [:void, []],
|
|
51
|
+
SetCaptureFilePathTemplate: [:void, %i[string]],
|
|
52
|
+
GetCaptureFilePathTemplate: [:pointer, []],
|
|
53
|
+
GetNumCaptures: [:uint32, []],
|
|
54
|
+
GetCapture: [:uint32, %i[uint32 pointer pointer pointer]],
|
|
55
|
+
TriggerCapture: [:void, []],
|
|
56
|
+
IsTargetControlConnected: [:uint32, []],
|
|
57
|
+
LaunchReplayUI: [:uint32, %i[uint32 string]],
|
|
58
|
+
SetActiveWindow: [:void, %i[pointer pointer]],
|
|
59
|
+
StartFrameCapture: [:void, %i[pointer pointer]],
|
|
60
|
+
IsFrameCapturing: [:uint32, []],
|
|
61
|
+
EndFrameCapture: [:uint32, %i[pointer pointer]],
|
|
62
|
+
TriggerMultiFrameCapture: [:void, %i[uint32]],
|
|
63
|
+
SetCaptureFileComments: [:void, %i[string string]],
|
|
64
|
+
DiscardFrameCapture: [:uint32, %i[pointer pointer]],
|
|
65
|
+
ShowReplayUI: [:uint32, []],
|
|
66
|
+
SetCaptureTitle: [:void, %i[string]]
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
CAPTURE_OPTIONS = {
|
|
70
|
+
:allow_vsync => 0,
|
|
71
|
+
:allow_fullscreen => 1,
|
|
72
|
+
:api_validation => 2,
|
|
73
|
+
:debug_device_mode => 2,
|
|
74
|
+
:capture_callstacks => 3,
|
|
75
|
+
:capture_callstacks_only_draws => 4,
|
|
76
|
+
:capture_callstacks_only_actions => 4,
|
|
77
|
+
:delay_for_debugger => 5,
|
|
78
|
+
:verify_buffer_access => 6,
|
|
79
|
+
:verify_map_writes => 6,
|
|
80
|
+
:hook_into_children => 7,
|
|
81
|
+
:ref_all_resources => 8,
|
|
82
|
+
:save_all_initials => 9,
|
|
83
|
+
:capture_all_cmd_lists => 10,
|
|
84
|
+
:debug_output_mute => 11,
|
|
85
|
+
:allow_unsupported_vendor_extensions => 12
|
|
86
|
+
}.freeze
|
|
87
|
+
|
|
88
|
+
INPUT_BUTTONS = {
|
|
89
|
+
:"0" => 48,
|
|
90
|
+
:"1" => 49,
|
|
91
|
+
:"2" => 50,
|
|
92
|
+
:"3" => 51,
|
|
93
|
+
:"4" => 52,
|
|
94
|
+
:"5" => 53,
|
|
95
|
+
:"6" => 54,
|
|
96
|
+
:"7" => 55,
|
|
97
|
+
:"8" => 56,
|
|
98
|
+
:"9" => 57,
|
|
99
|
+
:a => 65,
|
|
100
|
+
:b => 66,
|
|
101
|
+
:c => 67,
|
|
102
|
+
:d => 68,
|
|
103
|
+
:e => 69,
|
|
104
|
+
:f => 70,
|
|
105
|
+
:g => 71,
|
|
106
|
+
:h => 72,
|
|
107
|
+
:i => 73,
|
|
108
|
+
:j => 74,
|
|
109
|
+
:k => 75,
|
|
110
|
+
:l => 76,
|
|
111
|
+
:m => 77,
|
|
112
|
+
:n => 78,
|
|
113
|
+
:o => 79,
|
|
114
|
+
:p => 80,
|
|
115
|
+
:q => 81,
|
|
116
|
+
:r => 82,
|
|
117
|
+
:s => 83,
|
|
118
|
+
:t => 84,
|
|
119
|
+
:u => 85,
|
|
120
|
+
:v => 86,
|
|
121
|
+
:w => 87,
|
|
122
|
+
:x => 88,
|
|
123
|
+
:y => 89,
|
|
124
|
+
:z => 90,
|
|
125
|
+
:divide => 257,
|
|
126
|
+
:multiply => 258,
|
|
127
|
+
:subtract => 259,
|
|
128
|
+
:plus => 260,
|
|
129
|
+
:f1 => 261,
|
|
130
|
+
:f2 => 262,
|
|
131
|
+
:f3 => 263,
|
|
132
|
+
:f4 => 264,
|
|
133
|
+
:f5 => 265,
|
|
134
|
+
:f6 => 266,
|
|
135
|
+
:f7 => 267,
|
|
136
|
+
:f8 => 268,
|
|
137
|
+
:f9 => 269,
|
|
138
|
+
:f10 => 270,
|
|
139
|
+
:f11 => 271,
|
|
140
|
+
:f12 => 272,
|
|
141
|
+
:home => 273,
|
|
142
|
+
:end => 274,
|
|
143
|
+
:insert => 275,
|
|
144
|
+
:delete => 276,
|
|
145
|
+
:page_up => 277,
|
|
146
|
+
:page_down => 278,
|
|
147
|
+
:backspace => 279,
|
|
148
|
+
:tab => 280,
|
|
149
|
+
:print_screen => 281,
|
|
150
|
+
:pause => 282
|
|
151
|
+
}.freeze
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "native/generated"
|
|
7
|
+
|
|
8
|
+
module RenderDoc
|
|
9
|
+
module Native
|
|
10
|
+
extend FFI::Library
|
|
11
|
+
|
|
12
|
+
class APITable < FFI::Struct
|
|
13
|
+
layout(*API_TABLE_FIELDS.flat_map { |field| [field, :pointer] })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class API
|
|
17
|
+
def initialize(pointer, owner: nil)
|
|
18
|
+
@table = APITable.new(pointer)
|
|
19
|
+
@owner = owner
|
|
20
|
+
@functions = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(name, *arguments)
|
|
24
|
+
function(name).call(*arguments)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def function(name)
|
|
30
|
+
@functions[name] ||= begin
|
|
31
|
+
return_type, argument_types = FUNCTION_SIGNATURES.fetch(name)
|
|
32
|
+
pointer = @table[name]
|
|
33
|
+
raise VersionError, "RenderDoc returned a null pointer for #{name}" if pointer.null?
|
|
34
|
+
|
|
35
|
+
FFI::Function.new(return_type, argument_types, pointer)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class WindowsAPI
|
|
41
|
+
KERNEL_LIBRARY = "kernel32.dll"
|
|
42
|
+
|
|
43
|
+
def initialize(dynamic_library: FFI::DynamicLibrary)
|
|
44
|
+
flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_LOCAL
|
|
45
|
+
@kernel = dynamic_library.open(KERNEL_LIBRARY, flags)
|
|
46
|
+
@get_module_handle = windows_function(:pointer, [:string], "GetModuleHandleA")
|
|
47
|
+
@get_proc_address = windows_function(:pointer, %i[pointer string], "GetProcAddress")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def renderdoc_api_pointer
|
|
51
|
+
handle = @get_module_handle.call("renderdoc.dll")
|
|
52
|
+
return if handle.null?
|
|
53
|
+
|
|
54
|
+
pointer = @get_proc_address.call(handle, "RENDERDOC_GetAPI")
|
|
55
|
+
pointer unless pointer.null?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def windows_function(return_type, argument_types, name)
|
|
61
|
+
FFI::Function.new(return_type, argument_types, @kernel.find_function(name), convention: :stdcall)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class Loader
|
|
66
|
+
LINUX_LIBRARY = "librenderdoc.so"
|
|
67
|
+
GET_API_SYMBOL = "RENDERDOC_GetAPI"
|
|
68
|
+
|
|
69
|
+
def initialize(platform: RUBY_PLATFORM, dynamic_library: FFI::DynamicLibrary, windows_api: nil)
|
|
70
|
+
@platform = platform
|
|
71
|
+
@dynamic_library = dynamic_library
|
|
72
|
+
@windows_api = windows_api
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def load
|
|
76
|
+
return if macos?
|
|
77
|
+
|
|
78
|
+
function_pointer, owner = get_api_pointer
|
|
79
|
+
return unless function_pointer
|
|
80
|
+
|
|
81
|
+
request_api(function_pointer, owner)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def get_api_pointer
|
|
87
|
+
return [windows_api.renderdoc_api_pointer, windows_api] if windows?
|
|
88
|
+
|
|
89
|
+
library = open_loaded_library(LINUX_LIBRARY)
|
|
90
|
+
return [nil, nil] unless library
|
|
91
|
+
|
|
92
|
+
[library.find_function(GET_API_SYMBOL), library]
|
|
93
|
+
rescue FFI::NotFoundError
|
|
94
|
+
[nil, nil]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def open_loaded_library(name)
|
|
98
|
+
flags = FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_NOLOAD
|
|
99
|
+
@dynamic_library.open(name, flags)
|
|
100
|
+
rescue LoadError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def request_api(function_pointer, owner)
|
|
105
|
+
get_api = FFI::Function.new(:int, %i[int pointer], function_pointer)
|
|
106
|
+
output = FFI::MemoryPointer.new(:pointer)
|
|
107
|
+
result = get_api.call(API_VERSION, output)
|
|
108
|
+
pointer = output.read_pointer
|
|
109
|
+
|
|
110
|
+
if result.zero? || pointer.null?
|
|
111
|
+
raise VersionError, "Loaded RenderDoc does not support the required in-application API 1.6.0"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
API.new(pointer, owner: owner)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def windows_api
|
|
118
|
+
@windows_api ||= WindowsAPI.new(dynamic_library: @dynamic_library)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def windows?
|
|
122
|
+
@platform.match?(/mswin|mingw|cygwin/i)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def macos?
|
|
126
|
+
@platform.include?("darwin")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RenderDoc
|
|
4
|
+
module RSpecIntegration
|
|
5
|
+
class FailureCapture
|
|
6
|
+
def initialize(renderdoc: RenderDoc)
|
|
7
|
+
@renderdoc = renderdoc
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def capture(example)
|
|
11
|
+
return unless example.exception
|
|
12
|
+
return unless @renderdoc.available?
|
|
13
|
+
|
|
14
|
+
captures_before = @renderdoc.captures.length
|
|
15
|
+
rerun_example_in_capture(example)
|
|
16
|
+
report_capture(example, captures_before)
|
|
17
|
+
rescue RenderDoc::Error => error
|
|
18
|
+
example.reporter.message("RenderDoc capture failed: #{error.message}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def rerun_example_in_capture(example)
|
|
24
|
+
block = example.instance_variable_get(:@example_block)
|
|
25
|
+
raise RenderDoc::CaptureError, "RSpec example block is unavailable" unless block
|
|
26
|
+
|
|
27
|
+
@renderdoc.capture(title: example.full_description) do
|
|
28
|
+
rerun_example_block(example, block)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def rerun_example_block(example, block)
|
|
33
|
+
example.example_group_instance.instance_exec(example, &block)
|
|
34
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- RSpec failures inherit directly from Exception
|
|
35
|
+
raise if error.is_a?(SystemExit) || error.is_a?(SignalException) || error.is_a?(NoMemoryError)
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def report_capture(example, captures_before)
|
|
41
|
+
captures = @renderdoc.captures
|
|
42
|
+
capture = captures.drop(captures_before).last || captures.last
|
|
43
|
+
message = capture ? "RenderDoc capture: #{capture.path}" : "RenderDoc capture completed"
|
|
44
|
+
example.reporter.message(message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def install!(configuration = ::RSpec.configuration)
|
|
50
|
+
@installed_configurations ||= {}
|
|
51
|
+
return if @installed_configurations[configuration.object_id]
|
|
52
|
+
|
|
53
|
+
configuration.append_after(:each, :renderdoc_on_failure) do |example|
|
|
54
|
+
FailureCapture.new.capture(example)
|
|
55
|
+
end
|
|
56
|
+
@installed_configurations[configuration.object_id] = true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RenderDoc
|
|
4
|
+
module Settings
|
|
5
|
+
def overlay
|
|
6
|
+
with_api(default: false) { |api| (api.call(:GetOverlayBits) & 1) == 1 }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def overlay=(enabled)
|
|
10
|
+
with_api(default: false) do |api|
|
|
11
|
+
api.call(:MaskOverlayBits, 0, enabled ? 0xF : 0)
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def capture_keys=(keys)
|
|
17
|
+
set_keys(:SetCaptureKeys, keys)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def focus_toggle_keys=(keys)
|
|
21
|
+
set_keys(:SetFocusToggleKeys, keys)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def option(name, value)
|
|
25
|
+
option_id = enum_value(Native::CAPTURE_OPTIONS, name, "capture option")
|
|
26
|
+
function, native_value = option_argument(value)
|
|
27
|
+
with_api(default: false) { |api| !api.call(function, option_id, native_value).zero? }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def option_value(name, type: :uint32)
|
|
31
|
+
option_id = enum_value(Native::CAPTURE_OPTIONS, name, "capture option")
|
|
32
|
+
function = {uint32: :GetCaptureOptionU32, float: :GetCaptureOptionF32}.fetch(type) do
|
|
33
|
+
raise ArgumentError, "type must be :uint32 or :float"
|
|
34
|
+
end
|
|
35
|
+
with_api { |api| api.call(function, option_id) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def set_keys(function, keys)
|
|
41
|
+
values = Array(keys).map { |key| enum_value(Native::INPUT_BUTTONS, key, "input button") }
|
|
42
|
+
pointer = values.empty? ? NULL_POINTER : FFI::MemoryPointer.new(:int, values.length)
|
|
43
|
+
pointer.write_array_of_int(values) unless values.empty?
|
|
44
|
+
with_api(default: false) { |api| api.call(function, pointer, values.length); true }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def enum_value(mapping, name, kind)
|
|
48
|
+
mapping.fetch(name.to_s.downcase.to_sym)
|
|
49
|
+
rescue KeyError
|
|
50
|
+
raise ArgumentError, "unknown RenderDoc #{kind}: #{name.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def option_argument(value)
|
|
54
|
+
return [:SetCaptureOptionF32, value] if value.is_a?(Float)
|
|
55
|
+
return [:SetCaptureOptionU32, value ? 1 : 0] if value == true || value == false
|
|
56
|
+
return [:SetCaptureOptionU32, value] if value.is_a?(Integer) && value.between?(0, UINT32_MAX)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "option value must be a boolean, Float, or unsigned 32-bit Integer"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/renderdoc.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "renderdoc/version"
|
|
4
|
+
require_relative "renderdoc/errors"
|
|
5
|
+
require_relative "renderdoc/application"
|
|
6
|
+
require_relative "renderdoc/frame_trigger"
|
|
7
|
+
|
|
8
|
+
module RenderDoc
|
|
9
|
+
class << self
|
|
10
|
+
def configure(unavailable: nil)
|
|
11
|
+
configuration.unavailable_behavior = unavailable if unavailable
|
|
12
|
+
yield configuration if block_given?
|
|
13
|
+
configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def available? = application.available?
|
|
17
|
+
def api_version = application.api_version
|
|
18
|
+
def capture(**arguments, &block) = application.capture(**arguments, &block)
|
|
19
|
+
def trigger_capture(frames: 1) = application.trigger_capture(frames: frames)
|
|
20
|
+
def capture_path_template = application.capture_path_template
|
|
21
|
+
def capture_path_template=(path)
|
|
22
|
+
application.capture_path_template = path
|
|
23
|
+
end
|
|
24
|
+
def captures = application.captures
|
|
25
|
+
def launch_replay_ui(**arguments) = application.launch_replay_ui(**arguments)
|
|
26
|
+
def target_control_connected? = application.target_control_connected?
|
|
27
|
+
def show_replay_ui = application.show_replay_ui
|
|
28
|
+
def comments=(comments)
|
|
29
|
+
application.comments = comments
|
|
30
|
+
end
|
|
31
|
+
def set_capture_comments(comments, path: nil) = application.set_capture_comments(comments, path: path)
|
|
32
|
+
def overlay = application.overlay
|
|
33
|
+
def overlay=(enabled)
|
|
34
|
+
application.overlay = enabled
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def capture_keys=(keys)
|
|
38
|
+
application.capture_keys = keys
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def focus_toggle_keys=(keys)
|
|
42
|
+
application.focus_toggle_keys = keys
|
|
43
|
+
end
|
|
44
|
+
def option(name, value) = application.option(name, value)
|
|
45
|
+
def option_value(name, type: :uint32) = application.option_value(name, type: type)
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def configuration
|
|
50
|
+
@configuration ||= Configuration.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def application
|
|
54
|
+
@application ||= Application.new(configuration: configuration)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Usage: ruby script/generate_native_api.rb path/to/renderdoc_app.h [output]
|
|
5
|
+
#
|
|
6
|
+
# The committed binding was generated from:
|
|
7
|
+
# https://raw.githubusercontent.com/baldurk/renderdoc/v1.25/renderdoc/api/app/renderdoc_app.h
|
|
8
|
+
|
|
9
|
+
HEADER_PATH = ARGV.fetch(0) do
|
|
10
|
+
warn "usage: #{$PROGRAM_NAME} path/to/renderdoc_app.h [output]"
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
OUTPUT_PATH = ARGV[1]
|
|
14
|
+
|
|
15
|
+
TYPE_MAP = {
|
|
16
|
+
"float" => ":float",
|
|
17
|
+
"int" => ":int",
|
|
18
|
+
"uint32_t" => ":uint32",
|
|
19
|
+
"uint64_t" => ":uint64",
|
|
20
|
+
"void" => ":void"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def without_comments(source)
|
|
24
|
+
source.gsub(%r{/\*.*?\*/}m, "").gsub(%r{//[^\n]*}, "")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ffi_type(c_type, return_type: false)
|
|
28
|
+
normalized = c_type.gsub(/\s+/, " ").strip
|
|
29
|
+
return ":pointer" if normalized.include?("*") && (return_type || !normalized.start_with?("const char"))
|
|
30
|
+
return ":string" if normalized.start_with?("const char")
|
|
31
|
+
return ":pointer" if normalized.match?(/RENDERDOC_(DevicePointer|WindowHandle)/)
|
|
32
|
+
return ":int" if normalized.match?(/RENDERDOC_(CaptureOption|InputButton|Version)/)
|
|
33
|
+
|
|
34
|
+
base_type = normalized.sub(/\s+[a-zA-Z_]\w*\z/, "")
|
|
35
|
+
TYPE_MAP.fetch(base_type) { raise "unsupported C type: #{c_type.inspect}" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_signatures(header)
|
|
39
|
+
typedef_pattern = /typedef\s+([^;{}]+?)\(RENDERDOC_CC\s+\*pRENDERDOC_(\w+)\)\s*\((.*?)\)\s*;/m
|
|
40
|
+
|
|
41
|
+
without_comments(header).scan(typedef_pattern).to_h do |return_type, name, arguments|
|
|
42
|
+
argument_types = arguments.strip
|
|
43
|
+
argument_types = argument_types.split(",").map { |argument| ffi_type(argument) } unless argument_types.empty? || argument_types == "void"
|
|
44
|
+
argument_types = [] unless argument_types.is_a?(Array)
|
|
45
|
+
[name, [ffi_type(return_type, return_type: true), argument_types]]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_table_fields(header)
|
|
50
|
+
body = header.match(/typedef struct RENDERDOC_API_1_6_0\s*\{(.*?)\}\s*RENDERDOC_API_1_6_0\s*;/m)&.captures&.first
|
|
51
|
+
raise "RENDERDOC_API_1_6_0 was not found" unless body
|
|
52
|
+
|
|
53
|
+
body = without_comments(body).gsub(/union\s*\{(.*?)\}\s*;/m) do
|
|
54
|
+
Regexp.last_match(1).scan(/(pRENDERDOC_\w+)\s+(\w+)\s*;/).last.join(" ") + ";"
|
|
55
|
+
end
|
|
56
|
+
body.scan(/pRENDERDOC_\w+\s+(\w+)\s*;/).flatten
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_enum(header, name)
|
|
60
|
+
body = without_comments(header).match(/typedef enum #{name}\s*\{(.*?)\}\s*#{name}\s*;/m)&.captures&.first
|
|
61
|
+
raise "#{name} was not found" unless body
|
|
62
|
+
|
|
63
|
+
values = {}
|
|
64
|
+
current_value = -1
|
|
65
|
+
body.split(",").each do |entry|
|
|
66
|
+
next if entry.strip.empty?
|
|
67
|
+
|
|
68
|
+
constant, expression = entry.strip.split("=", 2).map(&:strip)
|
|
69
|
+
current_value = if expression
|
|
70
|
+
values.fetch(expression) { Integer(expression, 0) }
|
|
71
|
+
else
|
|
72
|
+
current_value + 1
|
|
73
|
+
end
|
|
74
|
+
values[constant] = current_value
|
|
75
|
+
end
|
|
76
|
+
values
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def snake_case(name)
|
|
80
|
+
name.gsub(/([a-z\d])([A-Z])/, "\\1_\\2").downcase
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def option_name(constant)
|
|
84
|
+
snake_case(constant.delete_prefix("eRENDERDOC_Option_"))
|
|
85
|
+
.gsub("v_sync", "vsync")
|
|
86
|
+
.gsub("apivalidation", "api_validation")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def key_name(constant)
|
|
90
|
+
name = snake_case(constant.delete_prefix("eRENDERDOC_Key_"))
|
|
91
|
+
{"page_dn" => "page_down", "prt_scrn" => "print_screen"}.fetch(name, name)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def format_symbol_array(values, indent: 6)
|
|
95
|
+
padding = " " * indent
|
|
96
|
+
values.map { |value| "#{padding}#{value}" }.join("\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def format_hash(values, key_transform, reject: [])
|
|
100
|
+
entries = values.filter_map do |constant, value|
|
|
101
|
+
next if reject.include?(constant)
|
|
102
|
+
|
|
103
|
+
" #{key_transform.call(constant).to_sym.inspect} => #{value}"
|
|
104
|
+
end
|
|
105
|
+
entries.join(",\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
header = File.read(HEADER_PATH)
|
|
109
|
+
fields = parse_table_fields(header)
|
|
110
|
+
signatures = parse_signatures(header)
|
|
111
|
+
missing_signatures = fields.reject { |field| signatures.key?(field) }
|
|
112
|
+
raise "missing signatures: #{missing_signatures.join(', ')}" unless missing_signatures.empty?
|
|
113
|
+
|
|
114
|
+
options = parse_enum(header, "RENDERDOC_CaptureOption")
|
|
115
|
+
keys = parse_enum(header, "RENDERDOC_InputButton")
|
|
116
|
+
|
|
117
|
+
function_signatures = fields.map do |field|
|
|
118
|
+
return_type, argument_types = signatures.fetch(field)
|
|
119
|
+
arguments = argument_types.empty? ? "[]" : "%i[#{argument_types.map { |type| type.delete_prefix(':') }.join(' ')}]"
|
|
120
|
+
" #{field}: [#{return_type}, #{arguments}]"
|
|
121
|
+
end.join(",\n")
|
|
122
|
+
|
|
123
|
+
generated = <<~RUBY
|
|
124
|
+
# frozen_string_literal: true
|
|
125
|
+
|
|
126
|
+
# Generated from RenderDoc's renderdoc_app.h by script/generate_native_api.rb.
|
|
127
|
+
# Source: RenderDoc v1.25, RENDERDOC_API_1_6_0. Do not edit by hand.
|
|
128
|
+
module RenderDoc
|
|
129
|
+
module Native
|
|
130
|
+
API_VERSION = 10_600
|
|
131
|
+
|
|
132
|
+
API_TABLE_FIELDS = %i[
|
|
133
|
+
#{format_symbol_array(fields)}
|
|
134
|
+
].freeze
|
|
135
|
+
|
|
136
|
+
FUNCTION_SIGNATURES = {
|
|
137
|
+
#{function_signatures}
|
|
138
|
+
}.freeze
|
|
139
|
+
|
|
140
|
+
CAPTURE_OPTIONS = {
|
|
141
|
+
#{format_hash(options, method(:option_name))}
|
|
142
|
+
}.freeze
|
|
143
|
+
|
|
144
|
+
INPUT_BUTTONS = {
|
|
145
|
+
#{format_hash(keys, method(:key_name), reject: %w[eRENDERDOC_Key_NonPrintable eRENDERDOC_Key_Max])}
|
|
146
|
+
}.freeze
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
RUBY
|
|
150
|
+
|
|
151
|
+
if OUTPUT_PATH
|
|
152
|
+
File.write(OUTPUT_PATH, generated)
|
|
153
|
+
else
|
|
154
|
+
puts generated
|
|
155
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: renderdoc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.17'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.17'
|
|
26
|
+
description: Programmatically capture and inspect GPU frames from Ruby applications
|
|
27
|
+
with RenderDoc.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- lib/renderdoc.rb
|
|
38
|
+
- lib/renderdoc/application.rb
|
|
39
|
+
- lib/renderdoc/capture.rb
|
|
40
|
+
- lib/renderdoc/configuration.rb
|
|
41
|
+
- lib/renderdoc/errors.rb
|
|
42
|
+
- lib/renderdoc/frame_trigger.rb
|
|
43
|
+
- lib/renderdoc/native.rb
|
|
44
|
+
- lib/renderdoc/native/generated.rb
|
|
45
|
+
- lib/renderdoc/rspec.rb
|
|
46
|
+
- lib/renderdoc/rspec_integration.rb
|
|
47
|
+
- lib/renderdoc/settings.rb
|
|
48
|
+
- lib/renderdoc/version.rb
|
|
49
|
+
- script/generate_native_api.rb
|
|
50
|
+
homepage: https://github.com/ydah/renderdoc
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/ydah/renderdoc
|
|
55
|
+
source_code_uri: https://github.com/ydah/renderdoc/tree/main
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.2.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 4.0.6
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Ruby bindings for RenderDoc's in-application API
|
|
74
|
+
test_files: []
|