wgpu 1.1.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 (63) 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 +36 -11
  28. data/lib/wgpu/core/async_waiter.rb +32 -4
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +138 -42
  31. data/lib/wgpu/core/instance.rb +8 -4
  32. data/lib/wgpu/core/queue.rb +80 -49
  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 +4 -1
  40. data/lib/wgpu/native/capabilities.rb +18 -2
  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 +60 -8
  44. data/lib/wgpu/native/functions.rb +10 -10
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +39 -21
  47. data/lib/wgpu/native_resource.rb +171 -0
  48. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  49. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  50. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  51. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  52. data/lib/wgpu/pipeline/shader_module.rb +50 -8
  53. data/lib/wgpu/resources/buffer.rb +148 -73
  54. data/lib/wgpu/resources/query_set.rb +14 -12
  55. data/lib/wgpu/resources/sampler.rb +36 -20
  56. data/lib/wgpu/resources/texture.rb +44 -42
  57. data/lib/wgpu/resources/texture_view.rb +11 -3
  58. data/lib/wgpu/texture_format.rb +82 -0
  59. data/lib/wgpu/version.rb +1 -1
  60. data/lib/wgpu/window.rb +8 -1
  61. data/lib/wgpu.rb +32 -0
  62. data/sig/wgpu.rbs +381 -0
  63. metadata +31 -16
@@ -7,36 +7,38 @@ module WGPU
7
7
  def initialize(device, label: nil, size:, usage:, mapped_at_creation: false)
8
8
  @device = device
9
9
  @size = size
10
- @usage = normalize_usage(usage)
10
+ @usage =
11
+ begin
12
+ normalize_usage(usage)
13
+ rescue ArgumentError => e
14
+ raise BufferError, buffer_error_message(e.message, label)
15
+ end
11
16
  @mapped = mapped_at_creation
12
- @map_callbacks = []
17
+ @map_state = mapped_at_creation ? :mapped : :unmapped
13
18
 
14
- desc = Native::BufferDescriptor.new
15
- desc[:next_in_chain] = nil
16
- if label
17
- label_ptr = FFI::MemoryPointer.from_string(label)
18
- desc[:label][:data] = label_ptr
19
- desc[:label][:length] = label.bytesize
20
- else
21
- desc[:label][:data] = nil
22
- desc[:label][:length] = 0
23
- end
24
- desc[:usage] = @usage
25
- desc[:size] = size
26
- desc[:mapped_at_creation] = mapped_at_creation ? 1 : 0
19
+ desc, keepalive = build_descriptor(
20
+ label:,
21
+ size:,
22
+ usage: @usage,
23
+ mapped_at_creation:
24
+ )
25
+ @descriptor_keepalive = keepalive
27
26
 
28
27
  device.push_error_scope(:validation)
29
28
  @handle = Native.wgpuDeviceCreateBuffer(device.handle, desc)
30
29
  error = device.pop_error_scope
30
+ @descriptor_keepalive = nil
31
31
 
32
32
  if @handle.null? || (error[:type] && error[:type] != :no_error)
33
33
  msg = error[:message] || "Failed to create buffer"
34
- raise BufferError, msg
34
+ raise BufferError, buffer_error_message(msg, label)
35
35
  end
36
36
  end
37
37
 
38
- def write(data, offset: 0)
39
- ptr, byte_size = data_to_pointer(data)
38
+ def write(data, offset: 0, type: :f32)
39
+ ptr, byte_size = DataTypes.to_pointer(data, type:)
40
+ DataTypes.validate_alignment!(offset, 4, name: "offset")
41
+ DataTypes.validate_alignment!(byte_size, 4, name: "data size")
40
42
  Native.wgpuQueueWriteBuffer(@device.queue.handle, @handle, offset, ptr, byte_size)
41
43
  end
42
44
 
@@ -44,6 +46,7 @@ module WGPU
44
46
  raise BufferError, "Buffer is not mapped" unless @mapped
45
47
 
46
48
  size ||= @size - offset
