wgpu 1.0.0 → 1.2.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +26 -3
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +132 -0
  6. data/docs/async.md +31 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +24 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +61 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +38 -0
  15. data/docs/releasing.md +22 -0
  16. data/docs/resource_lifetime.md +94 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +19 -0
  19. data/docs/troubleshooting.md +48 -0
  20. data/docs/upgrading_wgpu_native.md +26 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/commands/command_buffer.rb +11 -0
  23. data/lib/wgpu/commands/command_encoder.rb +65 -8
  24. data/lib/wgpu/commands/compute_pass.rb +10 -0
  25. data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
  26. data/lib/wgpu/commands/render_pass.rb +34 -8
  27. data/lib/wgpu/core/adapter.rb +49 -20
  28. data/lib/wgpu/core/async_waiter.rb +92 -0
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +160 -52
  31. data/lib/wgpu/core/instance.rb +9 -5
  32. data/lib/wgpu/core/queue.rb +81 -57
  33. data/lib/wgpu/core/surface.rb +16 -17
  34. data/lib/wgpu/data_types.rb +67 -0
  35. data/lib/wgpu/descriptor_helpers.rb +39 -0
  36. data/lib/wgpu/error.rb +55 -0
  37. data/lib/wgpu/logging.rb +63 -0
  38. data/lib/wgpu/native/abi_verifier.rb +109 -0
  39. data/lib/wgpu/native/callbacks.rb +9 -6
  40. data/lib/wgpu/native/capabilities.rb +31 -0
  41. data/lib/wgpu/native/distribution.rb +113 -0
  42. data/lib/wgpu/native/enum_helper.rb +63 -0
  43. data/lib/wgpu/native/enums.rb +82 -13
  44. data/lib/wgpu/native/functions.rb +17 -8
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +40 -21
  47. data/lib/wgpu/native/structs.rb +19 -5
  48. data/lib/wgpu/native_resource.rb +171 -0
  49. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  50. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  51. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  52. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  53. data/lib/wgpu/pipeline/shader_module.rb +51 -12
  54. data/lib/wgpu/resources/buffer.rb +167 -84
  55. data/lib/wgpu/resources/query_set.rb +14 -12
  56. data/lib/wgpu/resources/sampler.rb +36 -20
  57. data/lib/wgpu/resources/texture.rb +44 -42
  58. data/lib/wgpu/resources/texture_view.rb +11 -3
  59. data/lib/wgpu/texture_format.rb +82 -0
  60. data/lib/wgpu/version.rb +1 -1
  61. data/lib/wgpu/window.rb +8 -1
  62. data/lib/wgpu.rb +33 -0
  63. data/sig/wgpu.rbs +381 -0
  64. metadata +33 -16
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class << self
5
+ attr_accessor :debug_leaks
6
+ end
7
+
8
+ self.debug_leaks = ENV["WGPU_DEBUG_LEAKS"] == "1"
9
+
10
+ module LeakTracker
11
+ @resources = {}
12
+ @mutex = Mutex.new
13
+
14
+ module_function
15
+
16
+ def register(resource)
17
+ return unless WGPU.debug_leaks
18
+
19
+ object_id = resource.object_id
20
+ description = describe(resource)
21
+ @mutex.synchronize { @resources[object_id] = description }
22
+ ObjectSpace.define_finalizer(resource, finalizer(object_id))
23
+ end
24
+
25
+ def unregister(resource)
26
+ @mutex.synchronize { @resources.delete(resource.object_id) }
27
+ end
28
+
29
+ def warn_remaining
30
+ resources = @mutex.synchronize do
31
+ remaining = @resources.values
32
+ @resources.clear
33
+ remaining
34
+ end
35
+ resources.each { |description| warn "WGPU resource leaked: #{description}" }
36
+ end
37
+
38
+ def describe(resource)
39
+ label = resource.label
40
+ label_text = label ? " label=#{label.inspect}" : ""
41
+ "#{resource.class}#{label_text}"
42
+ end
43
+ private_class_method :describe
44
+
45
+ def finalizer(object_id)
46
+ proc do
47
+ description = @mutex.synchronize { @resources.delete(object_id) }
48
+ warn "WGPU resource leaked: #{description}" if description
49
+ end
50
+ end
51
+ private_class_method :finalizer
52
+ end
53
+
54
+ at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
55
+
56
+ module CallbackKeepalive
57
+ INITIALIZATION_MUTEX = Mutex.new
58
+
59
+ module_function
60
+
61
+ def retain(owner, callback)
62
+ mutex, callbacks = storage_for(owner)
63
+ token = Object.new
64
+ mutex.synchronize { callbacks[token] = callback }
65
+ token
66
+ end
67
+
68
+ def release(owner, token)
69
+ return unless token
70
+
71
+ mutex, callbacks = storage_for(owner)
72
+ mutex.synchronize { callbacks.delete(token) }
73
+ end
74
+
75
+ def count(owner)
76
+ mutex, callbacks = storage_for(owner)
77
+ mutex.synchronize { callbacks.length }
78
+ end
79
+
80
+ def storage_for(owner)
81
+ INITIALIZATION_MUTEX.synchronize do
82
+ mutex = owner.instance_variable_get(:@wgpu_callback_keepalive_mutex)
83
+ callbacks = owner.instance_variable_get(:@wgpu_callback_keepalive)
84
+ unless mutex && callbacks
85
+ mutex = Mutex.new
86
+ callbacks = {}
87
+ owner.instance_variable_set(:@wgpu_callback_keepalive_mutex, mutex)
88
+ owner.instance_variable_set(:@wgpu_callback_keepalive, callbacks)
89
+ end
90
+ [mutex, callbacks]
91
+ end
92
+ end
93
+ private_class_method :storage_for
94
+ end
95
+
96
+ module NativeResource
97
+ GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
98
+
99
+ module Lifecycle
100
+ def initialize(*args, **kwargs, &block)
101
+ super
102
+ @released = false
103
+ @label = kwargs[:label] if kwargs.key?(:label)
104
+ LeakTracker.register(self)
105
+ end
106
+
107
+ def release(...)
108
+ return if released?
109
+
110
+ result = super
111
+ @released = true
112
+ LeakTracker.unregister(self)
113
+ result
114
+ end
115
+ end
116
+
117
+ def self.included(base)
118
+ guard = Module.new
119
+ base.public_instance_methods(false).each do |method_name|
120
+ next if GUARDED_METHOD_EXEMPTIONS.include?(method_name)
121
+
122
+ guard.define_method(method_name) do |*args, **kwargs, &block|
123
+ ensure_not_released!
124
+ super(*args, **kwargs, &block)
125
+ end
126
+ end
127
+ base.prepend(guard)
128
+ base.prepend(Lifecycle)
129
+ end
130
+
131
+ attr_reader :label
132
+
133
+ def released?
134
+ return true if @released
135
+ return false unless instance_variable_defined?(:@handle)
136
+ return true if @handle.nil?
137
+
138
+ @handle.respond_to?(:null?) && @handle.null?
139
+ end
140
+
141
+ # Yields this wrapper and always releases it when the block exits.
142
+ #
143
+ # This is Ruby convenience API; WebGPU itself has no block-scoped resource
144
+ # primitive.
145
+ #
146
+ # @yieldparam resource [NativeResource] this resource
147
+ # @return the block result
148
+ def use
149
+ raise ArgumentError, "block is required" unless block_given?
150
+
151
+ ensure_not_released!
152
+ yield self
153
+ ensure
154
+ release if block_given? && !released?
155
+ end
156
+
157
+ def inspect
158
+ label_text = @label ? " label=#{@label.inspect}" : ""
159
+ "#<#{self.class}#{label_text} released=#{released?}>"
160
+ end
161
+
162
+ private
163
+
164
+ def ensure_not_released!
165
+ return unless released?
166
+
167
+ label_text = @label ? " (#{@label})" : ""
168
+ raise ResourceError, "#{self.class}#{label_text} has been released"
169
+ end
170
+ end
171
+ end
@@ -47,6 +47,18 @@ module WGPU
47
47
  private
