RUST.md: iris's bindless texture array does not survive real Android GPUs
Iris asked whether the 'unknown number of images' approach even works on mobile. It does not, measured with a new rig (rigs/gpu-probe, no APK needed) and sourced rather than recalled: the emulator's software Vulkan refuses iris's descriptor-indexing request outright, and on real hardware the current Android Vulkan Profile baseline (80.1% of active devices) does not require VK_EXT_descriptor_indexing either -- Arm's own docs say only Valhall/5th-Gen Mali (2019+) support it. iris already solved the identical problem for text in I1 (the glyph atlas). The recommendation is to generalize it to images rather than widen the binding array further; not yet implemented, since it changes iris's render core. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c70a670356
commit
79b9cd789a
6 files changed
+1592
-12
No files matched your search
@@ -37,19 +37,36 @@ session spending an afternoon on them again.
|
||||
## Where things stand (2026-09-04)
|
||||
|
||||
- **Done**: E0 (toolchain), E1 (Masonry on android-view, which found the
|
||||
keyboard gap), I0a, I0b (iris builds on a pinned nightly and runs), I1
|
||||
(parley + glyph atlas).
|
||||
- **Next**: **I2** — iris on android-view. E1 says the first thing to
|
||||
answer there is why the editor gets keystrokes but no autocorrect or
|
||||
suggestions, since that is the constraint the framework decision turns
|
||||
on. **E2** (a transcript in Masonry) can go in parallel in another
|
||||
session.
|
||||
keyboard gap — now explained, see below), I0a, I0b (iris builds on a
|
||||
pinned nightly and runs), I1 (parley + glyph atlas).
|
||||
- **E1's keyboard gap is Masonry's `as_input_connection` returning `None`
|
||||
(a TODO), not android-view or `EditorInfo`.** android-view's own demo
|
||||
implements the `InputConnection` trait over a parley editor and gets
|
||||
real Gboard suggestions on this emulator — screenshotted 2026-09-04.
|
||||
android-view's `accesskit_android` adapter also has a reproducible abort
|
||||
(a client detaching, not attaching, is the trigger) — see E1 below for
|
||||
both, with the mitigation iris/I4 needs to carry.
|
||||
- **Blocking, found 2026-09-04, before I2 can be called done**: iris's
|
||||
texture pipeline asks every device, unconditionally, for
|
||||
`VK_EXT_descriptor_indexing` ("bindless" binding arrays) — and a
|
||||
sourced check says a real share of Android hardware does not have it,
|
||||
not just the emulator's software renderer. See "iris's binding array
|
||||
does not survive real Android hardware" under the iris track for the
|
||||
measurement, the sources, and the recommended fix (generalize the
|
||||
glyph atlas to images, the same way I1 already did for text). Not yet
|
||||
implemented; a design decision to confirm before iris's render core
|
||||
changes.
|
||||
- **Next**: decide on the atlas-based image fix above, then **I2** —
|
||||
iris on android-view. **E2** (a transcript in Masonry) can go in
|
||||
parallel in another session.
|
||||
- **Not started**: `client-core`, which is item 1 of the recommendation
|
||||
below and does not depend on the framework choice. Nothing has been
|
||||
built for it, and it is not one of the numbered boxes — worth picking up
|
||||
in a session that wants work independent of the emulator.
|
||||
- **The app itself is untouched.** Everything so far is in `iris/` and in
|
||||
the rigs; nothing under `app/` or `server/` has changed.
|
||||
- **The app itself is untouched.** Everything so far is in `iris/`, in
|
||||
`rigs/gpu-probe` (a headless wgpu/Vulkan feature probe, pushable to a
|
||||
device with no APK — see the binding-array section), and in the other
|
||||
rigs; nothing under `app/` or `server/` has changed.
|
||||
- **Changed outside this repo**, both in `emulator-tools` and both pushed:
|
||||
`avd_serial` now validates its cache by asking the device its AVD name
|
||||
rather than by checking the serial is still attached (a recycled port
|
||||
@@ -851,6 +868,97 @@ step measured.
|
||||
nightly gates — `portable_simd` (the old glyph compositing) and
|
||||
`gen_blocks` (the deleted line iterator). **Eleven left.**
|
||||
|
||||
### iris's binding array does not survive real Android hardware (found 2026-09-04)
|
||||
|
||||
Iris asked, of the "unknown number of images" case — a transcript with an
|
||||
unbounded number of attached screenshots — whether iris's approach even
|
||||
works on a phone, since her recollection was that mobile does not support
|
||||
it. Checked rather than assumed, and the recollection is right, with
|
||||
sources rather than a guess.
|
||||
|
||||
**What iris does today.** Every texture — every `Image` widget
|
||||
(`src/widget/image.rs`) and every glyph atlas page — gets its own
|
||||
permanent slot in one array via `Textures::add`
|
||||
(`core/src/primitive/texture.rs:65`), and both the `TEXTURE` and `GLYPH`
|
||||
primitives sample it by `view_idx` into `binding_array<texture_2d<f32>>`
|
||||
at `core/src/render/shader.wgsl:56`, sized by `UiLimits::default` — 100,000
|
||||
textures, 1,000 samplers (`core/src/render/mod.rs:347`). That needs three
|
||||
wgpu features: `TEXTURE_BINDING_ARRAY`,
|
||||
`SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`,
|
||||
`PARTIALLY_BOUND_BINDING_ARRAY` — Vulkan's `VK_EXT_descriptor_indexing`
|
||||
("bindless"), promoted to core in 1.2. So a transcript with an unbounded
|
||||
number of images is exactly the case that grows this array without bound,
|
||||
one permanent slot per image.
|
||||
|
||||
**Measured first on the emulator, and it fails outright.** A rig
|
||||
(`rigs/gpu-probe`, a plain executable with no window, pushed with `adb
|
||||
push` and run from `/data/local/tmp` — no APK needed to ask a device what
|
||||
it supports) asks `wgpu::Adapter::request_device` for exactly iris's
|
||||
features and limits. Against the emulator's guest Vulkan — both
|
||||
SwiftShader (`vk_swiftshader_icd.json`) and lavapipe (`lvp_icd.json`,
|
||||
cold-booted) — `request_device` **fails**: `Unsupported features were
|
||||
requested: TEXTURE_BINDING_ARRAY |
|
||||
SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING |
|
||||
PARTIALLY_BOUND_BINDING_ARRAY`. A second, raw query through `ash`
|
||||
(`rigs/gpu-probe/src/vk.rs`, bypassing wgpu) shows lavapipe's
|
||||
`vkGetPhysicalDeviceFeatures2` actually reporting all seven descriptor-
|
||||
indexing sub-features as `true` at device api version 1.3 — so on this
|
||||
software renderer wgpu-hal's own feature detection is being more
|
||||
conservative than the driver, for a reason not chased further (a likely
|
||||
instance-version negotiation gap, since `VK_EXT_descriptor_indexing` was
|
||||
only promoted to core at 1.2 and wgpu-hal's own `Instance::init` may be
|
||||
requesting less). That part is an emulator/wgpu-hal question and not the
|
||||
finding that matters.
|
||||
|
||||
**The finding that matters is about real phones, not the emulator, and it
|
||||
is sourced rather than recalled.** The **Android Vulkan Profile 2025** —
|
||||
Google and Khronos's current baseline, covering **80.1% of active
|
||||
Vulkan-capable Android devices** as of October 2025
|
||||
([developer.android.com/ndk/guides/graphics/android-vulkan-profile](https://developer.android.com/ndk/guides/graphics/android-vulkan-profile))
|
||||
— does **not** require `VK_EXT_descriptor_indexing` or any descriptor-
|
||||
indexing feature. It requires `shaderSampledImageArrayDynamicIndexing`
|
||||
(indexing an array of samplers by a value uniform across the invocation —
|
||||
Vulkan 1.0 baseline, unrelated to bindless) and stops there; the same is
|
||||
true of the 2021 and 2022 profiles. On the hardware side, Arm's own
|
||||
developer documentation states **"`VK_EXT_descriptor_indexing` is
|
||||
supported on all Valhall and 5th Gen GPUs"**
|
||||
([developer.arm.com/mobile-graphics-and-gaming/vulkan-api-best-practices-on-arm-gpus](https://developer.arm.com/mobile-graphics-and-gaming/vulkan-api-best-practices-on-arm-gpus)) —
|
||||
Mali generations from roughly 2019 (Mali-G77) onward, named affirmatively
|
||||
with no claim made for Bifrost, Midgard or Utgard, which are still common
|
||||
in budget and older Android phones still in use. So this is not a
|
||||
software-renderer artifact: a real, currently-shipping share of the
|
||||
Android fleet lacks the feature iris's texture pipeline asks for
|
||||
unconditionally, and the newest official baseline does not promise it
|
||||
either. (A crates.io/search-engine claim of "1% support on Android" for
|
||||
this extension was checked against its cited source, an Arm blog post,
|
||||
and was not actually there — that number does not appear anywhere primary
|
||||
and should not be repeated; the 80.1%-baseline-excludes-it finding above
|
||||
is the one with an attributable source.)
|
||||
|
||||
**Recommendation, not yet implemented.** iris already solved the
|
||||
identical problem for text in I1: the glyph atlas
|
||||
(`core/src/render/atlas.rs`) packs many small rasters into a handful of
|
||||
shared 1024×1024 pages and samples them by UV offset, so **text needs
|
||||
none of the three features above** — only ordinary single-texture
|
||||
sampling. The same technique generalizes to images: route an `Image`
|
||||
widget through a shared atlas when it is small enough to pack (thumbnails,
|
||||
downscaled attachment previews, avatars, icons), and fall back to one
|
||||
ordinary, non-array texture bind group — selected per batched draw call
|
||||
the way every immediate-mode 2D renderer already does — for anything too
|
||||
large to atlas well (a photo opened at full resolution). Either path is
|
||||
plain Vulkan 1.0 / GLES texture sampling, so it removes the descriptor-
|
||||
indexing requirement from iris's device request entirely, which is also
|
||||
what would make the emulator work regardless of the wgpu-hal question
|
||||
above: a device that never asks for the feature cannot be refused for
|
||||
lacking it. This is a change to iris's rendering core — the shader's
|
||||
binding group layout, `Textures`, the texture and glyph primitives, and
|
||||
`ui/painter.rs` — so it is written here as a recommendation rather than
|
||||
started, per the project's rule to confirm a load-bearing design change
|
||||
before making it. **It should be resolved before I2 is called done**,
|
||||
since I2's pass condition is the phone, not just the emulator, and this
|
||||
is exactly the kind of thing that passes on a desktop GPU and fails
|
||||
silently on real hardware.
|
||||
|
||||
- [ ] **I2 — iris on android-view.** An `android-view` surface as a second
|
||||
backend beside winit: `wgpu` on the view's surface (GLES here, see
|
||||
the Vulkan section; Vulkan on the phone), touch as pointer events,
|
||||
@@ -895,9 +1003,12 @@ re-derived:
|
||||
5. Run the existing rigs rather than inventing new ones: `ui-sandbox.sh`
|
||||
for a server with fixtures, `transcript-bench.sh` for the scroll
|
||||
baseline, `ui-trace` for anything positional, `emu up` for the
|
||||
emulator, and `iris/run-headless.sh EXAMPLE --shot PNG` for an iris
|
||||
example on this displayless machine. The Vulkan section below says how
|
||||
to get a Vulkan path in the emulator when a `wgpu` backend needs one.
|
||||
emulator, `iris/run-headless.sh EXAMPLE --shot PNG` for an iris
|
||||
example on this displayless machine, and `rigs/gpu-probe` to ask a
|
||||
device (this VM, the emulator, or a real phone over `adb push`) what
|
||||
`wgpu` features and limits it actually has before building anything on
|
||||
the assumption it does. The Vulkan section below says how to get a
|
||||
Vulkan path in the emulator when a `wgpu` backend needs one.
|
||||
6. **Bound anything heavy at the moment you start it.** An emulator or a
|
||||
long build gets a deadline — `timeout`, or a watchdog scoped to the pid
|
||||
you just started — rather than a plan to stop it later. Scope it to
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target
|
||||
Generated
+1188
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "gpu-probe"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# Deliberately its own crate rather than a member of iris's workspace: the
|
||||
# vendored `iris/` tree is meant to stay reconcilable with the iris/iris
|
||||
# repository, and this is a rig belonging to ai-app.
|
||||
|
||||
[dependencies]
|
||||
# Pinned to what iris asks for, so the answer is about iris rather than
|
||||
# about a different wgpu.
|
||||
wgpu = "28.0.0"
|
||||
pollster = "0.4.0"
|
||||
# Queried directly, because wgpu and `cmd gpu vkjson` disagreed about
|
||||
# descriptor indexing in the emulator and only the raw call says which is
|
||||
# right.
|
||||
ash = "0.38"
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Ask a device whether it can give iris the GPU it asks for.
|
||||
//!
|
||||
//! iris's renderer binds every texture it has drawn as one binding array and
|
||||
//! indexes it non-uniformly from the shader, which needs descriptor indexing
|
||||
//! and a very large per-stage binding-array limit (101,000 elements: 100,000
|
||||
//! textures and 1,000 samplers, `UiLimits::default`). Those are ordinary on a
|
||||
//! desktop and not obviously available on a phone, so this reports what the
|
||||
//! adapter offers before anything is built on the assumption.
|
||||
//!
|
||||
//! It runs as a plain executable with no window and no APK, because
|
||||
//! `request_adapter` needs no surface -- so it can be pushed to a device with
|
||||
//! `adb push` and run from `/data/local/tmp`, which is far cheaper than an
|
||||
//! app. What it therefore cannot answer is anything about presenting to a
|
||||
//! surface; that is the Android backend's own problem.
|
||||
|
||||
mod vk;
|
||||
|
||||
use wgpu::*;
|
||||
|
||||
/// What `iris/src/default/render.rs` asks `request_device` for.
|
||||
fn iris_features() -> Features {
|
||||
Features::TEXTURE_BINDING_ARRAY
|
||||
| Features::PARTIALLY_BOUND_BINDING_ARRAY
|
||||
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
|
||||
}
|
||||
|
||||
/// `UiLimits::default()`: 100,000 textures + 1,000 samplers.
|
||||
const IRIS_MAX_BINDING_ARRAY: u32 = 101_000;
|
||||
const IRIS_MAX_BINDING_ARRAY_SAMPLERS: u32 = 1_000;
|
||||
|
||||
fn main() {
|
||||
vk::report();
|
||||
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::from_env().unwrap_or(Backends::PRIMARY),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let adapters = pollster::block_on(instance.enumerate_adapters(Backends::all()));
|
||||
println!("adapters: {}", adapters.len());
|
||||
for adapter in &adapters {
|
||||
let info = adapter.get_info();
|
||||
println!(
|
||||
" {:?} {} ({:?})",
|
||||
info.backend, info.name, info.device_type
|
||||
);
|
||||
}
|
||||
|
||||
let Some(adapter) = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
}))
|
||||
.ok() else {
|
||||
println!("\nNO ADAPTER");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let info = adapter.get_info();
|
||||
println!(
|
||||
"\nchosen: {:?} {} ({:?})",
|
||||
info.backend, info.name, info.device_type
|
||||
);
|
||||
println!("driver: {} {}", info.driver, info.driver_info);
|
||||
|
||||
let have = adapter.features();
|
||||
println!("\nfeatures iris requires:");
|
||||
let mut missing = Features::empty();
|
||||
for f in iris_features().iter() {
|
||||
let ok = have.contains(f);
|
||||
println!(
|
||||
" {:60} {}",
|
||||
format!("{f:?}"),
|
||||
if ok { "yes" } else { "NO" }
|
||||
);
|
||||
if !ok {
|
||||
missing |= f;
|
||||
}
|
||||
}
|
||||
|
||||
let limits = adapter.limits();
|
||||
println!("\nlimits iris requires:");
|
||||
for (name, want, got) in [
|
||||
(
|
||||
"max_binding_array_elements_per_shader_stage",
|
||||
IRIS_MAX_BINDING_ARRAY,
|
||||
limits.max_binding_array_elements_per_shader_stage,
|
||||
),
|
||||
(
|
||||
"max_binding_array_sampler_elements_per_shader_stage",
|
||||
IRIS_MAX_BINDING_ARRAY_SAMPLERS,
|
||||
limits.max_binding_array_sampler_elements_per_shader_stage,
|
||||
),
|
||||
] {
|
||||
println!(
|
||||
" {name:52} want {want:>7} have {got:>7} {}",
|
||||
if got >= want { "ok" } else { "TOO SMALL" }
|
||||
);
|
||||
}
|
||||
println!(
|
||||
" {:52} want {:>7} have {:>7}",
|
||||
"max_buffer_size (iris asks 1<<30)",
|
||||
1u64 << 30,
|
||||
limits.max_buffer_size
|
||||
);
|
||||
|
||||
// The question that actually matters: does the device iris builds come
|
||||
// back, or does wgpu refuse it?
|
||||
let mut wanted = Limits {
|
||||
max_binding_array_elements_per_shader_stage: IRIS_MAX_BINDING_ARRAY,
|
||||
max_binding_array_sampler_elements_per_shader_stage: IRIS_MAX_BINDING_ARRAY_SAMPLERS,
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
};
|
||||
match pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
||||
required_features: iris_features(),
|
||||
required_limits: wanted.clone(),
|
||||
..Default::default()
|
||||
})) {
|
||||
Ok(_) => println!("\nIRIS DEVICE: ok"),
|
||||
Err(e) => println!("\nIRIS DEVICE: FAILED -- {e}"),
|
||||
}
|
||||
|
||||
// If it failed, say how far down it has to be turned before it works, so
|
||||
// the report names a number to design against rather than just "no".
|
||||
if missing.is_empty() {
|
||||
for cap in [
|
||||
limits.max_binding_array_elements_per_shader_stage,
|
||||
1024,
|
||||
128,
|
||||
16,
|
||||
] {
|
||||
if cap >= IRIS_MAX_BINDING_ARRAY {
|
||||
continue;
|
||||
}
|
||||
wanted.max_binding_array_elements_per_shader_stage = cap;
|
||||
wanted.max_binding_array_sampler_elements_per_shader_stage =
|
||||
cap.min(IRIS_MAX_BINDING_ARRAY_SAMPLERS);
|
||||
let ok = pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
||||
required_features: iris_features(),
|
||||
required_limits: wanted.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.is_ok();
|
||||
println!(
|
||||
" binding array capped at {cap:>7}: {}",
|
||||
if ok { "ok" } else { "no" }
|
||||
);
|
||||
if ok {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//! The raw Vulkan half of the probe.
|
||||
//!
|
||||
//! wgpu reports a feature only after a chain of its own decisions -- which
|
||||
//! physical device, which API version, which extension list -- so "wgpu says
|
||||
//! no" and "the driver says no" are different claims. This asks
|
||||
//! `vkGetPhysicalDeviceFeatures2` itself and prints the inputs to that chain,
|
||||
//! so a disagreement can be attributed rather than guessed at.
|
||||
|
||||
use ash::{Entry, vk};
|
||||
use std::ffi::CStr;
|
||||
|
||||
pub fn report() {
|
||||
let entry = match unsafe { Entry::load() } {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
println!("\nraw vulkan: cannot load loader -- {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let instance_version = match unsafe { entry.try_enumerate_instance_version() } {
|
||||
Ok(Some(v)) => v,
|
||||
Ok(None) => vk::API_VERSION_1_0,
|
||||
Err(e) => {
|
||||
println!("\nraw vulkan: enumerate_instance_version failed -- {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
println!("\nraw vulkan:");
|
||||
println!(" loader instance version: {}", ver(instance_version));
|
||||
|
||||
// Ask for the highest instance version the loader admits to: wgpu clamps
|
||||
// the device version by the instance's, so an instance created at 1.0
|
||||
// makes a 1.3 device look like 1.0.
|
||||
let app_info = vk::ApplicationInfo::default().api_version(instance_version);
|
||||
let create = vk::InstanceCreateInfo::default().application_info(&app_info);
|
||||
let instance = match unsafe { entry.create_instance(&create, None) } {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
println!(" create_instance failed -- {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let devices = unsafe { instance.enumerate_physical_devices() }.unwrap_or_default();
|
||||
for phd in devices {
|
||||
let props = unsafe { instance.get_physical_device_properties(phd) };
|
||||
let name = unsafe { CStr::from_ptr(props.device_name.as_ptr()) };
|
||||
println!(" device: {}", name.to_string_lossy());
|
||||
println!(" device api version: {}", ver(props.api_version));
|
||||
|
||||
let exts =
|
||||
unsafe { instance.enumerate_device_extension_properties(phd) }.unwrap_or_default();
|
||||
let has_ext = exts.iter().any(|e| {
|
||||
(unsafe { CStr::from_ptr(e.extension_name.as_ptr()) }) == c"VK_EXT_descriptor_indexing"
|
||||
});
|
||||
println!(" VK_EXT_descriptor_indexing advertised: {has_ext}");
|
||||
println!(" device extensions: {}", exts.len());
|
||||
|
||||
let mut indexing = vk::PhysicalDeviceDescriptorIndexingFeatures::default();
|
||||
let mut features2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut indexing);
|
||||
unsafe { instance.get_physical_device_features2(phd, &mut features2) };
|
||||
|
||||
for (name, v) in [
|
||||
(
|
||||
"shaderSampledImageArrayNonUniformIndexing",
|
||||
indexing.shader_sampled_image_array_non_uniform_indexing,
|
||||
),
|
||||
(
|
||||
"descriptorBindingSampledImageUpdateAfterBind",
|
||||
indexing.descriptor_binding_sampled_image_update_after_bind,
|
||||
),
|
||||
(
|
||||
"shaderStorageImageArrayNonUniformIndexing",
|
||||
indexing.shader_storage_image_array_non_uniform_indexing,
|
||||
),
|
||||
(
|
||||
"descriptorBindingStorageImageUpdateAfterBind",
|
||||
indexing.descriptor_binding_storage_image_update_after_bind,
|
||||
),
|
||||
(
|
||||
"shaderStorageBufferArrayNonUniformIndexing",
|
||||
indexing.shader_storage_buffer_array_non_uniform_indexing,
|
||||
),
|
||||
(
|
||||
"descriptorBindingStorageBufferUpdateAfterBind",
|
||||
indexing.descriptor_binding_storage_buffer_update_after_bind,
|
||||
),
|
||||
(
|
||||
"descriptorBindingPartiallyBound",
|
||||
indexing.descriptor_binding_partially_bound,
|
||||
),
|
||||
] {
|
||||
println!(" {name:48} {}", if v != 0 { "yes" } else { "NO" });
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { instance.destroy_instance(None) };
|
||||
}
|
||||
|
||||
fn ver(v: u32) -> String {
|
||||
format!(
|
||||
"{}.{}.{}",
|
||||
vk::api_version_major(v),
|
||||
vk::api_version_minor(v),
|
||||
vk::api_version_patch(v)
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user