Skip to content

Commit

Permalink
Introduce tetromino-xlock-mode crate
Browse files Browse the repository at this point in the history
This change introduces the tetromino-xlock-mode workspace member, which
provides the means for creating a xlock mode that runs Tetromino's
auto-playing AI while the screen lock is active. In its current form it
is still somewhat limited and not particularly configurable, but it is
good enough to demonstrate workings.
  • Loading branch information
d-e-s-o committed Nov 29, 2023
1 parent d3696b1 commit 466da07
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Unreleased
----------
- Added `xlock` screen lock "mode"
- Made initial AI state configurable via configuration
- Updated `glutin` dependency to `0.31.0`
- Updated `winit` dependency to `0.29.2`
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
".",
"xlock/bindings",
"xlock/mode",
]

[package]
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,13 @@ Please refer to the help text (`tetromino --help`) for details on what
can be configured and how.


Screen Lock
-----------

**tetromino** comes with an `xlock(1)` mode that runs the game with the
auto-playing AI enabled while the screen is locked. Please refer to the
[**tetromino-xlock-mode** crate][tetromino-xlock-mode] for details.


[tetris]: https://github.com/d-e-s-o/tetris
[tetromino-xlock-mode]: xlock/mode
15 changes: 15 additions & 0 deletions xlock/mode/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "tetromino-xlock-mode"
version = "0.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
xlock = {package = "tetromino-xlock-bindings", path = "../bindings"}
tetromino = {path = "../.."}
raw-window-handle = {version = "0.5.2", default-features = false}
winit = {version = "0.29.2", default-features = false, features = ["x11"]}
32 changes: 32 additions & 0 deletions xlock/mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
tetromino-xlock-mode
====================

**tetromino-xlock-mode** is an `xlock(1)` mode running **tetromino**
(with auto-playing AI enabled) when the screen is locked.


Usage
-----

To use the mode please make sure that your `xlock` is built with module
support. **tetromino-xlock-mode** creates a module that you can install
and which, subsequently, `xlock` can pick up and use.

To install the module, use `cargo build --release` to build it and then
install the binary:
```sh
install --no-target-directory \
target/release/libtetromino_xlock_mode.so \
/usr/local/lib/X11/xlock/modules/tetromino.xlk
```

The `xlock` module path may differ on your system. Path such as
`/usr/lib/X11/xlock/modules/` have also been seen.

To use the lock screen mode, use `xlock -mode tetromino`. Note that you
*may* have to regenerate `xlock` bindings and redo the above steps if
your version of the program has changed its ABI compared to the shipped
bindings. Refer to the [**tetromino-xlock-bindings**
README][tetromino-xlock-bindings-readme] for details.

[tetromino-xlock-bindings-readme]: ../bindings/README.md
186 changes: 186 additions & 0 deletions xlock/mode/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (C) 2023 Daniel Mueller <deso@posteo.net>
// SPDX-License-Identifier: GPL-3.0-or-later

#![allow(clippy::let_unit_value)]

use std::mem::size_of;
use std::mem::transmute;
use std::num::NonZeroU32;
use std::os::raw::c_char;
use std::ptr;
use std::time::Instant;

use raw_window_handle::XlibDisplayHandle;
use raw_window_handle::XlibWindowHandle;

use winit::event_loop::EventLoop;
use winit::event_loop::EventLoopBuilder;
use winit::platform::x11::EventLoopBuilderExtX11 as _;

use tetromino_impl::Game;
use tetromino_impl::GameConfig;
use tetromino_impl::Renderer;
use tetromino_impl::Window;


// SAFETY: `ModeSpecOpt` is just a C-style POD with all bit patterns
// being valid.
static TETROMINO_OPTS: xlock::ModeSpecOpt =
unsafe { transmute([0u8; size_of::<xlock::ModeSpecOpt>()]) };

/// The "description" of the module as required by xlock proper. It
/// describes relevant callbacks and contains some other data.
#[no_mangle]
static tetromino_description: xlock::ModStruct = xlock::ModStruct {
cmdline_arg: b"tetromino\0" as *const _ as *const c_char,
init_name: b"init_tetromino\0" as *const _ as *const c_char,
callback_name: b"render_tetromino\0" as *const _ as *const c_char,
release_name: b"release_tetromino\0" as *const _ as *const c_char,
refresh_name: b"render_tetromino\0" as *const _ as *const c_char,
change_name: b"change_tetromino\0" as *const _ as *const c_char,
unused_name: ptr::null(),
msopt: &TETROMINO_OPTS as *const _ as *mut _,
// The number of microseconds between callbacks. 20ms means up to 50
// changes could be displayed fluently per second. That is sufficient
// for fluidity up to very high levels of game play.
def_delay: 20000,
def_count: 0,
def_cycles: 0,
def_size: 0,
def_ncolors: 64,
def_saturation: 0.0,
def_bitmap: b"\0" as *const _ as *const c_char,
desc: b"Plays Tetromino -- a Tetris clone\0" as *const _ as *const c_char,
flags: 0,
userdata: ptr::null_mut(),
};


