use super::SwRenderer;
use crate::render::font::sdf::sample_signed_distance;
use crate::types::{Color, Fixed};
impl SwRenderer<'_> {
#[allow(clippy::too_many_arguments)]
pub(super) fn blit_sdf_glyph(
&mut self,
atlas: &[u8],
source_size: u16,
bit_depth: u8,
spread: u16,
cx: i32,
cy: i32,
target_size: u16,
phys_bounds: (i32, i32, i32, i32),
color: &Color,
opa: u8,
) {
let (clip_x, clip_y, clip_x2, clip_y2) = phys_bounds;
let target_size = target_size.max(1) as i32;
let inv_scale = Fixed::from_int(source_size as i32) / Fixed::from_int(target_size);
let half_texel = Fixed::ONE / 2;
let edge_half = inv_scale / 2;
let target_w = self.target.width as usize;
let clip_mask = self.clip_stack.last().map(|m| m.alpha.as_slice());
for dy in 0..target_size {
let py = cy + dy;
if py < clip_y || py >= clip_y2 {
continue;
}
let sy = (Fixed::from_int(dy) + half_texel) * inv_scale - half_texel;
let row_mask_off = py as usize * target_w;
for dx in 0..target_size {
let px = cx + dx;
if px < clip_x || px >= clip_x2 {
continue;
}
let sx = (Fixed::from_int(dx) + half_texel) * inv_scale - half_texel;
let dist = sample_signed_distance(atlas, source_size, bit_depth, spread, sx, sy);
let cov = if dist <= -edge_half {
continue;
} else if dist >= edge_half {
Fixed::ONE
} else {
(dist + edge_half) / (edge_half * 2)
};
let mut final_alpha =
(cov * Fixed::from_int(opa as i32)).to_int().clamp(0, 255) as u8;
if final_alpha == 0 {
continue;
}
if let Some(mask) = clip_mask {
let ca = mask[row_mask_off + px as usize];
if ca == 0 {
continue;
}
if ca < 255 {
final_alpha = ((final_alpha as u16 * ca as u16 + 127) / 255) as u8;
if final_alpha == 0 {
continue;
}
}
}
self.target.blend_pixel_int(px, py, color, final_alpha);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::backends::sw::SwRenderer;
use crate::render::texture::{ColorFormat, Texture};
use alloc::vec;
fn pixel_alpha(buf: &[u8], stride: usize, x: usize, y: usize) -> u8 {
buf[(y * stride + x) * 4 + 3]
}
fn build_inside_outside_atlas() -> alloc::vec::Vec<u8> {
let mut atlas = vec![0u8; 8];
let set = |buf: &mut [u8], x: usize, y: usize, q: u8| {
let idx = y * 4 + x;
let byte_idx = idx >> 1;
if idx & 1 == 0 {
buf[byte_idx] = (buf[byte_idx] & 0xF0) | (q & 0x0F);
} else {
buf[byte_idx] = (buf[byte_idx] & 0x0F) | ((q & 0x0F) << 4);
}
};
for y in 0..4 {
for x in 0..4 {
let inside = (1..=2).contains(&x) && (1..=2).contains(&y);
set(&mut atlas, x, y, if inside { 15 } else { 0 });
}
}
atlas
}
#[test]
fn blit_inside_pixel_opaque_outside_blank() {
let mut buf = vec![0u8; 8 * 8 * 4];
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let atlas = build_inside_outside_atlas();
let color = Color::rgba(255, 255, 255, 255);
backend.blit_sdf_glyph(&atlas, 4, 4, 1, 0, 0, 4, (0, 0, 8, 8), &color, 255);
assert!(
pixel_alpha(&buf, 8, 1, 1) >= 200,
"expected high alpha inside, got {}",
pixel_alpha(&buf, 8, 1, 1)
);
assert_eq!(
pixel_alpha(&buf, 8, 0, 0),
0,
"expected blank corner, got {}",
pixel_alpha(&buf, 8, 0, 0)
);
}
fn build_thin_stem_atlas(spread: u16) -> alloc::vec::Vec<u8> {
let n = 8usize;
let mut atlas = vec![0u8; n * n / 2];
let set = |buf: &mut [u8], x: usize, y: usize, q: u8| {
let idx = y * n + x;
let bi = idx >> 1;
if idx & 1 == 0 {
buf[bi] = (buf[bi] & 0xF0) | (q & 0x0F);
} else {
buf[bi] = (buf[bi] & 0x0F) | ((q & 0x0F) << 4);
}
};
for y in 0..n {
for x in 0..n {
let cx = x as f32 + 0.5;
let d = 0.5 - (cx - 4.5).abs();
let dc = d.clamp(-(spread as f32), spread as f32);
let q = (dc / spread as f32 * 7.5 + 7.5).round().clamp(0.0, 15.0) as u8;
set(&mut atlas, x, y, q);
}
}
atlas
}
#[test]
fn thin_stem_renders_solid_not_half_coverage() {
let spread = 2u16;
let atlas = build_thin_stem_atlas(spread);
let mut buf = vec![0u8; 8 * 8 * 4];
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let color = Color::rgba(255, 255, 255, 255);
backend.blit_sdf_glyph(&atlas, 8, 4, spread, 0, 0, 8, (0, 0, 8, 8), &color, 255);
let stem = pixel_alpha(&buf, 8, 4, 3);
assert!(
stem >= 200,
"thin stem should render near-solid, got alpha {stem}",
);
}
#[test]
fn blit_respects_clip_bounds() {
let mut buf = vec![0u8; 8 * 8 * 4];
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let atlas = build_inside_outside_atlas();
let color = Color::rgba(255, 255, 255, 255);
backend.blit_sdf_glyph(&atlas, 4, 4, 1, 0, 0, 4, (0, 0, 8, 2), &color, 255);
assert_eq!(pixel_alpha(&buf, 8, 1, 2), 0);
assert_eq!(pixel_alpha(&buf, 8, 1, 3), 0);
}
#[test]
fn blit_upscales_to_non_integer_target() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let atlas = build_inside_outside_atlas();
let color = Color::rgba(255, 255, 255, 255);
backend.blit_sdf_glyph(&atlas, 4, 4, 1, 0, 0, 10, (0, 0, 16, 16), &color, 255);
assert!(
pixel_alpha(&buf, 16, 5, 5) >= 200,
"expected solid centre at 2.5x, got {}",
pixel_alpha(&buf, 16, 5, 5)
);
assert_eq!(pixel_alpha(&buf, 16, 0, 0), 0);
assert!(
pixel_alpha(&buf, 16, 5, 7) > 0,
"render did not reach 10px tall"
);
}
}