diff --git a/Cargo.lock b/Cargo.lock index f9116c5..abaf87b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,11 +254,11 @@ dependencies = [ [[package]] name = "neotron-sdk" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2043d6202942b6ebecbf02c95a0d1498bade2ffb287355af02fee6a5ab5de331" +source = "git+https://github.com/neotron-compute/neotron-sdk.git?branch=add-gfx-ioctl-defines#49509fce305a4d7cf956dcfcaba06a06a307fd7e" dependencies = [ "crossterm", "neotron-api 0.2.0", + "neotron-common-bios", "neotron-ffi", ] @@ -459,6 +459,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vidtest" +version = "0.1.0" +dependencies = [ + "neotron-sdk", +] + [[package]] name = "vte" version = "0.12.1" diff --git a/Cargo.toml b/Cargo.toml index 2b7a204..61d1de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "neotron-os", "utilities/flames", + "utilities/vidtest", ] resolver = "2" exclude = [ @@ -9,7 +10,7 @@ exclude = [ ] [workspace.dependencies] -neotron-sdk = "0.2.0" +neotron-sdk = { git = "https://github.com/neotron-compute/neotron-sdk.git", branch = "add-gfx-ioctl-defines" } [profile.release] lto = true diff --git a/nbuild/src/main.rs b/nbuild/src/main.rs index 90dac30..4141da2 100644 --- a/nbuild/src/main.rs +++ b/nbuild/src/main.rs @@ -56,6 +56,13 @@ fn packages() -> Vec { kind: nbuild::PackageKind::Utility, testable: false, }, + nbuild::Package { + name: "vidtest", + path: std::path::Path::new("./utilities/vidtest/Cargo.toml"), + output_template: Some("./target/{target}/{profile}/vidtest"), + kind: nbuild::PackageKind::Utility, + testable: false, + }, nbuild::Package { name: "Neotron OS", path: std::path::Path::new("./neotron-os/Cargo.toml"), diff --git a/neotron-os/src/commands/mod.rs b/neotron-os/src/commands/mod.rs index 3c3324e..67eabd2 100644 --- a/neotron-os/src/commands/mod.rs +++ b/neotron-os/src/commands/mod.rs @@ -35,7 +35,6 @@ pub static OS_MENU: menu::Menu = menu::Menu { &fs::ROM_ITEM, &screen::CLS_ITEM, &screen::MODE_ITEM, - &screen::GFX_ITEM, &input::KBTEST_ITEM, &hardware::SHUTDOWN_ITEM, &sound::MIXER_ITEM, diff --git a/neotron-os/src/commands/screen.rs b/neotron-os/src/commands/screen.rs index 04ae19a..f8cf592 100644 --- a/neotron-os/src/commands/screen.rs +++ b/neotron-os/src/commands/screen.rs @@ -1,12 +1,6 @@ //! Screen-related commands for Neotron OS -use crate::{ - bios::{ - video::{Format, Mode}, - ApiResult, - }, - osprint, osprintln, Ctx, -}; +use crate::{bios::video::Mode, osprint, osprintln, Ctx}; pub static CLS_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -29,18 +23,6 @@ pub static MODE_ITEM: menu::Item = menu::Item { help: Some("List/change video mode"), }; -pub static GFX_ITEM: menu::Item = menu::Item { - item_type: menu::ItemType::Callback { - function: gfx_cmd, - parameters: &[menu::Parameter::Mandatory { - parameter_name: "new_mode", - help: Some("The new gfx mode to try"), - }], - }, - command: "gfx", - help: Some("Test a graphics mode"), -}; - /// Called when the "cls" command is executed. fn cls_cmd(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { // Reset SGR, go home, clear screen, @@ -49,63 +31,8 @@ fn cls_cmd(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ct /// Called when the "mode" command is executed fn mode_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { - if let Some(new_mode) = menu::argument_finder(item, args, "new_mode").unwrap() { - let Ok(mode_num) = new_mode.parse::() else { - osprintln!("Invalid integer {:?}", new_mode); - return; - }; - let Some(mode) = Mode::try_from_u8(mode_num) else { - osprintln!("Invalid mode {:?}", new_mode); - return; - }; - let has_vga = { - let mut guard = crate::VGA_CONSOLE.lock(); - guard.as_mut().is_some() - }; - if !has_vga { - osprintln!("No VGA console."); - return; - } - let api = crate::API.get(); - match mode.format() { - Format::Text8x16 => {} - Format::Text8x8 => {} - _ => { - osprintln!("Not a text mode?"); - return; - } - } - if (api.video_mode_needs_vram)(mode) { - // The OS currently has no VRAM for text modes - osprintln!("That mode requires more VRAM than the BIOS has."); - return; - } - // # Safety - // - // It's always OK to pass NULl to this API. - match unsafe { (api.video_set_mode)(mode, core::ptr::null_mut()) } { - ApiResult::Ok(_) => { - let mut guard = crate::VGA_CONSOLE.lock(); - if let Some(console) = guard.as_mut() { - console.change_mode(mode); - } - osprintln!("Now in mode {}", mode.as_u8()); - } - ApiResult::Err(e) => { - osprintln!("Failed to change mode: {:?}", e); - } - } - } else { - print_modes(); - } -} - -/// Called when the "gfx" command is executed -/// -/// Performs a selection of graphical tests -fn gfx_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], ctx: &mut Ctx) { let Some(new_mode) = menu::argument_finder(item, args, "new_mode").unwrap() else { - osprintln!("Missing arg"); + print_modes(); return; }; let Ok(mode_num) = new_mode.parse::() else { @@ -116,174 +43,20 @@ fn gfx_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], ctx: osprintln!("Invalid mode {:?}", new_mode); return; }; - let api = crate::API.get(); - let old_mode = (api.video_get_mode)(); - let old_ptr = (api.video_get_framebuffer)(); - - let buffer = ctx.tpa.as_slice_u8(); - if mode.frame_size_bytes() > buffer.len() { - osprintln!("Not enough space in TPA"); + let mut lock = crate::VGA_CONSOLE.lock(); + let Some(vga_console) = lock.as_mut() else { + osprintln!("No VGA console."); return; - } - let buffer_ptr = buffer.as_mut_ptr() as *mut u32; - - if let neotron_common_bios::FfiResult::Err(e) = - unsafe { (api.video_set_mode)(mode, buffer_ptr) } - { - osprintln!("Couldn't set mode {}: {:?}", mode_num, e); - } - - let gen_value = |x: u16, y: u16, colour: u32| -> u64 { - (x as u64) << 48 | (y as u64) << 32 | (colour & 0xFFFFFF) as u64 }; - - // plots a vertical colour stripe pattern. - let vertical = |handle| -> Result<(), neotron_api::Error> { - for y in 0..mode.vertical_lines() { - let mut colour = 0u32; - for x in 0..mode.horizontal_pixels() { - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CHUNKY_PLOT, - gen_value(x, y, colour), - ) - .into(); - result?; - colour = colour.wrapping_add(1) & 0xFFFFFF; - } + // Passing a null pointer is OK here because the BIOS will either allocate + // space, or give us a blank screen. + match unsafe { vga_console.change_mode(mode, core::ptr::null_mut()) } { + Ok(_) => { + osprintln!("Changed to mode {}", mode_num); } - - // Now wait for user input - while crate::STD_INPUT.lock().get_raw().is_none() { - // spin + Err(e) => { + osprintln!("Failed to set mode {}: BIOS said {:?}", mode_num, e); } - - Ok(()) - }; - - // plots a horizontal colour stripe pattern. - let horizontal = |handle| -> Result<(), neotron_api::Error> { - let mut colour = 0u32; - for y in 0..mode.vertical_lines() { - for x in 0..mode.horizontal_pixels() { - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CHUNKY_PLOT, - gen_value(x, y, colour), - ) - .into(); - result?; - } - colour = colour.wrapping_add(1) & 0xFFFFFF; - } - - // Now wait for user input - while crate::STD_INPUT.lock().get_raw().is_none() { - // spin - } - - Ok(()) - }; - - // plots a rolling stripe pattern. - let rolling = |handle| -> Result<(), neotron_api::Error> { - for y in 0..mode.vertical_lines() { - let mut colour = y as u32; - for x in 0..mode.horizontal_pixels() { - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CHUNKY_PLOT, - gen_value(x, y, colour), - ) - .into(); - result?; - colour = colour.wrapping_add(1) & 0xFFFFFF; - } - } - - // Now wait for user input - while crate::STD_INPUT.lock().get_raw().is_none() { - // spin - } - - Ok(()) - }; - - // plots a grid - let grid = |handle| -> Result<(), neotron_api::Error> { - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CLEAR_SCREEN, - 0, - ) - .into(); - result?; - - let width = mode.horizontal_pixels() / 10; - let height = mode.vertical_lines() / 10; - - for y in 0..mode.vertical_lines() { - if (y % height) == 0 || (y == mode.vertical_lines() - 1) { - // solid line - for x in 0..mode.horizontal_pixels() { - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CHUNKY_PLOT, - gen_value(x, y, 15), - ) - .into(); - result?; - } - } else { - // stripes - for x in 0..mode.horizontal_pixels() { - let colour = if (x % width) == 0 || (x == mode.horizontal_pixels() - 1) { - 15 - } else { - 0 - }; - let result: Result<_, _> = (crate::program::CALLBACK_TABLE.ioctl)( - handle, - crate::program::GFX_COMMAND_CHUNKY_PLOT, - gen_value(x, y, colour), - ) - .into(); - result?; - } - } - } - - // Now wait for user input - while crate::STD_INPUT.lock().get_raw().is_none() { - // spin - } - - Ok(()) - }; - - let handle: Result<_, _> = - (crate::program::CALLBACK_TABLE.open)("GFX:".into(), neotron_api::file::Flags::WRITE) - .into(); - if let Ok(handle) = handle { - if let Err(e) = vertical(handle) { - osprintln!("Draw failure on vertical: {:?}", e); - } - if let Err(e) = horizontal(handle) { - osprintln!("Draw failure on horizontal: {:?}", e); - } - if let Err(e) = rolling(handle) { - osprintln!("Draw failure on rolling: {:?}", e); - } - if let Err(e) = grid(handle) { - osprintln!("Draw failure on grid: {:?}", e); - } - // close the handle - let _ = (crate::program::CALLBACK_TABLE.close)(handle); - } - - // Put it back as it was - unsafe { - (api.video_set_mode)(old_mode, old_ptr); } } diff --git a/neotron-os/src/program.rs b/neotron-os/src/program.rs index f6d3dd1..ecb3a67 100644 --- a/neotron-os/src/program.rs +++ b/neotron-os/src/program.rs @@ -302,6 +302,11 @@ impl TransientProgramArea { return Err(Error::NothingLoaded); } + // Record the current video mode + let api = API.get(); + let old_mode = (api.video_get_mode)(); + let old_ptr = (api.video_get_framebuffer)(); + // Setup the default file handles let mut open_handles = OPEN_HANDLES.lock(); open_handles[0] = OpenHandle::StdIn; @@ -332,6 +337,12 @@ impl TransientProgramArea { } drop(open_handles); + let mut lock = crate::VGA_CONSOLE.lock(); + if let Some(console) = lock.as_mut() { + // put the video mode back as it was + let _ = unsafe { console.change_mode(old_mode, old_ptr) }; + } + self.last_entry = 0; Ok(result) } @@ -795,6 +806,16 @@ pub const GFX_COMMAND_CLEAR_SCREEN: u64 = 0; /// * `colour` is 24 bits, and is taken modulo the number of on-screen colours pub const GFX_COMMAND_CHUNKY_PLOT: u64 = 1; +/// Change graphics mode +/// +/// The command contains the video mode in the upper 32 bits and a pointer to a +/// framebuffer in the lower 32 bits. +/// +/// The framebuffer pointer must point to a 32-bit aligned region of memory +/// that is large enough for the selected mode. If you pass `null`, then the OS +/// will attempt to allocate a framebuffer for you. +pub const GFX_COMMAND_CHANGE_MODE: u64 = 2; + /// Handle framebuffer-specific ioctls fn ioctl_gfx(_h: &mut OpenHandle, command: u64, value: u64) -> neotron_api::Result { let api = API.get(); @@ -852,6 +873,9 @@ fn ioctl_gfx(_h: &mut OpenHandle, command: u64, value: u64) -> neotron_api::Resu if y >= video_mode.vertical_lines() { return neotron_api::Result::Err(neotron_api::Error::InvalidArg); } + if fb_ptr.is_null() { + return neotron_api::Result::Err(neotron_api::Error::NotFound); + } // our video line starts here let line_start = unsafe { fb_ptr.byte_add(video_mode.line_size_bytes() * (y as usize)) } as *mut u8; @@ -874,6 +898,24 @@ fn ioctl_gfx(_h: &mut OpenHandle, command: u64, value: u64) -> neotron_api::Resu }; result.into() } + GFX_COMMAND_CHANGE_MODE => { + let mode = (value >> 32) as u8; + let Some(mode) = neotron_common_bios::video::Mode::try_from_u8(mode) else { + return neotron_api::Result::Err(neotron_api::Error::InvalidArg); + }; + let ptr = value as u32 as usize as *mut u32; + let mut lock = crate::VGA_CONSOLE.lock(); + if let Some(console) = lock.as_mut() { + // change the video mode + match unsafe { console.change_mode(mode, ptr) } { + Ok(_) => neotron_api::Result::Ok(0), + Err(_) => neotron_api::Result::Err(neotron_api::Error::DeviceSpecific), + } + } else { + // there is no console to change the mode for + neotron_api::Result::Err(neotron_api::Error::NotFound) + } + } _ => neotron_api::Result::Err(neotron_api::Error::InvalidArg), } } diff --git a/neotron-os/src/vgaconsole.rs b/neotron-os/src/vgaconsole.rs index 1af3a36..24027a1 100644 --- a/neotron-os/src/vgaconsole.rs +++ b/neotron-os/src/vgaconsole.rs @@ -73,13 +73,36 @@ impl VgaConsole { /// Change the video mode /// - /// Non text modes are ignored. - pub fn change_mode(&mut self, mode: Mode) { + /// The `fb_ptr` is given to the BIOS. It can be null, or it must point to a + /// region big enough to handle the chosen graphics mode. + pub unsafe fn change_mode( + &mut self, + mode: Mode, + fb_ptr: *mut u32, + ) -> Result<(), neotron_common_bios::Error> { + // TODO: support bitmap text rendering whilst in graphics mode + + // Change mode with the BIOS and return the result + let api = crate::API.get(); + if let neotron_common_bios::FfiResult::Err(e) = (api.video_set_mode)(mode, fb_ptr) { + return Err(e); + } + // set up the console for this mode if let (Some(height), Some(width)) = (mode.text_height(), mode.text_width()) { + // it's a text mode self.inner.height = height as isize; self.inner.width = width as isize; + // get whatever buffer the BIOS chose to use + self.inner.addr = (api.video_get_framebuffer)(); self.clear(); + } else { + // it's a graphics mode - disable output + self.inner.height = 0; + self.inner.width = 0; + self.inner.addr = core::ptr::null_mut(); } + + Ok(()) } /// Clear the screen. @@ -245,6 +268,10 @@ impl ConsoleInner { /// /// Don't do this if the cursor is enabled. fn write_at(&mut self, row: isize, col: isize, glyph: u8, is_cursor: bool) { + if self.addr.is_null() { + // console disabled + return; + } assert!(row < self.height, "{} >= {}?", row, self.height); assert!(col < self.width, "{} => {}?", col, self.width); if !crate::IS_PANIC.load(core::sync::atomic::Ordering::Relaxed) && !is_cursor { @@ -276,6 +303,10 @@ impl ConsoleInner { /// /// Don't do this if the cursor is enabled. fn read_at(&mut self, row: isize, col: isize) -> u8 { + if self.addr.is_null() { + // console disabled - everything is a blank space + return b' '; + } assert!(row < self.height, "{} >= {}?", row, self.height); assert!(col < self.width, "{} => {}?", col, self.width); if !crate::IS_PANIC.load(core::sync::atomic::Ordering::Relaxed) { @@ -290,6 +321,10 @@ impl ConsoleInner { /// /// The bottom line will be all space characters. fn scroll_page(&mut self) { + if self.height == 0 && self.width == 0 { + // console disabled + return; + } let row_len_words = self.width / 2; unsafe { // Scroll rows[1..=height-1] to become rows[0..=height-2]. diff --git a/utilities/vidtest/Cargo.toml b/utilities/vidtest/Cargo.toml new file mode 100644 index 0000000..bae4b4d --- /dev/null +++ b/utilities/vidtest/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vidtest" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Jonathan 'theJPster' Pallant "] +description = "Video Test for Neotron systems" + +[dependencies] +neotron-sdk = { workspace = true } + +# See workspace for profile settings diff --git a/utilities/vidtest/README.md b/utilities/vidtest/README.md new file mode 100644 index 0000000..0d6bc4b --- /dev/null +++ b/utilities/vidtest/README.md @@ -0,0 +1,10 @@ +# VidTest + +Displays some patterns in framebuffer mode. + +Requires a 'mode' argument. + +```text +> rom vidtest +> run 5 +``` diff --git a/utilities/vidtest/build.rs b/utilities/vidtest/build.rs new file mode 100644 index 0000000..4fbd085 --- /dev/null +++ b/utilities/vidtest/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-link-arg-bin=vidtest=-Tneotron-cortex-m.ld"); +} diff --git a/utilities/vidtest/src/lib.rs b/utilities/vidtest/src/lib.rs new file mode 100644 index 0000000..734429a --- /dev/null +++ b/utilities/vidtest/src/lib.rs @@ -0,0 +1,200 @@ +//! Logic for the vidtest utility + +#![no_std] +#![deny(missing_docs)] + +use core::{cell::UnsafeCell, fmt::Write}; + +// Big enough for Mode 5 (640x480 @ 16 colours) +struct RacyBuffer { + inner: UnsafeCell<[u32; 640 * 480 / 8]>, +} + +unsafe impl Sync for RacyBuffer {} + +static FRAMEBUFFER: RacyBuffer = RacyBuffer { + inner: UnsafeCell::new([0; 640 * 480 / 8]), +}; + +/// Entry point to the program +pub fn main() -> i32 { + let mut stdout = neotron_sdk::stdout(); + let Some(new_mode) = neotron_sdk::arg(0) else { + _ = writeln!(stdout, "Must supply a mode argument, like 7 for Mode 7"); + return -1; + }; + + let Ok(mode_num) = new_mode.parse::() else { + _ = writeln!(stdout, "Invalid integer {:?}", new_mode); + return -1; + }; + let Some(mode) = neotron_sdk::VideoMode::try_from_u8(mode_num) else { + _ = writeln!(stdout, "Invalid mode {:?}", new_mode); + return -1; + }; + + let free_space = core::mem::size_of_val(&FRAMEBUFFER); + if mode.frame_size_bytes() > free_space { + _ = writeln!( + stdout, + "Mode requires {} bytes, we have {} bytes", + mode.frame_size_bytes(), + free_space + ); + return -1; + } + + let handle: Result<_, _> = neotron_sdk::File::open( + neotron_sdk::path::Path::new("GFX:").unwrap(), + neotron_sdk::Flags::WRITE, + ); + if let Ok(handle) = handle { + if let Err(e) = unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHANGE_MODE, + neotron_sdk::ioctls::gfx::change_mode_value( + mode, + FRAMEBUFFER.inner.get() as *mut u32, + ), + ) + } { + _ = writeln!(stdout, "Mode change failure: {:?}", e); + return -1; + } + if let Err(e) = vertical(&handle, mode) { + _ = writeln!(stdout, "Draw failure on vertical: {:?}", e); + } + if let Err(e) = horizontal(&handle, mode) { + _ = writeln!(stdout, "Draw failure on horizontal: {:?}", e); + } + if let Err(e) = rolling(&handle, mode) { + _ = writeln!(stdout, "Draw failure on rolling: {:?}", e); + } + if let Err(e) = grid(&handle, mode) { + _ = writeln!(stdout, "Draw failure on grid: {:?}", e); + } + } + + 0 +} + +/// plots a vertical colour stripe pattern. +fn vertical( + handle: &neotron_sdk::File, + mode: neotron_sdk::VideoMode, +) -> Result<(), neotron_sdk::Error> { + for y in 0..mode.vertical_lines() { + let mut colour = 0u32; + for x in 0..mode.horizontal_pixels() { + unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHUNKY_PLOT, + neotron_sdk::ioctls::gfx::chunky_plot_value(x, y, colour), + )?; + } + + colour = colour.wrapping_add(1) & 0xFFFFFF; + } + } + + wait_for_key(); + + Ok(()) +} + +/// plots a horizontal colour stripe pattern. +fn horizontal( + handle: &neotron_sdk::File, + mode: neotron_sdk::VideoMode, +) -> Result<(), neotron_sdk::Error> { + let mut colour = 0u32; + for y in 0..mode.vertical_lines() { + for x in 0..mode.horizontal_pixels() { + unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHUNKY_PLOT, + neotron_sdk::ioctls::gfx::chunky_plot_value(x, y, colour), + )?; + } + } + colour = colour.wrapping_add(1) & 0xFFFFFF; + } + + wait_for_key(); + + Ok(()) +} + +/// plots a rolling stripe pattern. +fn rolling( + handle: &neotron_sdk::File, + mode: neotron_sdk::VideoMode, +) -> Result<(), neotron_sdk::Error> { + for y in 0..mode.vertical_lines() { + let mut colour = y as u32; + for x in 0..mode.horizontal_pixels() { + unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHUNKY_PLOT, + neotron_sdk::ioctls::gfx::chunky_plot_value(x, y, colour), + )?; + } + colour = colour.wrapping_add(1) & 0xFFFFFF; + } + } + + wait_for_key(); + + Ok(()) +} + +/// plots a grid +fn grid( + handle: &neotron_sdk::File, + mode: neotron_sdk::VideoMode, +) -> Result<(), neotron_sdk::Error> { + unsafe { handle.ioctl(neotron_sdk::ioctls::gfx::COMMAND_CLEAR_SCREEN, 0) }?; + + let width = mode.horizontal_pixels() / 10; + let height = mode.vertical_lines() / 10; + + for y in 0..mode.vertical_lines() { + if (y % height) == 0 || (y == mode.vertical_lines() - 1) { + // solid line + for x in 0..mode.horizontal_pixels() { + unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHUNKY_PLOT, + neotron_sdk::ioctls::gfx::chunky_plot_value(x, y, 15), + )?; + } + } + } else { + // stripes + for x in 0..mode.horizontal_pixels() { + if (x % width) == 0 || (x == mode.horizontal_pixels() - 1) { + unsafe { + handle.ioctl( + neotron_sdk::ioctls::gfx::COMMAND_CHUNKY_PLOT, + neotron_sdk::ioctls::gfx::chunky_plot_value(x, y, 15), + )?; + } + } + } + } + } + + wait_for_key(); + + Ok(()) +} + +fn wait_for_key() { + let stdin = neotron_sdk::stdin(); + let mut buffer = [0u8; 1]; + while let Ok(0) = stdin.read(&mut buffer) { + // spin + } +} + +// End of file diff --git a/utilities/vidtest/src/main.rs b/utilities/vidtest/src/main.rs new file mode 100644 index 0000000..369ccfb --- /dev/null +++ b/utilities/vidtest/src/main.rs @@ -0,0 +1,14 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +#[cfg(not(target_os = "none"))] +fn main() { + neotron_sdk::init(); +} + +#[no_mangle] +extern "C" fn neotron_main() -> i32 { + vidtest::main() +} + +// End of file