49
+ validate_map_range!(offset, size)
47
50
  ptr = Native.wgpuBufferGetMappedRange(@handle, offset, size)
48
51
  raise BufferError, "Failed to get mapped range" if ptr.null?
49
52
 
@@ -57,23 +60,24 @@ module WGPU
57
60
  def unmap
58
61
  Native.wgpuBufferUnmap(@handle)
59
62
  @mapped = false
63
+ @map_state = :unmapped
60
64
  end
61
65
 
62
- def map_sync(mode, offset: 0, size: nil)
63
- status_holder, callback, future = begin_map_request(mode, offset: offset, size: size)
64
- wait_for_map(status_holder, future)
66
+ def map_sync(mode, offset: 0, size: nil, timeout: nil)
67
+ status_holder, callback_token, future = begin_map_request(mode, offset: offset, size: size)
68
+ wait_for_map(status_holder, future, timeout:)
65
69
  finalize_map(status_holder)
66
70
  ensure
67
- @map_callbacks.delete(callback) if callback
71
+ CallbackKeepalive.release(self, callback_token)
68
72
  end
69
73
 
70
74
  def map_async(mode, offset: 0, size: nil)
71
- status_holder, callback, future = begin_map_request(mode, offset: offset, size: size)
75
+ status_holder, callback_token, future = begin_map_request(mode, offset: offset, size: size)
72
76
  AsyncTask.new do
73
77
  wait_for_map(status_holder, future)
74
78
  finalize_map(status_holder)
75
79
  ensure
76
- @map_callbacks.delete(callback)
80
+ CallbackKeepalive.release(self, callback_token)
77
81
  end
78
82
  end
79
83
 
@@ -81,20 +85,23 @@ module WGPU
81
85
  raise BufferError, "Buffer is not mapped" unless @mapped
82
86
 
83
87
  size ||= @size - offset
88
+ validate_map_range!(offset, size)
84
89
  ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
85
90
  raise BufferError, "Failed to get mapped range" if ptr.null?
86
91
 
87
92
  ptr.read_bytes(size)
88
93
  end
89
94
 
90
- def read_mapped(offset: 0, size: nil)
91
- read_mapped_data(offset: offset, size: size)
95
+ def read_mapped(offset: 0, size: nil, type: nil)
96
+ bytes = read_mapped_data(offset: offset, size: size)
97
+ type ? DataTypes.unpack(bytes, type:) : bytes
92
98
  end
93
99
 
94
- def write_mapped(data, offset: 0)
100
+ def write_mapped(data, offset: 0, type: :f32)
95
101
  raise BufferError, "Buffer is not mapped" unless @mapped
96
102
 
97
- ptr, byte_size = data_to_pointer(data)
103
+ ptr, byte_size = DataTypes.to_pointer(data, type:)
104
+ validate_map_range!(offset, byte_size)
98
105
  target = Native.wgpuBufferGetMappedRange(@handle, offset, byte_size)
99
106
  raise BufferError, "Failed to get mapped range" if target.null?
100
107
 
@@ -102,18 +109,39 @@ module WGPU
102
109
  end
103
110
 
104
111
  def read_mapped_floats(offset: 0, count: nil)
105
- raise BufferError, "Buffer is not mapped" unless @mapped
112
+ read_mapped_values(type: :f32, offset:, count:)
113
+ end
106
114
 
107
- size = count ? count * 4 : @size - offset
108
- ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
109
- raise BufferError, "Failed to get mapped range" if ptr.null?
115
+ def read_mapped_uint32s(offset: 0, count: nil)
116
+ read_mapped_values(type: :u32, offset:, count:)
117
+ end
118
+
119
+ def read_mapped_int32s(offset: 0, count: nil)
120
+ read_mapped_values(type: :i32, offset:, count:)
121
+ end
110
122
 
