wgpu 1.0.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-APACHE +190 -0
  3. data/LICENSE-MIT +21 -0
  4. data/README.md +228 -0
  5. data/ext/wgpu/Makefile +7 -0
  6. data/ext/wgpu/extconf.rb +161 -0
  7. data/lib/wgpu/async_task.rb +55 -0
  8. data/lib/wgpu/commands/command_buffer.rb +17 -0
  9. data/lib/wgpu/commands/command_encoder.rb +201 -0
  10. data/lib/wgpu/commands/compute_pass.rb +89 -0
  11. data/lib/wgpu/commands/render_bundle.rb +18 -0
  12. data/lib/wgpu/commands/render_bundle_encoder.rb +148 -0
  13. data/lib/wgpu/commands/render_pass.rb +207 -0
  14. data/lib/wgpu/core/adapter.rb +186 -0
  15. data/lib/wgpu/core/canvas_context.rb +104 -0
  16. data/lib/wgpu/core/device.rb +397 -0
  17. data/lib/wgpu/core/instance.rb +81 -0
  18. data/lib/wgpu/core/queue.rb +197 -0
  19. data/lib/wgpu/core/surface.rb +221 -0
  20. data/lib/wgpu/error.rb +16 -0
  21. data/lib/wgpu/native/callbacks.rb +26 -0
  22. data/lib/wgpu/native/enums.rb +529 -0
  23. data/lib/wgpu/native/functions.rb +419 -0
  24. data/lib/wgpu/native/loader.rb +61 -0
  25. data/lib/wgpu/native/structs.rb +646 -0
  26. data/lib/wgpu/pipeline/bind_group.rb +80 -0
  27. data/lib/wgpu/pipeline/bind_group_layout.rb +121 -0
  28. data/lib/wgpu/pipeline/compute_pipeline.rb +88 -0
  29. data/lib/wgpu/pipeline/pipeline_layout.rb +43 -0
  30. data/lib/wgpu/pipeline/render_pipeline.rb +278 -0
  31. data/lib/wgpu/pipeline/shader_module.rb +202 -0
  32. data/lib/wgpu/resources/buffer.rb +228 -0
  33. data/lib/wgpu/resources/query_set.rb +45 -0
  34. data/lib/wgpu/resources/sampler.rb +47 -0
  35. data/lib/wgpu/resources/texture.rb +136 -0
  36. data/lib/wgpu/resources/texture_view.rb +49 -0
  37. data/lib/wgpu/version.rb +5 -0
  38. data/lib/wgpu/window.rb +177 -0
  39. data/lib/wgpu.rb +36 -0
  40. metadata +125 -0
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Adapter
5
+ attr_reader :handle
6
+
7
+ CALLBACK_MODE_WAIT_ANY_ONLY = 1
8
+
9
+ def self.from_handle(handle)
10
+ adapter = allocate
11
+ adapter.instance_variable_set(:@handle, handle)
12
+ adapter
13
+ end
14
+
15
+ def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
16
+ adapter_ptr = FFI::MemoryPointer.new(:pointer)
17
+ status_holder = { value: nil, message: nil }
18
+
19
+ callback = FFI::Function.new(
20
+ :void, [:uint32, :pointer, Native::StringView.by_value, :pointer]
21
+ ) do |status, adapter, message, _userdata|
22
+ status_holder[:value] = Native::RequestAdapterStatus[status]
23
+ if message[:data] && !message[:data].null? && message[:length] > 0
24
+ status_holder[:message] = message[:data].read_string(message[:length])
25
+ end
26
+ adapter_ptr.write_pointer(adapter)
27
+ end
28
+
29
+ options = Native::RequestAdapterOptions.new
30
+ options[:next_in_chain] = nil
31
+ options[:feature_level] = feature_level
32
+ options[:power_preference] = power_preference
33
+ options[:force_fallback_adapter] = force_fallback_adapter ? 1 : 0
34
+ options[:backend_type] = backend || :undefined
35
+ options[:compatible_surface] = compatible_surface&.handle
36
+
37
+ callback_info = Native::RequestAdapterCallbackInfo.new
38
+ callback_info[:next_in_chain] = nil
39
+ callback_info[:mode] = CALLBACK_MODE_WAIT_ANY_ONLY
40
+ callback_info[:callback] = callback
41
+ callback_info[:userdata] = nil
42
+
43
+ Native.wgpuInstanceRequestAdapter(instance.handle, options, callback_info)
44
+
45
+ handle = adapter_ptr.read_pointer
46
+ if handle.null? || status_holder[:value] != :success
47
+ msg = status_holder[:message] || "Unknown error"
48
+ raise AdapterError, "Failed to request adapter: #{msg}"
49
+ end
50
+
51
+ new(handle)
52
+ end
53
+
54
+ def initialize(handle)
55
+ @handle = handle
56
+ end
57
+
58
+ def request_device(label: nil, required_features: [], required_limits: nil)
59
+ Device.request(self, label: label, required_features: required_features, required_limits: required_limits)
60
+ end
61
+
62
+ def request_device_async(label: nil, required_features: [], required_limits: nil)
63
+ AsyncTask.new do
64
+ request_device(
65
+ label: label,
66
+ required_features: required_features,
67
+ required_limits: required_limits
68
+ )
69
+ end
70
+ end
71
+
72
+ def info
73
+ info_struct = Native::AdapterInfo.new
74
+ Native.wgpuAdapterGetInfo(@handle, info_struct)
75
+
76
+ result = {
77
+ vendor: string_view_to_string(info_struct[:vendor]),
78
+ architecture: string_view_to_string(info_struct[:architecture]),
79
+ device: string_view_to_string(info_struct[:device]),
80
+ description: string_view_to_string(info_struct[:description]),
81
+ backend_type: info_struct[:backend_type],
82
+ adapter_type: info_struct[:adapter_type],
83
+ vendor_id: info_struct[:vendor_id],
84
+ device_id: info_struct[:device_id]
85
+ }
86
+
87
+ Native.wgpuAdapterInfoFreeMembers(info_struct)
88
+ result
89
+ end
90
+
91
+ def name
92
+ info[:device]
93
+ end
94
+
95
+ def vendor
96
+ info[:vendor]
97
+ end
98
+
99
+ def backend_type
100
+ info[:backend_type]
101
+ end
102
+
103
+ def adapter_type
104
+ info[:adapter_type]
105
+ end
106
+
107
+ def features
108
+ supported = Native::SupportedFeatures.new
109
+ Native.wgpuAdapterGetFeatures(@handle, supported)
110
+
111
+ result = []
112
+ if supported[:feature_count] > 0 && !supported[:features].null?
113
+ supported[:features].read_array_of_uint32(supported[:feature_count]).each do |f|
114
+ result << Native::FeatureName[f]
115
+ end
116
+ end
117
+ result
118
+ end
119
+
120
+ def has_feature?(feature)
121
+ features.include?(feature)
122
+ end
123
+
124
+ def limits
125
+ supported = Native::SupportedLimits.new
126
+ supported[:next_in_chain] = nil
127
+ Native.wgpuAdapterGetLimits(@handle, supported)
128
+ limits_to_hash(supported[:limits])
129
+ end
130
+
131
+ def summary
132
+ info_hash = info
133
+ "#{info_hash[:device]} (#{info_hash[:adapter_type]}) via #{info_hash[:backend_type]}"
134
+ end
135
+
136
+ def release
137
+ return if @handle.null?
138
+ Native.wgpuAdapterRelease(@handle)
139
+ @handle = FFI::Pointer::NULL
140
+ end
141
+
142
+ private
143
+
144
+ def string_view_to_string(string_view)
145
+ return "" if string_view[:data].null? || string_view[:length] == 0
146
+ string_view[:data].read_string(string_view[:length])
147
+ end
148
+
149
+ def limits_to_hash(limits)
150
+ {
151
+ max_texture_dimension_1d: limits[:max_texture_dimension_1d],
152
+ max_texture_dimension_2d: limits[:max_texture_dimension_2d],
153
+ max_texture_dimension_3d: limits[:max_texture_dimension_3d],
154
+ max_texture_array_layers: limits[:max_texture_array_layers],
155
+ max_bind_groups: limits[:max_bind_groups],
156
+ max_bind_groups_plus_vertex_buffers: limits[:max_bind_groups_plus_vertex_buffers],
157
+ max_bindings_per_bind_group: limits[:max_bindings_per_bind_group],
158
+ max_dynamic_uniform_buffers_per_pipeline_layout: limits[:max_dynamic_uniform_buffers_per_pipeline_layout],
159
+ max_dynamic_storage_buffers_per_pipeline_layout: limits[:max_dynamic_storage_buffers_per_pipeline_layout],
160
+ max_sampled_textures_per_shader_stage: limits[:max_sampled_textures_per_shader_stage],
161
+ max_samplers_per_shader_stage: limits[:max_samplers_per_shader_stage],
162
+ max_storage_buffers_per_shader_stage: limits[:max_storage_buffers_per_shader_stage],
163
+ max_storage_textures_per_shader_stage: limits[:max_storage_textures_per_shader_stage],
164
+ max_uniform_buffers_per_shader_stage: limits[:max_uniform_buffers_per_shader_stage],
165
+ max_uniform_buffer_binding_size: limits[:max_uniform_buffer_binding_size],
166
+ max_storage_buffer_binding_size: limits[:max_storage_buffer_binding_size],
167
+ min_uniform_buffer_offset_alignment: limits[:min_uniform_buffer_offset_alignment],
168
+ min_storage_buffer_offset_alignment: limits[:min_storage_buffer_offset_alignment],
169
+ max_vertex_buffers: limits[:max_vertex_buffers],
170
+ max_buffer_size: limits[:max_buffer_size],
171
+ max_vertex_attributes: limits[:max_vertex_attributes],
172
+ max_vertex_buffer_array_stride: limits[:max_vertex_buffer_array_stride],
173
+ max_inter_stage_shader_variables: limits[:max_inter_stage_shader_variables],
174
+ max_color_attachments: limits[:max_color_attachments],
175
+ max_color_attachment_bytes_per_sample: limits[:max_color_attachment_bytes_per_sample],
176
+ max_compute_workgroup_storage_size: limits[:max_compute_workgroup_storage_size],
177
+ max_compute_invocations_per_workgroup: limits[:max_compute_invocations_per_workgroup],
178
+ max_compute_workgroup_size_x: limits[:max_compute_workgroup_size_x],
179
+ max_compute_workgroup_size_y: limits[:max_compute_workgroup_size_y],
180
+ max_compute_workgroup_size_z: limits[:max_compute_workgroup_size_z],
181
+ max_compute_workgroups_per_dimension: limits[:max_compute_workgroups_per_dimension],
182
+ max_subgroup_size: limits[:max_subgroup_size]
183
+ }
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class CanvasContext
5
+ attr_reader :physical_size
6
+
7
+ def initialize(instance, present_info = {})
8
+ @instance = instance
9
+ @present_info = present_info || {}
10
+ @surface = @present_info[:surface]
11
+ @physical_size = [0, 0]
12
+ @config = nil
13
+ end
14
+
15
+ def set_physical_size(width, height)
16
+ raise ArgumentError, "width and height must be non-negative" if width.to_i.negative? || height.to_i.negative?
17
+
18
+ @physical_size = [width.to_i, height.to_i]
19
+ end
20
+
21
+ def get_preferred_format(adapter)
22
+ ensure_surface
23
+ @surface.get_preferred_format(adapter)
24
+ end
25
+
26
+ def get_configuration
27
+ @config
28
+ end
29
+
30
+ def configure(device:, format: nil, usage: :render_attachment, view_formats: [], color_space: "srgb", tone_mapping: nil, alpha_mode: :opaque, width: nil, height: nil, present_mode: :fifo)
31
+ ensure_surface
32
+ color_space # reserved for API parity
33
+ tone_mapping # reserved for API parity
34
+
35
+ width = width || @physical_size[0]
36
+ height = height || @physical_size[1]
37
+ raise SurfaceError, "Surface size must be positive before configure" if width.to_i <= 0 || height.to_i <= 0
38
+
39
+ resolved_format = format || get_preferred_format(device.adapter)
40
+ @surface.configure(
41
+ device: device,
42
+ format: resolved_format,
43
+ usage: usage,
44
+ width: width,
45
+ height: height,
46
+ present_mode: present_mode,
47
+ alpha_mode: alpha_mode,
48
+ view_formats: view_formats
49
+ )
50
+ @config = {
51
+ device: device,
52
+ format: resolved_format,
53
+ usage: usage,
54
+ view_formats: view_formats,
55
+ color_space: color_space,
56
+ tone_mapping: tone_mapping,
57
+ alpha_mode: alpha_mode,
58
+ width: width,
59
+ height: height,
60
+ present_mode: present_mode
61
+ }
62
+ end
63
+
64
+ def unconfigure
65
+ @surface&.unconfigure
66
+ @config = nil
67
+ end
68
+
69
+ def get_current_texture
70
+ raise SurfaceError, "Canvas context must be configured before get_current_texture" unless @config
71
+
72
+ @surface.current_texture
73
+ end
74
+
75
+ def present
76
+ @surface&.present
77
+ end
78
+
79
+ def release
80
+ @surface&.release
81
+ @surface = nil
82
+ @config = nil
83
+ end
84
+
85
+ private
86
+
87
+ def ensure_surface
88
+ return if @surface
89
+
90
+ @surface = case @present_info[:platform]&.to_sym
91
+ when :macos
92
+ Surface.from_metal_layer(@instance, @present_info.fetch(:layer))
93
+ when :windows
94
+ Surface.from_windows_hwnd(@instance, @present_info[:hinstance], @present_info.fetch(:hwnd))
95
+ when :x11, :linux_x11
96
+ Surface.from_xlib_window(@instance, @present_info.fetch(:display), @present_info.fetch(:window))
97
+ when :wayland, :linux_wayland
98
+ Surface.from_wayland_surface(@instance, @present_info.fetch(:display), @present_info.fetch(:surface))
99
+ else
100
+ raise SurfaceError, "Cannot build surface from present_info: #{@present_info.inspect}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Device
5
+ attr_reader :handle, :queue, :adapter
6
+
7
+ CALLBACK_MODE_WAIT_ANY_ONLY = 1
8
+ LIMIT_FIELDS = Native::Limits.members.freeze
9
+
10
+ def self.request(adapter, label: nil, required_features: [], required_limits: nil)
11
+ device_ptr = FFI::MemoryPointer.new(:pointer)
12
+ status_holder = { value: nil, message: nil }
13
+
14
+ callback = FFI::Function.new(
15
+ :void, [:uint32, :pointer, Native::StringView.by_value, :pointer]
16
+ ) do |status, device, message, _userdata|
17
+ status_holder[:value] = Native::RequestDeviceStatus[status]
18
+ if message[:data] && !message[:data].null? && message[:length] > 0
19
+ status_holder[:message] = message[:data].read_string(message[:length])
20
+ end
21
+ device_ptr.write_pointer(device)
22
+ end
23
+
24
+ queue_desc = Native::QueueDescriptor.new
25
+ queue_desc[:next_in_chain] = nil
26
+ queue_desc[:label][:data] = nil
27
+ queue_desc[:label][:length] = 0
28
+
29
+ device_lost_info = Native::DeviceLostCallbackInfo.new
30
+ device_lost_info[:next_in_chain] = nil
31
+ device_lost_info[:mode] = 0
32
+ device_lost_info[:callback] = nil
33
+ device_lost_info[:userdata] = nil
34
+
35
+ error_info = Native::UncapturedErrorCallbackInfo.new
36
+ error_info[:next_in_chain] = nil
37
+ error_info[:callback] = nil
38
+ error_info[:userdata] = nil
39
+
40
+ desc = Native::DeviceDescriptor.new
41
+ desc[:next_in_chain] = nil
42
+ if label
43
+ label_ptr = FFI::MemoryPointer.from_string(label)
44
+ desc[:label][:data] = label_ptr
45
+ desc[:label][:length] = label.bytesize
46
+ else
47
+ desc[:label][:data] = nil
48
+ desc[:label][:length] = 0
49
+ end
50
+
51
+ feature_values = normalize_required_features(required_features)
52
+ if feature_values.empty?
53
+ desc[:required_feature_count] = 0
54
+ desc[:required_features] = nil
55
+ else
56
+ features_ptr = FFI::MemoryPointer.new(:uint32, feature_values.size)
57
+ features_ptr.write_array_of_uint32(feature_values)
58
+ desc[:required_feature_count] = feature_values.size
59
+ desc[:required_features] = features_ptr
60
+ end
61
+
62
+ required_limits_struct = build_required_limits(adapter, required_limits)
63
+ desc[:required_limits] = required_limits_struct ? required_limits_struct.to_ptr : nil
64
+ desc[:default_queue] = queue_desc
65
+ desc[:device_lost_callback_info] = device_lost_info
66
+ desc[:uncaptured_error_callback_info] = error_info
67
+
68
+ callback_info = Native::RequestDeviceCallbackInfo.new
69
+ callback_info[:next_in_chain] = nil
70
+ callback_info[:mode] = CALLBACK_MODE_WAIT_ANY_ONLY
71
+ callback_info[:callback] = callback
72
+ callback_info[:userdata] = nil
73
+
74
+ Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
75
+
76
+ handle = device_ptr.read_pointer
77
+ if handle.null? || status_holder[:value] != :success
78
+ msg = status_holder[:message] || "Unknown error"
79
+ raise DeviceError, "Failed to request device: #{msg}"
80
+ end
81
+
82
+ new(handle, adapter: adapter)
83
+ end
84
+
85
+ def initialize(handle, adapter: nil)
86
+ @handle = handle
87
+ @adapter = adapter
88
+ @queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
89
+ end
90
+
91
+ def adapter_info
92
+ @adapter&.info
93
+ end
94
+
95
+ def create_buffer(label: nil, size:, usage:, mapped_at_creation: false)
96
+ Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
97
+ end
98
+
99
+ def create_shader_module(label: nil, code:, compilation_hints: [])
100
+ ShaderModule.new(self, label: label, code: code, compilation_hints: compilation_hints)
101
+ end
102
+
103
+ def create_command_encoder(label: nil)
104
+ CommandEncoder.new(self, label: label)
105
+ end
106
+
107
+ def create_bind_group_layout(label: nil, entries:)
108
+ BindGroupLayout.new(self, label: label, entries: entries)
109
+ end
110
+
111
+ def create_bind_group(label: nil, layout:, entries:)
112
+ BindGroup.new(self, label: label, layout: layout, entries: entries)
113
+ end
114
+
115
+ def create_pipeline_layout(label: nil, bind_group_layouts:)
116
+ PipelineLayout.new(self, label: label, bind_group_layouts: bind_group_layouts)
117
+ end
118
+
119
+ def create_compute_pipeline(label: nil, layout:, compute:)
120
+ ComputePipeline.new(self, label: label, layout: layout, compute: compute)
121
+ end
122
+
123
+ def create_compute_pipeline_async(label: nil, layout:, compute:)
124
+ AsyncTask.new do
125
+ create_compute_pipeline(label: label, layout: layout, compute: compute)
126
+ end
127
+ end
128
+
129
+ def create_render_pipeline(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
130
+ RenderPipeline.new(self,
131
+ label: label,
132
+ layout: layout,
133
+ vertex: vertex,
134
+ primitive: primitive,
135
+ depth_stencil: depth_stencil,
136
+ multisample: multisample,
137
+ fragment: fragment
138
+ )
139
+ end
140
+
141
+ def create_render_pipeline_async(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
142
+ AsyncTask.new do
143
+ create_render_pipeline(
144
+ label: label,
145
+ layout: layout,
146
+ vertex: vertex,
147
+ primitive: primitive,
148
+ depth_stencil: depth_stencil,
149
+ multisample: multisample,
150
+ fragment: fragment
151
+ )
152
+ end
153
+ end
154
+
155
+ def create_texture(label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
156
+ Texture.new(self,
157
+ label: label,
158
+ size: size,
159
+ format: format,
160
+ usage: usage,
161
+ dimension: dimension,
162
+ mip_level_count: mip_level_count,
163
+ sample_count: sample_count,
164
+ view_formats: view_formats
165
+ )
166
+ end
167
+
168
+ def create_sampler(label: nil, address_mode_u: :clamp_to_edge, address_mode_v: :clamp_to_edge, address_mode_w: :clamp_to_edge, mag_filter: :nearest, min_filter: :nearest, mipmap_filter: :nearest, lod_min_clamp: 0.0, lod_max_clamp: 32.0, compare: nil, max_anisotropy: 1)
169
+ Sampler.new(self,
170
+ label: label,
171
+ address_mode_u: address_mode_u,
172
+ address_mode_v: address_mode_v,
173
+ address_mode_w: address_mode_w,
174
+ mag_filter: mag_filter,
175
+ min_filter: min_filter,
176
+ mipmap_filter: mipmap_filter,
177
+ lod_min_clamp: lod_min_clamp,
178
+ lod_max_clamp: lod_max_clamp,
179
+ compare: compare,
180
+ max_anisotropy: max_anisotropy
181
+ )
182
+ end
183
+
184
+ def create_buffer_with_data(label: nil, data:, usage:)
185
+ data_ptr, byte_size = data_to_pointer(data)
186
+ buffer = create_buffer(
187
+ label: label,
188
+ size: byte_size,
189
+ usage: usage,
190
+ mapped_at_creation: true
191
+ )
192
+ buffer.mapped_range.write_bytes(data_ptr.read_bytes(byte_size))
193
+ buffer.unmap
194
+ buffer
195
+ end
196
+
197
+ def create_query_set(label: nil, type:, count:)
198
+ QuerySet.new(self, label: label, type: type, count: count)
199
+ end
200
+
201
+ def create_render_bundle_encoder(color_formats:, depth_stencil_format: nil, sample_count: 1,
202
+ depth_read_only: false, stencil_read_only: false, label: nil)
203
+ RenderBundleEncoder.new(self,
204
+ color_formats: color_formats,
205
+ depth_stencil_format: depth_stencil_format,
206
+ sample_count: sample_count,
207
+ depth_read_only: depth_read_only,
208
+ stencil_read_only: stencil_read_only,
209
+ label: label
210
+ )
211
+ end
212
+
213
+ def features
214
+ supported = Native::SupportedFeatures.new
215
+ Native.wgpuDeviceGetFeatures(@handle, supported)
216
+
217
+ result = []
218
+ if supported[:feature_count] > 0 && !supported[:features].null?
219
+ supported[:features].read_array_of_uint32(supported[:feature_count]).each do |f|
220
+ result << Native::FeatureName[f]
221
+ end
222
+ end
223
+ result
224
+ end
225
+
226
+ def has_feature?(feature)
227
+ features.include?(feature)
228
+ end
229
+
230
+ def limits
231
+ supported = Native::SupportedLimits.new
232
+ supported[:next_in_chain] = nil
233
+ Native.wgpuDeviceGetLimits(@handle, supported)
234
+ limits_to_hash(supported[:limits])
235
+ end
236
+
237
+ def poll(wait: false)
238
+ Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
239
+ end
240
+
241
+ def push_error_scope(filter = :validation)
242
+ Native.wgpuDevicePushErrorScope(@handle, filter)
243
+ end
244
+
245
+ def pop_error_scope
246
+ error_holder = { type: nil, message: nil }
247
+
248
+ callback = FFI::Function.new(
249
+ :void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
250
+ ) do |_status, error_type, message, _userdata1, _userdata2|
251
+ error_holder[:type] = Native::ErrorType[error_type]
252
+ if message[:data] && !message[:data].null? && message[:length] > 0
253
+ error_holder[:message] = message[:data].read_string(message[:length])
254
+ end
255
+ end
256
+
257
+ callback_info = Native::PopErrorScopeCallbackInfo.new
258
+ callback_info[:next_in_chain] = nil
259
+ callback_info[:mode] = 1
260
+ callback_info[:callback] = callback
261
+ callback_info[:userdata1] = nil
262
+ callback_info[:userdata2] = nil
263
+
264
+ Native.wgpuDevicePopErrorScope(@handle, callback_info)
265
+
266
+ error_holder
267
+ end
268
+
269
+ def pop_error_scope_async
270
+ AsyncTask.new { pop_error_scope }
271
+ end
272
+
273
+ def with_error_scope(filter = :validation)
274
+ push_error_scope(filter)
275
+ result = yield
276
+ error = pop_error_scope
277
+ if error[:type] && error[:type] != :no_error
278
+ raise Error, "GPU error (#{error[:type]}): #{error[:message]}"
279
+ end
280
+ result
281
+ end
282
+
283
+ def destroy
284
+ return if @handle.null?
285
+ Native.wgpuDeviceDestroy(@handle)
286
+ end
287
+
288
+ def release
289
+ @queue&.release
290
+ return if @handle.null?
291
+ Native.wgpuDeviceRelease(@handle)
292
+ @handle = FFI::Pointer::NULL
293
+ end
294
+
295
+ private
296
+
297
+ def self.normalize_required_features(required_features)
298
+ Array(required_features).map do |feature|
299
+ normalize_feature_name(feature)
300
+ end
301
+ end
302
+
303
+ def self.normalize_feature_name(feature)
304
+ return feature if feature.is_a?(Integer)
305
+
306
+ key = feature.to_s.strip.tr("-", "_").to_sym
307
+ value = Native::FeatureName[key]
308
+ raise ArgumentError, "Unknown feature name: #{feature}" if value.nil?
309
+
310
+ value
311
+ end
312
+
313
+ def self.build_required_limits(adapter, required_limits)
314
+ return nil if required_limits.nil?
315
+ raise ArgumentError, "required_limits must be a Hash" unless required_limits.is_a?(Hash)
316
+
317
+ resolved_limits = adapter.limits.dup
318
+ required_limits.each do |name, value|
319
+ key = canonical_limit_key(name)
320
+ resolved_limits[key] = value unless value.nil?
321
+ end
322
+
323
+ required = Native::RequiredLimits.new
324
+ required[:next_in_chain] = nil
325
+ LIMIT_FIELDS.each do |field|
326
+ required[:limits][field] = resolved_limits[field] || 0
327
+ end
328
+ required
329
+ end
330
+
331
+ def self.canonical_limit_key(name)
332
+ normalized = name.to_s
333
+ .gsub(/([A-Z])/, "_\\1")
334
+ .downcase
335
+ .tr("-", "_")
336
+ .sub(/^_/, "")
337
+ .to_sym
338
+ return normalized if LIMIT_FIELDS.include?(normalized)
339
+
340
+ raise ArgumentError, "Unknown limit key: #{name}"
341
+ end
342
+
343
+ def data_to_pointer(data)
344
+ case data
345
+ when String
346
+ ptr = FFI::MemoryPointer.new(:char, data.bytesize)
347
+ ptr.put_bytes(0, data)
348
+ [ptr, data.bytesize]
349
+ when Array
350
+ ptr = FFI::MemoryPointer.new(:float, data.size)
351
+ ptr.write_array_of_float(data)
352
+ [ptr, data.size * 4]
353
+ when FFI::Pointer
354
+ [data, data.size]
355
+ else
356
+ raise ArgumentError, "Unsupported data type: #{data.class}"
357
+ end
358
+ end
359
+
360
+ def limits_to_hash(limits)
361
+ {
362
+ max_texture_dimension_1d: limits[:max_texture_dimension_1d],
363
+ max_texture_dimension_2d: limits[:max_texture_dimension_2d],
364
+ max_texture_dimension_3d: limits[:max_texture_dimension_3d],
365
+ max_texture_array_layers: limits[:max_texture_array_layers],
366
+ max_bind_groups: limits[:max_bind_groups],
367
+ max_bind_groups_plus_vertex_buffers: limits[:max_bind_groups_plus_vertex_buffers],
368
+ max_bindings_per_bind_group: limits[:max_bindings_per_bind_group],
369
+ max_dynamic_uniform_buffers_per_pipeline_layout: limits[:max_dynamic_uniform_buffers_per_pipeline_layout],
370
+ max_dynamic_storage_buffers_per_pipeline_layout: limits[:max_dynamic_storage_buffers_per_pipeline_layout],
371
+ max_sampled_textures_per_shader_stage: limits[:max_sampled_textures_per_shader_stage],
372
+ max_samplers_per_shader_stage: limits[:max_samplers_per_shader_stage],
373
+ max_storage_buffers_per_shader_stage: limits[:max_storage_buffers_per_shader_stage],
374
+ max_storage_textures_per_shader_stage: limits[:max_storage_textures_per_shader_stage],
375
+ max_uniform_buffers_per_shader_stage: limits[:max_uniform_buffers_per_shader_stage],
376
+ max_uniform_buffer_binding_size: limits[:max_uniform_buffer_binding_size],
377
+ max_storage_buffer_binding_size: limits[:max_storage_buffer_binding_size],
378
+ min_uniform_buffer_offset_alignment: limits[:min_uniform_buffer_offset_alignment],
379
+ min_storage_buffer_offset_alignment: limits[:min_storage_buffer_offset_alignment],
380
+ max_vertex_buffers: limits[:max_vertex_buffers],
381
+ max_buffer_size: limits[:max_buffer_size],
382
+ max_vertex_attributes: limits[:max_vertex_attributes],
383
+ max_vertex_buffer_array_stride: limits[:max_vertex_buffer_array_stride],
384
+ max_inter_stage_shader_variables: limits[:max_inter_stage_shader_variables],
385
+ max_color_attachments: limits[:max_color_attachments],
386
+ max_color_attachment_bytes_per_sample: limits[:max_color_attachment_bytes_per_sample],
387
+ max_compute_workgroup_storage_size: limits[:max_compute_workgroup_storage_size],
388
+ max_compute_invocations_per_workgroup: limits[:max_compute_invocations_per_workgroup],
389
+ max_compute_workgroup_size_x: limits[:max_compute_workgroup_size_x],
390
+ max_compute_workgroup_size_y: limits[:max_compute_workgroup_size_y],
391
+ max_compute_workgroup_size_z: limits[:max_compute_workgroup_size_z],
392
+ max_compute_workgroups_per_dimension: limits[:max_compute_workgroups_per_dimension],
393
+ max_subgroup_size: limits[:max_subgroup_size]
394
+ }
395
+ end
396
+ end
397
+ end