/// Our "mode's" main state object.
struct State {
/// The event loop we use.
event_loop: EventLoop<()>,
/// Relevant Tetromino related data.
data: Option<(Window, Game, Renderer)>,
}


/// Handler for the "init" callback.
#[no_mangle]
extern "C" fn init_tetromino(mode_info: *const xlock::ModeInfo) {
// SAFETY: The hook is always called with a valid `ModeInfo` object.
let mode_info = unsafe { &*mode_info };
// SAFETY: The hook is always called with a valid `lockstruct` object.
let lock_struct = unsafe { &mut *mode_info.lockstruct };

if lock_struct.userdata.is_null() {
let event_loop = EventLoopBuilder::new()
.with_any_thread(true)
.build()
.unwrap();

let state = State {
event_loop,
data: None,
};
lock_struct.userdata = Box::into_raw(Box::new(state)).cast();
}

// We only ever set up shop on the very first screen.
if mode_info.windowinfo.screen == 0 {
// SAFETY: We are sure that `userdata` points to a valid `State` at
// this point.
let state = unsafe { lock_struct.userdata.cast::<State>().as_mut().unwrap() };
// At this point the Tetromino `Window` type doesn't support
// creation of multiple instances at the same time. Make sure to
// drop any previous data before we start over.
state.data = None;

// TODO: We certainly should not re-create the game when one is
// already present. It's unclear how to handle the window,
// because textures may logically "belong" to it (or at least
// the associated OpenGL context). We may not *have to*
// recreate it, conceptually. If we do, we may need to
// serialize the game and restore it to keep the state.
let mut display = XlibDisplayHandle::empty();
display.display = unsafe { transmute(mode_info.windowinfo.display) };
display.screen = mode_info.windowinfo.screen;

let mut window = XlibWindowHandle::empty();
window.window = mode_info.windowinfo.window;
window.visual_id = unsafe { (*(*mode_info.screeninfo).visual).visualid };

let phys_w = NonZeroU32::new(u32::try_from(mode_info.windowinfo.width).unwrap_or_default())
.unwrap_or_else(|| unsafe { NonZeroU32::new_unchecked(1) });
let phys_h = NonZeroU32::new(u32::try_from(mode_info.windowinfo.height).unwrap_or_default())
.unwrap_or_else(|| unsafe { NonZeroU32::new_unchecked(1) });
let window = Window::from_xlib_data(&state.event_loop, display, window as _).unwrap();

let mut config = GameConfig::default();
config.start_level = 200;
config.enable_ai = true;

let game = Game::with_config(&config).unwrap();
let renderer = Renderer::new(phys_w, phys_h, game.width(), game.height());

state.data = Some((window, game, renderer));
} else {
// TODO: We probably still want to be sure to clear the window on
// all other screens.
}
}

/// Handler for the "render" callback.
#[no_mangle]
extern "C" fn render_tetromino(mode_info: *const xlock::ModeInfo) {
// SAFETY: The hook is always called with a valid `ModeInfo` object.
let mode_info = unsafe { &*mode_info };
// SAFETY: The hook is always called with a valid `lockstruct` object.
let lock_struct = unsafe { &mut *mode_info.lockstruct };
// SAFETY The "render" callback is only ever invoked after "init", so
// we are sure we have a `State` object set.
let state = unsafe { lock_struct.userdata.cast::<State>().as_mut().unwrap() };

if let Some((window, game, renderer)) = &mut state.data {
let now = Instant::now();
let (_change, _wait) = game.tick(now);

let renderer = renderer.on_pre_render(window);
let () = game.render(&renderer);
let () = drop(renderer);
let () = window.swap_buffers();
}
}

/// Handler for the "change" callback.
///
/// We restart the game when receiving this callback.
#[no_mangle]
extern "C" fn change_tetromino(mode_info: *const xlock::ModeInfo) {
// SAFETY: The hook is always called with a valid `ModeInfo` object.
let mode_info = unsafe { &*mode_info };
// SAFETY: The hook is always called with a valid `lockstruct` object.
let lock_struct = unsafe { &mut *mode_info.lockstruct };
// SAFETY The "change" callback is only ever invoked after "init", so
// we are sure we have a `State` object set.
let state = unsafe { lock_struct.userdata.cast::<State>().as_mut().unwrap() };

if let Some((_window, game, _renderer)) = &mut state.data {
let _change = game.restart();
}
}

/// Handler for the "release" callback.
#[no_mangle]
extern "C" fn release_tetromino(mode_info: *const xlock::ModeInfo) {
// SAFETY: The hook is always called with a valid `ModeInfo` object.
let mode_info = unsafe { &*mode_info };
// SAFETY: The hook is always called with a valid `lockstruct` object.
let lock_struct = unsafe { &mut *mode_info.lockstruct };
if !lock_struct.userdata.is_null() {
// SAFETY: If `userdata` is set it points to a valid boxed up
// `State` object.
let _state = unsafe { Box::<State>::from_raw(lock_struct.userdata.cast()) };
lock_struct.userdata = ptr::null_mut();
}
}

0 comments on commit 466da07

Please sign in to comment.