111
- ptr.read_array_of_float(size / 4)
123
+ def read_mapped_float64s(offset: 0, count: nil)
124
+ read_mapped_values(type: :f64, offset:, count:)
125
+ end
126
+
127
+ def read_mapped_uint16s(offset: 0, count: nil)
128
+ read_mapped_values(type: :u16, offset:, count:)
129
+ end
130
+
131
+ def read_mapped_uint8s(offset: 0, count: nil)
132
+ read_mapped_values(type: :u8, offset:, count:)
133
+ end
134
+
135
+ def read_mapped_values(type: :f32, offset: 0, count: nil)
136
+ element_size = DataTypes.byte_size(type)
137
+ size = count ? count * element_size : @size - offset
138
+ DataTypes.unpack(read_mapped_data(offset:, size:), type:)
112
139
  end
113
140
 
114
141
  def map_state
115
- state = Native.wgpuBufferGetMapState(@handle)
116
- state || (@mapped ? :mapped : :unmapped)
142
+ return @map_state unless Native.buffer_map_state_available?
143
+
144
+ Native.wgpuBufferGetMapState(@handle) || @map_state
117
145
  end
118
146
 
119
147
  def destroy
@@ -128,14 +156,21 @@ module WGPU
128
156
 
129
157
  private
130
158
 
159
+ def build_descriptor(label:, size:, usage:, mapped_at_creation:)
160
+ keepalive = []
161
+ desc = Native::BufferDescriptor.new
162
+ desc[:next_in_chain] = nil
163
+ DescriptorHelpers.set_label(desc, label, keepalive:)
164
+ desc[:usage] = usage
165
+ desc[:size] = size
166
+ desc[:mapped_at_creation] = mapped_at_creation ? 1 : 0
167
+ [desc, keepalive]
168
+ end
169
+
131
170
  def begin_map_request(mode, offset:, size:)
132
171
  size ||= @size - offset
133
- mode_flag = case mode
134
- when :read then Native::MapMode[:read]
135
- when :write then Native::MapMode[:write]
136
- when Integer then mode
137
- else raise ArgumentError, "Invalid map mode: #{mode}"
138
- end
172
+ validate_map_range!(offset, size)
173
+ mode_flag = Native::EnumHelper.coerce(Native::MapMode, mode, name: "map mode")
139
174
 
140
175
  status_holder = { done: false, status: nil, message: nil }
141
176
  callback = FFI::Function.new(:void, [:uint32, Native::StringView.by_value, :pointer, :pointer]) do |status, message, _userdata1, _userdata2|
@@ -145,7 +180,7 @@ module WGPU
145
180
  status_holder[:message] = message[:data].read_string(message[:length])
146
181
  end
147
182
  end
148
- @map_callbacks << callback
183
+ callback_token = CallbackKeepalive.retain(self, callback)
149
184
 
150
185
  callback_info = Native::BufferMapCallbackInfo.new
151
186
  callback_info[:next_in_chain] = nil
@@ -155,24 +190,28 @@ module WGPU
155
190
  callback_info[:userdata2] = nil
156
191
 
157
192
  future = Native.wgpuBufferMapAsync(@handle, mode_flag, offset, size, callback_info)
193
+ @map_state = :pending
158
194
 
159
- [status_holder, callback, future]
195
+ [status_holder, callback_token, future]
160
196
  end
161
197
 
162
- def wait_for_map(status_holder, future)
198
+ def wait_for_map(status_holder, future, timeout: nil)
163
199
  AsyncWaiter.wait(
164
200
  status_holder: status_holder,
165
201
  instance: @device.adapter&.instance,
166
202
  device: @device,
167
- future: future
203
+ future: future,
204
+ timeout: timeout
168
205
  )
169
206
  end
170
207
 
171
208
  def finalize_map(status_holder)
172
209
  if status_holder[:status] == :success
173
210
  @mapped = true
211
+ @map_state = :mapped
174
212
  true
175
213
  else
214
+ @map_state = :unmapped
176
215
  detail = status_holder[:message]
177
216
  base = "Failed to map buffer: #{status_holder[:status]}"
178
217
  raise BufferError, detail && !detail.empty? ? "#{base} (#{detail})" : base
