use crate::render_types::TextDrawCall;
use alloc::string::String;
use concinnity_core::math::{ceil, floor};
pub fn clip_rect_to_scissor(
clip: [f32; 4],
ui: (f32, f32),
attach: (u32, u32),
) -> Option<(i32, i32, u32, u32)> {
let aw = attach.0 as f32;
let ah = attach.1 as f32;
let sx = if ui.0 > 0.0 { aw / ui.0 } else { 1.0 };
let sy = if ui.1 > 0.0 { ah / ui.1 } else { 1.0 };
let x0 = floor(clip[0] * sx).clamp(0.0, aw);
let y0 = floor(clip[1] * sy).clamp(0.0, ah);
let x1 = ceil((clip[0] + clip[2]) * sx).clamp(0.0, aw);
let y1 = ceil((clip[1] + clip[3]) * sy).clamp(0.0, ah);
if x1 <= x0 || y1 <= y0 {
return None;
}
Some((x0 as i32, y0 as i32, (x1 - x0) as u32, (y1 - y0) as u32))
}
pub fn align_up(offset: u64, align: u64) -> u64 {
(offset + align - 1) & !(align - 1)
}
pub fn text_upload_bytes(text_calls: &[TextDrawCall], align: u64) -> u64 {
text_calls
.iter()
.map(|c| {
let v = core::mem::size_of_val(c.vertices.as_slice()) as u64;
let i = core::mem::size_of_val(c.indices.as_slice()) as u64;
align_up(v, align) + align_up(i, align)
})
.sum()
}
pub trait BloomEncoder {
type Rec;
type Args;
fn bloom_mip_count(&self) -> usize;
fn begin_bloom(&self, rec: &Self::Rec, args: &Self::Args);
fn bloom_prefilter(&self, rec: &Self::Rec, args: &Self::Args);
fn bloom_downsample(&self, rec: &Self::Rec, args: &Self::Args, dst: usize);
fn bloom_upsample(&self, rec: &Self::Rec, args: &Self::Args, dst: usize);
}
pub fn encode_bloom_chain<E: BloomEncoder>(enc: &E, rec: &E::Rec, args: E::Args) {
let n = enc.bloom_mip_count();
if n == 0 {
return;
}
enc.begin_bloom(rec, &args);
enc.bloom_prefilter(rec, &args);
for dst in 1..n {
enc.bloom_downsample(rec, &args, dst);
}
for dst in (0..n - 1).rev() {
enc.bloom_upsample(rec, &args, dst);
}
}
pub trait CompositeEncoder {
type Rec;
type Args;
fn begin_composite(&self, rec: &Self::Rec, args: &Self::Args);
fn composite_draw(&self, rec: &Self::Rec, args: &Self::Args);
fn begin_text(&self, rec: &Self::Rec, args: &Self::Args) -> bool;
fn text_draw(
&self,
rec: &Self::Rec,
args: &Self::Args,
call: &TextDrawCall,
) -> Result<(), String>;
fn end_composite(&self, rec: &Self::Rec, args: &Self::Args);
}
pub fn encode_composite_chain<E: CompositeEncoder>(
enc: &E,
rec: &E::Rec,
args: &E::Args,
text_calls: &[TextDrawCall],
) -> Result<(), String> {
enc.begin_composite(rec, args);
enc.composite_draw(rec, args);
if !text_calls.is_empty() && enc.begin_text(rec, args) {
for call in text_calls {
enc.text_draw(rec, args, call)?;
}
}
enc.end_composite(rec, args);
Ok(())
}
pub trait FullscreenPass {
type Rec;
fn begin(&self, rec: &Self::Rec);
fn draw(&self, rec: &Self::Rec);
fn end(&self, rec: &Self::Rec);
}
pub fn encode_fullscreen<E: FullscreenPass>(enc: &E, rec: &E::Rec) {
enc.begin(rec);
enc.draw(rec);
enc.end(rec);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_types::TextDrawCall;
use core::cell::RefCell;
use alloc::format;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn clip_inside_attachment_passes_through() {
assert_eq!(
clip_rect_to_scissor([100.0, 50.0, 300.0, 200.0], (1280.0, 720.0), (1280, 720)),
Some((100, 50, 300, 200))
);
}
#[test]
fn clip_scales_from_logical_units_to_a_hi_dpi_attachment() {
assert_eq!(
clip_rect_to_scissor([100.0, 50.0, 300.0, 200.0], (1024.0, 768.0), (2048, 1536)),
Some((200, 100, 600, 400))
);
assert_eq!(
clip_rect_to_scissor([10.0, 10.0, 100.0, 100.0], (1000.0, 1000.0), (1500, 1500)),
Some((15, 15, 150, 150))
);
}
#[test]
fn clip_is_clamped_to_attachment_bounds() {
assert_eq!(
clip_rect_to_scissor([1200.0, 700.0, 400.0, 400.0], (1280.0, 720.0), (1280, 720)),
Some((1200, 700, 80, 20))
);
assert_eq!(
clip_rect_to_scissor([-40.0, -10.0, 100.0, 100.0], (1280.0, 720.0), (1280, 720)),
Some((0, 0, 60, 90))
);
assert_eq!(
clip_rect_to_scissor([600.0, 350.0, 200.0, 200.0], (640.0, 360.0), (1280, 720)),
Some((1200, 700, 80, 20))
);
}
#[test]
fn fully_offscreen_clip_is_skipped() {
assert_eq!(
clip_rect_to_scissor([2000.0, 50.0, 100.0, 100.0], (1280.0, 720.0), (1280, 720)),
None
);
assert_eq!(
clip_rect_to_scissor([10.0, 10.0, 0.0, 50.0], (1280.0, 720.0), (1280, 720)),
None
);
}
#[test]
fn a_zero_logical_size_falls_back_to_an_unscaled_clip() {
assert_eq!(
clip_rect_to_scissor([10.0, 20.0, 100.0, 100.0], (0.0, 0.0), (1280, 720)),
Some((10, 20, 100, 100))
);
}
fn text_call() -> TextDrawCall {
TextDrawCall {
vertices: Vec::new(),
indices: Vec::new(),
atlas_slot: 0,
clip_rect: None,
layer: 0,
}
}
fn glyph_call(glyphs: usize) -> TextDrawCall {
TextDrawCall {
vertices: vec![
crate::render_types::TextVertex {
pos: [0.0; 2],
uv: [0.0; 2],
color: [0.0; 3],
mode: 0.0,
};
glyphs * 4
],
indices: vec![0u16; glyphs * 6],
atlas_slot: 0,
clip_rect: None,
layer: 0,
}
}
#[test]
fn align_up_rounds_to_multiple() {
assert_eq!(align_up(0, 16), 0);
assert_eq!(align_up(1, 16), 16);
assert_eq!(align_up(16, 16), 16);
assert_eq!(align_up(17, 16), 32);
assert_eq!(align_up(257, 256), 512);
}
#[test]
fn text_upload_bytes_is_zero_without_calls() {
assert_eq!(text_upload_bytes(&[], 256), 0);
assert_eq!(text_upload_bytes(&[text_call()], 256), 0);
}
#[test]
fn text_upload_bytes_aligns_each_block() {
assert_eq!(text_upload_bytes(&[glyph_call(1)], 16), 128 + 16);
assert_eq!(text_upload_bytes(&[glyph_call(1)], 256), 256 + 256);
}
#[test]
fn text_upload_bytes_bounds_a_simulated_cursor() {
let calls = [glyph_call(3), glyph_call(1), glyph_call(17), glyph_call(0)];
for align in [16u64, 256] {
let total = text_upload_bytes(&calls, align);
let mut cursor = 0u64;
for c in &calls {
for block in [
core::mem::size_of_val(c.vertices.as_slice()) as u64,
core::mem::size_of_val(c.indices.as_slice()) as u64,
] {
cursor = align_up(cursor, align) + block;
assert!(cursor <= total, "cursor {cursor} exceeded reserved {total}");
}
}
}
}
struct MockBloom {
mips: usize,
log: RefCell<Vec<String>>,
}
impl BloomEncoder for MockBloom {
type Rec = ();
type Args = ();
fn bloom_mip_count(&self) -> usize {
self.mips
}
fn begin_bloom(&self, _rec: &(), _args: &()) {
self.log.borrow_mut().push("begin".into());
}
fn bloom_prefilter(&self, _rec: &(), _args: &()) {
self.log.borrow_mut().push("prefilter".into());
}
fn bloom_downsample(&self, _rec: &(), _args: &(), dst: usize) {
self.log.borrow_mut().push(format!("down{dst}"));
}
fn bloom_upsample(&self, _rec: &(), _args: &(), dst: usize) {
self.log.borrow_mut().push(format!("up{dst}"));
}
}
#[test]
fn bloom_chain_encodes_prefilter_downsample_upsample_in_order() {
let enc = MockBloom {
mips: 3,
log: RefCell::new(Vec::new()),
};
encode_bloom_chain(&enc, &(), ());
assert_eq!(
*enc.log.borrow(),
["begin", "prefilter", "down1", "down2", "up1", "up0"]
);
}
#[test]
fn bloom_chain_with_zero_mips_is_a_noop() {
let enc = MockBloom {
mips: 0,
log: RefCell::new(Vec::new()),
};
encode_bloom_chain(&enc, &(), ());
assert!(enc.log.borrow().is_empty());
}
struct MockComposite {
text_ready: bool,
fail_at: Option<usize>,
log: RefCell<Vec<String>>,
text_seen: RefCell<usize>,
}
impl MockComposite {
fn new(text_ready: bool, fail_at: Option<usize>) -> Self {
Self {
text_ready,
fail_at,
log: RefCell::new(Vec::new()),
text_seen: RefCell::new(0),
}
}
}
impl CompositeEncoder for MockComposite {
type Rec = ();
type Args = ();
fn begin_composite(&self, _rec: &(), _args: &()) {
self.log.borrow_mut().push("begin".into());
}
fn composite_draw(&self, _rec: &(), _args: &()) {
self.log.borrow_mut().push("draw".into());
}
fn begin_text(&self, _rec: &(), _args: &()) -> bool {
self.log.borrow_mut().push("begin_text".into());
self.text_ready
}
fn text_draw(&self, _rec: &(), _args: &(), _call: &TextDrawCall) -> Result<(), String> {
let mut n = self.text_seen.borrow_mut();
self.log.borrow_mut().push(format!("text{}", *n));
let fail = self.fail_at == Some(*n);
*n += 1;
if fail {
return Err("text upload failed".into());
}
Ok(())
}
fn end_composite(&self, _rec: &(), _args: &()) {
self.log.borrow_mut().push("end".into());
}
}
#[test]
fn composite_chain_orders_passes_then_text_then_end() {
let enc = MockComposite::new(true, None);
let calls = [text_call(), text_call()];
let r = encode_composite_chain(&enc, &(), &(), &calls);
assert!(r.is_ok());
assert_eq!(
*enc.log.borrow(),
["begin", "draw", "begin_text", "text0", "text1", "end"]
);
}
#[test]
fn composite_chain_propagates_text_error_without_ending() {
let enc = MockComposite::new(true, Some(0));
let calls = [text_call(), text_call()];
let r = encode_composite_chain(&enc, &(), &(), &calls);
assert_eq!(r, Err("text upload failed".into()));
let log = enc.log.borrow();
assert_eq!(*log, ["begin", "draw", "begin_text", "text0"]);
assert!(!log.contains(&"end".to_string()), "pass must stay open");
}
#[test]
fn composite_chain_with_no_text_skips_the_text_loop() {
let enc = MockComposite::new(true, None);
let r = encode_composite_chain(&enc, &(), &(), &[]);
assert!(r.is_ok());
assert_eq!(*enc.log.borrow(), ["begin", "draw", "end"]);
}
#[test]
fn composite_chain_skips_draws_when_text_is_inert() {
let enc = MockComposite::new(false, None);
let calls = [text_call()];
let r = encode_composite_chain(&enc, &(), &(), &calls);
assert!(r.is_ok());
assert_eq!(*enc.log.borrow(), ["begin", "draw", "begin_text", "end"]);
}
struct MockFullscreen {
log: RefCell<Vec<String>>,
}
impl FullscreenPass for MockFullscreen {
type Rec = ();
fn begin(&self, _rec: &()) {
self.log.borrow_mut().push("begin".into());
}
fn draw(&self, _rec: &()) {
self.log.borrow_mut().push("draw".into());
}
fn end(&self, _rec: &()) {
self.log.borrow_mut().push("end".into());
}
}
#[test]
fn fullscreen_encodes_begin_draw_end() {
let enc = MockFullscreen {
log: RefCell::new(Vec::new()),
};
encode_fullscreen(&enc, &());
assert_eq!(*enc.log.borrow(), ["begin", "draw", "end"]);
}
}