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

Implement pixel aspect ratio #151

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The Minimum Supported Rust Version for `pixels` will always be made available in
- DirectX 11, WebGL2, and WebGPU support are a work in progress.
- Use your own custom shaders for special effects.
- Hardware accelerated scaling on perfect pixel boundaries.
- Supports non-square pixel aspect ratios. (WIP)
- Supports non-square pixel aspect ratios.

## Examples

Expand All @@ -36,6 +36,7 @@ The Minimum Supported Rust Version for `pixels` will always be made available in
- [Minimal example with SDL2](./examples/minimal-sdl2)
- [Minimal example with `winit`](./examples/minimal-winit)
- [Minimal example with `fltk`](./examples/minimal-fltk)
- [Non-square pixel aspect ratios](./examples/pixel-aspect-ratio)
- [Pixel Invaders](./examples/invaders)
- [`raqote` example](./examples/raqote-winit)

Expand Down
11 changes: 6 additions & 5 deletions examples/minimal-fltk/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ impl World {
for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
let x = (i % WIDTH as usize) as i16;
let y = (i / WIDTH as usize) as i16;
let d = {
let xd = x as i32 - self.circle_x as i32;
let yd = y as i32 - self.circle_y as i32;
((xd.pow(2) + yd.pow(2)) as f64).sqrt().powi(2)
let length = {
let x = (x - self.circle_x) as f64;
let y = (y - self.circle_y) as f64;

x.powf(2.0) + y.powf(2.0)
};
let inside_the_circle = d < (CIRCLE_RADIUS as f64).powi(2);
let inside_the_circle = length < (CIRCLE_RADIUS as f64).powi(2);

let rgba = if inside_the_circle {
[0xac, 0x00, 0xe6, 0xff]
Expand Down
17 changes: 17 additions & 0 deletions examples/pixel-aspect-ratio/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "pixel-aspect-ratio"
version = "0.1.0"
authors = ["Jay Oster <[email protected]>"]
edition = "2018"
publish = false

[features]
optimize = ["log/release_max_level_warn"]
default = ["optimize"]

[dependencies]
env_logger = "0.8"
log = "0.4"
pixels = { path = "../.." }
winit = "0.24"
winit_input_helper = "0.9"
19 changes: 19 additions & 0 deletions examples/pixel-aspect-ratio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Hello Pixel Aspect Ratio

![Hello Pixel Aspect Ratio](../../img/pixel-aspect-ratio.png)

## Running

```bash
cargo run --release --package pixel-aspect-ratio
```

## About

This example demonstrates pixel aspect ratios. PAR is similar to the common screen aspect ratio that many people are now familiar with (e.g. `16:9` wide screen displays), but applies to the ratio of a _pixel_'s width to its height instead of the screen. Pixel aspect ratios other than `1:1` are common on old computer and video game hardware that outputs NTSC or PAL video signals.

The screenshot above shows an ellipse with an `8:7` aspect ratio drawn on a pixel buffer with a matching pixel aspect ratio. In other words, it shows a circle! Below, the _same_ pixel buffer is rendered with a `1:1` pixel aspect ratio, which shows the actual distortion of the ellipse.

![Original Ellipse](../../img/pixel-aspect-ratio-2.png)

You might also take note that the window is slightly wider in the first image. This is ultimately what corrects the distortion and causes the ellipse to look like a circle.
142 changes: 142 additions & 0 deletions examples/pixel-aspect-ratio/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#![deny(clippy::all)]
#![forbid(unsafe_code)]

use log::error;
use pixels::{Error, PixelsBuilder, SurfaceTexture};
use winit::dpi::LogicalSize;
use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;

const WIDTH: u32 = 320;
const HEIGHT: u32 = 240;

// The circle is actually defined as an ellipse with minor axis 56 pixels and major axis 64 pixels.
const CIRCLE_AXES: (i16, i16) = (56, 64);
const CIRCLE_SEMI: (i16, i16) = (CIRCLE_AXES.0 / 2, CIRCLE_AXES.1 / 2);

// The Pixel Aspect Ratio is the difference between the physical width and height of a single pixel.
// For most users, this ratio will be 1:1, i.e. the value will be `1.0`. Some devices display
// non-square pixels, and the pixel aspect ratio can simulate this difference on devices with square
// pixels. In this example, the ellipse will be rendered as a circle if it is drawn with a pixel
// aspect ratio of 8:7.
const PAR: f32 = 8.0 / 7.0;

/// Representation of the application state. In this example, a circle will bounce around the screen.
struct World {
circle_x: i16,
circle_y: i16,
velocity_x: i16,
velocity_y: i16,
}

fn main() -> Result<(), Error> {
env_logger::init();
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let window = {
// The window size is horizontally stretched by the PAR.
let size = LogicalSize::new(WIDTH as f64 * PAR as f64, HEIGHT as f64);
WindowBuilder::new()
.with_title("Hello Pixel Aspect Ratio")
.with_inner_size(size)
.with_min_inner_size(size)
.build(&event_loop)
.unwrap()
};

let mut pixels = {
let window_size = window.inner_size();
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
PixelsBuilder::new(WIDTH, HEIGHT, surface_texture)
.pixel_aspect_ratio(PAR)
.build()?
};
let mut world = World::new();

event_loop.run(move |event, _, control_flow| {
// Draw the current frame
if let Event::RedrawRequested(_) = event {
world.draw(pixels.get_frame());
if pixels
.render()
.map_err(|e| error!("pixels.render() failed: {}", e))
.is_err()
{
*control_flow = ControlFlow::Exit;
return;
}
}

// Handle input events
if input.update(&event) {
// Close events
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
*control_flow = ControlFlow::Exit;
return;
}

// Resize the window
if let Some(size) = input.window_resized() {
pixels.resize_surface(size.width, size.height);
}

// Update internal state and request a redraw
world.update();
window.request_redraw();
}
});
}

impl World {
/// Create a new `World` instance that can draw a moving circle.
fn new() -> Self {
Self {
circle_x: CIRCLE_SEMI.0 + 24,
circle_y: CIRCLE_SEMI.1 + 16,
velocity_x: 1,
velocity_y: 1,
}
}

/// Update the `World` internal state; bounce the circle around the screen.
fn update(&mut self) {
if self.circle_x - CIRCLE_SEMI.0 <= 0 || self.circle_x + CIRCLE_SEMI.0 > WIDTH as i16 {
self.velocity_x *= -1;
}
if self.circle_y - CIRCLE_SEMI.1 <= 0 || self.circle_y + CIRCLE_SEMI.1 > HEIGHT as i16 {
self.velocity_y *= -1;
}

self.circle_x += self.velocity_x;
self.circle_y += self.velocity_y;
}

/// Draw the `World` state to the frame buffer.
///
/// Assumes the default texture format: `wgpu::TextureFormat::Rgba8UnormSrgb`
fn draw(&self, frame: &mut [u8]) {
for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
let x = (i % WIDTH as usize) as i16;
let y = (i / WIDTH as usize) as i16;
let length = {
let x = (x - self.circle_x) as f64;
let y = (y - self.circle_y) as f64;
let semi_minor = (CIRCLE_SEMI.0 as f64).powf(2.0);
let semi_major = (CIRCLE_SEMI.1 as f64).powf(2.0);

x.powf(2.0) / semi_minor + y.powf(2.0) / semi_major
};
let inside_the_circle = length < 1.0;

let rgba = if inside_the_circle {
[0x5e, 0x48, 0xe8, 0xff]
} else {
[0x48, 0xb2, 0xe8, 0xff]
};

pixel.copy_from_slice(&rgba);
}
}
}
Binary file added img/pixel-aspect-ratio-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/pixel-aspect-ratio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 13 additions & 9 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct PixelsBuilder<'req, 'dev, 'win, W: HasRawWindowHandle> {
backend: wgpu::Backends,
width: u32,
height: u32,
_pixel_aspect_ratio: f64,
pixel_aspect_ratio: f32,
present_mode: wgpu::PresentMode,
surface_texture: SurfaceTexture<'win, W>,
texture_format: wgpu::TextureFormat,
Expand Down Expand Up @@ -62,7 +62,7 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W>
}),
width,
height,
_pixel_aspect_ratio: 1.0,
pixel_aspect_ratio: 1.0,
present_mode: wgpu::PresentMode::Fifo,
surface_texture,
texture_format: wgpu::TextureFormat::Rgba8UnormSrgb,
Expand Down Expand Up @@ -107,11 +107,10 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W>
/// # Warning
///
/// This documentation is hidden because support for pixel aspect ratio is incomplete.
#[doc(hidden)]
pub fn pixel_aspect_ratio(mut self, pixel_aspect_ratio: f64) -> Self {
pub fn pixel_aspect_ratio(mut self, pixel_aspect_ratio: f32) -> Self {
assert!(pixel_aspect_ratio > 0.0);

self._pixel_aspect_ratio = pixel_aspect_ratio;
self.pixel_aspect_ratio = pixel_aspect_ratio;
self
}

Expand Down Expand Up @@ -187,7 +186,8 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W>
async fn build_impl(self) -> Result<Pixels, Error> {
let instance = wgpu::Instance::new(self.backend);

// TODO: Use `options.pixel_aspect_ratio` to stretch the scaled texture
let pixel_aspect_ratio = self.pixel_aspect_ratio;
let texture_format = self.texture_format;
let surface = unsafe { instance.create_surface(self.surface_texture.window) };
let compatible_surface = Some(&surface);
let request_adapter_options = &self.request_adapter_options;
Expand Down Expand Up @@ -241,6 +241,7 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W>
// Backing texture values
self.width,
self.height,
pixel_aspect_ratio,
self.texture_format,
// Render texture values
&surface_size,
Expand All @@ -258,13 +259,14 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W>
surface,
texture,
texture_extent,
texture_format: self.texture_format,
texture_format_size: get_texture_format_size(self.texture_format),
texture_format,
texture_format_size: get_texture_format_size(texture_format),
scaling_renderer,
};

let pixels = Pixels {
context,
pixel_aspect_ratio,
surface_size,
present_mode,
render_texture_format,
Expand Down Expand Up @@ -320,6 +322,7 @@ pub(crate) fn create_backing_texture(
device: &wgpu::Device,
width: u32,
height: u32,
pixel_aspect_ratio: f32,
backing_texture_format: wgpu::TextureFormat,
surface_size: &SurfaceSize,
render_texture_format: wgpu::TextureFormat,
Expand All @@ -331,7 +334,7 @@ pub(crate) fn create_backing_texture(
usize,
) {
let scaling_matrix_inverse = ScalingMatrix::new(
(width as f32, height as f32),
(width as f32, height as f32, pixel_aspect_ratio),
(surface_size.width as f32, surface_size.height as f32),
)
.transform
Expand All @@ -358,6 +361,7 @@ pub(crate) fn create_backing_texture(
device,
&texture_view,
&texture_extent,
pixel_aspect_ratio,
surface_size,
render_texture_format,
);
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub struct PixelsContext {
#[derive(Debug)]
pub struct Pixels {
context: PixelsContext,
pixel_aspect_ratio: f32,
surface_size: SurfaceSize,
present_mode: wgpu::PresentMode,
render_texture_format: wgpu::TextureFormat,
Expand Down Expand Up @@ -258,6 +259,7 @@ impl Pixels {
// Backing texture values
width,
height,
self.pixel_aspect_ratio,
self.context.texture_format,
// Render texture values
&self.surface_size,
Expand Down Expand Up @@ -299,6 +301,7 @@ impl Pixels {
(
self.context.texture_extent.width as f32,
self.context.texture_extent.height as f32,
self.pixel_aspect_ratio,
),
(width as f32, height as f32),
)
Expand Down
Loading