@@ -180,33 +219,17 @@ module WGPU
180
219
  end
181
220
 
182
221
  def normalize_usage(usage)
183
- case usage
184
- when Integer
185
- usage
186
- when Symbol
187
- Native::BufferUsage[usage]
188
- when Array
189
- usage.reduce(0) { |acc, u| acc | Native::BufferUsage[u] }
190
- else
191
- raise ArgumentError, "Invalid usage type: #{usage.class}"
192
- end
222
+ Native::EnumHelper.coerce_flags(Native::BufferUsage, usage, name: "buffer usage")
193
223
  end
194
224
 
195
- def data_to_pointer(data)
196
- case data
197
- when String
198
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
199
- ptr.put_bytes(0, data)
200
- [ptr, data.bytesize]
201
- when Array
202
- ptr = FFI::MemoryPointer.new(:float, data.size)
203
- ptr.write_array_of_float(data)
204
- [ptr, data.size * 4]
205
- when FFI::Pointer
206
- [data, data.size]
207
- else
208
- raise ArgumentError, "Unsupported data type: #{data.class}"
209
- end
225
+ def buffer_error_message(message, label)
226
+ context = label ? " #{label.inspect}" : ""
227
+ "create buffer#{context}: #{message}"
228
+ end
229
+
230
+ def validate_map_range!(offset, size)
231
+ DataTypes.validate_alignment!(offset, 8, name: "map offset")
232
+ DataTypes.validate_alignment!(size, 4, name: "map size")
210
233
  end
211
234
  end
212
235
 
@@ -217,12 +240,64 @@ module WGPU
217
240
  end
218
241
 
219
242
  def read_floats(count = nil)
220
- count ||= @size / 4
221
- @pointer.read_array_of_float(count)
243
+ read(type: :f32, count:)
222
244
  end
223
245
 
224
246
  def write_floats(data)
225
- @pointer.write_array_of_float(data)
247
+ write(data, type: :f32)
248
+ end
249
+
250
+ def read_uint32s(count = nil)
251
+ read(type: :u32, count:)
252
+ end
253
+
254
+ def write_uint32s(data)
255
+ write(data, type: :u32)
256
+ end
257
+
258
+ def read_int32s(count = nil)
259
+ read(type: :i32, count:)
260
+ end
261
+
262
+ def write_int32s(data)
263
+ write(data, type: :i32)
264
+ end
265
+
266
+ def read_float64s(count = nil)
267
+ read(type: :f64, count:)
268
+ end
269
+
270
+ def write_float64s(data)
271
+ write(data, type: :f64)
272
+ end
273
+
274
+ def read_uint16s(count = nil)
275
+ read(type: :u16, count:)
276
+ end
277
+
278
+ def write_uint16s(data)
279
+ write(data, type: :u16)
280
+ end
281
+
282
+ def read_uint8s(count = nil)
283
+ read(type: :u8, count:)
284
+ end
285
+
286
+ def write_uint8s(data)
287
+ write(data, type: :u8)
288
+ end
289
+
290
+ def read(type: :f32, count: nil)
291
+ byte_size = DataTypes.byte_size(type)
292
+ count ||= @size / byte_size
293
+ DataTypes.unpack(@pointer.read_bytes(count * byte_size), type:)
294
+ end
295
+
296
+ def write(data, type: :f32)
297
+ bytes = data.is_a?(String) ? data : DataTypes.pack(data, type:)
298
+ raise ArgumentError, "data exceeds mapped range" if bytes.bytesize > @size
299
+
300
+ @pointer.put_bytes(0, bytes)
226
301
  end
227
302
 
228
303
  def read_bytes
@@ -2,10 +2,14 @@
2
2
 
3
3
  module WGPU
4
4
  class QuerySet
5
- attr_reader :handle
5
+ attr_reader :handle, :count, :type
6
6
 
7
7
  def initialize(device, label: nil, type:, count:)
8
8
  @device = device
