Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Indexed drawing #42

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions citro3d/examples/cube.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might call this cube-indexed.rs or something just to make it clear that it uses an index buffer as compared to the existing triangle xample

Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
//! This example demonstrates the most basic usage of `citro3d`: rendering a simple
//! RGB triangle (sometimes called a "Hello triangle") to the 3DS screen.

#![feature(allocator_api)]

use citro3d::macros::include_shader;
use citro3d::math::{
AspectRatio, ClipPlanes, CoordinateOrientation, FVec3, Matrix4, Projection, StereoDisplacement,
};
use citro3d::render::ClearFlags;
use citro3d::{attrib, buffer, render, shader};
use citro3d::{texenv, IndexType};
use ctru::prelude::*;
use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D};

#[repr(C)]
#[derive(Copy, Clone)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}

impl Vec3 {
const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
}

#[repr(C)]
#[derive(Copy, Clone)]
struct Vertex {
pos: Vec3,
color: Vec3,
}

// borrowed from https://bevyengine.org/examples/3D%20Rendering/generate-custom-mesh/
const VERTS: &[[f32; 3]] = &[
// top (facing towards +y)
[-0.5, 0.5, -0.5], // vertex with index 0
[0.5, 0.5, -0.5], // vertex with index 1
[0.5, 0.5, 0.5], // etc. until 23
[-0.5, 0.5, 0.5],
// bottom (-y)
[-0.5, -0.5, -0.5],
[0.5, -0.5, -0.5],
[0.5, -0.5, 0.5],
[-0.5, -0.5, 0.5],
// right (+x)

Check warning on line 49 in citro3d/examples/cube.rs

View workflow job for this annotation

GitHub Actions / lint (nightly-2023-06-01)

Diff in /__w/citro3d-rs/citro3d-rs/citro3d/examples/cube.rs
[0.5, -0.5, -0.5],
[0.5, -0.5, 0.5],
[0.5, 0.5, 0.5], // This vertex is at the same position as vertex with index 2, but they'll have different UV and normal
[0.5, 0.5, -0.5],
// left (-x)
[-0.5, -0.5, -0.5],
[-0.5, -0.5, 0.5],
[-0.5, 0.5, 0.5],
[-0.5, 0.5, -0.5],
// back (+z)
[-0.5, -0.5, 0.5],
[-0.5, 0.5, 0.5],
[0.5, 0.5, 0.5],
[0.5, -0.5, 0.5],
// forward (-z)
[-0.5, -0.5, -0.5],
[-0.5, 0.5, -0.5],
[0.5, 0.5, -0.5],
[0.5, -0.5, -0.5],
];

static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica");
const CLEAR_COLOR: u32 = 0x68_B0_D8_FF;

