euv-engine 0.30.0

A high-performance 2D game engine built on the euv framework, featuring ECS, fixed-timestep game loop, canvas rendering, physics, collision detection, sprite animation, and audio.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
use super::*;

fn sized_2d() -> Texture2DDescriptor {
    Texture2DDescriptor::new(64, 32, GpuTextureFormat::Rgba8Unorm)
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_two_d_texture_reaches_the_device_with_its_own_extent() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    let created: JsValue = subject.create_texture_2d(&sized_2d());

    assert!(
        !created.is_undefined(),
        "a driver handle means the texture was allocated"
    );
    assert_eq!(
        ops_from(before),
        vec![String::from("device.createTexture")],
        "creating a 2D texture is one call and nothing else"
    );
    let args: Array = args_on("device", "createTexture", before);
    let descriptor: JsValue = args.get(0);
    assert_eq!(
        field(&descriptor, &["size", "width"]).as_f64(),
        Some(64.0),
        "the descriptor's own width reaches the extent, not a default"
    );
    assert_eq!(
        field(&descriptor, &["size", "height"]).as_f64(),
        Some(32.0),
        "and so does the height"
    );
    assert_eq!(
        field(&descriptor, &["size", "depthOrArrayLayers"]).as_f64(),
        Some(1.0),
        "a plain 2D texture is one layer deep even when the descriptor never said so"
    );
    assert_eq!(
        field(&descriptor, &["dimension"]).as_string(),
        Some(String::from("2d")),
        "and the narrow 2D preset always asks for the 2d dimension"
    );
    assert_eq!(
        field(&descriptor, &["format"]).as_string(),
        Some(String::from("rgba8unorm")),
        "the format enum becomes the spec's format string"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_texture_with_no_width_never_reaches_the_driver() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    let created: JsValue = subject.create_texture_2d(&Texture2DDescriptor::new(
        0,
        32,
        GpuTextureFormat::Rgba8Unorm,
    ));

    assert!(
        created.is_undefined(),
        "a zero extent has no meaning to the driver, so the caller gets the failure marker"
    );
    assert_eq!(
        count_on("device", "createTexture", before),
        0,
        "and the zero-extent descriptor must not be forwarded, or the driver raises a \
         validation error nobody in this engine reads"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_general_texture_forwards_the_dimensionality_it_was_given() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();
    let mut descriptor: TextureDescriptor =
        TextureDescriptor::new(8, 8, "3d", GpuTextureFormat::Rgba8Unorm, 0);
    descriptor.set_depth_or_layers(4);

    subject.create_texture(&descriptor);

    let args: Array = args_on("device", "createTexture", before);
    let wire: JsValue = args.get(0);
    assert_eq!(
        field(&wire, &["dimension"]).as_string(),
        Some(String::from("3d")),
        "the general constructor is the one that can ask for something other than 2d"
    );
    assert_eq!(
        field(&wire, &["size", "depthOrArrayLayers"]).as_f64(),
        Some(4.0),
        "and the depth it was set to travels in the extent rather than being flattened to 1"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_sampler_carries_its_filter_mipmap_and_wrap_decisions_across() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();
    let mut descriptor: SamplerDescriptor = SamplerDescriptor::new();
    descriptor.set_filter(FilterMode::Nearest);
    descriptor.set_mipmap_filter(MipmapFilter::Nearest);
    descriptor.set_address_mode_u(AddressMode::MirrorRepeat);

    subject.create_sampler(&descriptor);

    let args: Array = args_on("device", "createSampler", before);
    let wire: JsValue = args.get(0);
    assert_eq!(
        field(&wire, &["magFilter"]).as_string(),
        Some(String::from("nearest")),
        "the magnification filter is the one the descriptor named"
    );
    assert_eq!(
        field(&wire, &["minFilter"]).as_string(),
        Some(String::from("nearest")),
        "the minification filter travels with it, since the descriptor has one field for both"
    );
    assert_eq!(
        field(&wire, &["mipmapFilter"]).as_string(),
        Some(String::from("nearest")),
        "and mip blending is a separate decision, so it must not inherit the filter"
    );
    assert_eq!(
        field(&wire, &["addressModeU"]).as_string(),
        Some(String::from("mirror-repeat")),
        "wrapping on U is its own field rather than a shared default for all three axes"
    );
    assert!(
        field(&wire, &["compare"]).is_undefined(),
        "a sampler with no depth comparison must omit the compare key entirely, because WebGPU \
         reads a present-but-null compare as a request to enable one"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn an_index_buffer_is_allocated_at_the_payload_size_then_filled_from_zero() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    let created: JsValue = subject.create_index_buffer(&[1, 2, 3, 4, 5, 6]);

    assert!(!created.is_undefined(), "the driver handed back a handle");
    assert_eq!(
        ops_from(before),
        vec![
            String::from("device.createBuffer"),
            String::from("queue.writeBuffer")
        ],
        "allocate then fill, in that order, so the caller never sees an unwritten index buffer"
    );
    let allocated: Array = args_on("device", "createBuffer", before);
    assert_eq!(
        field(&allocated.get(0), &["size"]).as_f64(),
        Some(6.0),
        "the buffer is exactly the payload length, not a rounded-up guess"
    );
    let written: Array = args_on("queue", "writeBuffer", before);
    assert_eq!(
        written.get(1).as_f64(),
        Some(0.0),
        "the payload lands at the start of the buffer"
    );
    assert_eq!(
        written.get(3).as_f64(),
        Some(6.0),
        "and its byte length travels as the fourth argument, not as a view length the driver \
         has to re-derive"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn writing_no_bytes_is_skipped_entirely_rather_than_uploading_an_empty_view() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.write_buffer(&node("buffer").into(), 0, &[]);

    assert_eq!(
        count_on("queue", "writeBuffer", before),
        0,
        "an empty upload would still take the driver's lock and hand it a zero-length view"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_byte_upload_honours_the_offset_the_caller_named() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.write_buffer(&node("buffer").into(), 64, &[1, 2, 3, 4]);

    let written: Array = args_on("queue", "writeBuffer", before);
    assert_eq!(
        written.get(1).as_f64(),
        Some(64.0),
        "writing at a non-zero offset must not be silently rewritten to the buffer head"
    );
    assert_eq!(
        written.get(3).as_f64(),
        Some(4.0),
        "the fourth argument is this call's length, not the buffer's"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_uniform_bind_group_takes_group_zero_of_the_pipeline_it_was_given() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();
    let pipeline: Object = node("pipeline");

    subject.create_uniform_bind_group(&pipeline.clone().into(), &node("buffer").into());

    let layout: Array = args_on("pipeline", "getBindGroupLayout", before);
    assert_eq!(
        layout.get(0).as_f64(),
        Some(0.0),
        "the uniform convenience path is documented as group 0, and it must actually ask for 0"
    );
    let created: Array = args_on("device", "createBindGroup", before);
    let descriptor: JsValue = created.get(0);
    let entries: Array = field(&descriptor, &["entries"]).unchecked_into();
    assert_eq!(
        entries.length(),
        1,
        "one binding is enough for the uniform-only path"
    );
    assert_eq!(
        field(&entries.get(0), &["binding"]).as_f64(),
        Some(0.0),
        "and it fills binding 0"
    );
    assert_eq!(
        field(&entries.get(0), &["resource", "buffer"]).as_string(),
        Some(String::from("<gpu>")),
        "the buffer handle really did travel into the resource dict; the recorder renders a \
         stand-in handle as a marker rather than recursing into the proxy"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_buffer_binding_with_no_size_omits_the_size_key() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.create_bind_group(
        &node("pipeline").into(),
        1,
        &[BindGroupEntry::Buffer {
            binding: 3,
            buffer: node("buffer").into(),
            offset: 32,
            size: None,
        }],
    );

    let created: Array = args_on("device", "createBindGroup", before);
    let entries: Array = field(&created.get(0), &["entries"]).unchecked_into();
    assert_eq!(
        field(&entries.get(0), &["binding"]).as_f64(),
        Some(3.0),
        "the caller's binding slot is forwarded verbatim, not renumbered from zero"
    );
    assert_eq!(
        field(&entries.get(0), &["resource", "offset"]).as_f64(),
        Some(32.0),
        "and the buffer offset inside the resource dict is the one the caller gave"
    );
    assert!(
        field(&entries.get(0), &["resource", "size"]).is_undefined(),
        "a bound size of None must leave the key out; sending size: 0 would bind a zero-length \
         range and the driver would reject it"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_bind_group_is_created_inside_a_validation_error_scope() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.create_bind_group(&node("pipeline").into(), 0, &[]);

    let pushed: Array = args_on("device", "pushErrorScope", before);
    assert_eq!(
        pushed.get(0).as_string(),
        Some(String::from("validation")),
        "a bad bind group is a validation error, so the scope has to be the validation filter"
    );
    let order: Vec<String> = ops_from(before);
    let push_at: usize = order
        .iter()
        .position(|name: &String| name == "device.pushErrorScope")
        .unwrap_or(usize::MAX);
    let create_at: usize = order
        .iter()
        .position(|name: &String| name == "device.createBindGroup")
        .unwrap_or(0);
    assert!(
        push_at < create_at,
        "the scope has to open before the call it is meant to catch, not after"
    );
    assert_eq!(
        count_on("device", "popErrorScope", before),
        1,
        "and it has to be popped, or every later scope in the frame stacks onto this one"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn a_bind_group_layout_turns_each_entry_into_its_spec_slot() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.create_bind_group_layout(&[
        BindGroupLayoutEntry::uniform(0, ShaderStage::Vertex | ShaderStage::Fragment),
        BindGroupLayoutEntry::storage(1, ShaderStage::Compute, true),
    ]);

    let args: Array = args_on("device", "createBindGroupLayout", before);
    let entries: Array = field(&args.get(0), &["entries"]).unchecked_into();
    assert_eq!(entries.length(), 2, "one spec slot per layout entry");
    assert_eq!(
        field(&entries.get(0), &["binding"]).as_f64(),
        Some(0.0),
        "the first entry keeps its own binding index"
    );
    assert_eq!(
        field(&entries.get(0), &["visibility"]).as_f64(),
        Some(3.0),
        "vertex and fragment together are 0x1 | 0x2, not one of them alone"
    );
    assert_eq!(
        field(&entries.get(0), &["buffer", "type"]).as_string(),
        Some(String::from("uniform")),
        "the entry kind becomes the spec's buffer binding type string"
    );
    assert_eq!(
        field(&entries.get(1), &["buffer", "type"]).as_string(),
        Some(String::from("read-only-storage")),
        "a read-only storage buffer is its own binding type, not a uniform in disguise"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records js_sys stand-ins for a WebGPU device, which only exist under wasm"
)]
fn an_explicit_layout_skips_the_pipeline_derivation_entirely() {
    let subject: WebGpuRenderer = renderer(false);
    let before: u32 = mark();

    subject.create_bind_group_for_layout(&node("layout").into(), &[]);

    assert_eq!(
        count_on("pipeline", "getBindGroupLayout", before),
        0,
        "this entry point exists precisely so a caller can bind a layout it built itself, with \
         no pipeline in hand to derive one from"
    );
    assert_eq!(
        count_on("device", "createBindGroup", before),
        1,
        "and it still creates the bind group"
    );
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test::wasm_bindgen_test]
async fn reading_a_value_that_is_not_a_buffer_yields_nothing_rather_than_a_zero_length_read() {
    let subject: WebGpuRenderer = renderer(false);

    let bytes: Option<Vec<u8>> = subject.read_buffer(&JsValue::UNDEFINED, 0, 16).await;

    assert!(
        bytes.is_none(),
        "a value with no mapAsync is not a GPUBuffer; returning an empty Vec would read as \
         the buffer having contained zero bytes, and a caller checking length would accept a \
         failed read as a successful empty one"
    );
}