Skip to content

v0.12.6: Binding and renderer fixes

v0.12.6 is a patch release that fixes crashes, memory leaks, and wrong output, and closes parity gaps between the bindings. Every documented example now compiles and runs in Rust, JavaScript, Python, Swift, and Kotlin. The website opens on a playground where you edit a shader and watch it rebuild. Some fixes change behavior that existing code may rely on, and they come last, under Breaking and behavior changes.

Pass.set_viewport takes effect. A pass draws only inside its region and crops the part past the target, while the clear color still fills the whole target. Earlier releases ignored the viewport. Passes in one render call also keep their own uniforms instead of all drawing with the last pass’s values.

Three fixes cut memory use and per-frame work. FragmentColor frees passes, shaders, and their storage buffers when the last handle drops, which ends a leak of about 30 MB per rebuild in a simulation of a million particles. Storage buffers upload only when created, resized, or changed by set. At eight million particles, the old upload copied about 256 MB a frame. Shaders that share a source get one render pipeline per vertex layout, and compute passes reuse theirs. In 0.12.5, a mesh with a second vertex layout drew wrong vertices, and every frame leaked memory and compiled new compute pipelines that it never freed.

  • After a timeout or failure, get_image and read_storage drop their pooled buffer, and later readbacks no longer hit BufferStillMapped.
  • A render made only of compute passes acquires no surface frame and keeps running while a window is minimized.
  • In a shader with two @vertex or @fragment functions, render pipelines take the first of each. They took the last.
  • A mesh whose instance list shrinks draws only the instances it still holds.
  • Vertices or instances that disagree on an attribute’s format or length make the mesh fail to attach. Such a mismatch once shifted the attributes after it. A value added after the attach fails the render with MeshError.
  • Scene.load reads glTF files with application attributes such as _ID.

Shader.set follows the same rules in every language. Integers must be whole and in range, vectors and matrices take exact counts, and errors read like resolution: expected vec2<f32>, got 3 number(s).

  • Integer uniforms work on the web, and mat2x2 in JavaScript, Swift, and Kotlin. Indexed keys get the type check, Python raises on a list of the wrong length, and Shader.get returns current values. On a struct root it returned the defaults.
  • mat3x3<f32> values fill 48 bytes with WGSL’s column padding. Packed into 36, the last two columns shifted.
  • On the web, Shader.set binds only a real Texture and throws for any other object. It used to read that object’s memory as a texture.
  • Storage buffers hold atomic<u32> and atomic<i32>, and another shader can read them as array<u32, N>.
  • Shader.set_registry works on native builds, with the embedded library as fallback. Full URLs into the default registry use it too, which spares Swift and Kotlin their network errors.
  • The registry’s sdf2d/heart returns Inigo Quilez’s exact signed distance, and outlines and glows trace the shape. pattern/hexgrid draws hexagons instead of tall, overlapping cells.

Sampler options take effect at creation and after set_sampler_options, which 0.12.5 ignored. A sampler picks up its group’s texture options, PBR’s shared sampler follows the base color map, and sampler_comparison defaults to LessEqual.

  • Texture.write and write_region take tightly packed rows. A 2×2 RGBA write needs 16 bytes, down from 264. Padded rows still upload, and bytes_per_row only has to cover the row.
  • create_texture honors TextureOptions.usage: TextureUsage::COPY_SRC makes a texture readable. read_texture failed on every texture create_texture made.
  • Python texture methods read NumPy arrays in row-major order. Fortran-ordered arrays uploaded scrambled pixels, and flipped or sliced views raised ndarray must be contiguous.
  • Swift createTexture(pixels, size) builds a full mip chain and throws for a size of other than 2 or 3 numbers. In 0.12.5 it decoded the pixels as an image.

Android renderer.createTarget(surface) works again. Every call since 0.12.3 ended in Failed to create surface. The target lives on the calling renderer, and render no longer fails with NoContext.

  • Swift and Kotlin pass.setDepthTarget(texture) hand Rust the Texture itself. They passed its handle as a TextureTarget, which is undefined behavior.
  • iOS createTarget throws Init for a null CAMetalLayer pointer instead of reading through it.
  • Model(mesh, material) and model.material() share the Material in every binding. A rebuilt copy reset its alpha mode and drew Mask and Blend materials wrong.
  • On Swift and Kotlin, and for a list on the web, Pass.require links each dependency once. The docs example failed on mobile with DuplicateDependency: color. Python render runs the passes a Pass requires.
  • In the browser, scene.setPasses copies its handles, which ends the Attempt to use a moved value error on later calls. render records a list as one frame rather than letting its last item replace the others, and load and WindowTarget.getImage() stop throwing.
  • Python Renderer.create_target keeps one target per renderer instead of sharing a canvas’s target across devices.
  • Swift writeRegion takes 2, 4, or 6 numbers and throws for other arrays, which it ignored. Vertex.set and Instance.set throw for a list of other than 1 to 4 numbers, and TextureRegion.from traps with a message on an array it can’t read. All three misread such input before.

