wgpu 1.2.0 → 1.2.1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +18 -2
  4. data/docs/README.md +2 -2
  5. data/docs/api_coverage.md +23 -15
  6. data/docs/async.md +10 -0
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/getting_started_compute.md +1 -0
  9. data/docs/pipeline_descriptors.md +7 -2
  10. data/docs/releasing.md +11 -1
  11. data/docs/resource_lifetime.md +16 -2
  12. data/docs/texture_readback.md +27 -0
  13. data/docs/troubleshooting.md +5 -0
  14. data/docs/upgrading_wgpu_native.md +16 -5
  15. data/lib/wgpu/async_task.rb +19 -0
  16. data/lib/wgpu/commands/command_buffer.rb +14 -1
  17. data/lib/wgpu/commands/command_encoder.rb +58 -1
  18. data/lib/wgpu/commands/compute_pass.rb +43 -0
  19. data/lib/wgpu/commands/render_bundle.rb +9 -1
  20. data/lib/wgpu/commands/render_bundle_encoder.rb +55 -1
  21. data/lib/wgpu/commands/render_pass.rb +102 -0
  22. data/lib/wgpu/core/adapter.rb +91 -12
  23. data/lib/wgpu/core/async_waiter.rb +15 -0
  24. data/lib/wgpu/core/canvas_context.rb +32 -0
  25. data/lib/wgpu/core/device.rb +296 -46
  26. data/lib/wgpu/core/instance.rb +20 -0
  27. data/lib/wgpu/core/queue.rb +139 -24
  28. data/lib/wgpu/core/surface.rb +49 -1
  29. data/lib/wgpu/data_types.rb +16 -0
  30. data/lib/wgpu/descriptor_helpers.rb +65 -0
  31. data/lib/wgpu/error.rb +15 -0
  32. data/lib/wgpu/native/abi_verifier.rb +37 -3
  33. data/lib/wgpu/native/callbacks.rb +6 -0
  34. data/lib/wgpu/native/capabilities.rb +16 -2
  35. data/lib/wgpu/native/distribution.rb +63 -0
  36. data/lib/wgpu/native/enum_helper.rb +17 -0
  37. data/lib/wgpu/native/enums.rb +8 -0
  38. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  39. data/lib/wgpu/native/functions.rb +11 -0
  40. data/lib/wgpu/native/installer.rb +48 -17
  41. data/lib/wgpu/native/loader.rb +22 -0
  42. data/lib/wgpu/native/structs.rb +18 -1
  43. data/lib/wgpu/native_resource.rb +174 -3
  44. data/lib/wgpu/pipeline/bind_group.rb +9 -0
  45. data/lib/wgpu/pipeline/bind_group_layout.rb +17 -4
  46. data/lib/wgpu/pipeline/compute_pipeline.rb +20 -32
  47. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  48. data/lib/wgpu/pipeline/render_pipeline.rb +27 -42
  49. data/lib/wgpu/pipeline/shader_module.rb +54 -29
  50. data/lib/wgpu/resources/buffer.rb +258 -31
  51. data/lib/wgpu/resources/query_set.rb +12 -0
  52. data/lib/wgpu/resources/sampler.rb +7 -0
  53. data/lib/wgpu/resources/texture.rb +45 -6
  54. data/lib/wgpu/resources/texture_view.rb +24 -4
  55. data/lib/wgpu/texture_format.rb +14 -0
  56. data/lib/wgpu/version.rb +1 -1
  57. data/lib/wgpu/window.rb +26 -0
  58. data/sig/wgpu.rbs +84 -5
  59. metadata +3 -1
@@ -2,6 +2,9 @@
2
2
 
3
3
  module WGPU
4
4
  module Native
5
+ attach_function :wgpuGetVersion,
6
+ [], :uint32
7
+
5
8
  attach_function :wgpuCreateInstance,
6
9
  [InstanceDescriptor.by_ref], :pointer
7
10
 
@@ -68,9 +71,17 @@ module WGPU
68
71
  attach_function :wgpuDeviceCreateRenderPipeline,
69
72
  [:pointer, :pointer], :pointer
70
73
 
74
+ attach_function :wgpuDeviceCreateRenderPipelineAsync,
75
+ [:pointer, RenderPipelineDescriptor.by_ref, CreateRenderPipelineAsyncCallbackInfo.by_value],
76
+ Future.by_value
77
+
71
78
  attach_function :wgpuDeviceCreateComputePipeline,
72
79
  [:pointer, ComputePipelineDescriptor.by_ref], :pointer
73
80
 
81
+ attach_function :wgpuDeviceCreateComputePipelineAsync,
82
+ [:pointer, ComputePipelineDescriptor.by_ref, CreateComputePipelineAsyncCallbackInfo.by_value],
83
+ Future.by_value
84
+
74
85
  attach_function :wgpuDeviceCreateCommandEncoder,
75
86
  [:pointer, CommandEncoderDescriptor.by_ref], :pointer
76
87
 
@@ -11,22 +11,50 @@ module WGPU
11
11
  module Native
12
12
  class InstallError < StandardError; end
13
13
 
14
+ # Downloads, verifies, and installs the pinned wgpu-native artifact.
14
15
  class Installer
15
16
  attr_reader :platform
16
17
 
18
+ # Creates an installer for a target Ruby platform.
19
+ #
20
+ # The final four dependencies are injectable to keep network, process,
21
+ # archive, and distribution branches testable without external effects.
22
+ #
23
+ # @param platform [String] target Ruby platform
24
+ # @param env [Hash] environment variables
25
+ # @param home [String] home directory used for cache resolution
26
+ # @param host_os [String] host operating-system identifier
27
+ # @param output [IO] progress output
28
+ # @param command_runner [#call, nil] process runner
29
+ # @param http_factory [#call, nil] factory receiving a URI
30
+ # @param zip_file_loader [#call, nil] factory returning Zip::File
31
+ # @param distribution [Module] artifact metadata provider
17
32
  def initialize(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
18
- host_os: RbConfig::CONFIG["host_os"], output: $stdout)
33
+ host_os: RbConfig::CONFIG["host_os"], output: $stdout,
34
+ command_runner: nil, http_factory: nil, zip_file_loader: nil,
35
+ distribution: Distribution)
19
36
  @platform = platform
20
37
  @env = env
21
38
  @home = home
22
39
  @host_os = host_os
23
40
  @output = output
24
- end
25
-
41
+ @command_runner = command_runner || method(:system)
42
+ @http_factory = http_factory || ->(uri) { Net::HTTP.new(uri.host, uri.port) }
43
+ @zip_file_loader = zip_file_loader || lambda {
44
+ require "zip"
45
+ Zip::File
46
+ }
47
+ @distribution = distribution
48
+ end
49
+
50
+ # Installs or reuses the native library for {#platform}.
51
+ #
52
+ # @return [String] absolute path to the native library
53
+ # @raise [InstallError] if the artifact cannot be obtained or verified
26
54
  def install
27
55
  return custom_library_path if custom_library_path
28
56
 
29
- artifact = Distribution.artifact_for(platform)
57
+ artifact = @distribution.artifact_for(platform)
30
58
  cached_path = existing_library_path(artifact)
31
59
  if cached_path
32
60
  @output.puts "wgpu-native already cached at #{cached_path}"
@@ -38,8 +66,11 @@ module WGPU
38
66
  raise InstallError, "#{error.message}\n\n#{recovery_instructions}"
39
67
  end
40
68
 
69
+ # Returns the primary versioned cache directory.
70
+ #
71
+ # @return [String]
41
72
  def clean_path
42
- Distribution.primary_cache_dir(env: @env, home: @home, host_os: @host_os)
73
+ @distribution.primary_cache_dir(env: @env, home: @home, host_os: @host_os)
43
74
  end
44
75
 
45
76
  private
@@ -53,7 +84,7 @@ module WGPU
53
84
  end
54
85
 
55
86
  def existing_library_path(artifact)
56
- Distribution.cache_directories(env: @env, home: @home, host_os: @host_os).each do |directory|
87
+ @distribution.cache_directories(env: @env, home: @home, host_os: @host_os).each do |directory|
57
88
  path = File.join(directory, "lib", artifact.fetch(:library))
58
89
  return path if File.file?(path)
59
90
  end
@@ -66,9 +97,9 @@ module WGPU
66
97
  library_path = File.join(cache_dir, "lib", artifact.fetch(:library))
67
98
  FileUtils.mkdir_p(cache_dir)
68
99
 
69
- @output.puts "Downloading wgpu-native #{Distribution::VERSION} from GitHub..."
70
- @output.puts " URL: #{Distribution.release_url(artifact)}"
71
- download_file(Distribution.release_url(artifact), archive_path)
100
+ @output.puts "Downloading wgpu-native #{@distribution.const_get(:VERSION)} from GitHub..."
101
+ @output.puts " URL: #{@distribution.release_url(artifact)}"
102
+ download_file(@distribution.release_url(artifact), archive_path)
72
103
  verify_checksum!(archive_path, artifact.fetch(:sha256))
73
104
 
74
105
  @output.puts "Extracting to #{cache_dir}..."
@@ -97,18 +128,18 @@ module WGPU
97
128
  end
98
129
 
99
130
  def curl_available?
100
- system("curl", "--version", out: File::NULL, err: File::NULL)
131
+ @command_runner.call("curl", "--version", out: File::NULL, err: File::NULL)
101
132
  end
102
133
 
103
134
  def download_with_curl(url, destination)
104
- system("curl", "-fsSL", "-o", destination, url)
135
+ @command_runner.call("curl", "-fsSL", "-o", destination, url)
105
136
  end
106
137
 
107
138
  def download_with_ruby(url, destination, redirects = 5)
108
139
  raise InstallError, "Too many redirects while downloading #{url}" if redirects.zero?
109
140
 
110
141
  uri = URI.parse(url)
111
- http = Net::HTTP.new(uri.host, uri.port)
142
+ http = @http_factory.call(uri)
112
143
  http.use_ssl = uri.scheme == "https"
113
144
  http.open_timeout = 10
114
145
  http.read_timeout = 120
@@ -144,10 +175,10 @@ module WGPU
144
175
  end
145
176
 
146
177
  def extract_with_rubyzip(archive_path, destination)
147
- require "zip"
178
+ zip_file_class = @zip_file_loader.call
148
179
  destination_root = "#{File.expand_path(destination)}#{File::SEPARATOR}"
149
180
 
150
- Zip::File.open(archive_path) do |zip_file|
181
+ zip_file_class.open(archive_path) do |zip_file|
151
182
  zip_file.each do |entry|
152
183
  target = File.expand_path(entry.name, destination)
153
184
  unless target.start_with?(destination_root)
@@ -164,7 +195,7 @@ module WGPU
164
195
  end
165
196
 
166
197
  def extract_with_powershell(archive_path, destination)
167
- system(
198
+ @command_runner.call(
168
199
  "powershell",
169
200
  "-NoProfile",
170
201
  "-Command",
@@ -173,7 +204,7 @@ module WGPU
173
204
  end
174
205
 
175
206
  def extract_with_unzip(archive_path, destination)
176
- system("unzip", "-o", "-q", archive_path, "-d", destination)
207
+ @command_runner.call("unzip", "-o", "-q", archive_path, "-d", destination)
177
208
  end
178
209
 
179
210
  def windows?
@@ -182,7 +213,7 @@ module WGPU
182
213
 
183
214
  def recovery_instructions
184
215
  <<~INSTRUCTIONS.chomp
185
- Download the matching #{Distribution::VERSION} artifact manually and set:
216
+ Download the matching #{@distribution.const_get(:VERSION)} artifact manually and set:
186
217
  WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
187
218
  See docs/installation.md for platform details.
188
219
  INSTRUCTIONS
@@ -6,6 +6,12 @@ require_relative "distribution"
6
6
  module WGPU
7
7
  module Native
8
8
  module OptionalFunctions
9
+ # Attaches a native function without failing library initialization.
10
+ #
11
+ # @param name [Symbol] exported function name
12
+ # @param arguments [Array] FFI argument types
13
+ # @param result [Symbol, FFI::Type] FFI return type
14
+ # @return [void]
9
15
  def attach_optional_function(name, arguments, result)
10
16
  attach_function(name, arguments, result)
11
17
  optional_functions[name] = true
@@ -18,10 +24,16 @@ module WGPU
18
24
  end
19
25
  end
20
26
 
27
+ # Reports whether an optional native function was attached.
28
+ # @param name [Symbol] exported function name
29
+ # @return [Boolean]
21
30
  def optional_function_available?(name)
22
31
  optional_functions.fetch(name, false)
23
32
  end
24
33
 
34
+ # Returns availability information for optional native functions.
35
+ #
36
+ # @return [Hash{Symbol => Boolean}] immutable capability snapshot
25
37
  def optional_capabilities
26
38
  optional_functions.dup.freeze
27
39
  end
@@ -37,6 +49,9 @@ module WGPU
37
49
  extend OptionalFunctions
38
50
 
39
51
  class << self
52
+ # Resolves the native library from an override or the versioned cache.
53
+ # @return [String] absolute shared-library path
54
+ # @raise [LoadError] when no library can be found
40
55
  def library_path
41
56
  if ENV["WGPU_LIB_PATH"] && !ENV["WGPU_LIB_PATH"].empty?
42
57
  path = File.expand_path(ENV["WGPU_LIB_PATH"])
@@ -60,10 +75,17 @@ module WGPU
60
75
  MSG
61
76
  end
62
77
 
78
+ # Returns the preferred native-library cache directory.
79
+ #
80
+ # @return [String] absolute cache directory
63
81
  def cache_dir
64
82
  Distribution.primary_cache_dir
65
83
  end
66
84
 
85
+ # Returns the shared-library filename for this platform.
86
+ #
87
+ # @return [String] platform-specific filename
88
+ # @raise [LoadError] if the platform is unsupported
67
89
  def library_name
68
90
  Distribution.artifact_for.fetch(:library)
69
91
  end
@@ -378,6 +378,22 @@ module WGPU
378
378
  :userdata2, :pointer
379
379
  end
380
380
 
381
+ class CreateComputePipelineAsyncCallbackInfo < FFI::Struct
382
+ layout :next_in_chain, :pointer,
383
+ :mode, :uint32,
384
+ :callback, :pointer,
385
+ :userdata1, :pointer,
386
+ :userdata2, :pointer
387
+ end
388
+
389
+ class CreateRenderPipelineAsyncCallbackInfo < FFI::Struct
390
+ layout :next_in_chain, :pointer,
391
+ :mode, :uint32,
392
+ :callback, :pointer,
393
+ :userdata1, :pointer,
394
+ :userdata2, :pointer
395
+ end
396
+
381
397
  class BufferMapCallbackInfo < FFI::Struct
382
398
  layout :next_in_chain, :pointer,
383
399
  :mode, :uint32,
@@ -395,7 +411,8 @@ module WGPU
395
411
  :mip_level_count, :uint32,
396
412
  :base_array_layer, :uint32,
397
413
  :array_layer_count, :uint32,
398
- :aspect, TextureAspect
414
+ :aspect, TextureAspect,
415
+ :usage, :uint64
399
416
  end
400
417
 
401
418
  class SamplerDescriptor < FFI::Struct
@@ -13,6 +13,10 @@ module WGPU
13
13
 
14
14
  module_function
15
15
 
16
+ # Registers a live native resource when leak debugging is enabled.
17
+ #
18
+ # @param resource [NativeResource] resource to track
19
+ # @return [void]
16
20
  def register(resource)
17
21
  return unless WGPU.debug_leaks
18
22
 
@@ -22,10 +26,17 @@ module WGPU
22
26
  ObjectSpace.define_finalizer(resource, finalizer(object_id))
23
27
  end
24
28
 
29
+ # Removes a resource from leak tracking.
30
+ #
31
+ # @param resource [NativeResource] resource that was released
32
+ # @return [void]
25
33
  def unregister(resource)
26
34
  @mutex.synchronize { @resources.delete(resource.object_id) }
27
35
  end
28
36
 
37
+ # Warns about and clears every tracked resource.
38
+ #
39
+ # @return [void]
29
40
  def warn_remaining
30
41
  resources = @mutex.synchronize do
31
42
  remaining = @resources.values
@@ -53,25 +64,100 @@ module WGPU
53
64
 
54
65
  at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
55
66
 
67
+ class DeviceCallbackLifetime
68
+ # Creates a shared lifetime that runs cleanup after its final owner releases.
69
+ # @yield cleanup invoked exactly once
70
+ def initialize(&cleanup)
71
+ @cleanup = cleanup
72
+ @references = 0
73
+ @completed = false
74
+ @mutex = Mutex.new
75
+ end
76
+
77
+ # Adds an owning native wrapper.
78
+ # @return [void]
79
+ def retain
80
+ @mutex.synchronize do
81
+ raise ResourceError, "device callback lifetime is already complete" if @completed
82
+
83
+ @references += 1
84
+ end
85
+ end
86
+
87
+ # Removes an owning native wrapper and cleans up after the final owner.
88
+ # @return [void]
89
+ def release
90
+ cleanup = @mutex.synchronize do
91
+ return if @completed
92
+
93
+ @references -= 1
94
+ raise ResourceError, "device callback lifetime reference underflow" if @references.negative?
95
+ next unless @references.zero?
96
+
97
+ @completed = true
98
+ @cleanup
99
+ end
100
+ cleanup&.call
101
+ end
102
+ end
103
+ private_constant :DeviceCallbackLifetime
104
+
56
105
  module CallbackKeepalive
57
106
  INITIALIZATION_MUTEX = Mutex.new
107
+ RETAINED_MUTEX = Mutex.new
108
+ RETAINED_CALLBACKS = {}
58
109
 
59
110
  module_function
60
111
 
112
+ # Retains an FFI callback for as long as an operation needs it.
113
+ #
114
+ # @param owner [Object] object that owns the callback registry
115
+ # @param callback [FFI::Function] callback to retain
116
+ # @return [Object] opaque token used by {.release}
61
117
  def retain(owner, callback)
62
118
  mutex, callbacks = storage_for(owner)
63
119
  token = Object.new
64
120
  mutex.synchronize { callbacks[token] = callback }
121
+ RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] = callback }
65
122
  token
