Safe Rust bindings for Apple's SpriteKit framework on macOS.
Status: v0.4.0 keeps physics contact delegates alive for as long as their world lives, sizes texture readback by pixel format, returns errors instead of
SpriteKitexceptions or Metal aborts for invalid hierarchy, joint and render calls, and keepsSKViewon the main thread. It adds action completion, block, custom and keyed actions, the full scene frame-phase delegate and physics-joint management. Every public macOSSpriteKittype, protocol and category is reachable from Rust, but this is symbol-level coverage, not selector-for-selector parity.
[dependencies]
spritekit-rs = "0.4"The library is imported as spritekit. It needs macOS 11 or later, Rust 1.82 or later, and a Swift toolchain (Xcode or the Command Line Tools), which build.rs uses to compile the bundled Swift bridge.
use apple_cf::cg::{CGPoint, CGRect, CGSize};
use apple_metal::{pixel_format, storage_mode, texture_usage, MetalDevice, TextureDescriptor};
use spritekit::{
read_texture_bytes, Color, NodeExt, RenderPassDescriptor, Renderer, Scene,
SceneScaleMode, SpriteNode,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let device = MetalDevice::system_default().expect("no Metal device");
let queue = device.new_command_queue().expect("no command queue");
let texture = device
.new_texture(TextureDescriptor {
usage: texture_usage::RENDER_TARGET | texture_usage::SHADER_READ,
storage_mode: storage_mode::SHARED,
..TextureDescriptor::new_2d(256, 256, pixel_format::BGRA8UNORM)
})
.expect("texture");
let scene = Scene::with_size(CGSize::new(256.0, 256.0)).expect("scene");
scene.set_background_color(Color::black());
scene.set_scale_mode(SceneScaleMode::AspectFit);
scene.set_anchor_point(CGPoint::new(0.5, 0.5));
let sprite = SpriteNode::with_color(Color::green(), CGSize::new(64.0, 64.0)).expect("sprite");
sprite.set_position(CGPoint::new(0.0, 0.0));
scene.add_child(&sprite)?;
let renderer = Renderer::new(&device).expect("renderer");
renderer.set_scene(Some(&scene));
let pass = RenderPassDescriptor::for_texture(&texture, Color::black()).expect("pass");
renderer.update_at_time(0.0);
let command_buffer = renderer.render(CGRect::new(0.0, 0.0, 256.0, 256.0), &queue, &pass)?;
command_buffer.commit()?;
command_buffer.wait_until_completed()?;
let pixels = spritekit::read_texture_bytes(&texture)?;
assert!(pixels.iter().any(|&b| b != 0));
Ok(())
}- Bridge architecture follows the
screencapturekit-rspattern: per-area Rust modules, per-area Swift bridge files,@_cdeclexports, retained opaque handles, and explicitsk_releasecleanup. - Scene graph coverage now spans
SKScene,SKNode,SKCameraNode,SKCropNode,SKShapeNode,SKTransformNode,SKSpriteNode,SKLabelNode,SKEffectNode,SKLightNode,SKFieldNode,SKReferenceNode, andSK3DNode, plus sharedNodeExthelpers for hierarchy, transforms, accessibility, actions, physics bodies, and reach constraints. - Animation and simulation coverage includes
SKActionplus the physics-body, playback, mixer, audio-node, and warpable categories, block and custom actions, completion handlers and keyed actions,SKPhysicsBody,SKPhysicsWorldwith joint management,SKPhysicsContact/delegate bridging,SKPhysicsJointwrappers includingSKPhysicsJointLimit,SKConstraint/SKRange, andSKKeyframeSequence. - Rendering and asset coverage includes
SKTexture,SKMutableTexturepixel-data updates,SKTextureAtlaslookup plus completion-handler and optionalasyncpreloading,SKShader,SKAttribute,SKAttributeValue,SKTransition, tile-map/tile-set APIs, warp geometry,SKView, and offlineSKRendererrendering intoapple-metaltextures. - Media coverage includes
SKAudioNodeandSKVideoNodewith asset-free AVFoundation-backed constructors for examples/tests. - The repository includes 18 numbered examples under
examples/and 26 integration test targets undertests/, including delegate-lifetime, delegate-race, action-callback, hierarchy/joint, renderer and texture-readback tests driven throughSKRenderer. - Detailed logical-area status lives in
COVERAGE.md, and the full header audit lives inCOVERAGE_AUDIT.md.
SKViewis main-thread only.View::with_framereturns an error on other threads, view calls made there do nothing and return defaults, andScene::viewreturnsNoneoff the main thread.Viewis notSend.- The SDK declares
SKPhysicsWorld.contactDelegateunretained (assign), andSpriteKitreads it on the thread that runs the simulation. The world therefore keeps every contact delegate that was set on it until the world itself is freed, so clearing or replacing a delegate from another thread while the simulation runs cannot free it under a callback. (The macOS 27 implementation happens to store the property weakly; earlier releases are not guaranteed to.) Dropping aPhysicsContactDelegatestops its callbacks at once; its bridge object and closures are freed with the world, so replacing delegates many times on one world holds one of each per delegate.SpriteKitfrees a scene's physics world on the main queue when the scene is released on another thread. - Scene and view delegates are weak references in
SpriteKit: keep the RustSceneDelegateorViewDelegatealive for as long as you want callbacks. - Delegate and action closures must be
Send + Sync, and completion closuresSend, becauseSpriteKitmay call them on its own threads. Panics are caught at the FFI boundary. Closures thatSpriteKitdrops without calling are still released. NodeExt::add_childandmove_to_parentreturn an error for children that already have a parent, for self-parenting and for cycles, instead of lettingSpriteKitraise an exception.PhysicsWorld::add_jointrejects joints that were already added and joints whose bodies are not attached to nodes in a scene, whichSpriteKitwould crash on.read_texture_bytessizes its buffer withapple_metal::bytes_per_pixeland rejects compressed, depth/stencil, framebuffer-only, non-2D and private or memoryless textures.Renderer::rendertakes aCommandQueue, encodes the frame into a new command buffer and returns it uncommitted, so no other encoder or thread can use that buffer whileSpriteKitencodes.Renderer::render_intoencodes into an existing command buffer through apple-metal'sCommandBuffer::encode_foreign: whileSpriteKitencodes, commits, enqueues and new encoders on that buffer fail withActiveEncoderon every thread. Both return an error for a command buffer that was already committed or failed or still has an apple-metal encoder open, where Metal would abort the process, and for a command buffer, renderer and render target from different Metal devices.- With the
asyncfeature,TextureAtlas::preload_async,preload_texture_atlases_asyncandpreload_texture_atlases_named_asyncresolve with an error ifSpriteKitreleases the completion handler without calling it, instead of staying pending forever. Renderer::newserializesSKRenderercreation, whose shader loading is not thread-safe. DriveRenderer::update_at_timewith a positive, increasing clock:SpriteKitdoes not advance actions or physics from a zero time base.
cargo test --all-features
cargo run --example 01_offline_render_smoke
cargo run --example 10_view_present_scene
cargo run --example 18_three_d_node_basicexamples/01_*.rsthroughexamples/18_*.rsprovide headless smoke coverage for the renderer plus every requestedSpriteKitlogical area.tests/*_area.rs, includingtests/coverage_fill_area.rs, exercise the same surfaces undercargo test.tests/main_thread.rsruns theSKViewtests on the main thread (harness = false).COVERAGE.mdrecords logical-area status, whileCOVERAGE_AUDIT.mdrecords the symbol-level header audit: a symbol counts as verified when at least one Rust API reaches it.
SKMutableTextureexposesmodify_pixel_data, andSKTextureAtlasexposes completion-handler preloading plus an optionalasyncfeature for atlas preload futures.SKKeyframeSequencewraps scalar payloads only,SKShaderwraps float uniforms only, andSKAudioNode/SKVideoNodeuse default AVFoundation-backed constructors so examples and tests stay asset-free; file and URL constructors are not wrapped.
Licensed under either of Apache-2.0 or MIT at your option.