48
48
 
49
49
  def create_entry(entry_hash)
50
+ DescriptorHelpers.validate_keys!(
51
+ entry_hash,
52
+ allowed: %i[binding buffer offset size sampler texture_view],
53
+ required: [:binding],
54
+ context: "bind group entry"
55
+ )
56
+ resources = %i[buffer sampler texture_view].select { |key| entry_hash[key] }
57
+ unless resources.one?
58
+ raise ArgumentError,
59
+ "bind group entry must define exactly one resource (:buffer, :sampler, or :texture_view)"
60
+ end
61
+
50
62
  entry = Native::BindGroupEntry.new
51
63
  entry[:next_in_chain] = nil
52
64
  entry[:binding] = entry_hash[:binding]
@@ -13,30 +13,12 @@ module WGPU
13
13
 
14
14
  def initialize(device, label: nil, entries:)
15
15
  @device = device
16
-
17
- entries_array = entries.map { |e| create_entry(e) }
18
- entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
19
- entries_array.each_with_index do |entry, i|
20
- offset = i * Native::BindGroupLayoutEntry.size
21
- (entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
22
- end
23
-
24
- desc = Native::BindGroupLayoutDescriptor.new
25
- desc[:next_in_chain] = nil
26
- if label
27
- label_ptr = FFI::MemoryPointer.from_string(label)
28
- desc[:label][:data] = label_ptr
29
- desc[:label][:length] = label.bytesize
30
- else
31
- desc[:label][:data] = nil
32
- desc[:label][:length] = 0
33
- end
34
- desc[:entry_count] = entries_array.size
35
- desc[:entries] = entries_ptr
16
+ desc, @descriptor_keepalive = build_descriptor(label:, entries:)
36
17
 
37
18
  device.push_error_scope(:validation)
38
19
  @handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
39
20
  error = device.pop_error_scope
21
+ @descriptor_keepalive = nil
40
22
 
41
23
  if @handle.null? || (error[:type] && error[:type] != :no_error)
42
24
  msg = error[:message] || "Failed to create bind group layout"
@@ -52,7 +34,39 @@ module WGPU
52
34
 
53
35
  private
54
36
 
37
+ def build_descriptor(label:, entries:)
38
+ keepalive = []
39
+ entries_array = entries.map { |entry| create_entry(entry) }
40
+ entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
41
+ entries_array.each_with_index do |entry, index|
42
+ offset = index * Native::BindGroupLayoutEntry.size
43
+ (entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
44
+ end
45
+ keepalive.concat(entries_array)
46
+ keepalive << entries_ptr
47
+
48
+ desc = Native::BindGroupLayoutDescriptor.new
49
+ desc[:next_in_chain] = nil
50
+ DescriptorHelpers.set_label(desc, label, keepalive:)
51
+ desc[:entry_count] = entries_array.size
52
+ desc[:entries] = entries_ptr
53
+ [desc, keepalive]
54
+ end
55
+
55
56
  def create_entry(entry_hash)
57
+ DescriptorHelpers.validate_keys!(
58
+ entry_hash,
59
+ allowed: %i[binding visibility buffer sampler texture storage_texture],
60
+ required: %i[binding visibility],
61
+ context: "bind group layout entry"
62
+ )
63
+ variants = %i[buffer sampler texture storage_texture].select { |key| entry_hash[key] }
64
+ unless variants.one?
65
+ raise ArgumentError,
66
+ "bind group layout entry must define exactly one resource variant " \
67
+ "(:buffer, :sampler, :texture, or :storage_texture)"
68
+ end
69
+
56
70
  entry = Native::BindGroupLayoutEntry.new
57
71
  entry[:next_in_chain] = nil
58
72
  entry[:binding] = entry_hash[:binding]
@@ -78,44 +92,84 @@ module WGPU
78
92
 
79
93
  if entry_hash[:buffer]
80
94
  buffer_info = entry_hash[:buffer]
81
- entry[:buffer][:type] = buffer_info[:type] || :storage
95
+ DescriptorHelpers.validate_keys!(
96
+ buffer_info,
97
+ allowed: %i[type has_dynamic_offset min_binding_size],
98
+ context: "buffer binding layout"
99
+ )
100
+ entry[:buffer][:type] = Native::EnumHelper.coerce(
101
+ Native::BufferBindingType,
102
+ buffer_info[:type] || :storage,
103
+ name: "buffer binding type"
104
+ )
82
105
  entry[:buffer][:has_dynamic_offset] = buffer_info[:has_dynamic_offset] ? 1 : 0
83
106
  entry[:buffer][:min_binding_size] = buffer_info[:min_binding_size] || 0
84
107
  end
85
108
 
86
109
  if entry_hash[:sampler]
87
110
  sampler_info = entry_hash[:sampler]
88
- entry[:sampler][:type] = sampler_info[:type] || :filtering
111
+ DescriptorHelpers.validate_keys!(
112
+ sampler_info,
113
+ allowed: [:type],
114
+ context: "sampler binding layout"
115
+ )
116
+ entry[:sampler][:type] = Native::EnumHelper.coerce(
117
+ Native::SamplerBindingType,
118
+ sampler_info[:type] || :filtering,
119
+ name: "sampler binding type"
120
+ )
89
121
  end
90
122
 
91
123
  if entry_hash[:texture]
92
124
  texture_info = entry_hash[:texture]
93
- entry[:texture][:sample_type] = texture_info[:sample_type] || :float
94
- entry[:texture][:view_dimension] = texture_info[:view_dimension] || :d2
125
+ DescriptorHelpers.validate_keys!(
126
+ texture_info,
127
+ allowed: %i[sample_type view_dimension multisampled],
128
+ context: "texture binding layout"
129
+ )
130
+ entry[:texture][:sample_type] = Native::EnumHelper.coerce(
131
+ Native::TextureSampleType,
132
+ texture_info[:sample_type] || :float,
133
+ name: "texture sample type"
134
+ )
135
+ entry[:texture][:view_dimension] = Native::EnumHelper.coerce(
136
+ Native::TextureViewDimension,
137
+ texture_info[:view_dimension] || :d2,
138
+ name: "texture view dimension"
139
+ )
95
140
  entry[:texture][:multisampled] = texture_info[:multisampled] ? 1 : 0
96
141
  end
97
142
 
98
143
  if entry_hash[:storage_texture]
99
144
  st_info = entry_hash[:storage_texture]
100
- entry[:storage_texture][:access] = st_info[:access] || :write_only
101
- entry[:storage_texture][:format] = st_info[:format]
102
- entry[:storage_texture][:view_dimension] = st_info[:view_dimension] || :d2
145
+ DescriptorHelpers.validate_keys!(
146
+ st_info,
147
+ allowed: %i[access format view_dimension],
148
+ required: [:format],
149
+ context: "storage texture binding layout"
150
+ )
151
+ entry[:storage_texture][:access] = Native::EnumHelper.coerce(
152
+ Native::StorageTextureAccess,
153
+ st_info[:access] || :write_only,
154
+ name: "storage texture access"
155
+ )
156
+ entry[:storage_texture][:format] = Native::EnumHelper.coerce(
157
+ Native::TextureFormat,
158
+ st_info.fetch(:format),
159
+ name: "storage texture format"
160
+ )
161
+ entry[:storage_texture][:view_dimension] = Native::EnumHelper.coerce(
162
+ Native::TextureViewDimension,
163
+ st_info[:view_dimension] || :d2,
164
+ name: "texture view dimension"
165
+ )
103
166
  end
104
167
 
105
168
  entry
106
169
  end
107
170
 
108
171
  def normalize_visibility(visibility)
109
- case visibility
110
- when Integer
111
- visibility
112
- when Symbol
113
- Native::ShaderStage[visibility]
114
- when Array
115
- visibility.reduce(0) { |acc, v| acc | Native::ShaderStage[v] }
116
- else
117
- raise ArgumentError, "Invalid visibility: #{visibility}"
118
- end
172
+ Native::EnumHelper.coerce_flags(Native::ShaderStage, visibility, name: "shader visibility")
119
173
  end
120
174
  end
121
175
  end
@@ -6,32 +6,7 @@ module WGPU
6
6
 
7
7
  def initialize(device, label: nil, layout:, compute:)
8
8
  @device = device
9
- @pointers = []
10
-
11
- entry_point = compute[:entry_point] || "main"
12
- entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
13
- @pointers << entry_point_ptr
14
-
15
- desc = Native::ComputePipelineDescriptor.new
16
- desc[:next_in_chain] = nil
17
- if label
18
- label_ptr = FFI::MemoryPointer.from_string(label)
19
- @pointers << label_ptr
20
- desc[:label][:data] = label_ptr
21
- desc[:label][:length] = label.bytesize
22
- else
23
- desc[:label][:data] = nil
24
- desc[:label][:length] = 0
25
- end
26
- desc[:layout] = normalize_layout(layout)
27
-
28
- desc[:compute][:next_in_chain] = nil
29
- desc[:compute][:module] = compute.fetch(:module).handle
30
- desc[:compute][:entry_point][:data] = entry_point_ptr
31
- desc[:compute][:entry_point][:length] = entry_point.bytesize
32
- desc[:compute][:constant_count] = 0
33
- desc[:compute][:constants] = nil
34
- setup_constants(desc[:compute], compute[:constants])
9
+ desc, @pointers = build_descriptor(label:, layout:, compute:)
35
10
 
36
11
  device.push_error_scope(:validation)
37
12
  @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
@@ -57,6 +32,33 @@ module WGPU
57
32
 
58
33
  private
59
34
 
35
+ def build_descriptor(label:, layout:, compute:)
36
+ DescriptorHelpers.validate_keys!(
37
+ compute,
38
+ allowed: %i[module entry_point constants],
39
+ required: [:module],
40
+ context: "compute pipeline stage"
41
+ )
42
+ @pointers = []
43
+ entry_point = compute[:entry_point] || "main"
44
+ entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
45
+ @pointers << entry_point_ptr
46
+
47
+ desc = Native::ComputePipelineDescriptor.new
48
+ desc[:next_in_chain] = nil
49
+ DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
50
+ desc[:layout] = normalize_layout(layout)
51
+ desc[:compute][:next_in_chain] = nil
52
+ 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])
58
+
59
+ [desc, @pointers]
60
+ end
61
+
60
62
  def normalize_layout(layout)
61
63
  return nil if layout.nil? || layout == :auto || layout == "auto"
62
64
  layout.handle