66
123
  end
67
124
 
125
+ # Releases a retained callback token.
126
+ #
127
+ # @param owner [Object] object that owns the callback registry
128
+ # @param token [Object, nil] token returned by {.retain}
129
+ # @return [void]
68
130
  def release(owner, token)
69
131
  return unless token
70
132
 
71
133
  mutex, callbacks = storage_for(owner)
72
134
  mutex.synchronize { callbacks.delete(token) }
135
+ RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS.delete(token) }
136
+ end
137
+
138
+ # Moves a retained callback token to another owner without unrooting it.
139
+ #
140
+ # @param from [Object] current registry owner
141
+ # @param to [Object] new registry owner
142
+ # @param token [Object] token returned by {.retain}
143
+ # @return [Boolean] whether the live token was transferred
144
+ def transfer(from, to, token)
145
+ return false unless token
146
+
147
+ callback = RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] }
148
+ return false unless callback
149
+
150
+ from_mutex, from_callbacks = storage_for(from)
151
+ to_mutex, to_callbacks = storage_for(to)
152
+ from_mutex.synchronize { from_callbacks.delete(token) }
153
+ to_mutex.synchronize { to_callbacks[token] = callback }
154
+ true
73
155
  end
74
156
 
157
+ # Returns the number of callbacks retained for an owner.
158
+ #
159
+ # @param owner [Object] object that owns the callback registry
160
+ # @return [Integer] retained callback count
75
161
  def count(owner)
