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,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Instance
5
+ attr_reader :handle
6
+
7
+ def initialize
8
+ desc = Native::InstanceDescriptor.new
9
+ desc[:next_in_chain] = nil
10
+ desc[:features][:next_in_chain] = nil
11
+ desc[:features][:timed_wait_any_enable] = 0
12
+ desc[:features][:timed_wait_any_max_count] = 0
13
+
14
+ @handle = Native.wgpuCreateInstance(desc)
15
+ raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
16
+ end
17
+
18
+ def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
19
+ Adapter.request(
20
+ self,
21
+ power_preference: power_preference,
22
+ backend: backend,
23
+ feature_level: feature_level,
24
+ force_fallback_adapter: force_fallback_adapter,
25
+ compatible_surface: compatible_surface
26
+ )
27
+ end
28
+
29
+ def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
30
+ AsyncTask.new do
31
+ request_adapter(
32
+ power_preference: power_preference,
33
+ backend: backend,
34
+ feature_level: feature_level,
35
+ force_fallback_adapter: force_fallback_adapter,
36
+ compatible_surface: compatible_surface
37
+ )
38
+ end
39
+ end
40
+
41
+ def enumerate_adapters(backends: nil)
42
+ options = nil
43
+ if backends
44
+ options = Native::InstanceEnumerateAdapterOptions.new
45
+ options[:next_in_chain] = nil
46
+ options[:backends] = backends
47
+ end
48
+
49
+ count = Native.wgpuInstanceEnumerateAdapters(@handle, options, nil)
50
+ return [] if count == 0
51
+
52
+ adapters_ptr = FFI::MemoryPointer.new(:pointer, count)
53
+ Native.wgpuInstanceEnumerateAdapters(@handle, options, adapters_ptr)
54
+
55
+ adapters_ptr.read_array_of_pointer(count).map do |ptr|
56
+ Adapter.from_handle(ptr)
57
+ end
58
+ end
59
+
60
+ def enumerate_adapters_async(backends: nil)
61
+ AsyncTask.new do
62
+ enumerate_adapters(backends: backends)
63
+ end
64
+ end
65
+
66
+ def get_canvas_context(present_info)
67
+ CanvasContext.new(self, present_info)
68
+ end
69
+
70
+ def process_events
71
+ Native.wgpuInstanceProcessEvents(@handle)
72
+ end
73
+
74
+ def release
75
+ return if @handle.null?
76
+ Native.wgpuInstanceRelease(@handle)
77
+ @handle = FFI::Pointer::NULL
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Queue
5
+ attr_reader :handle
6
+
7
+ def initialize(handle, device: nil)
8
+ @handle = handle
9
+ @device = device
10
+ end
11
+
12
+ def submit(command_buffers)
13
+ buffers = Array(command_buffers)
14
+ return if buffers.empty?
15
+
16
+ handles = buffers.map(&:handle)
17
+ ptr = FFI::MemoryPointer.new(:pointer, handles.size)
18
+ ptr.write_array_of_pointer(handles)
19
+
20
+ Native.wgpuQueueSubmit(@handle, handles.size, ptr)
21
+ end
22
+
23
+ def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil)
24
+ data_ptr, byte_size = data_to_pointer(data)
25
+ data_offset = Integer(data_offset)
26
+ raise ArgumentError, "data_offset must be non-negative" if data_offset.negative?
27
+ raise ArgumentError, "data_offset out of range" if data_offset > byte_size
28
+
29
+ write_size = size.nil? ? (byte_size - data_offset) : Integer(size)
30
+ raise ArgumentError, "size must be non-negative" if write_size.negative?
31
+ raise ArgumentError, "data_offset + size out of range" if data_offset + write_size > byte_size
32
+
33
+ Native.wgpuQueueWriteBuffer(
34
+ @handle,
35
+ buffer.handle,
36
+ buffer_offset,
37
+ data_ptr + data_offset,
38
+ write_size
39
+ )
40
+ end
41
+
42
+ def write_texture(destination:, data:, data_layout:, size:)
43
+ data_ptr, byte_size = data_to_pointer(data)
44
+
45
+ dst = Native::ImageCopyTexture.new
46
+ dst[:texture] = destination[:texture].handle
47
+ dst[:mip_level] = destination[:mip_level] || 0
48
+ dst[:origin][:x] = destination.dig(:origin, :x) || 0
49
+ dst[:origin][:y] = destination.dig(:origin, :y) || 0
50
+ dst[:origin][:z] = destination.dig(:origin, :z) || 0
51
+ dst[:aspect] = destination[:aspect] || :all
52
+
53
+ extent = Native::Extent3D.new
54
+ if size.is_a?(Array)
55
+ extent[:width] = size[0]
56
+ extent[:height] = size[1] || 1
57
+ extent[:depth_or_array_layers] = size[2] || 1
58
+ else
59
+ extent[:width] = size[:width]
60
+ extent[:height] = size[:height] || 1
61
+ extent[:depth_or_array_layers] = size[:depth_or_array_layers] || 1
62
+ end
63
+
64
+ layout = Native::TextureDataLayout.new
65
+ layout[:offset] = data_layout[:offset] || 0
66
+ layout[:bytes_per_row] = data_layout[:bytes_per_row]
67
+ layout[:rows_per_image] = data_layout[:rows_per_image] || extent[:height]
68
+
69
+ Native.wgpuQueueWriteTexture(@handle, dst, data_ptr, byte_size, layout, extent)
70
+ end
71
+
72
+ def read_buffer(buffer, offset: 0, size: nil, device:)
73
+ size ||= buffer.size - offset
74
+
75
+ staging = Buffer.new(device,
76
+ size: size,
77
+ usage: [:map_read, :copy_dst]
78
+ )
79
+
80
+ encoder = CommandEncoder.new(device)
81
+ encoder.copy_buffer_to_buffer(
82
+ source: buffer,
83
+ source_offset: offset,
84
+ destination: staging,
85
+ destination_offset: 0,
86
+ size: size
87
+ )
88
+ command_buffer = encoder.finish
89
+ submit([command_buffer])
90
+
91
+ staging.map_sync(:read)
92
+ data = staging.read_mapped_data
93
+ staging.unmap
94
+ staging.release
95
+
96
+ data
97
+ end
98
+
99
+ def read_texture(source:, data_layout:, size:, device:)
100
+ width = size[:width] || size[0]
101
+ height = size[:height] || size[1] || 1
102
+ depth = size[:depth_or_array_layers] || size[2] || 1
103
+ bytes_per_row = data_layout[:bytes_per_row]
104
+ rows_per_image = data_layout[:rows_per_image] || height
105
+ buffer_size = bytes_per_row * rows_per_image * depth
106
+
107
+ staging = Buffer.new(device,
108
+ size: buffer_size,
109
+ usage: [:map_read, :copy_dst]
110
+ )
111
+
112
+ encoder = CommandEncoder.new(device)
113
+ encoder.copy_texture_to_buffer(
114
+ source: source,
115
+ destination: {
116
+ buffer: staging,
117
+ offset: 0,
118
+ bytes_per_row: bytes_per_row,
119
+ rows_per_image: rows_per_image
120
+ },
121
+ copy_size: size
122
+ )
123
+ command_buffer = encoder.finish
124
+ submit([command_buffer])
125
+
126
+ staging.map_sync(:read)
127
+ data = staging.read_mapped_data
128
+ staging.unmap
129
+ staging.release
130
+
131
+ data
132
+ end
133
+
134
+ def on_submitted_work_done(device: nil)
135
+ device ||= @device
136
+ status_holder = { done: false, status: nil }
137
+
138
+ callback = FFI::Function.new(
139
+ :void, [:uint32, :pointer, :pointer]
140
+ ) do |status, _userdata1, _userdata2|
141
+ status_holder[:done] = true
142
+ status_holder[:status] = Native::QueueWorkDoneStatus[status]
143
+ end
144
+
145
+ callback_info = Native::QueueWorkDoneCallbackInfo.new
146
+ callback_info[:next_in_chain] = nil
147
+ callback_info[:mode] = 1
148
+ callback_info[:callback] = callback
149
+ callback_info[:userdata1] = nil
150
+ callback_info[:userdata2] = nil
151
+
152
+ Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
153
+
154
+ if device
155
+ until status_holder[:done]
156
+ Native.wgpuDevicePoll(device.handle, 0, nil)
157
+ sleep(0.001)
158
+ end
159
+ else
160
+ sleep(0.001) until status_holder[:done]
161
+ end
162
+
163
+ status_holder[:status]
164
+ end
165
+
166
+ def on_submitted_work_done_async(device: nil)
167
+ AsyncTask.new do
168
+ on_submitted_work_done(device: device)
169
+ end
170
+ end
171
+
172
+ def release
173
+ return if @handle.null?
174
+ Native.wgpuQueueRelease(@handle)
175
+ @handle = FFI::Pointer::NULL
176
+ end
177
+
178
+ private
179
+
180
+ def data_to_pointer(data)
181
+ case data
182
+ when String
183
+ ptr = FFI::MemoryPointer.new(:char, data.bytesize)
184
+ ptr.put_bytes(0, data)
185
+ [ptr, data.bytesize]
186
+ when Array
187
+ ptr = FFI::MemoryPointer.new(:float, data.size)
188
+ ptr.write_array_of_float(data)
189
+ [ptr, data.size * 4]
190
+ when FFI::Pointer
191
+ [data, data.size]
192
+ else
193
+ raise ArgumentError, "Unsupported data type: #{data.class}"
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Surface
5
+ attr_reader :handle
6
+
7
+ def self.from_metal_layer(instance, layer)
8
+ source = Native::SurfaceSourceMetalLayer.new
9
+ source[:chain][:next] = nil
10
+ source[:chain][:s_type] = Native::SType[:surface_source_metal_layer]
11
+ source[:layer] = layer
12
+
13
+ desc = Native::SurfaceDescriptor.new
14
+ desc[:next_in_chain] = source.to_ptr
15
+ desc[:label][:data] = nil
16
+ desc[:label][:length] = 0
17
+
18
+ handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
19
+ raise SurfaceError, "Failed to create surface from Metal layer" if handle.null?
20
+
21
+ new(handle, instance)
22
+ end
23
+
24
+ def self.from_windows_hwnd(instance, hinstance, hwnd)
25
+ source = Native::SurfaceSourceWindowsHWND.new
26
+ source[:chain][:next] = nil
27
+ source[:chain][:s_type] = Native::SType[:surface_source_windows_hwnd]
28
+ source[:hinstance] = hinstance
29
+ source[:hwnd] = hwnd
30
+
31
+ desc = Native::SurfaceDescriptor.new
32
+ desc[:next_in_chain] = source.to_ptr
33
+ desc[:label][:data] = nil
34
+ desc[:label][:length] = 0
35
+
36
+ handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
37
+ raise SurfaceError, "Failed to create surface from Windows HWND" if handle.null?
38
+
39
+ new(handle, instance)
40
+ end
41
+
42
+ def self.from_xlib_window(instance, display, window)
43
+ source = Native::SurfaceSourceXlibWindow.new
44
+ source[:chain][:next] = nil
45
+ source[:chain][:s_type] = Native::SType[:surface_source_xlib_window]
46
+ source[:display] = display
47
+ source[:window] = window
48
+
49
+ desc = Native::SurfaceDescriptor.new
50
+ desc[:next_in_chain] = source.to_ptr
51
+ desc[:label][:data] = nil
52
+ desc[:label][:length] = 0
53
+
54
+ handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
55
+ raise SurfaceError, "Failed to create surface from Xlib window" if handle.null?
56
+
57
+ new(handle, instance)
58
+ end
59
+
60
+ def self.from_wayland_surface(instance, display, surface)
61
+ source = Native::SurfaceSourceWaylandSurface.new
62
+ source[:chain][:next] = nil
63
+ source[:chain][:s_type] = Native::SType[:surface_source_wayland_surface]
64
+ source[:display] = display
65
+ source[:surface] = surface
66
+
67
+ desc = Native::SurfaceDescriptor.new
68
+ desc[:next_in_chain] = source.to_ptr
69
+ desc[:label][:data] = nil
70
+ desc[:label][:length] = 0
71
+
72
+ handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
73
+ raise SurfaceError, "Failed to create surface from Wayland surface" if handle.null?
74
+
75
+ new(handle, instance)
76
+ end
77
+
78
+ def initialize(handle, instance)
79
+ @handle = handle
80
+ @instance = instance
81
+ @configured = false
82
+ @config = nil
83
+ end
84
+
85
+ def configure(device:, format:, usage: :render_attachment, width:, height:, present_mode: :fifo, alpha_mode: :auto, view_formats: [])
86
+ config = Native::SurfaceConfiguration.new
87
+ config[:next_in_chain] = nil
88
+ config[:device] = device.handle
89
+ config[:format] = format
90
+ config[:usage] = normalize_usage(usage)
91
+ config[:width] = width
92
+ config[:height] = height
93
+ config[:view_format_count] = view_formats.size
94
+ if view_formats.empty?
95
+ @view_formats_ptr = nil
96
+ config[:view_formats] = nil
97
+ else
98
+ format_values = view_formats.map do |vf|
99
+ vf.is_a?(Integer) ? vf : Native::TextureFormat[vf]
100
+ end
101
+ @view_formats_ptr = FFI::MemoryPointer.new(:uint32, format_values.size)
102
+ @view_formats_ptr.write_array_of_uint32(format_values)
103
+ config[:view_formats] = @view_formats_ptr
104
+ end
105
+ config[:alpha_mode] = Native::CompositeAlphaMode[alpha_mode]
106
+ config[:present_mode] = Native::PresentMode[present_mode]
107
+
108
+ Native.wgpuSurfaceConfigure(@handle, config)
109
+ @configured = true
110
+ @device = device
111
+ @config = {
112
+ device: device,
113
+ format: format,
114
+ usage: usage,
115
+ width: width,
116
+ height: height,
117
+ present_mode: present_mode,
118
+ alpha_mode: alpha_mode,
119
+ view_formats: view_formats
120
+ }
121
+ end
122
+
123
+ def unconfigure
124
+ Native.wgpuSurfaceUnconfigure(@handle)
125
+ @configured = false
126
+ @config = nil
127
+ end
128
+
129
+ def current_texture
130
+ raise SurfaceError, "Surface is not configured" unless @configured
131
+
132
+ surface_texture = Native::SurfaceTexture.new
133
+ Native.wgpuSurfaceGetCurrentTexture(@handle, surface_texture)
134
+
135
+ status = Native::SurfaceGetCurrentTextureStatus[surface_texture[:status]]
136
+ unless status == :success_optimal || status == :success_suboptimal
137
+ raise SurfaceError, "Failed to get current texture: #{status}"
138
+ end
139
+
140
+ texture_ptr = surface_texture[:texture]
141
+ if texture_ptr.nil? || texture_ptr.null?
142
+ raise SurfaceError, "Surface returned null texture"
143
+ end
144
+
145
+ Texture.from_handle(texture_ptr)
146
+ end
147
+
148
+ def get_current_texture
149
+ current_texture
150
+ end
151
+
152
+ def present
153
+ Native.wgpuSurfacePresent(@handle)
154
+ end
155
+
156
+ def get_configuration
157
+ @config
158
+ end
159
+
160
+ def get_preferred_format(adapter)
161
+ caps = capabilities(adapter)
162
+ caps[:formats].first || :bgra8_unorm
163
+ end
164
+
165
+ def capabilities(adapter)
166
+ caps = Native::SurfaceCapabilities.new
167
+ Native.wgpuSurfaceGetCapabilities(@handle, adapter.handle, caps)
168
+
169
+ formats = []
170
+ if caps[:format_count] > 0 && !caps[:formats].null?
171
+ formats = caps[:formats].read_array_of_uint32(caps[:format_count]).map do |f|
172
+ Native::TextureFormat[f]
173
+ end
174
+ end
175
+
176
+ present_modes = []
177
+ if caps[:present_mode_count] > 0 && !caps[:present_modes].null?
178
+ present_modes = caps[:present_modes].read_array_of_uint32(caps[:present_mode_count]).map do |m|
179
+ Native::PresentMode[m]
180
+ end
181
+ end
182
+
183
+ alpha_modes = []
184
+ if caps[:alpha_mode_count] > 0 && !caps[:alpha_modes].null?
185
+ alpha_modes = caps[:alpha_modes].read_array_of_uint32(caps[:alpha_mode_count]).map do |a|
186
+ Native::CompositeAlphaMode[a]
187
+ end
188
+ end
189
+
190
+ {
191
+ formats: formats,
192
+ present_modes: present_modes,
193
+ alpha_modes: alpha_modes,
194
+ usages: caps[:usages]
195
+ }
196
+ ensure
197
+ Native.wgpuSurfaceCapabilitiesFreeMembers(caps) if caps
198
+ end
199
+
200
+ def release
201
+ return if @handle.null?
202
+ Native.wgpuSurfaceRelease(@handle)
203
+ @handle = FFI::Pointer::NULL
204
+ end
205
+
206
+ private
207
+
208
+ def normalize_usage(usage)
209
+ case usage
210
+ when Integer
211
+ usage
212
+ when Symbol
213
+ Native::TextureUsage[usage]
214
+ when Array
215
+ usage.reduce(0) { |acc, u| acc | Native::TextureUsage[u] }
216
+ else
217
+ raise ArgumentError, "Invalid usage: #{usage}"
218
+ end
219
+ end
220
+ end
221
+ end
data/lib/wgpu/error.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class Error < StandardError; end
5
+ class InitializationError < Error; end
6
+ class AdapterError < Error; end
7
+ class DeviceError < Error; end
8
+ class BufferError < Error; end
9
+ class TextureError < Error; end
10
+ class ResourceError < Error; end
11
+ class ShaderError < Error; end
12
+ class PipelineError < Error; end
13
+ class CommandError < Error; end
14
+ class SurfaceError < Error; end
15
+ class RenderBundleError < Error; end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module Native
5
+ callback :request_adapter_callback,
6
+ [RequestAdapterStatus, :pointer, StringView.by_value, :pointer], :void
7
+
8
+ callback :request_device_callback,
9
+ [RequestDeviceStatus, :pointer, StringView.by_value, :pointer], :void
10
+
11
+ callback :buffer_map_callback,
12
+ [MapAsyncStatus, :pointer], :void
13
+
14
+ callback :error_callback,
15
+ [:uint32, StringView.by_value, :pointer], :void
16
+
17
+ callback :device_lost_callback,
18
+ [:pointer, :uint32, StringView.by_value, :pointer], :void
19
+
20
+ callback :pop_error_scope_callback,
21
+ [PopErrorScopeStatus, ErrorType, StringView.by_value, :pointer, :pointer], :void
22
+
23
+ callback :queue_work_done_callback,
24
+ [QueueWorkDoneStatus, :pointer, :pointer], :void
25
+ end
26
+ end