[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:
John Doty 2023-07-08 17:54:48 -07:00
parent 12cc715873
commit 89045ccbcc
7 changed files with 287 additions and 24 deletions

View file

@ -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);
}

View file

@ -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());
}

View file

@ -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
}

View file

@ -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 })
}

View file

@ -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,