wgpu 1.2.0 → 1.2.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -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 +13 -1
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/errors.md +13 -0
  9. data/docs/getting_started_compute.md +1 -0
  10. data/docs/getting_started_rendering.md +5 -0
  11. data/docs/pipeline_descriptors.md +7 -2
  12. data/docs/releasing.md +11 -1
  13. data/docs/resource_lifetime.md +25 -2
  14. data/docs/texture_readback.md +27 -0
  15. data/docs/troubleshooting.md +5 -0
  16. data/docs/upgrading_wgpu_native.md +16 -5
  17. data/lib/wgpu/async_task.rb +19 -0
  18. data/lib/wgpu/commands/command_buffer.rb +14 -1
  19. data/lib/wgpu/commands/command_encoder.rb +82 -27
  20. data/lib/wgpu/commands/compute_pass.rb +49 -6
  21. data/lib/wgpu/commands/render_bundle.rb +9 -1
  22. data/lib/wgpu/commands/render_bundle_encoder.rb +63 -9
  23. data/lib/wgpu/commands/render_pass.rb +116 -14
  24. data/lib/wgpu/core/adapter.rb +95 -13
  25. data/lib/wgpu/core/async_waiter.rb +30 -4
  26. data/lib/wgpu/core/canvas_context.rb +37 -2
  27. data/lib/wgpu/core/device.rb +377 -70
  28. data/lib/wgpu/core/instance.rb +20 -0
  29. data/lib/wgpu/core/queue.rb +143 -43
  30. data/lib/wgpu/core/surface.rb +55 -7
  31. data/lib/wgpu/data_types.rb +16 -0
  32. data/lib/wgpu/descriptor_helpers.rb +87 -0
  33. data/lib/wgpu/error.rb +15 -0
  34. data/lib/wgpu/native/abi_verifier.rb +37 -3
  35. data/lib/wgpu/native/callbacks.rb +6 -0
  36. data/lib/wgpu/native/capabilities.rb +16 -2
  37. data/lib/wgpu/native/distribution.rb +63 -0
  38. data/lib/wgpu/native/enum_helper.rb +17 -0
  39. data/lib/wgpu/native/enums.rb +8 -0
  40. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  41. data/lib/wgpu/native/functions.rb +14 -0
  42. data/lib/wgpu/native/installer.rb +55 -18
  43. data/lib/wgpu/native/loader.rb +22 -0
  44. data/lib/wgpu/native/structs.rb +18 -1
  45. data/lib/wgpu/native_resource.rb +190 -3
  46. data/lib/wgpu/pipeline/bind_group.rb +16 -7
  47. data/lib/wgpu/pipeline/bind_group_layout.rb +20 -7
  48. data/lib/wgpu/pipeline/compute_pipeline.rb +25 -37
  49. data/lib/wgpu/pipeline/pipeline_layout.rb +12 -4
  50. data/lib/wgpu/pipeline/render_pipeline.rb +33 -48
  51. data/lib/wgpu/pipeline/shader_module.rb +57 -32
  52. data/lib/wgpu/resources/buffer.rb +330 -59
  53. data/lib/wgpu/resources/query_set.rb +13 -1
  54. data/lib/wgpu/resources/sampler.rb +10 -3
  55. data/lib/wgpu/resources/texture.rb +49 -12
  56. data/lib/wgpu/resources/texture_view.rb +25 -5
  57. data/lib/wgpu/texture_format.rb +14 -0
  58. data/lib/wgpu/version.rb +1 -1
  59. data/lib/wgpu/window.rb +26 -0
  60. data/sig/wgpu.rbs +85 -5
  61. metadata +12 -4
@@ -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
 
@@ -281,6 +292,9 @@ module WGPU
281
292
  attach_function :wgpuTextureViewRelease,
282
293
  [:pointer], :void
283
294
 
295
+ attach_function :wgpuSupportedFeaturesFreeMembers,
296
+ [SupportedFeatures.by_value], :void
297
+
284
298
  attach_function :wgpuAdapterInfoFreeMembers,
285
299
  [AdapterInfo.by_value], :void
286
300
 
@@ -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)
@@ -155,7 +186,13 @@ module WGPU
155
186
  end
156
187
 
157
188
  FileUtils.mkdir_p(entry.directory? ? target : File.dirname(target))
158
- entry.extract(target) { true } unless entry.directory?
189
+ next if entry.directory?
190
+
191
+ if entry.method(:extract).parameters.include?([:key, :destination_directory])
192
+ entry.extract(entry.name, destination_directory: destination) { true }
193
+ else
194
+ entry.extract(target) { true }
195
+ end
159
196
  end