fn main() {
let mut soc = Soc::new().expect("failed to get SOC");
drop(soc.redirect_to_3dslink(true, true));

let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller");

let mut instance = citro3d::Instance::new().expect("failed to initialize Citro3D");

let top_screen = TopScreen3D::from(&gfx.top_screen);

let (mut top_left, mut top_right) = top_screen.split_mut();

let RawFrameBuffer { width, height, .. } = top_left.raw_framebuffer();
let mut top_left_target =
render::Target::new(width, height, top_left, None).expect("failed to create render target");

let RawFrameBuffer { width, height, .. } = top_right.raw_framebuffer();
let mut top_right_target = render::Target::new(width, height, top_right, None)
.expect("failed to create render target");

let mut bottom_screen = gfx.bottom_screen.borrow_mut();
let RawFrameBuffer { width, height, .. } = bottom_screen.raw_framebuffer();

let mut bottom_target = render::Target::new(width, height, bottom_screen, None)
.expect("failed to create bottom screen render target");

let shader = shader::Library::from_bytes(SHADER_BYTES).unwrap();
let vertex_shader = shader.get(0).unwrap();

let program = shader::Program::new(vertex_shader).unwrap();
instance.bind_program(&program);
let mut vbo_data = Vec::with_capacity_in(VERTS.len(), ctru::linear::LinearAllocator);
for vert in VERTS.iter().enumerate().map(|(i, v)| Vertex {
pos: Vec3 {
x: v[0],
y: v[1],
z: v[2],
},
color: Vec3::new(1.0, 0.7, 0.5),
}) {
vbo_data.push(vert);
}

let mut buf_info = buffer::Info::new();
let (attr_info, vbo_data) = prepare_vbos(&mut buf_info, &vbo_data);

// Configure the first fragment shading substage to just pass through the vertex color
// See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight
let stage0 = texenv::Stage::new(0).unwrap();
instance
.texenv(stage0)
.src(texenv::Mode::BOTH, texenv::Source::PrimaryColor, None, None)
.func(texenv::Mode::BOTH, texenv::CombineFunc::Replace);

let projection_uniform_idx = program.get_uniform("projection").unwrap();
let camera_transform = Matrix4::looking_at(
FVec3::new(1.8, 1.8, 1.8),
FVec3::new(0.0, 0.0, 0.0),
FVec3::new(0.0, 1.0, 0.0),
CoordinateOrientation::RightHanded,
);
let indecies_a = [
0, 3, 1, 1, 3, 2, // triangles making up the top (+y) facing side.
4, 5, 7, 5, 6, 7, // bottom (-y)
8, 11, 9, 9, 11, 10, // right (+x)
12, 13, 15, 13, 14, 15, // left (-x)
16, 19, 17, 17, 19, 18, // back (+z)
20, 21, 23, 21, 22, 23, // forward (-z)
];
let mut indecies = Vec::with_capacity_in(indecies_a.len(), ctru::linear::LinearAllocator);
indecies.extend_from_slice(&indecies_a);
Comment on lines +145 to +146
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to enforce lifetimes for index buffers we could have something like buffer::Info::index_buffer(&[usize]). I think if we had that it could resolve at least partially some of the lifetime issues, since Slice already captures the lifetime of the vertex buffer. Then I think the code would look something like this?

let (attr_info, _vbo_data) = prepare_vbos(&mut buf_info, &vbo_data);

let mut index_buffer = buf_info.index_buffer();
// This possibly also checks that all  the new indices are valid for the vertex data:
index_buffer.extend_from_slice(&indices_a);

Edit: actually, I guess we'd also need to make sure the vertex buffer was added at some point, so maybe this would be a method on buffer::Slice instead, or a new buffer::Info::add_indexed(vbo_data, attrib_info, indices) or something. I'm not sure what the best API would be but maybe something along those lines


while apt.main_loop() {
hid.scan_input();

if hid.keys_down().contains(KeyPad::START) {
break;
}

instance.render_frame_with(|instance| {
let mut render_to = |target: &mut render::Target, projection| {
target.clear(ClearFlags::ALL, CLEAR_COLOR, 0);

instance
.select_render_target(target)
.expect("failed to set render target");

instance.bind_vertex_uniform(
projection_uniform_idx,
&(projection * camera_transform.clone()),
);

instance.set_attr_info(&attr_info);
unsafe {
instance.draw_elements(
buffer::Primitive::Triangles,
&buf_info,
IndexType::U16(&indecies),
);
}

//instance.draw_arrays(buffer::Primitive::Triangles, vbo_data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just leftover from copy-pasting the existing triangle example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, good catch

};

let Projections {
left_eye,
right_eye,
center,
} = calculate_projections();

render_to(&mut top_left_target, &left_eye);
render_to(&mut top_right_target, &right_eye);
render_to(&mut bottom_target, &center);
});
}
}

fn prepare_vbos<'a>(
buf_info: &'a mut buffer::Info,
vbo_data: &'a [Vertex],
) -> (attrib::Info, buffer::Slice<'a>) {
// Configure attributes for use with the vertex shader
let mut attr_info = attrib::Info::new();

let reg0 = attrib::Register::new(0).unwrap();
let reg1 = attrib::Register::new(1).unwrap();

attr_info
.add_loader(reg0, attrib::Format::Float, 3)
.unwrap();

attr_info
.add_loader(reg1, attrib::Format::Float, 3)
.unwrap();

let buf_idx = buf_info.add(vbo_data, &attr_info).unwrap();

(attr_info, buf_idx)
}

struct Projections {
left_eye: Matrix4,
right_eye: Matrix4,
center: Matrix4,
}

fn calculate_projections() -> Projections {
// TODO: it would be cool to allow playing around with these parameters on
// the fly with D-pad, etc.
let slider_val = ctru::os::current_3d_slider_state();
let interocular_distance = slider_val / 2.0;

let vertical_fov = 40.0_f32.to_radians();
let screen_depth = 2.0;

let clip_planes = ClipPlanes {
near: 0.01,
far: 100.0,
};

let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth);