76
162
  mutex, callbacks = storage_for(owner)
77
163
  mutex.synchronize { callbacks.length }
@@ -96,22 +182,53 @@ module WGPU
96
182
  module NativeResource
97
183
  GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
98
184
 
185
+ module ClassMethods
186
+ private
187
+
188
+ def adopt_native_handle(handle, label: nil)
189
+ resource = allocate
190
+ resource.send(:initialize_native_resource, handle: handle, label: label)
191
+ end
192
+ end
193
+
99
194
  module Lifecycle
195
+ UNSET = Object.new.freeze
196
+ private_constant :UNSET
197
+
198
+ # Initializes a wrapper and registers its native-resource lifecycle.
199
+ #
200
+ # @param args [Array] positional arguments forwarded to the resource
201
+ # @param kwargs [Hash] keyword arguments forwarded to the resource
202
+ # @yield block forwarded to the resource initializer
203
+ # @return [NativeResource] initialized resource
100
204
  def initialize(*args, **kwargs, &block)
101
205
  super
102
- @released = false
103
- @label = kwargs[:label] if kwargs.key?(:label)
104
- LeakTracker.register(self)
206
+ initialize_native_resource(label: kwargs.fetch(:label, UNSET))
207
+ attach_device_callback_lifetime_from_parent
105
208
  end
106
209
 
210
+ # Releases a resource once and unregisters it from leak tracking.
211
+ #
212
+ # @return [void]
107
213
  def release(...)
108
214
  return if released?
109
215
 
110
216
  result = super
217
+ release_device_callback_lifetime
111
218
  @released = true
112
219
  LeakTracker.unregister(self)
113
220
  result
114
221
  end
222
+
223
+ private
224
+
225
+ def initialize_native_resource(handle: UNSET, label: UNSET)
226
+ @handle = handle unless handle.equal?(UNSET)
227
+ @label = label unless label.equal?(UNSET)
228
+ @released = false
229
+ LeakTracker.register(self)
230
+ self
231
+ end
115
232
  end
116
233
 
117
234
  def self.included(base)
@@ -124,12 +241,15 @@ module WGPU
124
241
  super(*args, **kwargs, &block)
125
242
  end
126
243
  end
244
+ base.extend(ClassMethods)
127
245
  base.prepend(guard)
128
246
  base.prepend(Lifecycle)
129
247
  end
130
248
 
131
249
  attr_reader :label
132
250
 
251
+ # Reports whether the native handle has been released or is null.
252
+ # @return [Boolean]
133
253
  def released?
134
254
  return true if @released
135
255
  return false unless instance_variable_defined?(:@handle)
@@ -154,6 +274,9 @@ module WGPU
154
274
  release if block_given? && !released?
155
275
  end
156
276
 
277
+ # Returns a concise lifecycle-oriented representation.
278
+ #
279
+ # @return [String] class, optional label, and release state
157
280
  def inspect
158
281
  label_text = @label ? " label=#{@label.inspect}" : ""
159
282
  "#<#{self.class}#{label_text} released=#{released?}>"
@@ -161,6 +284,54 @@ module WGPU
161
284
 
162
285
  private