Every binding uses Rust’s type names, such as WindowTarget and TextureRegion. CanvasTarget, RenderCanvasTarget, TextureRegionMobile, MobileWindowTarget, and MobileTextureTarget remain aliases.

  • Pass.add and the Scene builders return the receiver, and scene.add(model).add(camera) chains. Pass.require takes a Scene. ScreenRegion(origin, size), [x, y, w, h] viewports, and [r, g, b] clear colors work everywhere.
  • Shader.set accepts each platform’s number types, matrices flat or as columns, raw bytes for storage and push-constant roots, and bools as 1 or 0. Rust’s set("scale", 2) fills an f32, and Shader::get reads matrices and bytes.
  • Swift and Kotlin createTexture takes bytes alone or with a format, a [w, h] size, or options, and every binding has the TextureUsage flags. In Rust, path and URL strings work, the web reads every option, and JavaScript and Python load KTX2.
  • Python has one readable Size class, and texture.size.width no longer raises AttributeError. The binding also builds SamplerOptions and CompareFunction and compares value types by value.
  • A number or list of numbers on a Vertex or Instance takes the type the shader declares at its location when the mesh attaches, as uniforms do. vertex.set("id", 7) fills an id: u32 input in every language, and a value that doesn’t fit fails the attach with an error like Mesh attribute id: expected u32, got -1 at vertex 2.
  • Vertex.UV0 and the other constants reach every binding, along with (position, uv, color) vertices. Python render takes a Mesh, and Scene.load works from .glb bytes and can skip cameras or lights.
  • Material texture setters take any createTexture source. Kotlin scene types accept Double, and the TypeScript typings allow plain arrays.

The iOS and Android healthchecks run each documented example as a test named after its docs/api page, rather than only compiling it. Uncaptured GPU errors go to the file that FC_GPU_ERROR_LOG names and fail the iOS, Android, and Python healthchecks.

Generated examples match their Rust source, and the build removes those of deleted pages. Kotlin’s new createTexture(pixels, listOf(w, h)) fixes its examples, which sent raw pixels to the image decoder and failed.

On this site, method links open their heading instead of a 404, and each method’s anchor is its name, as in /api/scene/scene#remove_pass. Instance and Mipmap.level get pages of their own, and four missing method pages reach the site. The regenerated Python stub gains Scene, Mipmap, and the other types it lacked.

The playground on the website rebuilds the shader as you edit the WGSL and JavaScript, derives controls from its uniforms, and shows the same program in Rust, Python, Swift, and Kotlin. It also holds the shader artworks and two compute demos with a million particles each, which need WebGPU. /demos redirects to the home page.

In the particles example, particle life grows on a grid: four species follow or flee each other’s density, and the mouse, R, C, and the arrow keys steer it. The three broken particle examples are gone.

Each release tag pins its own xcframework. A dependency declared from: "0.12.5" got 0.12.4’s binary. Move Swift dependencies to 0.12.6. SwiftPM compiles a regenerated wrapper. The committed one predated 0.12.0 and lacked Scene, Model, and the other 0.12 types.

  • FragmentColor builds from read-only sources, and docs.rs documents it again, having built no docs for 0.10.7 through 0.12.5.
  • Python wheels require numpy>=1.16. Without it, TextureTarget.get_image panicked.
  • The npm package drops the test harness’s WasmBindgenTestContext and __wbgtest_* exports.

These changes can break code written against 0.12.5.

Rust

  • Vertex::set and Instance::set are #[must_use]: use their result. They take impl Into<VertexData>, a transport type with no page of its own, in place of VertexValue. An f32 or u32 keeps its type and must match the input, and an unsuffixed literal, f64, or i32 takes the shader’s type.
  • PassError::NoCompatibleShader carries the reason the pass’s last shader gave, and a pass with no shader returns PassError::NoShader.
  • Items that were public by accident leave the API: the built-in shader sources, the wgpu device setup helpers, RenderContext, the buffer and texture pools, TextureData::DynamicImage, and the glam and wgpu conversions of ScreenRegion and UniformData. Pass v.to_array() where code passed a glam vector.