let (left_eye, right_eye) =
Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes)
.stereo_matrices(left, right);

let center =
Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into();

Projections {
left_eye,
right_eye,
center,
}
}
66 changes: 65 additions & 1 deletion citro3d/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ pub mod render;
pub mod shader;
pub mod texenv;
pub mod uniform;
mod util;

use std::cell::OnceCell;
use std::fmt;

pub use error::{Error, Result};
use util::is_linear_ptr;

use self::texenv::TexEnv;
use self::uniform::Uniform;
Expand Down Expand Up @@ -152,7 +154,6 @@ impl Instance {
self.set_buffer_info(vbo_data.info());

// TODO: should we also require the attrib info directly here?

unsafe {
citro3d_sys::C3D_DrawArrays(
primitive as ctru_sys::GPU_Primitive_t,
Expand All @@ -161,6 +162,44 @@ impl Instance {
);
}
}
/// Indexed drawing
///
/// Draws the vertices in `buf` indexed by `indices`. `indices` must be linearly allocated
///
/// # Safety
/// If `indices` goes out of scope before the current frame ends it will cause a use-after-free (possibly by the GPU)
/// If `buf` does not contain all the vertices references by `indices` it will cause an invalid access by the GPU (this crashes citra)
Comment on lines +169 to +171
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm fine with having a new unsafe API for now, but definitely would like to see if we can solve this with the type system somehow. I mentioned a possible option for the second issue in my other comment and I think the first issue could be solved with other stuff for #41 so I think we at least have a path forward.

///
/// # Panics
/// If `indices` is not allocated in linear memory
#[doc(alias = "C3D_DrawElements")]
pub unsafe fn draw_elements<'a>(
&mut self,
primitive: buffer::Primitive,
buf: &buffer::Info,
indices: impl Into<IndexType<'a>>,
) {
self.set_buffer_info(buf);
let indices: IndexType<'a> = indices.into();
let elements = match indices {
IndexType::U16(v) => v.as_ptr() as *const _,
IndexType::U8(v) => v.as_ptr() as *const _,
};
assert!(
is_linear_ptr(elements),
"draw_elements requires linear allocated indices buffer"
);
ian-h-chamberlain marked this conversation as resolved.
Show resolved Hide resolved
citro3d_sys::C3D_DrawElements(
primitive as ctru_sys::GPU_Primitive_t,
indices.len() as i32,
// flag bit for short or byte
match indices {
IndexType::U16(_) => citro3d_sys::C3D_UNSIGNED_SHORT,
IndexType::U8(_) => citro3d_sys::C3D_UNSIGNED_BYTE,
} as i32,
elements,
);
}

/// Use the given [`shader::Program`] for subsequent draw calls.
pub fn bind_program(&mut self, program: &shader::Program) {
Expand Down Expand Up @@ -237,3 +276,28 @@ impl Drop for Instance {
}
}
}

pub enum IndexType<'a> {
U16(&'a [u16]),
U8(&'a [u8]),
}
impl IndexType<'_> {
fn len(&self) -> usize {
match self {
IndexType::U16(a) => a.len(),
IndexType::U8(a) => a.len(),
}
}
}

impl<'a> From<&'a [u8]> for IndexType<'a> {
fn from(v: &'a [u8]) -> Self {
Self::U8(v)
}
}

impl<'a> From<&'a [u16]> for IndexType<'a> {
fn from(v: &'a [u16]) -> Self {
Self::U16(v)
}
}
6 changes: 6 additions & 0 deletions citro3d/src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Check if pointer is in linear memory
pub fn is_linear_ptr<P>(p: *const P) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem useful to have — I've also thought about having some kind of type safety around this, e.g.

trait Linear {}

impl<T> Linear for Vec<T, LinearAllocator> {}

The problem is that other APIs would end up with somewhat inconvenient definitions, e.g.

    pub fn add<'this, 'a, T, V>(
        &'this mut self,
        vbo_data: &T,
        attrib_info: &attrib::Info,
    ) -> crate::Result<Slice<'a>>
    where
        'this: 'a,
        T: Deref<Target = [V]> + Linear,
    {

But maybe the type safety would be worth it and then we wouldn't need panics or Err types for all these things that require linear allocation.

let addr = p as usize;
addr >= ctru_sys::OS_FCRAM_VADDR as usize
&& addr < (ctru_sys::OS_FCRAM_VADDR as usize + ctru_sys::OS_FCRAM_SIZE as usize)
}
Loading