163
286
 
287
+ def attach_device_callback_lifetime(owner)
288
+ lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
289
+ return self unless lifetime
290
+
291
+ lifetime.retain
292
+ @device_callback_lifetime = lifetime
293
+ @device_callback_lifetime_retained = true
294
+ self
295
+ end
296
+
297
+ def attach_device_callback_lifetime_from_parent
298
+ return if @device_callback_lifetime_retained
299
+
300
+ if @device_callback_lifetime
301
+ @device_callback_lifetime.retain
302
+ @device_callback_lifetime_retained = true
303
+ return
304
+ end
305
+
306
+ owner = @device || @encoder || @texture
307
+ attach_device_callback_lifetime(owner)
308
+ end
309
+
310
+ def release_device_callback_lifetime
311
+ return unless @device_callback_lifetime_retained
312
+
313
+ @device_callback_lifetime_retained = false
314
+ @device_callback_lifetime.release
315
+ end
316
+
317
+ def device_callback_lifetime_lease
318
+ lifetime = @device_callback_lifetime
319
+ return proc {} unless lifetime
320
+
321
+ lifetime.retain
322
+ released = false
323
+ mutex = Mutex.new
324
+ proc do
325
+ should_release = mutex.synchronize do
326
+ next false if released
327
+
328
+ released = true
329
+ true
330
+ end
331
+ lifetime.release if should_release
332
+ end
333
+ end
334
+
164
335
  def ensure_not_released!
165
336
  return unless released?
166
337
 
@@ -4,6 +4,11 @@ module WGPU
4
4
  class BindGroup
5
5
  attr_reader :handle
6
6
 
7
+ # Creates bindings between shader slots and GPU resources.
8
+ # @param device [Device] owning device
9
+ # @param layout [BindGroupLayout] layout the entries must match
10
+ # @param entries [Array<Hash>] resource binding descriptors
11
+ # @raise [PipelineError] if native validation or creation fails
7
12
  def initialize(device, label: nil, layout:, entries:)
8
13
  @device = device
9
14
 
@@ -38,6 +43,10 @@ module WGPU
38
43
  end
39
44
  end
40
45
 
46
+ # Releases the native bind group handle.
47
+ #
48
+ # Calling this method more than once has no effect.
49
+ # @return [void]
41
50
  def release
42
51
  return if @handle.null?
43
52
  Native.wgpuBindGroupRelease(@handle)
@@ -4,13 +4,22 @@ module WGPU
4
4
  class BindGroupLayout
5
5
  attr_reader :handle
6
6
 
7
- def self.from_handle(handle)
8
- layout = allocate
9
- layout.instance_variable_set(:@handle, handle)
10
- layout.instance_variable_set(:@device, nil)
7
+ # Wraps a native bind group layout handle owned by the caller.
8
+ #
9
+ # @param handle [FFI::Pointer] native bind group layout handle
10
+ # @param device [Device, nil] device whose callbacks the layout may use
11
+ # @return [BindGroupLayout] adopted wrapper
12
+ def self.from_handle(handle, device: nil)
13
+ layout = adopt_native_handle(handle)
14
+ layout.instance_variable_set(:@device, device)
15
+ layout.send(:attach_device_callback_lifetime, device)
11
16
  layout
12
17
  end
13
18
 
19
+ # Creates a layout describing shader-visible resource bindings.
20
+ # @param device [Device] owning device
21
+ # @param entries [Array<Hash>] binding layout descriptors
22
+ # @raise [PipelineError] if native validation or creation fails
14
23
  def initialize(device, label: nil, entries:)
15
24
  @device = device
16
25
  desc, @descriptor_keepalive = build_descriptor(label:, entries:)
@@ -26,6 +35,10 @@ module WGPU
26
35
  end
27
36
  end
28
37
 
38
+ # Releases the native bind group layout handle.
39
+ #
40
+ # Calling this method more than once has no effect.
41
+ # @return [void]
29
42
  def release
