[oden] Writable Textures and re-work TS API
Now we return Texture objects to make things a little bit more type-safe, at the cost of a small allocation (I hope!)
This commit is contained in:
parent
12cc715873
commit
89045ccbcc
7 changed files with 287 additions and 24 deletions
36
game/main.ts
36
game/main.ts
|
|
@ -1,26 +1,48 @@
|
|||
import { cls, print, spr, use_texture } from "./graphics";
|
||||
import { cls, print, spr, use_texture, Texture } from "./graphics";
|
||||
import { load_texture } from "./assets";
|
||||
import { since_start } from "./time";
|
||||
import { btn, Button } from "./input";
|
||||
// import { load_string } from "./io";
|
||||
import { new_v2, vadd, vmul, vnorm } from "./vector";
|
||||
|
||||
/// TODO: Support reload by saving and restoring state from init/update/restore.
|
||||
|
||||
/// A nice looping frame counter.
|
||||
let clock = 0;
|
||||
|
||||
let bot_sprite: number | undefined = undefined;
|
||||
let bot_sprite: Texture | undefined = undefined;
|
||||
|
||||
// Note zelda overworld is 16x8 screens
|
||||
// zelda screen is 16x11 tiles
|
||||
// from a feeling point of view this is sufficient, apparently :D
|
||||
|
||||
export function init() {
|
||||
print("Hello world!");
|
||||
let loaded = false;
|
||||
|
||||
async function load_map(path: string) {
|
||||
// print("Loading map:", path);
|
||||
// const blob = await load_string(path);
|
||||
// const map = JSON.parse(blob);
|
||||
// print("Loaded map:", map);
|
||||
}
|
||||
|
||||
function load_assets() {
|
||||
// Start this load, but then...
|
||||
load_texture("./bot.png").then((n) => {
|
||||
let texture_load = load_texture("./bot.png").then((n) => {
|
||||
print("Bot loaded at", since_start());
|
||||
bot_sprite = n;
|
||||
});
|
||||
|
||||
let map_load = load_map("./overworld.ldtk");
|
||||
|
||||
Promise.all([texture_load, map_load]).then(() => {
|
||||
loaded = true;
|
||||
print("All are loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
export function init() {
|
||||
print("Hello world!");
|
||||
load_assets();
|
||||
}
|
||||
|
||||
const friction = 0.6;
|
||||
|
|
@ -80,6 +102,10 @@ const robo_info = {
|
|||
|
||||
export function draw() {
|
||||
cls(0.1, 0.2, 0.3);
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bot_sprite != undefined) {
|
||||
// ...it gets resolved here?
|
||||
use_texture(bot_sprite);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as io from "./io.ts";
|
||||
import * as gfx from "./graphics.ts";
|
||||
|
||||
export async function load_texture(path: string): Promise<number> {
|
||||
export async function load_texture(path: string): Promise<gfx.Texture> {
|
||||
const buffer = await io.load(path);
|
||||
return gfx.create_texture(buffer, path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,16 @@ export function spr(
|
|||
core.spr(x, y, w, h, sx, sy, sw, sh);
|
||||
}
|
||||
|
||||
export class Texture {
|
||||
#id: number;
|
||||
constructor(id: number) {
|
||||
this.#id = id;
|
||||
}
|
||||
id(): number {
|
||||
return this.#id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a texture based on the loaded buffer.
|
||||
*
|
||||
|
|
@ -61,15 +71,41 @@ export function spr(
|
|||
export function create_texture(
|
||||
buffer: ArrayBuffer,
|
||||
label: string | undefined = undefined
|
||||
): number {
|
||||
return core.create_texture(buffer, label);
|
||||
): Texture {
|
||||
const id = core.create_texture(buffer, label);
|
||||
return new Texture(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the specified texture as the current texture for calls to e.g. spr().
|
||||
*
|
||||
* @param id - The identifier of the texture to use.
|
||||
* @param texture - The texture to use.
|
||||
*/
|
||||
export function use_texture(id: number) {
|
||||
core.use_texture(id);
|
||||
export function use_texture(id: Texture) {
|
||||
core.use_texture(id.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a texture that we can render to.
|
||||
*/
|
||||
export function create_writable_texture(
|
||||
width: number,
|
||||
height: number,
|
||||
label: string | undefined = undefined
|
||||
): Texture {
|
||||
return new Texture(core.create_writable_texture(width, height, label));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current render target to the screen.
|
||||
*/
|
||||
export function write_to_screen() {
|
||||
core.write_to_screen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current render target to the specified texture.
|
||||
*/
|
||||
export function write_to_texture(texture: Texture) {
|
||||
core.write_to_texture(texture.id());
|
||||
}
|
||||
|
|
|
|||
104
src/lib.rs
104
src/lib.rs
|
|
@ -1,5 +1,6 @@
|
|||
use bytemuck;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::time::Instant;
|
||||
use tracy_client::{frame_mark, set_thread_name, span};
|
||||
|
|
@ -72,6 +73,8 @@ struct State {
|
|||
sprite_bind_group_layout: wgpu::BindGroupLayout,
|
||||
sprite_textures: HashMap<u32, wgpu::BindGroup>,
|
||||
|
||||
write_textures: HashMap<u32, texture::Texture>,
|
||||
|
||||
screen_uniform: ScreenUniforms,
|
||||
screen_uniform_buffer: wgpu::Buffer,
|
||||
screen_uniform_bind_group: wgpu::BindGroup,
|
||||
|
|
@ -279,6 +282,7 @@ impl State {
|
|||
max_vertices,
|
||||
sprite_bind_group_layout,
|
||||
sprite_textures: HashMap::new(),
|
||||
write_textures: HashMap::new(),
|
||||
screen_uniform,
|
||||
screen_uniform_buffer,
|
||||
screen_uniform_bind_group,
|
||||
|
|
@ -313,14 +317,19 @@ impl State {
|
|||
fn render(&mut self, commands: Vec<GraphicsCommand>) -> Result<(), wgpu::SurfaceError> {
|
||||
let _span = span!("context render");
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// Group the commands into passes.
|
||||
let screen_view = Rc::new(
|
||||
output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
let mut last_view = screen_view.clone();
|
||||
|
||||
struct Pass {
|
||||
color: Option<[f64; 4]>,
|
||||
commands: Vec<GraphicsCommand>,
|
||||
target: Rc<wgpu::TextureView>,
|
||||
}
|
||||
let mut passes = Vec::new();
|
||||
for command in commands {
|
||||
|
|
@ -328,7 +337,9 @@ impl State {
|
|||
GraphicsCommand::Clear(cc) => passes.push(Pass {
|
||||
color: Some(cc.color),
|
||||
commands: Vec::new(),
|
||||
target: last_view.clone(),
|
||||
}),
|
||||
|
||||
GraphicsCommand::CreateTexture(ct) => {
|
||||
let texture = texture::Texture::from_image(
|
||||
&self.device,
|
||||
|
|
@ -360,12 +371,93 @@ impl State {
|
|||
|
||||
self.sprite_textures.insert(ct.id, sprite_bind_group);
|
||||
}
|
||||
|
||||
GraphicsCommand::CreateWritableTexture {
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
label,
|
||||
} => {
|
||||
let texture = texture::Texture::new_writable(
|
||||
&self.device,
|
||||
width,
|
||||
height,
|
||||
match &label {
|
||||
Some(l) => Some(&l),
|
||||
None => None,
|
||||
},
|
||||
);
|
||||
let sprite_bind_group =
|
||||
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &self.sprite_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&texture.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&texture.sampler),
|
||||
},
|
||||
],
|
||||
label: match &label {
|
||||
Some(l) => Some(&l),
|
||||
None => None,
|
||||
},
|
||||
});
|
||||
|
||||
self.sprite_textures.insert(id, sprite_bind_group);
|
||||
self.write_textures.insert(id, texture);
|
||||
}
|
||||
|
||||
GraphicsCommand::WriteToTexture(id) => {
|
||||
let texture = self.write_textures.get(&id).unwrap();
|
||||
last_view = Rc::new(
|
||||
texture
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
let new_pass = match passes.last_mut() {
|
||||
Some(pass) => {
|
||||
if pass.commands.is_empty() {
|
||||
pass.target = last_view.clone();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
if new_pass {
|
||||
passes.push(Pass {
|
||||
color: None,
|
||||
commands: vec![],
|
||||
target: last_view.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsCommand::WriteToScreen => {
|
||||
if !Rc::ptr_eq(&last_view, &screen_view) {
|
||||
// If I have a pass already I need a new one.
|
||||
last_view = screen_view.clone();
|
||||
if !passes.is_empty() {
|
||||
passes.push(Pass {
|
||||
color: None,
|
||||
commands: vec![],
|
||||
target: last_view.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsCommand::EndFrame => (),
|
||||
other => match passes.last_mut() {
|
||||
Some(pass) => pass.commands.push(other),
|
||||
None => passes.push(Pass {
|
||||
color: None,
|
||||
commands: vec![other],
|
||||
target: last_view.clone(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
@ -388,7 +480,7 @@ impl State {
|
|||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
view: &pass.target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: if let Some([r, g, b, a]) = pass.color {
|
||||
|
|
@ -443,7 +535,11 @@ impl State {
|
|||
}
|
||||
|
||||
GraphicsCommand::UseTexture(id) => texture_id = Some(id),
|
||||
|
||||
GraphicsCommand::CreateTexture(_) => (), // Already handled
|
||||
GraphicsCommand::CreateWritableTexture { .. } => (), // Already handled
|
||||
GraphicsCommand::WriteToTexture(_) => (), // Already handled
|
||||
GraphicsCommand::WriteToScreen => (), // Already handled
|
||||
GraphicsCommand::Clear(_) => (), // Already handled
|
||||
GraphicsCommand::EndFrame => (), // Should never appear
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,15 @@ pub enum GraphicsCommand {
|
|||
Print(PrintCommand),
|
||||
Sprite(SpriteCommand),
|
||||
CreateTexture(CreateTextureCommand),
|
||||
CreateWritableTexture {
|
||||
id: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
label: Option<String>,
|
||||
},
|
||||
UseTexture(u32),
|
||||
WriteToTexture(u32),
|
||||
WriteToScreen,
|
||||
EndFrame,
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +117,31 @@ impl GraphicsImpl {
|
|||
fn use_texture(&self, id: u32) {
|
||||
let _ = self.sender.send(GraphicsCommand::UseTexture(id));
|
||||
}
|
||||
|
||||
fn create_writable_texture(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
label: Option<String>,
|
||||
) -> Result<u32> {
|
||||
let id = self.next_texture_id.fetch_add(1, Ordering::SeqCst);
|
||||
let _ = self.sender.send(GraphicsCommand::CreateWritableTexture {
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
label,
|
||||
});
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn write_to_screen(&self) {
|
||||
let _ = self.sender.send(GraphicsCommand::WriteToScreen);
|
||||
}
|
||||
|
||||
fn write_to_texture(&self, id: u32) {
|
||||
let _ = self.sender.send(GraphicsCommand::WriteToTexture(id));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GraphicsAPI {
|
||||
|
|
@ -168,6 +201,32 @@ impl GraphicsAPI {
|
|||
)?,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let gfx = gfx.clone();
|
||||
builder.export(
|
||||
"create_writable_texture",
|
||||
ctx.new_fn(
|
||||
move |_: &ContextRef, width: u32, height: u32, label: Option<String>| {
|
||||
gfx.create_writable_texture(width, height, label)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let gfx = gfx.clone();
|
||||
builder.export(
|
||||
"write_to_screen",
|
||||
ctx.new_fn(move |_: &ContextRef| gfx.write_to_screen())?,
|
||||
)?;
|
||||
}
|
||||
{
|
||||
let gfx = gfx.clone();
|
||||
builder.export(
|
||||
"write_to_texture",
|
||||
ctx.new_fn(move |_: &ContextRef, id: u32| gfx.write_to_texture(id))?,
|
||||
)?;
|
||||
}
|
||||
|
||||
builder.build("graphics-core")?;
|
||||
Ok(GraphicsAPI { gfx })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,46 @@ pub struct Texture {
|
|||
}
|
||||
|
||||
impl Texture {
|
||||
// pub fn from_bytes(
|
||||
// device: &wgpu::Device,
|
||||
// queue: &wgpu::Queue,
|
||||
// bytes: &[u8],
|
||||
// label: &str,
|
||||
// ) -> Result<Self> {
|
||||
// let img = image::load_from_memory(bytes)?;
|
||||
// Ok(Self::from_image(device, queue, &img, Some(label)))
|
||||
// }
|
||||
pub fn new_writable(
|
||||
device: &wgpu::Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
label: Option<&str>,
|
||||
) -> Self {
|
||||
let size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label,
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
texture,
|
||||
view,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_image(
|
||||
device: &wgpu::Device,
|
||||
|
|
@ -31,6 +62,7 @@ impl Texture {
|
|||
height: dimensions.1,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label,
|
||||
size,
|
||||
|
|
|
|||
14
types/graphics-core.d.ts
vendored
14
types/graphics-core.d.ts
vendored
|
|
@ -1,7 +1,9 @@
|
|||
// These are the functions exposed by the native graphics module.
|
||||
//
|
||||
export function cls(r: number, g: number, b: number);
|
||||
|
||||
export function print(msg: string);
|
||||
|
||||
export function spr(
|
||||
x: number,
|
||||
y: number,
|
||||
|
|
@ -12,8 +14,20 @@ export function spr(
|
|||
sw: number,
|
||||
sh: number
|
||||
);
|
||||
|
||||
export function create_texture(
|
||||
buffer: ArrayBuffer,
|
||||
label: string | undefined
|
||||
): number;
|
||||
|
||||
export function use_texture(id: number);
|
||||
|
||||
export function create_writable_texture(
|
||||
width: number,
|
||||
height: number,
|
||||
label: string | undefined
|
||||
): number;
|
||||
|
||||
export function write_to_screen();
|
||||
|
||||
export function write_to_texture(id: number);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue