#![forbid(unsafe_code)]
use atrinik_render_api::{
Error as RenderError, FrameOutput, RecoveryMetrics, RenderRequest, Renderer, TargetDescriptor,
TargetKind, TargetLimits,
};
use atrinik_render_resources::{
DEFAULT_MAXIMUM_RESOURCE_BYTES, Error as ResourceError, ResourceBlob, ResourceProvider,
ResourceRequest, Rgba8Image, add_working_set_bytes, for_each_sprite_pixel, sample_rgba8,
shade_rgba8,
};
use atrinik_scene::{
Digest256, Layer, ResourceId, Sampling, SceneBundle, SceneLimits, SemanticId, Sprite, Viewport,
};
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, HashMap, VecDeque},
sync::{Arc, RwLock},
};
#[derive(Default)]
pub struct FakeResourceProvider {
blobs: RwLock<BTreeMap<(ResourceId, u64), Vec<u8>>>,
}
impl FakeResourceProvider {
pub fn insert(
&self,
id: ResourceId,
revision: u64,
bytes: Vec<u8>,
) -> Result<(), ResourceError> {
self.blobs
.write()
.map_err(|_| ResourceError::Unavailable)?
.insert((id, revision), bytes);
Ok(())
}
}
impl ResourceProvider for FakeResourceProvider {
fn load(&self, request: ResourceRequest) -> Result<ResourceBlob, ResourceError> {
request.validate()?;
let bytes = self
.blobs
.read()
.map_err(|_| ResourceError::Unavailable)?
.get(&(request.id, request.revision))
.cloned()
.ok_or(ResourceError::NotFound)?;
ResourceBlob::for_request(request, bytes)
}
}
pub struct ReferenceRenderer {
target: TargetDescriptor,
}
pub struct FakeRenderer {
target: TargetDescriptor,
responses: VecDeque<Result<FrameOutput, RenderError>>,
rendered_revisions: Vec<u64>,
}
impl FakeRenderer {
pub fn new(target: TargetDescriptor) -> Result<Self, RenderError> {
Ok(Self {
target: target.validate(TargetLimits::default())?,
responses: VecDeque::new(),
rendered_revisions: Vec::new(),
})
}
pub fn push_response(&mut self, response: Result<FrameOutput, RenderError>) {
self.responses.push_back(response);
}
#[must_use]
pub fn rendered_revisions(&self) -> &[u64] {
&self.rendered_revisions
}
}
impl Renderer for FakeRenderer {
fn target(&self) -> TargetDescriptor {
self.target
}
fn resize(&mut self, width: u32, height: u32) -> Result<(), RenderError> {
self.target = TargetDescriptor {
width,
height,
..self.target
}
.validate(TargetLimits::default())?;
Ok(())
}
fn render(&mut self, request: RenderRequest<'_>) -> Result<FrameOutput, RenderError> {
self.rendered_revisions.push(request.scene.revision());
self.responses.pop_front().unwrap_or_else(|| {
Err(RenderError::Internal(
"fake response queue is empty".to_owned(),
))
})
}
}
impl ReferenceRenderer {
pub fn new(target: TargetDescriptor) -> Result<Self, RenderError> {
Ok(Self {
target: target.validate(TargetLimits::default())?,
})
}
}
impl Renderer for ReferenceRenderer {
fn target(&self) -> TargetDescriptor {
self.target
}
fn resize(&mut self, width: u32, height: u32) -> Result<(), RenderError> {
self.target = TargetDescriptor {
width,
height,
..self.target
}
.validate(TargetLimits::default())?;
Ok(())
}
fn render(&mut self, request: RenderRequest<'_>) -> Result<FrameOutput, RenderError> {
if request.scene.viewport().width != self.target.width
|| request.scene.viewport().height != self.target.height
{
return Err(RenderError::InvalidTarget);
}
let pixel_count = pixel_count(self.target.width, self.target.height)?;
let clear = color_to_rgba8(request.scene.clear_color());
let mut output = FrameOutput {
width: self.target.width,
height: self.target.height,
rgba8: clear.repeat(pixel_count),
semantic_ids: vec![0; pixel_count],
depth: vec![i16::MIN; pixel_count],
coverage: vec![0; pixel_count],
metrics: RecoveryMetrics::default(),
};
let mut uploaded_bytes = 0_u64;
let mut resource_requests = 0_u64;
let mut images: HashMap<(ResourceId, u64, Digest256), Arc<Rgba8Image>> = HashMap::new();
for sprite in request.scene.sprites() {
let key = (
sprite.resource_id,
sprite.resource_revision,
sprite.resource_digest,
);
let image = if let Some(image) = images.get(&key) {
image.clone()
} else {
let blob = request
.resources
.load(ResourceRequest {
id: sprite.resource_id,
revision: sprite.resource_revision,
digest: sprite.resource_digest,
maximum_bytes: DEFAULT_MAXIMUM_RESOURCE_BYTES,
})
.map_err(|error| RenderError::Resource(error.to_string()))?;
let image = Arc::new(
Rgba8Image::decode(&blob, 8_192, 16_777_216)
.map_err(|error| RenderError::Resource(error.to_string()))?,
);
uploaded_bytes = add_working_set_bytes(uploaded_bytes, image.pixels().len() as u64)
.map_err(|_| RenderError::LimitExceeded)?;
resource_requests += 1;
images.insert(key, image.clone());
image
};
raster_sprite(&mut output, sprite, &image, request.scene.clock_millis());
}
output.metrics.frames_submitted = 1;
output.metrics.resource_requests = resource_requests;
output.metrics.uploaded_bytes = uploaded_bytes;
output.metrics.vertex_count = (request.scene.sprites().len() as u64) * 6;
output.metrics.vertex_allocation_bytes = output.metrics.vertex_count * 40;
output.metrics.target_allocation_bytes = (pixel_count as u64) * 11;
output.validate()?;
Ok(output)
}
}
fn pixel_count(width: u32, height: u32) -> Result<usize, RenderError> {
usize::try_from(width)
.ok()
.and_then(|width| {
usize::try_from(height)
.ok()
.and_then(|height| width.checked_mul(height))
})
.ok_or(RenderError::LimitExceeded)
}
fn raster_sprite(output: &mut FrameOutput, sprite: &Sprite, image: &Rgba8Image, clock_millis: u64) {
for_each_sprite_pixel(
sprite,
output.width,
output.height,
|_x, _y, index, u, v| {
let source = shade_rgba8(
sample_rgba8(image, u, v, sprite.sampling),
sprite.color,
sprite.layer,
clock_millis,
);
if source[3] != 0 {
blend(&mut output.rgba8[index * 4..index * 4 + 4], source);
output.semantic_ids[index] = sprite.semantic_id.0;
output.depth[index] = sprite.depth;
output.coverage[index] = source[3];
}
},
);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn color_to_rgba8(color: [f32; 4]) -> [u8; 4] {
color.map(|channel| (channel.mul_add(255.0, 0.5)).floor() as u8)
}
fn blend(destination: &mut [u8], source: [u8; 4]) {
let alpha = u32::from(source[3]);
let inverse = 255 - alpha;
for channel in 0..3 {
destination[channel] = u8::try_from(
(u32::from(source[channel]) * alpha + u32::from(destination[channel]) * inverse + 127)
/ 255,
)
.expect("8-bit blend channel remains in range");
}
destination[3] = u8::try_from(alpha + (u32::from(destination[3]) * inverse + 127) / 255)
.expect("8-bit alpha remains in range");
}
#[must_use]
pub fn synthetic_digest() -> Digest256 {
Digest256(Sha256::digest(synthetic_resource()).into())
}
#[must_use]
pub fn synthetic_resource() -> &'static [u8] {
b"ATX1\x02\x00\x00\x00\x02\x00\x00\x00\xff\xff\xff\xff\xff\x40\x20\xff\x20\xff\x80\xc0\x20\x60\xff\x40"
}
pub fn synthetic_provider() -> Result<Arc<FakeResourceProvider>, ResourceError> {
let provider = Arc::new(FakeResourceProvider::default());
provider.insert(ResourceId(1), 1, synthetic_resource().to_vec())?;
Ok(provider)
}
#[must_use = "synthetic scene construction can fail"]
#[allow(clippy::cast_precision_loss)]
pub fn synthetic_scene(width: u32, height: u32) -> Result<SceneBundle, atrinik_scene::Error> {
SceneBundle::new(
1,
375,
Viewport {
width,
height,
scale_milli: 1_000,
},
[0.02, 0.03, 0.05, 1.0],
[
Sprite {
semantic_id: SemanticId(1),
resource_id: ResourceId(1),
resource_revision: 1,
resource_digest: synthetic_digest(),
x: 1.0,
y: 1.0,
width: (width / 2).max(1) as f32,
height: (height / 2).max(1) as f32,
depth: 0,
painter_order: 0,
color: [0.85, 0.12, 0.18, 1.0],
sampling: Sampling::Nearest,
layer: Layer::World,
},
Sprite {
semantic_id: SemanticId(2),
resource_id: ResourceId(1),
resource_revision: 1,
resource_digest: synthetic_digest(),
x: (width / 3) as f32,
y: (height / 3) as f32,
width: (width / 2).max(1) as f32,
height: (height / 2).max(1) as f32,
depth: 1,
painter_order: 1,
color: [0.1, 0.55, 0.95, 0.75],
sampling: Sampling::Linear,
layer: Layer::Effect,
},
Sprite {
semantic_id: SemanticId(3),
resource_id: ResourceId(1),
resource_revision: 1,
resource_digest: synthetic_digest(),
x: -1.25,
y: height.saturating_sub(3) as f32,
width: width.saturating_add(2) as f32,
height: 2.5,
depth: 0,
painter_order: 0,
color: [0.95, 0.95, 0.75, 0.8],
sampling: Sampling::Nearest,
layer: Layer::Interface,
},
],
SceneLimits::default(),
)
}
#[allow(clippy::cast_precision_loss)]
pub fn dense_scene(width: u32, height: u32) -> Result<SceneBundle, atrinik_scene::Error> {
let mut sprites = Vec::with_capacity(1_024);
for index in 0..1_024_u32 {
let column = index % 32;
let row = index / 32;
let depth = i16::try_from(index % 17).map_err(|_| atrinik_scene::Error::InvalidValue)?;
sprites.push(Sprite {
semantic_id: SemanticId(index + 1),
resource_id: ResourceId(1),
resource_revision: 1,
resource_digest: synthetic_digest(),
x: (column * 4) as f32,
y: (row * 4) as f32,
width: 4.0,
height: 4.0,
depth,
painter_order: index,
color: [0.55, 0.7, 0.9, 0.9],
sampling: if index % 2 == 0 {
Sampling::Nearest
} else {
Sampling::Linear
},
layer: if index % 11 == 0 {
Layer::Effect
} else {
Layer::World
},
});
}
SceneBundle::new(
2,
375,
Viewport {
width,
height,
scale_milli: 1_000,
},
[0.01, 0.01, 0.02, 1.0],
sprites,
SceneLimits::default(),
)
}
pub fn structural_scene(width: u32, height: u32) -> Result<SceneBundle, atrinik_scene::Error> {
if width < 32 || height < 32 {
return Err(atrinik_scene::Error::InvalidValue);
}
let digest = synthetic_digest();
SceneBundle::new(
3,
375,
Viewport {
width,
height,
scale_milli: 1_500,
},
[0.04, 0.05, 0.08, 1.0],
[
fixture_sprite(1, [3.0, 20.0, 26.0, 5.0], -2, 0, Layer::World, digest),
fixture_sprite(2, [8.0, 7.0, 5.0, 20.0], -1, 1, Layer::World, digest),
fixture_sprite(3, [13.0, 11.0, 5.0, 16.0], 0, 2, Layer::World, digest),
fixture_sprite(4, [18.0, 15.0, 5.0, 12.0], 1, 3, Layer::World, digest),
fixture_sprite(5, [23.0, 19.0, 5.0, 8.0], 2, 4, Layer::World, digest),
Sprite {
color: [0.35, 0.55, 0.8, 0.28],
sampling: Sampling::Linear,
..fixture_sprite(6, [-2.0, 9.5, 36.0, 14.0], 3, 0, Layer::Effect, digest)
},
Sprite {
color: [1.0, 0.92, 0.55, 0.65],
..fixture_sprite(7, [10.0, 4.0, 12.0, 6.0], 4, 1, Layer::Effect, digest)
},
Sprite {
color: [0.12, 0.15, 0.2, 0.92],
..fixture_sprite(8, [0.0, 27.0, 32.0, 5.0], 0, 0, Layer::Interface, digest)
},
Sprite {
color: [0.95, 0.95, 0.9, 1.0],
..fixture_sprite(9, [35.0, 26.0, 2.0, 6.0], 1, 1, Layer::Interface, digest)
},
Sprite {
color: [0.95, 0.95, 0.9, 1.0],
..fixture_sprite(10, [41.0, 26.0, 2.0, 6.0], 1, 2, Layer::Interface, digest)
},
Sprite {
color: [0.95, 0.95, 0.9, 1.0],
..fixture_sprite(11, [35.0, 28.0, 8.0, 2.0], 1, 3, Layer::Interface, digest)
},
],
SceneLimits::default(),
)
}
fn fixture_sprite(
semantic_id: u32,
bounds: [f32; 4],
depth: i16,
painter_order: u32,
layer: Layer,
digest: Digest256,
) -> Sprite {
Sprite {
semantic_id: SemanticId(semantic_id),
resource_id: ResourceId(1),
resource_revision: 1,
resource_digest: digest,
x: bounds[0],
y: bounds[1],
width: bounds[2],
height: bounds[3],
depth,
painter_order,
color: [0.85, 0.75, 0.6, 1.0],
sampling: Sampling::Nearest,
layer,
}
}
#[must_use]
pub const fn offscreen_target(width: u32, height: u32) -> TargetDescriptor {
TargetDescriptor {
kind: TargetKind::Offscreen,
width,
height,
backend: atrinik_render_api::BackendPreference::Automatic,
}
}
#[cfg(test)]
mod tests {
use super::{
FakeRenderer, FakeResourceProvider, ReferenceRenderer, offscreen_target,
synthetic_provider, synthetic_scene,
};
use atrinik_render_api::{Error, RenderRequest, Renderer};
use atrinik_render_resources::{
Error as ResourceError, ResourceBlob, ResourceProvider, ResourceRequest,
};
use atrinik_scene::{Digest256, ResourceId, SceneBundle, SceneLimits};
use sha2::{Digest, Sha256};
use std::sync::Arc;
struct FailingProvider(ResourceError);
impl ResourceProvider for FailingProvider {
fn load(&self, _request: ResourceRequest) -> Result<ResourceBlob, ResourceError> {
Err(self.0)
}
}
#[test]
fn reference_output_is_exact_and_repeatable() {
let scene = synthetic_scene(16, 16).unwrap();
let request = RenderRequest {
scene: &scene,
resources: synthetic_provider().unwrap(),
};
let first = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(request.clone())
.unwrap();
let second = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(request)
.unwrap();
assert_eq!(first, second);
assert!(first.semantic_ids.contains(&1));
assert!(first.semantic_ids.contains(&2));
}
#[test]
fn missing_and_digest_mismatched_resources_fail_closed() {
let scene = synthetic_scene(16, 16).unwrap();
let missing = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(RenderRequest {
scene: &scene,
resources: Arc::new(FakeResourceProvider::default()),
})
.unwrap_err();
assert!(matches!(missing, Error::Resource(message) if message.contains("NotFound")));
let provider = Arc::new(FakeResourceProvider::default());
provider
.insert(ResourceId(1), 1, b"wrong bytes".to_vec())
.unwrap();
let mismatch = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(RenderRequest {
scene: &scene,
resources: provider,
})
.unwrap_err();
assert!(matches!(mismatch, Error::Resource(message) if message.contains("DigestMismatch")));
let corrupt_bytes = b"not-an-atx1-texture".to_vec();
let digest = Digest256(Sha256::digest(&corrupt_bytes).into());
let mut sprites = scene.sprites().to_vec();
for sprite in &mut sprites {
sprite.resource_digest = digest;
}
let corrupt_scene = SceneBundle::new(
scene.revision(),
scene.clock_millis(),
scene.viewport(),
scene.clear_color(),
sprites,
SceneLimits::default(),
)
.unwrap();
let corrupt_provider = Arc::new(FakeResourceProvider::default());
corrupt_provider
.insert(ResourceId(1), 1, corrupt_bytes)
.unwrap();
let corrupt = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(RenderRequest {
scene: &corrupt_scene,
resources: corrupt_provider,
})
.unwrap_err();
assert!(matches!(corrupt, Error::Resource(message) if message.contains("InvalidFormat")));
let oversized = ReferenceRenderer::new(offscreen_target(16, 16))
.unwrap()
.render(RenderRequest {
scene: &scene,
resources: Arc::new(FailingProvider(ResourceError::LimitExceeded)),
})
.unwrap_err();
assert!(matches!(oversized, Error::Resource(message) if message.contains("LimitExceeded")));
}
#[test]
fn programmable_fake_records_requests_and_failures() {
let scene = synthetic_scene(4, 4).unwrap();
let request = RenderRequest {
scene: &scene,
resources: synthetic_provider().unwrap(),
};
let recovered = ReferenceRenderer::new(offscreen_target(4, 4))
.unwrap()
.render(request.clone())
.unwrap();
let mut fake = FakeRenderer::new(offscreen_target(4, 4)).unwrap();
fake.push_response(Err(Error::SurfaceUnavailable));
fake.push_response(Err(Error::SurfaceLost));
fake.push_response(Err(Error::DeviceLost));
fake.push_response(Ok(recovered.clone()));
assert_eq!(fake.render(request.clone()), Err(Error::SurfaceUnavailable));
assert_eq!(fake.render(request.clone()), Err(Error::SurfaceLost));
assert_eq!(fake.render(request.clone()), Err(Error::DeviceLost));
assert_eq!(fake.render(request), Ok(recovered));
assert_eq!(fake.rendered_revisions(), [1, 1, 1, 1]);
}
}