160
197
  end
161
198
  true
@@ -164,7 +201,7 @@ module WGPU
164
201
  end
165
202
 
166
203
  def extract_with_powershell(archive_path, destination)
167
- system(
204
+ @command_runner.call(
168
205
  "powershell",
169
206
  "-NoProfile",
170
207
  "-Command",
@@ -173,7 +210,7 @@ module WGPU
173
210
  end
174
211
 
175
212
  def extract_with_unzip(archive_path, destination)
176
- system("unzip", "-o", "-q", archive_path, "-d", destination)
213
+ @command_runner.call("unzip", "-o", "-q", archive_path, "-d", destination)
177
214
  end
178
215
 
179
216
  def windows?
@@ -182,7 +219,7 @@ module WGPU
182
219
 
183
220
  def recovery_instructions
184
221
  <<~INSTRUCTIONS.chomp
185
- Download the matching #{Distribution::VERSION} artifact manually and set:
222
+ Download the matching #{@distribution.const_get(:VERSION)} artifact manually and set:
186
223
  WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
187
224
  See docs/installation.md for platform details.
188
225
  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) }
73
136
  end
74
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
155
+ end
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,65 @@ module WGPU
96
182
  module NativeResource
97
183
  GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
98
184
 
185
+ # Validates resource arguments before passing their handles to FFI.
186
+ def self.checked_handle(resource, expected_class:)
187
+ unless resource.is_a?(expected_class)
188
+ raise TypeError, "expected #{expected_class}, got #{resource.class}"
189
+ end
190
+ resource.send(:ensure_not_released!)
191
+ resource.handle
192
+ end
193
+
194
+ module ClassMethods
195
+ private
196
+
197
+ def adopt_native_handle(handle, label: nil)
198
+ resource = allocate
199
+ resource.send(:initialize_native_resource, handle: handle, label: label)
200
+ end
201
+ end
202
+
99
203
  module Lifecycle
204
+ UNSET = Object.new.freeze
205
+ private_constant :UNSET
206
+
207
+ # Initializes a wrapper and registers its native-resource lifecycle.
208
+ #
209
+ # @param args [Array] positional arguments forwarded to the resource
210
+ # @param kwargs [Hash] keyword arguments forwarded to the resource
211
+ # @yield block forwarded to the resource initializer
212
+ # @return [NativeResource] initialized resource
100
213
  def initialize(*args, **kwargs, &block)
101
214
  super
102
- @released = false
103
- @label = kwargs[:label] if kwargs.key?(:label)
104
- LeakTracker.register(self)
215
+ initialize_native_resource(label: kwargs.fetch(:label, UNSET))
216
+ attach_device_callback_lifetime_from_parent
217
+ initialized = true
218
+ ensure
219
+ release if !initialized && @handle && !released?
105
220
  end
106
221
 
222
+ # Releases a resource once and unregisters it from leak tracking.
223
+ #
224
+ # @return [void]
107
225
  def release(...)
108
226
  return if released?
109
227
 
110
228
  result = super
229
+ release_device_callback_lifetime
111
230
  @released = true
112
231
  LeakTracker.unregister(self)
113
232
  result
114
233
  end
234
+
235
+ private
236
+
237
+ def initialize_native_resource(handle: UNSET, label: UNSET)
238
+ @handle = handle unless handle.equal?(UNSET)
239
+ @label = label unless label.equal?(UNSET)
240
+ @released = false
241
+ LeakTracker.register(self)
242
+ self
243
+ end
115
244
  end
116
245
 
117
246
  def self.included(base)
@@ -124,12 +253,15 @@ module WGPU
124
253
  super(*args, **kwargs, &block)
125
254
  end
126
255
  end
256
+ base.extend(ClassMethods)
127
257
  base.prepend(guard)
128
258
  base.prepend(Lifecycle)
129
259
  end
130
260
 
131
261
  attr_reader :label
132
262
 
263
+ # Reports whether the native handle has been released or is null.
264
+ # @return [Boolean]
133
265
  def released?
134
266
  return true if @released
135
267
  return false unless instance_variable_defined?(:@handle)
@@ -154,6 +286,9 @@ module WGPU
154
286
  release if block_given? && !released?
155
287
  end
156
288
 
289
+ # Returns a concise lifecycle-oriented representation.
290
+ #
291
+ # @return [String] class, optional label, and release state
157
292
  def inspect
158
293
  label_text = @label ? " label=#{@label.inspect}" : ""
159
294
  "#<#{self.class}#{label_text} released=#{released?}>"
@@ -161,6 +296,58 @@ module WGPU
161
296
 
162
297
  private
163
298
 
299
+ def initialize_copy(_source)
300
+ raise TypeError, "#{self.class} owns a native resource and cannot be copied"
301
+ end
302
+
303
+ def attach_device_callback_lifetime(owner)
304
+ lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
305
+ return self unless lifetime
306
+
307
+ lifetime.retain
308
+ @device_callback_lifetime = lifetime
309
+ @device_callback_lifetime_retained = true
310
+ self
311
+ end
312
+
313
+ def attach_device_callback_lifetime_from_parent
314
+ return if @device_callback_lifetime_retained
315
+
316
+ if @device_callback_lifetime
317
+ @device_callback_lifetime.retain
318
+ @device_callback_lifetime_retained = true
319
+ return
320
+ end
321
+
322
+ owner = @device || @encoder || @texture
323
+ attach_device_callback_lifetime(owner)
324
+ end
325
+
326
+ def release_device_callback_lifetime
327
+ return unless @device_callback_lifetime_retained
328
+
329
+ @device_callback_lifetime_retained = false
330
+ @device_callback_lifetime.release
331
+ end
332
+
333
+ def device_callback_lifetime_lease
334
+ lifetime = @device_callback_lifetime
335
+ return proc {} unless lifetime
336
+
337
+ lifetime.retain
338
+ released = false
339
+ mutex = Mutex.new
340
+ proc do
341
+ should_release = mutex.synchronize do
342
+ next false if released
343
+
344
+ released = true
345
+ true
346
+ end
347
+ lifetime.release if should_release
348
+ end
349
+ end
350
+
164
351
  def ensure_not_released!
165
352
  return unless released?
166
353
 
@@ -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
 
@@ -24,13 +29,13 @@ module WGPU
24
29
  desc[:label][:data] = nil
25
30
  desc[:label][:length] = 0
26
31
  end
27
- desc[:layout] = layout.handle
32
+ desc[:layout] = NativeResource.checked_handle(layout, expected_class: BindGroupLayout)
28
33
  desc[:entry_count] = entries_array.size
29
34
  desc[:entries] = entries_ptr
30
35
 
31
- device.push_error_scope(:validation)
32
- @handle = Native.wgpuDeviceCreateBindGroup(device.handle, desc)
33
- error = device.pop_error_scope
36
+ error = device.send(:capture_error_scope) do
37
+ @handle = Native.wgpuDeviceCreateBindGroup(NativeResource.checked_handle(device, expected_class: Device), desc)
38
+ end
34
39
 
35
40
  if @handle.null? || (error[:type] && error[:type] != :no_error)
36
41
  msg = error[:message] || "Failed to create bind group"
@@ -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)
@@ -65,7 +74,7 @@ module WGPU
65
74
 
66
75
  if entry_hash[:buffer]
67
76
  buffer = entry_hash[:buffer]
68
- entry[:buffer] = buffer.handle
77
+ entry[:buffer] = NativeResource.checked_handle(buffer, expected_class: Buffer)
69
78
  entry[:offset] = entry_hash[:offset] || 0
70
79
  entry[:size] = entry_hash[:size] || buffer.size
71
80
  else
@@ -75,13 +84,13 @@ module WGPU
75
84
  end
76
85
 
77
86
  if entry_hash[:sampler]
78
- entry[:sampler] = entry_hash[:sampler].handle
87
+ entry[:sampler] = NativeResource.checked_handle(entry_hash[:sampler], expected_class: Sampler)
79
88
  else
80
89
  entry[:sampler] = nil
81
90
  end
82
91
 
83
92
  if entry_hash[:texture_view]
84
- entry[:texture_view] = entry_hash[:texture_view].handle
93
+ entry[:texture_view] = NativeResource.checked_handle(entry_hash[:texture_view], expected_class: TextureView)
85
94
  else
86
95
  entry[:texture_view] = nil
87
96
  end
@@ -4,20 +4,29 @@ 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:)
17
26
 
18
- device.push_error_scope(:validation)
19
- @handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
20
- error = device.pop_error_scope
27
+ error = device.send(:capture_error_scope) do
28
+ @handle = Native.wgpuDeviceCreateBindGroupLayout(NativeResource.checked_handle(device, expected_class: Device), desc)
29
+ end
21
30
  @descriptor_keepalive = nil
22
31
 
23
32
  if @handle.null? || (error[:type] && error[:type] != :no_error)
@@ -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)