30
43
  return if @handle.null?
31
44
  Native.wgpuBindGroupLayoutRelease(@handle)
@@ -4,6 +4,11 @@ module WGPU
4
4
  class ComputePipeline
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a compute pipeline.
8
+ # @param device [Device] owning device
9
+ # @param layout [PipelineLayout, :auto, nil] pipeline layout
10
+ # @param compute [Hash] programmable compute stage descriptor
11
+ # @raise [PipelineError] if native validation or creation fails
7
12
  def initialize(device, label: nil, layout:, compute:)
8
13
  @device = device
9
14
  desc, @pointers = build_descriptor(label:, layout:, compute:)
@@ -18,12 +23,20 @@ module WGPU
18
23
  end
19
24
  end
20
25
 
26
+ # Returns the bind group layout inferred or assigned at an index.
27
+ # @param index [Integer] bind group index
28
+ # @return [BindGroupLayout]
29
+ # @raise [PipelineError] if no native layout is returned
21
30
  def get_bind_group_layout(index)
22
31
  handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
23
32
  raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
24
- BindGroupLayout.from_handle(handle)
33
+ BindGroupLayout.from_handle(handle, device: @device)
25
34
  end
26
35
 
36
+ # Releases the native compute pipeline handle.
37
+ #
38
+ # Calling this method more than once has no effect.
39
+ # @return [void]
27
40
  def release
28
41
  return if @handle.null?
29
42
  Native.wgpuComputePipelineRelease(@handle)
@@ -40,9 +53,6 @@ module WGPU
40
53
  context: "compute pipeline stage"
41
54
  )
42
55
  @pointers = []
43
- entry_point = compute[:entry_point] || "main"
44
- entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
45
- @pointers << entry_point_ptr
46
56
 
47
57
  desc = Native::ComputePipelineDescriptor.new
48
58
  desc[:next_in_chain] = nil
@@ -50,11 +60,12 @@ module WGPU
50
60
  desc[:layout] = normalize_layout(layout)
51
61
  desc[:compute][:next_in_chain] = nil
52
62
  desc[:compute][:module] = compute.fetch(:module).handle
53
- desc[:compute][:entry_point][:data] = entry_point_ptr
54
- desc[:compute][:entry_point][:length] = entry_point.bytesize
55
- desc[:compute][:constant_count] = 0
56
- desc[:compute][:constants] = nil
57
- setup_constants(desc[:compute], compute[:constants])
63
+ DescriptorHelpers.set_nullable_string_view(
64
+ desc[:compute][:entry_point],
65
+ compute[:entry_point],
66
+ keepalive: @pointers
67
+ )
68
+ DescriptorHelpers.set_constants(desc[:compute], compute[:constants], keepalive: @pointers)
58
69
 
59
70
  [desc, @pointers]
60
71
  end
@@ -63,28 +74,5 @@ module WGPU
63
74
  return nil if layout.nil? || layout == :auto || layout == "auto"
64
75
  layout.handle
65
76
  end
66
-
67
- def setup_constants(stage_desc, constants)
68
- return if constants.nil? || constants.empty?
69
-
70
- constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
71
- @pointers << constants_ptr
72
-
73
- constants.each_with_index do |(key, value), i|
74
- entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
75
- entry = Native::ConstantEntry.new(entry_ptr)
76
- entry[:next_in_chain] = nil
77
-
78
- key_str = key.to_s
79
- key_ptr = FFI::MemoryPointer.from_string(key_str)
80
- @pointers << key_ptr
81
- entry[:key][:data] = key_ptr
82
- entry[:key][:length] = key_str.bytesize
83
- entry[:value] = value.to_f
84
- end
85
-
86
- stage_desc[:constant_count] = constants.size
87
- stage_desc[:constants] = constants_ptr
88
- end
89
77
  end
90
78
  end