JavaScript

  • Methods throw Error objects. Most threw bare strings. Read e.message instead of comparing the caught value to a string.
  • Invalid source makes new Shader(source) throw and Shader.fetch reject. Both returned the red fallback, which Shader.default() still gives. Paths and slugs need Shader.fetch, and a live editor can keep its last good Shader.
  • Indices, counts, and setComputeDispatch sizes take whole numbers, and -1 stops wrapping to 4294967295. Pass.require, createTexture, Vertex.set, and Instance.set throw for wrong types, which they ignored or read as 0.
  • Sizes take 2 or 3 numbers, regions 2, 4, or 6, and bytes 0 to 255, each finite and non-negative. Anything else fails. JavaScript clamped or zeroed it. Python follows the same rule.

Python

  • get_image returns a uint8 array shaped (height, width, ...), layers first for array textures. TextureTarget returned the transpose: swap [x, y] indices. Texture returned flat bytes, which bytes(image) still gives.
  • Renderer and texture failures raise FragmentColorError or a subclass. Handlers that caught the plain Exception they raised, or the RuntimeError from read_storage, should catch FragmentColorError. Scene.load raises SceneLoadError, also a RuntimeError, and light setters LightError, also a ValueError.
  • Shader, texture, mesh, and Pass.require calls raise TypeError for an argument of the wrong type, where most raised FragmentColorError. Catch TypeError there. require rejects a str, and Shader.set rejects the TextureMeta that Shader.get returns.
  • Vertex.set and Instance.set raise FragmentColorError for a list of other than 1 to 4 numbers. A position passed as an instance raises TypeError instead of adding an empty instance.
  • A window target’s depth changes from 1 to None, and create_texture reads http(s) strings as URLs.
  • usage= replaces the deprecated usage_bits=. The stub drops plumbing such as passes, which leaves the bindings in 0.13.

Swift and Kotlin

  • Where Rust takes a u32 or u64, such as indices, counts, sizes, and regions, Swift takes only UInt32 or UInt64 and Kotlin only UInt or ULong. An Int variable fails to compile. Check its sign and convert with UInt32(n) or n.toUInt().
  • Swift literals compile as written, as in createTextureTarget([64, 64]), and Kotlin writes 64u and listOf(64u, 64u). The removed Int overloads trapped on or wrapped negatives. Byte lists keep their Int forms and throw outside 0 to 255.
  • A GPU that fails to start reports FragmentColorError.Init from every renderer method, where several said Render. Kotlin argument errors throw FragmentColorException in place of IllegalArgumentException.
  • The generated set(key:value:) takes UniformValue, replacing UniformData, and documented shader.set calls are unchanged. Swift addVertex and addVertices don’t throw: drop their try. Vertex.set and Instance.set do throw in Swift: add try before them.
  • Swift setViewport([x, y, w, h]) reads an origin and size instead of two corners, and the Swift compiler warns on an unused result from Instance.set and from generated builders such as noDefaults().
  • Kotlin Shader.setRegistry returns nothing instead of a throwaway Shader. Rebuild libraries compiled against 0.12.5.
  • Swift deprecates Shader.new([...]) for Shader([...]), setRegistry(baseUrl:) for setRegistry(_:), and the tuple forms of createTexture and createStorageTexture. Kotlin deprecates ShaderFetch for Shader.fetch.

Every language

  • Pass.get_input and PassInput leave the API, and Swift and Kotlin lose MobilePassInput. A pass’s settings are write-only: load_previous, set_clear_color, set_viewport, and set_compute_dispatch.
  • VertexValue leaves every binding: pass numbers. A vertex or instance attribute set from a plain number gets its type from what the shader declares at that location, and JavaScript Int32Array and Uint32Array values count as plain numbers too. The bindings sent every number as an f32.
  • The attach looks past the first vertex and instance and checks them all. The earliest shader to type an attribute fixes it for the mesh, and a shader that declares another type fails to attach.
  • A region built from a size covers exactly that size. ScreenRegion::from([w, h]) added a pixel per side, which failed write_region.
  • create_target returns InitializationError when the adapter can’t present, as on a second WebGL2 canvas, which needs its own Renderer. It used to succeed and only log.
  • Samplers with no texture in their group filter linearly instead of picking the nearest texel. glTF textures follow their glTF sampler, and UVs outside 0 to 1 tile unless the file’s sampler clamps.
  • pattern/hexgrid returns the edge distance and an integer id, x in half cells and y the row. The cell center it returned split hashed cells into stripes along the axes. Its page derives the center.

Install it with:

Terminal window
# Rust
cargo add fragmentcolor
# JavaScript
npm install fragmentcolor
# Python
pip install fragmentcolor
# Swift (Package.swift)
.package(url: "https://github.com/vista-art/fragmentcolor", from: "0.12.6")
# Kotlin (download fragmentcolor-0.12.6.aar from the GitHub Release)

Release notes: GitHub v0.12.6.

Rafael