9
+ @count = Integer(count)
10
+ type_value = Native::EnumHelper.coerce(Native::QueryType, type, name: "query type")
11
+ @type = Native::QueryType[type_value]
12
+ @destroyed = false
9
13
 
10
14
  desc = Native::QuerySetDescriptor.new
11
15
  desc[:next_in_chain] = nil
@@ -17,28 +21,26 @@ module WGPU
17
21
  desc[:label][:data] = nil
18
22
  desc[:label][:length] = 0
19
23
  end
20
- desc[:type] = type
21
- desc[:count] = count
24
+ desc[:type] = type_value
25
+ desc[:count] = @count
22
26
 
23
27
  @handle = Native.wgpuDeviceCreateQuerySet(device.handle, desc)
24
28
  raise ResourceError, "Failed to create query set" if @handle.null?
25
29
  end
26
30
 
27
- def count
28
- Native.wgpuQuerySetGetCount(@handle)
29
- end
30
-
31
- def type
32
- Native.wgpuQuerySetGetType(@handle)
33
- end
34
-
35
31
  def destroy
32
+ return if @destroyed
33
+
36
34
  Native.wgpuQuerySetDestroy(@handle)
35
+ @destroyed = true
37
36
  end
38
37
 
39
38
  def release
40
39
  return if @handle.null?
41
- Native.wgpuQuerySetRelease(@handle)
40
+
41
+ # Pinned wgpu-native v27 removes a destroyed query set from its storage;
42
+ # calling Release afterward aborts inside Rust instead of being harmless.
43
+ Native.wgpuQuerySetRelease(@handle) unless @destroyed
42
44
  @handle = FFI::Pointer::NULL
43
45
  end
44
46
  end
@@ -7,30 +7,25 @@ module WGPU
7
7
  def initialize(device, 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)
8
8
  @device = device
9
9
 
10
- desc = Native::SamplerDescriptor.new
11
- desc[:next_in_chain] = nil
12
- if label
13
- @label_ptr = FFI::MemoryPointer.from_string(label)
14
- desc[:label][:data] = @label_ptr
15
- desc[:label][:length] = label.bytesize
16
- else
17
- desc[:label][:data] = nil
18
- desc[:label][:length] = 0
19
- end
20
- desc[:address_mode_u] = address_mode_u
21
- desc[:address_mode_v] = address_mode_v
22
- desc[:address_mode_w] = address_mode_w
23
- desc[:mag_filter] = mag_filter
24
- desc[:min_filter] = min_filter
25
- desc[:mipmap_filter] = mipmap_filter
26
- desc[:lod_min_clamp] = lod_min_clamp
27
- desc[:lod_max_clamp] = lod_max_clamp
28
- desc[:compare] = compare || :undefined
29
- desc[:max_anisotropy] = max_anisotropy
10
+ desc, keepalive = build_descriptor(
11
+ label:,
12
+ address_mode_u:,
13
+ address_mode_v:,
14
+ address_mode_w:,
15
+ mag_filter:,
16
+ min_filter:,
17
+ mipmap_filter:,
18
+ lod_min_clamp:,
19
+ lod_max_clamp:,
20
+ compare:,
21
+ max_anisotropy:
22
+ )
23
+ @descriptor_keepalive = keepalive
30
24
 
31
25
  device.push_error_scope(:validation)
32
26
  @handle = Native.wgpuDeviceCreateSampler(device.handle, desc)
33
27
  error = device.pop_error_scope
28
+ @descriptor_keepalive = nil
34
29
 
35
30
  if @handle.null? || (error[:type] && error[:type] != :no_error)
36
31
  msg = error[:message] || "Failed to create sampler"
@@ -43,5 +38,26 @@ module WGPU
43
38
  Native.wgpuSamplerRelease(@handle)
44
39
  @handle = FFI::Pointer::NULL
45
40
  end
