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
//! Device-limit and memory policy for mobile GPU device requests (Android and
//! iOS).
//!
//! Host-compilable so the policy stays unit-tested in regular CI even though
//! the mobile runtime modules only build for `target_os = "android"` / `"ios"`.
/// Allocation strategy for a mobile GPU device request.
///
/// `MemoryHints::default()` is `Performance`, which tells wgpu's Vulkan backend
/// to sub-allocate resources out of memory blocks of 128 MiB (device-local) or
/// 64 MiB (host-visible), growing to 256 MiB / 128 MiB. Desktop GPUs have the
/// headroom for that; phones and watches do not, and on them the choice is
/// worse than it looks. Mobile GPUs have unified memory, so *every* Vulkan
/// memory type is `HOST_VISIBLE` — `gpu-allocator` therefore treats even the
/// device-local heap as host memory and reserves the 64 MiB block for the very
/// first allocation. That first allocation is wgpu's own 512 KiB zero-init
/// buffer, created inside `request_device`, so the process gives up 64 MiB of
/// GPU memory (and, on 32-bit Wear OS, 64 MiB of address space) before the
/// renderer has drawn anything. It never shrinks, and it does not scale with
/// the screen: a 454x454 watch face paid exactly what a 1280x2856 phone did.
///
/// `MemoryUsage` asks for 4 MiB host / 8 MiB device blocks growing to 32 / 64
/// MiB, which is a far better match for a renderer whose live GPU footprint is
/// a few megabytes. The cost is more `VkDeviceMemory` objects on a workload
/// that does grow past the first block — bounded by the doubling in
/// `gpu-allocator`, and cheap next to reserving 60 MiB that nothing will use.
///
/// Backends that do not sub-allocate (Metal, GLES) ignore the hint.
pub(crate) fn mobile_memory_hints() -> wgpu::MemoryHints {
wgpu::MemoryHints::MemoryUsage
}
/// Device limits for a mobile GPU device request.
///
/// `Limits::default()` requests caps that mobile/simulator Metal cannot grant
/// (for example `max_inter_stage_shader_variables: 16`, where the iOS Simulator
/// allows 15), so start from `downlevel_defaults()`.
///
/// `downlevel_defaults()` caps uniform bindings at 16 KiB and
/// `using_resolution()` only raises texture limits, never buffer limits, while
/// the renderer's desktop-sized shape batch uniform needs up to 60 KiB. Request
/// the uniform binding size the adapter actually supports, up to the regular
/// desktop default; the renderer derives its batch capacities from whatever is
/// granted, so true 16 KiB-minimum devices still work with smaller batches.
pub(crate) fn mobile_device_limits(adapter_limits: wgpu::Limits) -> wgpu::Limits {
let mut limits = wgpu::Limits::downlevel_defaults().using_resolution(adapter_limits.clone());
limits.max_uniform_buffer_binding_size = adapter_limits
.max_uniform_buffer_binding_size
.min(wgpu::Limits::default().max_uniform_buffer_binding_size);
// The renderer uses neither compute pipelines nor storage buffers (the
// latter aren't available to WebGL fragment shaders, so the renderer avoids
// them everywhere), but `downlevel_defaults` still requests both. Some
// adapters — notably the Android emulator's GLES driver — report zero for
// these limits, which fails device creation. Request only what the adapter
// grants for the unused capabilities so the device is always created.
limits.max_compute_workgroup_storage_size = adapter_limits.max_compute_workgroup_storage_size;
limits.max_compute_invocations_per_workgroup =
adapter_limits.max_compute_invocations_per_workgroup;
limits.max_compute_workgroup_size_x = adapter_limits.max_compute_workgroup_size_x;
limits.max_compute_workgroup_size_y = adapter_limits.max_compute_workgroup_size_y;
limits.max_compute_workgroup_size_z = adapter_limits.max_compute_workgroup_size_z;
limits.max_compute_workgroups_per_dimension =
adapter_limits.max_compute_workgroups_per_dimension;
limits.max_storage_buffer_binding_size = adapter_limits.max_storage_buffer_binding_size;
limits.max_storage_buffers_per_shader_stage =
adapter_limits.max_storage_buffers_per_shader_stage;
limits.max_storage_textures_per_shader_stage =
adapter_limits.max_storage_textures_per_shader_stage;
limits.max_dynamic_storage_buffers_per_pipeline_layout =
adapter_limits.max_dynamic_storage_buffers_per_pipeline_layout;
limits
}
#[cfg(test)]
mod tests {
use super::mobile_device_limits;
#[test]
fn uniform_binding_size_follows_adapter_up_to_desktop_default() {
let desktop_binding = wgpu::Limits::default().max_uniform_buffer_binding_size;
// A capable adapter grants the full desktop-sized binding so shape
// batches keep their desktop capacity (the 0.1.13 Android crash was
// requesting only the 16 KiB downlevel cap on such devices).
let capable = mobile_device_limits(wgpu::Limits::default());
assert_eq!(capable.max_uniform_buffer_binding_size, desktop_binding);
// An adapter at the spec minimum is never asked for more than it has.
let mut minimal = wgpu::Limits::downlevel_defaults();
minimal.max_uniform_buffer_binding_size = 16384;
let limits = mobile_device_limits(minimal);
assert_eq!(limits.max_uniform_buffer_binding_size, 16384);
// Texture resolution still follows the adapter as before.
let big_textures = wgpu::Limits {
max_texture_dimension_2d: 16384,
..wgpu::Limits::default()
};
let limits = mobile_device_limits(big_textures);
assert_eq!(limits.max_texture_dimension_2d, 16384);
}
#[test]
fn compute_limits_never_exceed_adapter() {
// The Android emulator's GLES driver reports zero compute limits; the
// renderer uses no compute, so request only what the adapter grants.
let no_compute = wgpu::Limits {
max_compute_workgroups_per_dimension: 0,
max_compute_invocations_per_workgroup: 0,
max_compute_workgroup_size_x: 0,
max_compute_workgroup_size_y: 0,
max_compute_workgroup_size_z: 0,
max_compute_workgroup_storage_size: 0,
..wgpu::Limits::downlevel_defaults()
};
let limits = mobile_device_limits(no_compute);
assert_eq!(limits.max_compute_workgroups_per_dimension, 0);
assert_eq!(limits.max_compute_invocations_per_workgroup, 0);
assert_eq!(limits.max_compute_workgroup_storage_size, 0);
}
#[test]
fn storage_limits_never_exceed_adapter() {
// The Android emulator's GLES driver reports zero storage-buffer limits;
// the renderer uses no storage buffers, so request only what the adapter
// grants (`downlevel_defaults` otherwise asks for 128 MiB and fails).
let no_storage = wgpu::Limits {
max_storage_buffer_binding_size: 0,
max_storage_buffers_per_shader_stage: 0,
max_storage_textures_per_shader_stage: 0,
max_dynamic_storage_buffers_per_pipeline_layout: 0,
..wgpu::Limits::downlevel_defaults()
};
let limits = mobile_device_limits(no_storage);
assert_eq!(limits.max_storage_buffer_binding_size, 0);
assert_eq!(limits.max_storage_buffers_per_shader_stage, 0);
assert_eq!(limits.max_storage_textures_per_shader_stage, 0);
assert_eq!(limits.max_dynamic_storage_buffers_per_pipeline_layout, 0);
}
}