spectrusty_core/
video.rs

1/*
2    Copyright (C) 2020-2022  Rafal Michalski
3
4    This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6    For the full copyright notice, see the lib.rs file.
7*/
8//! # Video API.
9pub mod pixel;
10
11use core::str::FromStr;
12use core::convert::TryFrom;
13use core::fmt::{self, Debug};
14use core::ops::{BitAnd, BitOr, Shl, Shr, Range};
15
16#[cfg(feature = "snapshot")]
17use serde::{Serialize, Deserialize};
18
19use bitflags::bitflags;
20
21use crate::clock::{Ts, FTs, VideoTs, VFrameTsCounter, MemoryContention};
22use crate::chip::UlaPortFlags;
23
24pub use pixel::{Palette, PixelBuffer};
25
26/// A halved count of PAL `pixel lines` (low resolution).
27pub const PAL_VC: u32 = 576/2;
28/// A halved count of PAL `pixel columns` (low resolution).
29pub const PAL_HC: u32 = 704/2;
30/// Maximum border size measured in low-resolution pixels.
31pub const MAX_BORDER_SIZE: u32 = 6*8;
32
33/// This enum is used to select border size when rendering video frames.
34#[cfg_attr(feature = "snapshot", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "snapshot", serde(try_from = "u8", into = "u8"))]
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
37#[repr(u8)]
38pub enum BorderSize {
39    Full    = 6,
40    Large   = 5,
41    Medium  = 4,
42    Small   = 3,
43    Tiny    = 2,
44    Minimal = 1,
45    Nil     = 0
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct TryFromUIntBorderSizeError(pub u8);
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct ParseBorderSizeError;
53
54/// General-purpose coordinates, used by various video related methods as a return or an argument type.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct CellCoords {
57    /// 5 lowest bits: 0 - 31 indicates cell column
58    pub column: u8,
59    /// INK/PAPER row range is: [0, 192), screen attributes row range is: [0, 24).
60    pub row: u8
61}
62
63bitflags! {
64    /// Bitflags defining ZX Spectrum's border colors.
65    #[cfg_attr(feature = "snapshot", derive(Serialize, Deserialize))]
66    #[cfg_attr(feature = "snapshot", serde(try_from = "u8", into = "u8"))]
67    #[derive(Default)]
68    pub struct BorderColor: u8 {
69        const BLACK   = 0b000;
70        const BLUE    = 0b001;
71        const RED     = 0b010;
72        const MAGENTA = 0b011;
73        const GREEN   = 0b100;
74        const CYAN    = 0b101;
75        const YELLOW  = 0b110;
76        const WHITE   = 0b111;
77    }
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct TryFromU8BorderColorError(pub u8);
82
83/// An interface for rendering Spectrum's pixel data to frame buffers.
84pub trait Video {
85    /// The horizontal pixel density.
86    ///
87    /// This is: 1 for chipsets that can render only low-resolution modes, and 2 for chipsets that
88    /// are capable of displaying high-resolution screen modes.
89    const PIXEL_DENSITY: u32 = 1;
90    /// The type implementing [VideoFrame], that is being used by the chipset emulator.
91    type VideoFrame: VideoFrame;
92    /// The type implementing [MemoryContention], that is being used by the chipset emulator.
93    type Contention: MemoryContention;
94    /// Returns the current border color.
95    fn border_color(&self) -> BorderColor;
96    /// Force sets the border area to the given color.
97    fn set_border_color(&mut self, border: BorderColor);
98    /// Renders last emulated frame's video data into the provided pixel `buffer`.
99    ///
100    /// * `pitch` is the number of bytes in a single row of pixel data, including padding between lines.
101    /// * `border_size` determines the size of the border rendered around the INK and PAPER area.
102    ///
103    /// Note that different [BorderSize]s will result in different sizes of the rendered `buffer` area.
104    ///
105    /// To predetermine the size of the rendered buffer area use [Video::render_size_pixels].
106    /// To get the size of the video screen in low-resolution pixels only, call [VideoFrame::screen_size_pixels]
107    /// e.g. for an aspect ratio calculation.
108    ///
109    /// * [PixelBuffer] implementation is used to write pixels into the `buffer`.
110    /// * [Palette] implementation is used to create colors from the Spectrum colors.
111    ///
112    /// **NOTE**: Currently this is a one-time action (per frame), as internal data will be drained
113    /// during the rendering. Calling it twice will succeed but the image rendered the second time
114    /// will be most probably incorrect due to the missing data.
115    fn render_video_frame<'a, B: PixelBuffer<'a>, P: Palette<Pixel=B::Pixel>>(
116        &mut self,
117        buffer: &'a mut [u8],
118        pitch: usize,
119        border_size: BorderSize
120    );
121    /// Returns rendered screen pixel size (horizontal, vertical), including the border area, measured
122    /// in pixels depending on [Video::PIXEL_DENSITY].
123    ///
124    /// The size depends on the given `border_size`.
125    fn render_size_pixels(border_size: BorderSize) -> (u32, u32) {
126        let (width, height) = Self::VideoFrame::screen_size_pixels(border_size);
127        (width * Self::pixel_density(), height)
128    }
129    /// Returns the horizontal pixel density.
130    fn pixel_density() -> u32 {
131        Self::PIXEL_DENSITY
132    }
133    /// Returns the screen bank index of the currently visible screen.
134    ///
135    /// The screen banks are different from memory banks.
136    /// E.g. Spectrum 128k returns `0` for the screen bank which resides in a memory bank 5 and `1`
137    /// for the screen bank which resides in a memory bank 7. For 16k/48k Spectrum, this method always
138    /// returns `0`.
139    fn visible_screen_bank(&self) -> usize { 0 }
140    /// Returns the current value of the video T-state counter.
141    fn current_video_ts(&self) -> VideoTs;
142    /// Modifies the current value of the video T-state counter.
143    fn set_video_ts(&mut self, vts: VideoTs);
144    /// Returns the current value of the video T-state clock.
145    fn current_video_clock(&self) -> VFrameTsCounter<Self::VideoFrame, Self::Contention>;
146    /// Returns the temporary video flash attribute state.
147    fn flash_state(&self) -> bool;
148}
149/// A collection of static methods and constants related to video parameters.
150/// ```text
151///                               - 0
152///     +-------------------------+ VSL_BORDER_TOP
153///     |                         |
154///     |  +-------------------+  | -
155///     |  |                   |  | |
156///     |  |                   |  |  
157///     |  |                   |  | VSL_PIXELS
158///     |  |                   |  |  
159///     |  |                   |  | |
160///     |  +-------------------+  | -
161///     |                         |
162///     +-------------------------+ VSL_BORDER_BOT
163///                               - VSL_COUNT
164/// |----- 0 -- HTS_RANGE ---------|
165/// |           HTS_COUNT          |
166/// ```
167pub trait VideoFrame: Copy + Debug {
168    /// A range of horizontal T-states, 0 should be where the frame starts.
169    const HTS_RANGE: Range<Ts>;
170    /// The number of horizontal T-states.
171    const HTS_COUNT: Ts = Self::HTS_RANGE.end - Self::HTS_RANGE.start;
172    /// The first visible video scan line index of the top border.
173    const VSL_BORDER_TOP: Ts;
174    /// A range of video scan line indexes where pixel data is being drawn.
175    const VSL_PIXELS: Range<Ts>;
176    /// The last visible video scan line index of the bottom border.
177    const VSL_BORDER_BOT: Ts;
178    /// The total number of video scan lines including the beam retrace.
179    const VSL_COUNT: Ts;
180    /// The total number of T-states per frame.
181    const FRAME_TSTATES_COUNT: FTs = Self::HTS_COUNT as FTs * Self::VSL_COUNT as FTs;
182    /// A rendered screen border size in pixels depending on the border size selection.
183    ///
184    /// **NOTE**: The upper and lower border size may be lower than the value returned here
185    ///  e.g. in the NTSC video frame.
186    fn border_size_pixels(border_size: BorderSize) -> u32 {
187        match border_size {
188            BorderSize::Full    => MAX_BORDER_SIZE,
189            BorderSize::Large   => MAX_BORDER_SIZE -   8,
190            BorderSize::Medium  => MAX_BORDER_SIZE - 2*8,
191            BorderSize::Small   => MAX_BORDER_SIZE - 3*8,
192            BorderSize::Tiny    => MAX_BORDER_SIZE - 4*8,
193            BorderSize::Minimal => MAX_BORDER_SIZE - 5*8,
194            BorderSize::Nil     => 0
195        }
196    }
197    /// Returns output screen pixel size (horizontal, vertical), including the border area, measured
198    /// in low-resolution pixels.
199    ///
200    /// The size depends on the given `border_size`.
201    fn screen_size_pixels(border_size: BorderSize) -> (u32, u32) {
202        let border = 2 * Self::border_size_pixels(border_size);
203        let w = PAL_HC - 2*MAX_BORDER_SIZE + border;
204        let h = (PAL_VC - 2*MAX_BORDER_SIZE + border)
205                .min((Self::VSL_BORDER_BOT + 1 - Self::VSL_BORDER_TOP) as u32);
206        (w, h)
207    }
208    /// Returns an iterator of the top border low-resolution scan line indexes.
209    fn border_top_vsl_iter(border_size: BorderSize) -> Range<Ts> {
210        let border = Self::border_size_pixels(border_size) as Ts;
211        let top = (Self::VSL_PIXELS.start - border).max(Self::VSL_BORDER_TOP);
212        top..Self::VSL_PIXELS.start
213    }
214    /// Returns an iterator of the bottom border low-resolution scan line indexes.
215    fn border_bot_vsl_iter(border_size: BorderSize) -> Range<Ts> {
216        let border = Self::border_size_pixels(border_size) as Ts;
217        let bot = (Self::VSL_PIXELS.end + border).min(Self::VSL_BORDER_BOT);
218        Self::VSL_PIXELS.end..bot
219    }
220
221    /// An iterator for rendering borders.
222    type BorderHtsIter: Iterator<Item=Ts>;
223    /// Returns an iterator of border latch horizontal T-states.
224    fn border_whole_line_hts_iter(border_size: BorderSize) -> Self::BorderHtsIter;
225    /// Returns an iterator of left border latch horizontal T-states.
226    fn border_left_hts_iter(border_size: BorderSize) -> Self::BorderHtsIter;
227    /// Returns an iterator of right border latch horizontal T-states.
228    fn border_right_hts_iter(border_size: BorderSize) -> Self::BorderHtsIter;
229
230    /// Returns a horizontal T-state counter after adding an additional T-states required for emulating 
231    /// a memory contention, while rendering lines that require reading video memory.
232    fn contention(hc: Ts) -> Ts;
233    /// Returns an optional floating bus horizontal offset for the given horizontal timestamp.
234    fn floating_bus_offset(_hc: Ts) -> Option<u16> {
235        None
236    }
237    /// Returns an optional floating bus screen address (in the screen address space) for the given timestamp.
238    ///
239    /// The returned screen address range is: [0x0000, 0x1B00).
240    #[inline]
241    fn floating_bus_screen_address(VideoTs { vc, hc }: VideoTs) -> Option<u16> {
242        let line = vc - Self::VSL_PIXELS.start;
243        if line >= 0 && vc < Self::VSL_PIXELS.end {
244            Self::floating_bus_offset(hc).map(|offs| {
245                let y = line as u16;
246                let col = (offs >> 3) << 1;
247                // println!("got offs: {} col:{} y:{}", offs, col, y);
248                match offs & 3 {
249                    0 =>          pixel_line_offset(y) + col,
250                    1 => 0x1800 + color_line_offset(y) + col,
251                    2 => 0x0001 + pixel_line_offset(y) + col,
252                    3 => 0x1801 + color_line_offset(y) + col,
253                    _ => unsafe { core::hint::unreachable_unchecked() }
254                }
255            })
256        }
257        else {
258            None
259        }
260    }
261    /// Returns an optional cell coordinates of a "snow effect" interference.
262    fn snow_interference_coords(_ts: VideoTs) -> Option<CellCoords> {
263        None
264    }
265    /// Returns `true` if the given scan line index is contended for MREQ (memory request) access.
266    ///
267    /// This indicates if the contention should be applied during the indicated video scan line.
268    #[inline]
269    fn is_contended_line_mreq(vsl: Ts) -> bool {
270        vsl >= Self::VSL_PIXELS.start && vsl < Self::VSL_PIXELS.end
271    }
272    /// Returns `true` if the given scan line index is contended for other than MREQ (memory request) access.
273    ///
274    /// This indicates if the contention should be applied during the indicated video scan line.
275    /// Other accesses include IORQ and instruction cycles not requiring memory access.
276    #[inline]
277    fn is_contended_line_no_mreq(vsl: Ts) -> bool {
278        vsl >= Self::VSL_PIXELS.start && vsl < Self::VSL_PIXELS.end
279    }
280    /// Converts video scan line and horizontal T-state counters to the frame T-state count without any normalization.
281    #[inline]
282    fn vc_hc_to_tstates(vc: Ts, hc: Ts) -> FTs {
283        vc as FTs * Self::HTS_COUNT as FTs + hc as FTs
284    }
285}
286
287impl From<BorderSize> for &'static str {
288    fn from(border: BorderSize) -> &'static str {
289        match border {
290            BorderSize::Full    => "full",
291            BorderSize::Large   => "large",
292            BorderSize::Medium  => "medium",
293            BorderSize::Small   => "small",
294            BorderSize::Tiny    => "tiny",
295            BorderSize::Minimal => "minimal",
296            BorderSize::Nil     => "none",
297        }
298    }
299}
300
301impl fmt::Display for BorderSize {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        f.write_str(<&str>::from(*self))
304    }
305}
306
307impl std::error::Error for ParseBorderSizeError {}
308
309impl fmt::Display for ParseBorderSizeError {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        f.write_str("unrecognized border size")
312    }
313}
314
315impl FromStr for BorderSize {
316    type Err = ParseBorderSizeError;
317    /// Parses a single word describing border size using case insensitive matching
318    /// or a single digit from 0 to 6.
319    fn from_str(name: &str) -> Result<Self, Self::Err> {
320        if name.eq_ignore_ascii_case("full") ||
321           name.eq_ignore_ascii_case("maxi") ||
322           name.eq_ignore_ascii_case("max") {
323            Ok(BorderSize::Full)
324        }
325        else if name.eq_ignore_ascii_case("large") ||
326                name.eq_ignore_ascii_case("big") {
327            Ok(BorderSize::Large)
328        }
329        else if name.eq_ignore_ascii_case("medium") ||
330                name.eq_ignore_ascii_case("medi")   ||
331                name.eq_ignore_ascii_case("med") {
332            Ok(BorderSize::Medium)
333        }
334        else if name.eq_ignore_ascii_case("small") ||
335                name.eq_ignore_ascii_case("sm") {
336            Ok(BorderSize::Small)
337        }
338        else if name.eq_ignore_ascii_case("tiny") {
339            Ok(BorderSize::Tiny)
340        }
341        else if name.eq_ignore_ascii_case("minimal") ||
342                name.eq_ignore_ascii_case("mini")    ||
343                name.eq_ignore_ascii_case("min") {
344            Ok(BorderSize::Minimal)
345        }
346        else if name.eq_ignore_ascii_case("none") ||
347                name.eq_ignore_ascii_case("nil")  ||
348                name.eq_ignore_ascii_case("null") ||
349                name.eq_ignore_ascii_case("zero") {
350            Ok(BorderSize::Nil)
351        }
352        else {
353            u8::from_str(name).map_err(|_| ParseBorderSizeError)
354            .and_then(|size|
355                BorderSize::try_from(size).map_err(|_| ParseBorderSizeError)
356            )
357        }
358    }
359}
360
361impl From<BorderSize> for u8 {
362    fn from(border: BorderSize) -> u8 {
363        border as u8
364    }
365}
366
367impl std::error::Error for TryFromUIntBorderSizeError {}
368
369impl fmt::Display for TryFromUIntBorderSizeError {
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        write!(f, "converted integer ({}) out of range for `BorderSize`", self.0)
372    }
373}
374
375impl TryFrom<u8> for BorderSize {
376    type Error = TryFromUIntBorderSizeError;
377    fn try_from(border: u8) -> Result<Self, Self::Error> {
378        use BorderSize::*;
379        Ok(match border {
380            6 => Full,
381            5 => Large,
382            4 => Medium,
383            3 => Small,
384            2 => Tiny,
385            1 => Minimal,
386            0 => Nil,
387            _ => return Err(TryFromUIntBorderSizeError(border))
388        })
389    }
390}
391
392impl std::error::Error for TryFromU8BorderColorError {}
393
394impl fmt::Display for TryFromU8BorderColorError {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        write!(f, "converted integer ({}) out of range for `BorderColor`", self.0)
397    }
398}
399
400impl TryFrom<u8> for BorderColor {
401    type Error = TryFromU8BorderColorError;
402    fn try_from(color: u8) -> core::result::Result<Self, Self::Error> {
403        BorderColor::from_bits(color).ok_or(TryFromU8BorderColorError(color))
404    }
405}
406
407impl From<UlaPortFlags> for BorderColor {
408    #[inline]
409    fn from(flags: UlaPortFlags) -> Self {
410        BorderColor::from_bits_truncate((flags & UlaPortFlags::BORDER_MASK).bits())
411    }
412}
413
414impl From<BorderColor> for u8 {
415    fn from(color: BorderColor) -> u8 {
416        color.bits()
417    }
418}
419
420/// Returns an offset into INK/PAPER bitmap memory of the given vertical coordinate `y` [0, 192) (0 on top).
421#[inline(always)]
422pub fn pixel_line_offset<T>(y: T) -> T
423    where T: Copy + From<u16> + BitAnd<Output=T> + Shl<u16, Output=T> + BitOr<Output=T>
424{
425    (y & T::from(0b0000_0111) ) << 8 |
426    (y & T::from(0b0011_1000) ) << 2 |
427    (y & T::from(0b1100_0000) ) << 5
428}
429
430/// Returns an offset into attributes memory of the given vertical coordinate `y` [0, 192) (0 on top).
431#[inline(always)]
432pub fn color_line_offset<T>(y: T) -> T
433    where T: Copy + From<u16> + Shr<u16, Output=T> + Shl<u16, Output=T>
434{
435    (y >> 3) << 5
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn video_offsets_works() {
444        assert_eq!(pixel_line_offset(0usize), 0usize);
445        assert_eq!(pixel_line_offset(1usize), 256usize);
446        assert_eq!(pixel_line_offset(8usize), 32usize);
447        assert_eq!(color_line_offset(0usize), 0usize);
448        assert_eq!(color_line_offset(1usize), 0usize);
449        assert_eq!(color_line_offset(8usize), 32usize);
450        assert_eq!(color_line_offset(191usize), 736usize);
451    }
452}