41
+
42
+ private
43
+
44
+ def build_descriptor(label:, address_mode_u:, address_mode_v:, address_mode_w:, mag_filter:, min_filter:,
45
+ mipmap_filter:, lod_min_clamp:, lod_max_clamp:, compare:, max_anisotropy:)
46
+ keepalive = []
47
+ desc = Native::SamplerDescriptor.new
48
+ desc[:next_in_chain] = nil
49
+ DescriptorHelpers.set_label(desc, label, keepalive:)
50
+ desc[:address_mode_u] = Native::EnumHelper.coerce(Native::AddressMode, address_mode_u, name: "address mode")
51
+ desc[:address_mode_v] = Native::EnumHelper.coerce(Native::AddressMode, address_mode_v, name: "address mode")
52
+ desc[:address_mode_w] = Native::EnumHelper.coerce(Native::AddressMode, address_mode_w, name: "address mode")
53
+ desc[:mag_filter] = Native::EnumHelper.coerce(Native::FilterMode, mag_filter, name: "mag filter")
54
+ desc[:min_filter] = Native::EnumHelper.coerce(Native::FilterMode, min_filter, name: "min filter")
55
+ desc[:mipmap_filter] = Native::EnumHelper.coerce(Native::MipmapFilterMode, mipmap_filter, name: "mipmap filter")
56
+ desc[:lod_min_clamp] = lod_min_clamp
57
+ desc[:lod_max_clamp] = lod_max_clamp
58
+ desc[:compare] = Native::EnumHelper.coerce(Native::CompareFunction, compare || :undefined, name: "compare function")
59
+ desc[:max_anisotropy] = max_anisotropy
60
+ [desc, keepalive]
61
+ end
46
62
  end
47
63
  end
@@ -2,45 +2,27 @@
2
2
 
3
3
  module WGPU
4
4
  class Texture
5
- attr_reader :handle
5
+ attr_reader :handle, :surface_status
6
6
 
7
7
  def initialize(device, label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
8
8
  @device = device
9
9
 
10
- desc = Native::TextureDescriptor.new
11
- desc[:next_in_chain] = nil
12
- if label
13
- @label_ptr = FFI::MemoryPointer.from_string(label)
14
- desc[:label][:data] = @label_ptr
15
- desc[:label][:length] = label.bytesize
16
- else
17
- desc[:label][:data] = nil
18
- desc[:label][:length] = 0
19
- end
20
- desc[:usage] = normalize_usage(usage)
21
- desc[:dimension] = dimension
22
- desc[:size][:width] = size[:width] || size[0]
23
- desc[:size][:height] = size[:height] || size[1] || 1
24
- desc[:size][:depth_or_array_layers] = size[:depth_or_array_layers] || size[2] || 1
25
- desc[:format] = format
26
- desc[:mip_level_count] = mip_level_count
27
- desc[:sample_count] = sample_count
28
- desc[:view_format_count] = view_formats.size
29
- if view_formats.empty?
30
- @view_formats_ptr = nil
31
- desc[:view_formats] = nil
32
- else
33
- format_values = view_formats.map do |vf|
34
- vf.is_a?(Integer) ? vf : Native::TextureFormat[vf]
35
- end
36
- @view_formats_ptr = FFI::MemoryPointer.new(:uint32, format_values.size)
37
- @view_formats_ptr.write_array_of_uint32(format_values)
38
- desc[:view_formats] = @view_formats_ptr
39
- end
10
+ desc, keepalive = build_descriptor(
11
+ label:,
12
+ size:,
13
+ format:,
14
+ usage:,
15
+ dimension:,
16
+ mip_level_count:,
17
+ sample_count:,
18
+ view_formats:
19
+ )
20
+ @descriptor_keepalive = keepalive
40
21
 
41
22
  device.push_error_scope(:validation)
42
23
  @handle = Native.wgpuDeviceCreateTexture(device.handle, desc)
43
24
  error = device.pop_error_scope
25
+ @descriptor_keepalive = nil
44
26
 
45
27
  if @handle.null? || (error[:type] && error[:type] != :no_error)
46
28
  msg = error[:message] || "Failed to create texture"
@@ -48,10 +30,11 @@ module WGPU
48
30
  end
49
31
  end
50
32
 
51
- def self.from_handle(handle)
33
+ def self.from_handle(handle, surface_status: nil)
52
34
  texture = allocate
53
35
  texture.instance_variable_set(:@handle, handle)
54
36
  texture.instance_variable_set(:@device, nil)
37
+ texture.instance_variable_set(:@surface_status, surface_status)
55
38
  texture
56
39
  end
57
40
 
@@ -120,17 +103,36 @@ module WGPU
120
103
 
121
104
  private
122
105
 
123
- def normalize_usage(usage)
124
- case usage
125
- when Integer
126
- usage
127
- when Symbol
128
- Native::TextureUsage[usage]
129
- when Array
130
- usage.reduce(0) { |acc, u| acc | Native::TextureUsage[u] }
131
- else
132
- raise ArgumentError, "Invalid usage: #{usage}"
106
+ def build_descriptor(label:, size:, format:, usage:, dimension:, mip_level_count:, sample_count:, view_formats:)
107
+ keepalive = []
108
+ DescriptorHelpers.validate_keys!(
109
+ size,
110
+ allowed: [:width, :height, :depth_or_array_layers],
111
+ required: [:width],
112
+ context: "texture size"
113
+ )
114
+
115
+ desc = Native::TextureDescriptor.new
116
+ desc[:next_in_chain] = nil
117
+ DescriptorHelpers.set_label(desc, label, keepalive:)
118
+ desc[:usage] = normalize_usage(usage)
119
+ desc[:dimension] = Native::EnumHelper.coerce(Native::TextureDimension, dimension, name: "texture dimension")
120
+ desc[:size][:width] = size[:width] || size[0]
121
+ desc[:size][:height] = size[:height] || size[1] || 1
122
+ desc[:size][:depth_or_array_layers] = size[:depth_or_array_layers] || size[2] || 1
123
+ desc[:format] = Native::EnumHelper.coerce(Native::TextureFormat, format, name: "texture format")
124
+ desc[:mip_level_count] = mip_level_count
125
+ desc[:sample_count] = sample_count
126
+ desc[:view_format_count] = view_formats.size
127
+ format_values = view_formats.map do |view_format|
128
+ Native::EnumHelper.coerce(Native::TextureFormat, view_format, name: "view format")
133
129
  end
130
+ desc[:view_formats] = DescriptorHelpers.uint32_array(format_values, keepalive:)
131
+ [desc, keepalive]
132
+ end
133
+
134
+ def normalize_usage(usage)
135
+ Native::EnumHelper.coerce_flags(Native::TextureUsage, usage, name: "texture usage")
134
136
  end
135
137
  end
136
138
  end
@@ -17,13 +17,21 @@ module WGPU
17
17
  desc[:label][:data] = nil
18
18
  desc[:label][:length] = 0
19
19
  end
20
- desc[:format] = format || :undefined
21
- desc[:dimension] = dimension || :undefined
20
+ desc[:format] = Native::EnumHelper.coerce(
21
+ Native::TextureFormat,
22
+ format || :undefined,
23
+ name: "texture view format"
24
+ )
25
+ desc[:dimension] = Native::EnumHelper.coerce(
26
+ Native::TextureViewDimension,
27
+ dimension || :undefined,
28
+ name: "texture view dimension"
29
+ )
22
30
  desc[:base_mip_level] = base_mip_level
23
31
  desc[:mip_level_count] = mip_level_count || 0xFFFFFFFF
24
32
  desc[:base_array_layer] = base_array_layer
25
33
  desc[:array_layer_count] = array_layer_count || 0xFFFFFFFF
26
- desc[:aspect] = aspect
34
+ desc[:aspect] = Native::EnumHelper.coerce(Native::TextureAspect, aspect, name: "texture aspect")
27
35
 
28
36
  @handle = Native.wgpuTextureCreateView(texture.handle, desc)
29
37
  raise ResourceError, "Failed to create texture view" if @handle.null?