Недавно я наткнулся на удивившее меня сообщение. Оказывается, официальное приложение «Сапёр» от Microsoft содержит рекламу, платные опции и весит 235 МБ, требуя не меньше 500 МБ оперативы. Да, вы не ослышались. У нынешнего «Сапёра» есть рекламное окошко сбоку, а ещё можно дополнительно посмотреть рекламу для получения бонусов в игре.
![](https://habrastorage.org/getpro/habr/upload_files/eee/129/a60/eee129a6064dcfca824a01339476a822.png)
И тогда я подумал, что было бы интересно сделать полноценное приложение с графическим интерфейсом. Воссоздать культовую видеоигру «Сапёр» с нуля. Сколько мегабайт будет весить готовая игра? Насколько это сложно? Может ли любитель сделать это за несколько часов?
Геймплей будет классическим. Enter перезапускает игру, а для открытия ячейки под курсором нужно нажать любую кнопку мыши.
![](https://habrastorage.org/getpro/habr/upload_files/76b/9fd/e6a/76b9fde6a711ce0e536eb06abd799f8e.png)
В результате получается статически связанный исполняемый файл объёмом ~ 300 Кбайт, который не требует библиотек и использует постоянный объем ~ 1 Мб оперативной памяти. Это примерно в тысячу раз меньше, чем у Microsoft. И это всего лишь несколько сотен строк кода.
Преимущество такого подхода в том, что приложение получается крошечным и автономным: статически связанное с несколькими используемыми битами libC, оно может быть скомпилировано в любой Unix и скопировано повсюду, и оно будет работать на любой машине (с той же ОС/архитектурой). Запустится даже на старых Linux 20-летней давности.
Репозиторий исходного кода игры находится здесь. Но давайте разберём наш код.
Что мы делаем
11-я версия протокола X появилась на свет в 1987 году и с тех пор не менялась. Поскольку протокол появился на десять лет раньше графических процессоров, его модель на самом деле не соответствует современным аппаратным средствам. Тем не менее, он присутствует повсюду. В любой Unix есть X‑сервер, даже в macOS с XQuartz, и теперь Windows поддерживает запуск приложений Linux с графическим интерфейсом внутри WALL! X11 ещё никогда не был таким доступным.
Протокол относительно прост, а порог входа невысок: достаточно создать сокет, и мы свободны. А для 2D‑приложений не нужно быть мастером Vulkan или даже взаимодействовать с графическим процессором. Черт возьми, это будет работать даже без графического процессора!
В наши дни все, кто пишет графические интерфейсы, используют огромное количество библиотек, начиная с чрезмерно сложных уважаемых библиотек libX11 и libxcb и заканчивая Qt и SDL.
Вот что нам нужно сделать:
Открыть окно
Загрузить графические данные (один спрайт со всеми ресурсами)
Отрисовать части спрайта в окне
Реагировать на нажатия клавиатуры/мыши
Всё. Обратите внимание: каждый шаг — это 1–3 сообщения X11, которые нам нужно создать и отправить. Единственные сообщения, которые мы получаем, — это нажатия клавиш и мыши.
Писать будем на языке программирования Odin, который мне очень нравится. Но если вы хотите использовать C или что‑то еще, дерзайте. Все, что нам нужно, — это уметь открывать сокет Unix, отправлять и получать по нему данные и загружать изображение в память. Для этого мы будем использовать формат PNG, поскольку в стандартной библиотеке Odin есть поддержка PNG. Но можно использовать формат PPM (я проделывал нечто подобное с Wayland), который легко поддаётся синтаксическому анализу. Поскольку Odin поддерживает и то, и другое в своей стандартной библиотеке, выбор не имеет значения. Я остановился на PNG, поскольку он более экономичен.
Многие вещи в X11 не задокументированы. Если вы разрабатываете приложения для X11, даже с использованием библиотек, эта статья может стать хорошим учебным пособием. Ну, или вам может быть просто интересно, как провернуть тот или иной трюк с кодом.
Аутентификация
В этом проекте мы будем использовать протокол аутентификации X. Это связано с тем, что при работе под Wayland с XWayland в некоторых окружениях рабочего стола, таких как Gnome, нам приходится использовать аутентификацию.
Это требует от нашего приложения считывания токена длиной 16 байт, который находится в файле в домашнем каталоге пользователя, и включения его в рукопожатие, которое мы посылаем X‑серверу.
Этот механизм называется MIT-MAGIC-COOKIE-1
.
Загвоздка в том, что этот файл содержит несколько токенов для различных механизмов аутентификации и сетевых хостов. Помните, X11 предназначен для работы по сети. Однако здесь нас интересует только запись для localhost.
Здесь придётся подумать, поразбираться. В основном, чтобы понять то, что делает libXau
. Из его документации:
The .Xauthority file is a binary file consisting of a sequence of entries
in the following format:
2 bytes Family value (second byte is as in protocol HOST)
2 bytes address length (always MSB first)
A bytes host address (as in protocol HOST)
2 bytes display "number" length (always MSB first)
S bytes display "number" string
2 bytes name length (always MSB first)
N bytes authorization name string
2 bytes data length (always MSB first)
D bytes authorization data string
Сначала давайте определим некоторые типы и константы:
AUTH_ENTRY_FAMILY_LOCAL: u16 : 1
AUTH_ENTRY_MAGIC_COOKIE: string : "MIT-MAGIC-COOKIE-1"
AuthToken :: [16]u8
AuthEntry :: struct {
family: u16,
auth_name: []u8,
auth_data: []u8,
}
Мы определяем только те поля, которые нас интересуют. Давайте теперь разберём каждую запись:
read_x11_auth_entry :: proc(buffer: ^bytes.Buffer) -> (AuthEntry, bool) {
entry := AuthEntry{}
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&entry.family))
if err == .EOF {return {}, false}
assert(err == .None)
assert(n_read == size_of(entry.family))
}
address_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&address_len))
assert(err == .None)
address_len = bits.byte_swap(address_len)
assert(n_read == size_of(address_len))
}
address := make([]u8, address_len)
{
n_read, err := bytes.buffer_read(buffer, address)
assert(err == .None)
assert(n_read == cast(int)address_len)
}
display_number_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&display_number_len))
assert(err == .None)
display_number_len = bits.byte_swap(display_number_len)
assert(n_read == size_of(display_number_len))
}
display_number := make([]u8, display_number_len)
{
n_read, err := bytes.buffer_read(buffer, display_number)
assert(err == .None)
assert(n_read == cast(int)display_number_len)
}
auth_name_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_name_len))
assert(err == .None)
auth_name_len = bits.byte_swap(auth_name_len)
assert(n_read == size_of(auth_name_len))
}
entry.auth_name = make([]u8, auth_name_len)
{
n_read, err := bytes.buffer_read(buffer, entry.auth_name)
assert(err == .None)
assert(n_read == cast(int)auth_name_len)
}
auth_data_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_data_len))
assert(err == .None)
auth_data_len = bits.byte_swap(auth_data_len)
assert(n_read == size_of(auth_data_len))
}
entry.auth_data = make([]u8, auth_data_len)
{
n_read, err := bytes.buffer_read(buffer, entry.auth_data)
assert(err == .None)
assert(n_read == cast(int)auth_data_len)
}
return entry, true
}
Теперь мы можем просмотреть различные записи в файле, чтобы найти ту, которая нам нужна:
load_x11_auth_token :: proc(allocator := context.allocator) -> (token: AuthToken, ok: bool) {
context.allocator = allocator
defer free_all(allocator)
filename_env := os.get_env("XAUTHORITY")
filename :=
len(filename_env) != 0 \
? filename_env \
: filepath.join([]string{os.get_env("HOME"), ".Xauthority"})
data := os.read_entire_file_from_filename(filename) or_return
buffer := bytes.Buffer{}
bytes.buffer_init(&buffer, data[:])
for {
auth_entry := read_x11_auth_entry(&buffer) or_break
if auth_entry.family == AUTH_ENTRY_FAMILY_LOCAL &&
slice.equal(auth_entry.auth_name, transmute([]u8)AUTH_ENTRY_MAGIC_COOKIE) &&
len(auth_entry.auth_data) == size_of(AuthToken) {
mem.copy_non_overlapping(
raw_data(&token),
raw_data(auth_entry.auth_data),
size_of(AuthToken),
)
return token, true
}
}
// Did not find a fitting token.
return {}, false
}
У Odin есть хорошее сокращение для раннего возврата ошибок: or_return
, что эквивалентно ?
в Rust или try
в Zig. То же самое и с or_break
.
И мы используем его таким образом в main
:
main :: proc() {
auth_token, _ := load_x11_auth_token(context.temp_allocator)
}
Если мы не нашли подходящего токена, не беда, просто продолжим с пустым.
Одна интересная вещь: в Odin, аналогично Zig, аллокаторы передаются функциям, желающим выделить память. Однако, в отличие от Zig, в Odin есть механизм, позволяющий сделать это менее утомительным (и, как следствие, более неявным), по сути, передавая аллокатор в качестве последнего аргумента функции, который является необязательным.
Odin достаточно любезен, чтобы также предоставить нам два аллокатора, которые мы можем использовать сразу: аллокатор общего назначения и временный аллокатор, использующий арену.
Поскольку записи аутентификации могут быть большими, нам приходится выделять место — стек только такого размера. Было бы прискорбно переполнить стек из‑за того, что имя хоста в этом файле оказалось слишком длинным.
Возможно, вы скажете, что всё это поместится в стеке. Да, но это была прекрасная возможность описать подход Odin к управлению памятью.
Однако мы не хотим сохранять проанализированные записи из файла в памяти после нахождения 16-байтового токена, поэтому мы делаем defer free_all(allocator)
. Это гораздо лучше, чем перебирать каждую запись и освобождать по отдельности каждое поле. Мы просто освобождаем всю арену одним махом (но резервная память остаётся, чтобы быть использованной позже).
Кроме того, использование этой арены задаёт верхнюю границу (несколько мегабайт) на возможные выделения. Так что если одна запись в файле огромна или неправильно сформирована, мы точно не сможем выделить много гигабайт памяти. Это хорошая новость, потому что в противном случае ОС начнёт свопинг как сумасшедшая и будет убивать случайные программы. По моему опыту, обычно это убивает менеджер окон/рабочего стола, который щелчком Таноса испаряет все открытые окна. Очень эффективно с точки зрения ОС, и ужасно с точки зрения пользователя. Поэтому всегда полезно устанавливать верхнюю границу на все ресурсы, включая использование динамической памяти вашей программы.
В целом я нахожу подход Odin очень элегантным. Обычно я хочу иметь возможность использовать другой аллокатор в данной функции, но также, если мне всё равно, он поступит правильно и будет использовать стандартный аллокатор.
Открытие окна
Здесь всё почти то же самое, что и выше, так что я буду краток.
Сначала откроем сокет домена UNIX:
connect_x11_socket :: proc() -> os.Socket {
SockaddrUn :: struct #packed {
sa_family: os.ADDRESS_FAMILY,
sa_data: [108]u8,
}
socket, err := os.socket(os.AF_UNIX, os.SOCK_STREAM, 0)
assert(err == os.ERROR_NONE)
possible_socket_paths := [2]string{"/tmp/.X11-unix/X0", "/tmp/.X11-unix/X1"}
for &socket_path in possible_socket_paths {
addr := SockaddrUn {
sa_family = cast(u16)os.AF_UNIX,
}
mem.copy_non_overlapping(&addr.sa_data, raw_data(socket_path), len(socket_path))
err = os.connect(socket, cast(^os.SOCKADDR)&addr, size_of(addr))
if (err == os.ERROR_NONE) {return socket}
}
os.exit(1)
}
Мы пробуем несколько возможных путей для сокета, которые могут немного отличаться в зависимости от дистрибутива.
Теперь мы можем отправить рукопожатие и получить общую информацию с сервера. Давайте определим некоторые структуры для этого в соответствии с протоколом X11:
Screen :: struct #packed {
id: u32,
colormap: u32,
white: u32,
black: u32,
input_mask: u32,
width: u16,
height: u16,
width_mm: u16,
height_mm: u16,
maps_min: u16,
maps_max: u16,
root_visual_id: u32,
backing_store: u8,
save_unders: u8,
root_depth: u8,
depths_count: u8,
}
ConnectionInformation :: struct {
root_screen: Screen,
resource_id_base: u32,
resource_id_mask: u32,
}
Структуры #packed в соответствии с форматом сетевого протокола, в противном случае компилятор может вставить заполнение между полями.
Одна вещь, которую нужно знать о X11: все, что мы отправляем, должно быть заполнено до размера, кратного 4 байтам. Мы определяем помощника для этого, используя формулу ((i32)x + 3) & -4
вместе с юнит-тестом на всякий случай:
round_up_4 :: #force_inline proc(x: u32) -> u32 {
mask: i32 = -4
return transmute(u32)((transmute(i32)x + 3) & mask)
}
@(test)
test_round_up_4 :: proc(_: ^testing.T) {
assert(round_up_4(0) == 0)
assert(round_up_4(1) == 4)
assert(round_up_4(2) == 4)
assert(round_up_4(3) == 4)
assert(round_up_4(4) == 4)
assert(round_up_4(5) == 8)
assert(round_up_4(6) == 8)
assert(round_up_4(7) == 8)
assert(round_up_4(8) == 8)
}
Теперь можно отправить рукопожатие с маркером аутентификации внутри. Используем системный вызов writev
, чтобы отправить несколько отдельных буферов разной длины за один вызов.
Пропускаем большую часть информации, которую присылает нам сервер, поскольку нам нужны только несколько полей:
x11_handshake :: proc(socket: os.Socket, auth_token: ^AuthToken) -> ConnectionInformation {
Request :: struct #packed {
endianness: u8,
pad1: u8,
major_version: u16,
minor_version: u16,
authorization_len: u16,
authorization_data_len: u16,
pad2: u16,
}
request := Request {
endianness = 'l',
major_version = 11,
authorization_len = len(AUTH_ENTRY_MAGIC_COOKIE),
authorization_data_len = size_of(AuthToken),
}
{
padding := [2]u8{0, 0}
n_sent, err := linux.writev(
cast(linux.Fd)socket,
[]linux.IO_Vec {
{base = &request, len = size_of(Request)},
{base = raw_data(AUTH_ENTRY_MAGIC_COOKIE), len = len(AUTH_ENTRY_MAGIC_COOKIE)},
{base = raw_data(padding[:]), len = len(padding)},
{base = raw_data(auth_token[:]), len = len(auth_token)},
},
)
assert(err == .NONE)
assert(
n_sent ==
size_of(Request) + len(AUTH_ENTRY_MAGIC_COOKIE) + len(padding) + len(auth_token),
)
}
StaticResponse :: struct #packed {
success: u8,
pad1: u8,
major_version: u16,
minor_version: u16,
length: u16,
}
static_response := StaticResponse{}
{
n_recv, err := os.recv(socket, mem.ptr_to_bytes(&static_response), 0)
assert(err == os.ERROR_NONE)
assert(n_recv == size_of(StaticResponse))
assert(static_response.success == 1)
}
recv_buf: [1 << 15]u8 = {}
{
assert(len(recv_buf) >= cast(u32)static_response.length * 4)
n_recv, err := os.recv(socket, recv_buf[:], 0)
assert(err == os.ERROR_NONE)
assert(n_recv == cast(u32)static_response.length * 4)
}
DynamicResponse :: struct #packed {
release_number: u32,
resource_id_base: u32,
resource_id_mask: u32,
motion_buffer_size: u32,
vendor_length: u16,
maximum_request_length: u16,
screens_in_root_count: u8,
formats_count: u8,
image_byte_order: u8,
bitmap_format_bit_order: u8,
bitmap_format_scanline_unit: u8,
bitmap_format_scanline_pad: u8,
min_keycode: u8,
max_keycode: u8,
pad2: u32,
}
read_buffer := bytes.Buffer{}
bytes.buffer_init(&read_buffer, recv_buf[:])
dynamic_response := DynamicResponse{}
{
n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&dynamic_response))
assert(err == .None)
assert(n_read == size_of(DynamicResponse))
}
// Skip over the vendor information.
bytes.buffer_next(&read_buffer, cast(int)round_up_4(cast(u32)dynamic_response.vendor_length))
// Skip over the format information (each 8 bytes long).
bytes.buffer_next(&read_buffer, 8 * cast(int)dynamic_response.formats_count)
screen := Screen{}
{
n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&screen))
assert(err == .None)
assert(n_read == size_of(screen))
}
return(
ConnectionInformation {
resource_id_base = dynamic_response.resource_id_base,
resource_id_mask = dynamic_response.resource_id_mask,
root_screen = screen,
} \
)
}
И теперь main
становится:
main :: proc() {
auth_token, _ := load_x11_auth_token(context.temp_allocator)
socket := connect_x11_socket()
connection_information := x11_handshake(socket, &auth_token)
}
Следующим шагом является создание графического контекста. При создании новой сущности мы генерируем для нее идентификатор и отправляем его в запросе на создание. Впоследствии мы можем ссылаться на сущность по этому идентификатору:
next_x11_id :: proc(current_id: u32, info: ConnectionInformation) -> u32 {
return 1 + ((info.resource_id_mask & (current_id)) | info.resource_id_base)
}
Пора создать графический контекст:
x11_create_graphical_context :: proc(socket: os.Socket, gc_id: u32, root_id: u32) {
opcode: u8 : 55
FLAG_GC_BG: u32 : 8
BITMASK: u32 : FLAG_GC_BG
VALUE1: u32 : 0x00_00_ff_00
Request :: struct #packed {
opcode: u8,
pad1: u8,
length: u16,
id: u32,
drawable: u32,
bitmask: u32,
value1: u32,
}
request := Request {
opcode = opcode,
length = 5,
id = gc_id,
drawable = root_id,
bitmask = BITMASK,
value1 = VALUE1,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
Наконец, мы создаём окно. Мы также подписываемся на несколько событий:
Exposure
: когда наше окно станет видимымKEY_PRESS
: при нажатии клавиши клавиатурыKEY_RELEASE
: когда клавиша клавиатуры отпущенаBUTTON_PRESS
: при нажатии кнопки мышиBUTTON_RELEASE
: когда кнопка мыши отпущена
Мы также выбираем произвольный цвет фона. Пусть будет жёлтый. Это не имеет значения, потому что мы закроем все части окна нашими ассетами.
x11_create_window :: proc(
socket: os.Socket,
window_id: u32,
parent_id: u32,
x: u16,
y: u16,
width: u16,
height: u16,
root_visual_id: u32,
) {
FLAG_WIN_BG_PIXEL: u32 : 2
FLAG_WIN_EVENT: u32 : 0x800
FLAG_COUNT: u16 : 2
EVENT_FLAG_EXPOSURE: u32 = 0x80_00
EVENT_FLAG_KEY_PRESS: u32 = 0x1
EVENT_FLAG_KEY_RELEASE: u32 = 0x2
EVENT_FLAG_BUTTON_PRESS: u32 = 0x4
EVENT_FLAG_BUTTON_RELEASE: u32 = 0x8
flags: u32 : FLAG_WIN_BG_PIXEL | FLAG_WIN_EVENT
depth: u8 : 24
border_width: u16 : 0
CLASS_INPUT_OUTPUT: u16 : 1
opcode: u8 : 1
BACKGROUND_PIXEL_COLOR: u32 : 0x00_ff_ff_00
Request :: struct #packed {
opcode: u8,
depth: u8,
request_length: u16,
window_id: u32,
parent_id: u32,
x: u16,
y: u16,
width: u16,
height: u16,
border_width: u16,
class: u16,
root_visual_id: u32,
bitmask: u32,
value1: u32,
value2: u32,
}
request := Request {
opcode = opcode,
depth = depth,
request_length = 8 + FLAG_COUNT,
window_id = window_id,
parent_id = parent_id,
x = x,
y = y,
width = width,
height = height,
border_width = border_width,
class = CLASS_INPUT_OUTPUT,
root_visual_id = root_visual_id,
bitmask = flags,
value1 = BACKGROUND_PIXEL_COLOR,
value2 = EVENT_FLAG_EXPOSURE | EVENT_FLAG_BUTTON_RELEASE | EVENT_FLAG_BUTTON_PRESS | EVENT_FLAG_KEY_PRESS | EVENT_FLAG_KEY_RELEASE,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
Мы решаем, что в нашей игре будет 16 строк и 16 столбцов, а каждый ассет имеет размер 16x16 пикселей.
main
теперь:
ENTITIES_ROW_COUNT :: 16
ENTITIES_COLUMN_COUNT :: 16
ENTITIES_WIDTH :: 16
ENTITIES_HEIGHT :: 16
main :: proc() {
auth_token, _ := load_x11_auth_token(context.temp_allocator)
socket := connect_x11_socket()
connection_information := x11_handshake(socket, &auth_token)
gc_id := next_x11_id(0, connection_information)
x11_create_graphical_context(socket, gc_id, connection_information.root_screen.id)
window_id := next_x11_id(gc_id, connection_information)
x11_create_window(
socket,
window_id,
connection_information.root_screen.id,
200,
200,
ENTITIES_COLUMN_COUNT * ENTITIES_WIDTH,
ENTITIES_ROW_COUNT * ENTITIES_HEIGHT,
connection_information.root_screen.root_visual_id,
)
}
Обратите внимание, что размеры окна являются подсказкой: теперь они могут учитываться, например, в тайловом оконном менеджере. Мы не рассматриваем этот случай здесь, поскольку активы имеют фиксированный размер.
Если вы следили за развитием событий, то сейчас вы увидите... ничего. Это потому, что нужно попросить X11 показать наше окно при map_window
вызове:
x11_map_window :: proc(socket: os.Socket, window_id: u32) {
opcode: u8 : 8
Request :: struct #packed {
opcode: u8,
pad1: u8,
request_length: u16,
window_id: u32,
}
request := Request {
opcode = opcode,
request_length = 2,
window_id = window_id,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
И мы видим это:
![](https://habrastorage.org/getpro/habr/upload_files/b88/1b8/0b3/b881b80b34b60f0fa386245612448e29.png)
Пора заняться самой игрой.
Загрузка ассетов
Какая игра без красивых картинок, честно экспрориированных откуда-то из интернета?
Вот наш спрайт, все наши ассеты на одной картинке:
![](https://habrastorage.org/getpro/habr/upload_files/da4/a1e/9fc/da4a1e9fcd3a46663cd3bd0c80f44704.png)
У Odin есть хорошая функция для встраивания файла изображения в исполняемый файл, что упрощает перераспределение и ускоряет запуск. Поэтому делаем это:
png_data := #load("sprite.png")
sprite, err := png.load_from_bytes(png_data, {})
assert(err == nil)
Теперь вот в чём загвоздка: формат изображения X11 отличается от формата спрайта, поэтому придётся поменять байты местами:
sprite_data := make([]u8, sprite.height * sprite.width * 4)
// Convert the image format from the sprite (RGB) into the X11 image format (BGRX).
for i := 0; i < sprite.height * sprite.width - 3; i += 1 {
sprite_data[i * 4 + 0] = sprite.pixels.buf[i * 3 + 2] // R -> B
sprite_data[i * 4 + 1] = sprite.pixels.buf[i * 3 + 1] // G -> G
sprite_data[i * 4 + 2] = sprite.pixels.buf[i * 3 + 0] // B -> R
sprite_data[i * 4 + 3] = 0 // pad
}
Компонент A
фактически не используется, поскольку у нас нет прозрачности.
Теперь, когда изображение находится в памяти (клиентской), как сделать его доступным для сервера? Который в модели X11 может быть запущен на любой машине на другом конце света.
В X11 есть три полезных вызова изображений: CreatePixmap
и PutImage
. Pixmap
— внеэкранный буфер изображения. PutImage
загружает данные изображения либо в пиксмап, либо непосредственно в окно («drawable» на языке X11). CopyArea
копирует один прямоугольник из одного drawable в другой.
По моему скромному мнению, всё не так. CreatePixmap
должен был быть вызван CreateOffscreenImageBuffer
и PutImage
должен был быть UploadImageData
. CopyArea
: здесь всё круто, оставляем как есть.
Мы не можем просто использовать PutImage
, так как в этом случае на экран будет выведен весь спрайт (здесь нет полей для указания того, что должна быть показана только часть изображения). Мы могли бы показать только его части с помощью отдельных вызовов PutImage
, но это означает, что пришлось бы каждый раз загружать данные изображения на сервер.
Нам нужно загрузить данные изображения один раз, вне экрана, одним вызовом PutImage
, а затем скопировать его части в окно. Вот как это провернуть:
CreatePixmap
PutImage
для загрузки данных изображения в pixmap — в этот момент в окне ничего не отображается, все по‑прежнему находится за пределами экранаДля каждого объекта в нашей игре выполните дешёвый вызов
CopyArea
, который скопирует часть pixmap в окно — теперь его видно!
X-сервер может фактически загрузить данные изображения в GPU вызовом PutImage
(это зависит от реализации). После этого вызовыCopyArea
могут быть преобразованы X-сервером в команды GPU для копирования данных изображения из одного буфера GPU в другой: это действительно эффективно! Данные изображения загружаются на GPU только один раз и остаются там до конца работы программы.
К сожалению, в стандарте X это не прописано (там сказано: "может или не может [...]"), но это полезная идея, которую нужно иметь в виду.
Ещё одна полезная идея — представить, что происходит, когда X-сервер работает по сети: Мы хотим отправить данные изображения только один раз, потому что это занимает много времени, а затем выдать дешёвые команды CopyArea
, каждая из которых занимает всего несколько байт.
Реализуем это:
x11_create_pixmap :: proc(
socket: os.Socket,
window_id: u32,
pixmap_id: u32,
width: u16,
height: u16,
depth: u8,
) {
opcode: u8 : 53
Request :: struct #packed {
opcode: u8,
depth: u8,
request_length: u16,
pixmap_id: u32,
drawable_id: u32,
width: u16,
height: u16,
}
request := Request {
opcode = opcode,
depth = depth,
request_length = 4,
pixmap_id = pixmap_id,
drawable_id = window_id,
width = width,
height = height,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
x11_put_image :: proc(
socket: os.Socket,
drawable_id: u32,
gc_id: u32,
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
depth: u8,
data: []u8,
) {
opcode: u8 : 72
Request :: struct #packed {
opcode: u8,
format: u8,
request_length: u16,
drawable_id: u32,
gc_id: u32,
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
left_pad: u8,
depth: u8,
pad1: u16,
}
data_length_padded := round_up_4(cast(u32)len(data))
request := Request {
opcode = opcode,
format = 2, // ZPixmap
request_length = cast(u16)(6 + data_length_padded / 4),
drawable_id = drawable_id,
gc_id = gc_id,
width = width,
height = height,
dst_x = dst_x,
dst_y = dst_y,
depth = depth,
}
{
padding_len := data_length_padded - cast(u32)len(data)
n_sent, err := linux.writev(
cast(linux.Fd)socket,
[]linux.IO_Vec {
{base = &request, len = size_of(Request)},
{base = raw_data(data), len = len(data)},
{base = raw_data(data), len = cast(uint)padding_len},
},
)
assert(err == .NONE)
assert(n_sent == size_of(Request) + len(data) + cast(int)padding_len)
}
}
x11_copy_area :: proc(
socket: os.Socket,
src_id: u32,
dst_id: u32,
gc_id: u32,
src_x: u16,
src_y: u16,
dst_x: u16,
dst_y: u16,
width: u16,
height: u16,
) {
opcode: u8 : 62
Request :: struct #packed {
opcode: u8,
pad1: u8,
request_length: u16,
src_id: u32,
dst_id: u32,
gc_id: u32,
src_x: u16,
src_y: u16,
dst_x: u16,
dst_y: u16,
width: u16,
height: u16,
}
request := Request {
opcode = opcode,
request_length = 7,
src_id = src_id,
dst_id = dst_id,
gc_id = gc_id,
src_x = src_x,
src_y = src_y,
dst_x = dst_x,
dst_y = dst_y,
width = width,
height = height,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
А теперь возьмёмся за main
:
img_depth: u8 = 24
pixmap_id := next_x11_id(window_id, connection_information)
x11_create_pixmap(
socket,
window_id,
pixmap_id,
cast(u16)sprite.width,
cast(u16)sprite.height,
img_depth,
)
x11_put_image(
socket,
pixmap_id,
gc_id,
sprite_width,
sprite_height,
0,
0,
img_depth,
sprite_data,
)
// Let's render two different assets: an exploded mine and an idle mine.
x11_copy_area(
socket,
pixmap_id,
window_id,
gc_id,
32, // X coordinate on the sprite sheet.
40, // Y coordinate on the sprite sheet.
0, // X coordinate on the window.
0, // Y coordinate on the window.
16, // Width.
16, // Height.
)
x11_copy_area(
socket,
pixmap_id,
window_id,
gc_id,
64,
40,
16,
0,
16,
16,
)
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/88f/c53/6e2/88fc536e2065b0627eac2d76bafd0e81.png)
Теперь можно сосредоточиться на игровых сущностях.
Игровые сущности
У нас есть несколько различных сущностей, которые мы хотим показать, каждая из них представляет собой участок спрайтового листа размером 16x16. Давайте определимся с координатами, чтобы их можно было прочитать:
Position :: struct {
x: u16,
y: u16,
}
Entity_kind :: enum {
Covered,
Uncovered_0,
Uncovered_1,
Uncovered_2,
Uncovered_3,
Uncovered_4,
Uncovered_5,
Uncovered_6,
Uncovered_7,
Uncovered_8,
Mine_exploded,
Mine_idle,
}
ASSET_COORDINATES: [Entity_kind]Position = {
.Uncovered_0 = {x = 0 * 16, y = 22},
.Uncovered_1 = {x = 1 * 16, y = 22},
.Uncovered_2 = {x = 2 * 16, y = 22},
.Uncovered_3 = {x = 3 * 16, y = 22},
.Uncovered_4 = {x = 4 * 16, y = 22},
.Uncovered_5 = {x = 5 * 16, y = 22},
.Uncovered_6 = {x = 6 * 16, y = 22},
.Uncovered_7 = {x = 7 * 16, y = 22},
.Uncovered_8 = {x = 8 * 16, y = 22},
.Covered = {x = 0, y = 38},
.Mine_exploded = {x = 32, y = 40},
.Mine_idle = {x = 64, y = 40},
}
И сгруппируем всё, что нужно, в одну структуру под названием Scene
:
Scene :: struct {
window_id: u32,
gc_id: u32,
sprite_pixmap_id: u32,
displayed_entities: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]Entity_kind,
mines: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
}
Первое интересное поле — displayed_entities
, которое отслеживает, какие ассеты отображаются. Например, мина либо спрятана, либо раскрыта и взорвана, если игрок нажал на неё, либо раскрыта и простаивает, если игрок угадал её местонахождение.
Второй — mines
просто отслеживает, где находятся мины. Это могло быть битовое поле для оптимизации пространства, но я не стал заморачиваться.
В main
создаём новую сцену и случайным образом ставим мины:
scene := Scene {
window_id = window_id,
gc_id = gc_id,
sprite_pixmap_id = pixmap_id,
}
reset(&scene)
Мы поместили эту логику в reset
, чтобы игрок мог легко перезапустить игру одним нажатием клавиши:
reset :: proc(scene: ^Scene) {
for &entity in scene.displayed_entities {
entity = .Covered
}
for &mine in scene.mines {
mine = rand.choice([]bool{true, false, false, false})
}
}
Здесь я использовал вероятность 1/4, что в ячейке есть мина. Теперь мы готовы визуализировать нашу (пока что статическую) сцену:
render :: proc(socket: os.Socket, scene: ^Scene) {
for entity, i in scene.displayed_entities {
rect := ASSET_COORDINATES[entity]
row, column := idx_to_row_column(i)
x11_copy_area(
socket,
scene.sprite_pixmap_id,
scene.window_id,
scene.gc_id,
rect.x,
rect.y,
cast(u16)column * ENTITIES_WIDTH,
cast(u16)row * ENTITIES_HEIGHT,
ENTITIES_WIDTH,
ENTITIES_HEIGHT,
)
}
}
И вот что получилось:
![](https://habrastorage.org/getpro/habr/upload_files/dda/10f/ae6/dda10fae6ee9ff82e0bece0817b514ff.png)
Следующий шаг – реагирование на события.
Реакция на нажатия клавиатуры и мыши
Логика простая. Поскольку единственные сообщения, которые мы ожидаем, - это события клавиатуры и мыши, имеющие фиксированный размер 32 байта, мы просто считываем ровно 32 байта в блокирующем режиме. Первый байт указывает на тип события:
wait_for_x11_events :: proc(socket: os.Socket, scene: ^Scene) {
GenericEvent :: struct #packed {
code: u8,
pad: [31]u8,
}
assert(size_of(GenericEvent) == 32)
KeyReleaseEvent :: struct #packed {
code: u8,
detail: u8,
sequence_number: u16,
time: u32,
root_id: u32,
event: u32,
child_id: u32,
root_x: u16,
root_y: u16,
event_x: u16,
event_y: u16,
state: u16,
same_screen: bool,
pad1: u8,
}
assert(size_of(KeyReleaseEvent) == 32)
ButtonReleaseEvent :: struct #packed {
code: u8,
detail: u8,
seq_number: u16,
timestamp: u32,
root: u32,
event: u32,
child: u32,
root_x: u16,
root_y: u16,
event_x: u16,
event_y: u16,
state: u16,
same_screen: bool,
pad1: u8,
}
assert(size_of(ButtonReleaseEvent) == 32)
EVENT_EXPOSURE: u8 : 0xc
EVENT_KEY_RELEASE: u8 : 0x3
EVENT_BUTTON_RELEASE: u8 : 0x5
KEYCODE_ENTER: u8 : 36
for {
generic_event := GenericEvent{}
n_recv, err := os.recv(socket, mem.ptr_to_bytes(&generic_event), 0)
if err == os.EPIPE || n_recv == 0 {
os.exit(0) // The end.
}
assert(err == os.ERROR_NONE)
assert(n_recv == size_of(GenericEvent))
switch generic_event.code {
case EVENT_EXPOSURE:
render(socket, scene)
case EVENT_KEY_RELEASE:
event := transmute(KeyReleaseEvent)generic_event
if event.detail == KEYCODE_ENTER {
reset(scene)
render(socket, scene)
}
case EVENT_BUTTON_RELEASE:
event := transmute(ButtonReleaseEvent)generic_event
on_cell_clicked(event.event_x, event.event_y, scene)
render(socket, scene)
}
}
}
Если событие равно Exposed
, мы просто выполняем рендеринг (это наш первый рендеринг, когда окно становится видимым — или если окно было свёрнуто, а затем снова сделано видимым).
Если событием является клавиша Enter
, мы сбрасываем состояние игры и выполняем рендеринг. X11 различает физические и логические клавиши на клавиатуре, но здесь это не имеет значения (или, я бы сказал, в большинстве игр: нас интересует физическое расположение клавиши, а не то, с чем ее сопоставил пользователь).
Если событием является нажатие и отпускание кнопки мыши, мы запускаем игровую логику, чтобы найти ячейку и выполнить рендеринг.
Вот и всё.
Логика игры: раскрыть ячейку
Последнее, что нужно сделать: реализовать правила игры.
Насколько я помню, при открывании ячейки мы имеем два случая:
Если это мина, мы проиграли
Если это не мина, мы раскрываем эту ячейку и соседние ячейки методом заливки. Разумеется, мы обнаруживаем только незаминированные ячейки. Неоткрытая ячейка показывает, сколько ячеек пососедству заминировано (если 0, то ячейка пустая, число не отображается).
Единственное, что меня смутило, это то, что мы проверяем 8 соседних ячеек для подсчёта мин, но при заливке посещаем только 4 соседние ячейки: вверх, вправо, вниз, влево, а не соседей по диагонали. В противном случае заливка приведет к раскрытию всех ячеек в игре одновременно.
Так что нам нужно преобразовать положение мыши в окне в индекс/строку/столбец ячейки в нашей сетке:
row_column_to_idx :: #force_inline proc(row: int, column: int) -> int {
return cast(int)row * ENTITIES_COLUMN_COUNT + cast(int)column
}
locate_entity_by_coordinate :: proc(win_x: u16, win_y: u16) -> (idx: int, row: int, column: int) {
column = cast(int)win_x / ENTITIES_WIDTH
row = cast(int)win_y / ENTITIES_HEIGHT
idx = row_column_to_idx(row, column)
return idx, row, column
}
Дальше логика игры:
on_cell_clicked :: proc(x: u16, y: u16, scene: ^Scene) {
idx, row, column := locate_entity_by_coordinate(x, y)
mined := scene.mines[idx]
if mined {
scene.displayed_entities[idx] = .Mine_exploded
// Lose.
uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_exploded)
} else {
visited := [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool{}
uncover_cells_flood_fill(row, column, &scene.displayed_entities, &scene.mines, &visited)
// Win.
if count_remaining_goals(scene.displayed_entities, scene.mines) == 0 {
uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_idle)
}
}
}
Цель — обнаружить все ячейки без мин. Мы могли бы сохранить счётчик и каждый раз уменьшать его, но я просто сканирую сетку, чтобы подсчитать, сколько осталось непокрытых ячеек без мины под ней (в count_remaining_goals
). Никакого риска рассинхронизации между состоянием игры и тем, что отображается на экране, нет.
uncover_all_cells
безоговорочно раскрывает всю сетку, когда игрок выиграл или проиграл. Нам просто нужно показать взорванные мины, когда игрок проиграл, и деактивированные, когда он выиграл.
uncover_cells_flood_fill
это самое интересное. Мы используем рекурсию, и чтобы избежать многократного посещения одних и тех же ячеек и потенциально попадания в бесконечную рекурсию, мы отслеживаем, какие ячейки были посещены:
uncover_cells_flood_fill :: proc(
row: int,
column: int,
displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
visited: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
) {
i := row_column_to_idx(row, column)
if visited[i] {return}
visited[i] = true
// Do not uncover covered mines.
if mines[i] {return}
if displayed_entities[i] != .Covered {return}
// Uncover cell.
mines_around_count := count_mines_around_cell(row, column, mines[:])
assert(mines_around_count <= 8)
displayed_entities[i] =
cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
// Uncover neighbors.
// Up.
if !(row == 0) {
uncover_cells_flood_fill(row - 1, column, displayed_entities, mines, visited)
}
// Right
if !(column == (ENTITIES_COLUMN_COUNT - 1)) {
uncover_cells_flood_fill(row, column + 1, displayed_entities, mines, visited)
}
// Bottom.
if !(row == (ENTITIES_ROW_COUNT - 1)) {
uncover_cells_flood_fill(row + 1, column, displayed_entities, mines, visited)
}
// Left.
if !(column == 0) {
uncover_cells_flood_fill(row, column - 1, displayed_entities, mines, visited)
}
}
Тут есть несколько нюансов, но в целом всё готово. Всего 1000 строк кода без всяких ухищрений и заумных штучек.
![](https://habrastorage.org/getpro/habr/upload_files/af7/cce/dfc/af7ccedfc498236d1fe23bbe67b50de4.png)
Заключение
X11 устарел и неудобен, но в целом неплох. Как только будут реализованы несколько полезных функций, таких как открытие окна, получение событий и т. д., о нём можно будет забыть, и мы сможем сосредоточить все наше внимание на игре. Это очень ценно. Сколько библиотек, фреймворков и сред разработки могут сказать то же самое?
Мне также нравится, что он работает с любым языком программирования, с любым техническим стеком. Не нужно никаких привязок, никаких FFI, просто отправь несколько байт по сокету. Вы даже можете сделать это в Bash (не искушайте меня!).
Я не реализовал несколько дополнительных вещей из оригинальной игры, например, установку флага на клетке, в которой, как вы подозреваете, есть мина. Не стесняйтесь сделать это самостоятельно, это не такая уж трудная задачка.
Надеюсь, что вам было так же интересно, как и мне!
Весь код
package main
import "core:bytes"
import "core:image/png"
import "core:math/bits"
import "core:math/rand"
import "core:mem"
import "core:os"
import "core:path/filepath"
import "core:slice"
import "core:sys/linux"
import "core:testing"
TILE_WIDTH :: 16
TILE_HEIGHT :: 16
Position :: struct {
x: u16,
y: u16,
}
Entity_kind :: enum {
Covered,
Uncovered_0,
Uncovered_1,
Uncovered_2,
Uncovered_3,
Uncovered_4,
Uncovered_5,
Uncovered_6,
Uncovered_7,
Uncovered_8,
Mine_exploded,
Mine_idle,
}
ASSET_COORDINATES: [Entity_kind]Position = {
.Uncovered_0 = {x = 0 * 16, y = 22},
.Uncovered_1 = {x = 1 * 16, y = 22},
.Uncovered_2 = {x = 2 * 16, y = 22},
.Uncovered_3 = {x = 3 * 16, y = 22},
.Uncovered_4 = {x = 4 * 16, y = 22},
.Uncovered_5 = {x = 5 * 16, y = 22},
.Uncovered_6 = {x = 6 * 16, y = 22},
.Uncovered_7 = {x = 7 * 16, y = 22},
.Uncovered_8 = {x = 8 * 16, y = 22},
.Covered = {x = 0, y = 38},
.Mine_exploded = {x = 32, y = 40},
.Mine_idle = {x = 64, y = 40},
}
AuthToken :: [16]u8
AuthEntry :: struct {
family: u16,
auth_name: []u8,
auth_data: []u8,
}
Screen :: struct #packed {
id: u32,
colormap: u32,
white: u32,
black: u32,
input_mask: u32,
width: u16,
height: u16,
width_mm: u16,
height_mm: u16,
maps_min: u16,
maps_max: u16,
root_visual_id: u32,
backing_store: u8,
save_unders: u8,
root_depth: u8,
depths_count: u8,
}
ConnectionInformation :: struct {
root_screen: Screen,
resource_id_base: u32,
resource_id_mask: u32,
}
AUTH_ENTRY_FAMILY_LOCAL: u16 : 1
AUTH_ENTRY_MAGIC_COOKIE: string : "MIT-MAGIC-COOKIE-1"
round_up_4 :: #force_inline proc(x: u32) -> u32 {
mask: i32 = -4
return transmute(u32)((transmute(i32)x + 3) & mask)
}
read_x11_auth_entry :: proc(buffer: ^bytes.Buffer) -> (AuthEntry, bool) {
entry := AuthEntry{}
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&entry.family))
if err == .EOF {return {}, false}
assert(err == .None)
assert(n_read == size_of(entry.family))
}
address_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&address_len))
assert(err == .None)
address_len = bits.byte_swap(address_len)
assert(n_read == size_of(address_len))
}
address := make([]u8, address_len)
{
n_read, err := bytes.buffer_read(buffer, address)
assert(err == .None)
assert(n_read == cast(int)address_len)
}
display_number_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&display_number_len))
assert(err == .None)
display_number_len = bits.byte_swap(display_number_len)
assert(n_read == size_of(display_number_len))
}
display_number := make([]u8, display_number_len)
{
n_read, err := bytes.buffer_read(buffer, display_number)
assert(err == .None)
assert(n_read == cast(int)display_number_len)
}
auth_name_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_name_len))
assert(err == .None)
auth_name_len = bits.byte_swap(auth_name_len)
assert(n_read == size_of(auth_name_len))
}
entry.auth_name = make([]u8, auth_name_len)
{
n_read, err := bytes.buffer_read(buffer, entry.auth_name)
assert(err == .None)
assert(n_read == cast(int)auth_name_len)
}
auth_data_len: u16 = 0
{
n_read, err := bytes.buffer_read(buffer, mem.ptr_to_bytes(&auth_data_len))
assert(err == .None)
auth_data_len = bits.byte_swap(auth_data_len)
assert(n_read == size_of(auth_data_len))
}
entry.auth_data = make([]u8, auth_data_len)
{
n_read, err := bytes.buffer_read(buffer, entry.auth_data)
assert(err == .None)
assert(n_read == cast(int)auth_data_len)
}
return entry, true
}
load_x11_auth_token :: proc(allocator := context.allocator) -> (token: AuthToken, ok: bool) {
context.allocator = allocator
defer free_all(allocator)
filename_env := os.get_env("XAUTHORITY")
filename :=
len(filename_env) != 0 \
? filename_env \
: filepath.join([]string{os.get_env("HOME"), ".Xauthority"})
data := os.read_entire_file_from_filename(filename) or_return
buffer := bytes.Buffer{}
bytes.buffer_init(&buffer, data[:])
for {
auth_entry := read_x11_auth_entry(&buffer) or_break
if auth_entry.family == AUTH_ENTRY_FAMILY_LOCAL &&
slice.equal(auth_entry.auth_name, transmute([]u8)AUTH_ENTRY_MAGIC_COOKIE) &&
len(auth_entry.auth_data) == size_of(AuthToken) {
mem.copy_non_overlapping(
raw_data(&token),
raw_data(auth_entry.auth_data),
size_of(AuthToken),
)
return token, true
}
}
// Did not find a fitting token.
return {}, false
}
connect_x11_socket :: proc() -> os.Socket {
SockaddrUn :: struct #packed {
sa_family: os.ADDRESS_FAMILY,
sa_data: [108]u8,
}
socket, err := os.socket(os.AF_UNIX, os.SOCK_STREAM, 0)
assert(err == os.ERROR_NONE)
possible_socket_paths := [2]string{"/tmp/.X11-unix/X0", "/tmp/.X11-unix/X1"}
for &socket_path in possible_socket_paths {
addr := SockaddrUn {
sa_family = cast(u16)os.AF_UNIX,
}
mem.copy_non_overlapping(&addr.sa_data, raw_data(socket_path), len(socket_path))
err = os.connect(socket, cast(^os.SOCKADDR)&addr, size_of(addr))
if (err == os.ERROR_NONE) {return socket}
}
os.exit(1)
}
x11_handshake :: proc(socket: os.Socket, auth_token: ^AuthToken) -> ConnectionInformation {
Request :: struct #packed {
endianness: u8,
pad1: u8,
major_version: u16,
minor_version: u16,
authorization_len: u16,
authorization_data_len: u16,
pad2: u16,
}
request := Request {
endianness = 'l',
major_version = 11,
authorization_len = len(AUTH_ENTRY_MAGIC_COOKIE),
authorization_data_len = size_of(AuthToken),
}
{
padding := [2]u8{0, 0}
n_sent, err := linux.writev(
cast(linux.Fd)socket,
[]linux.IO_Vec {
{base = &request, len = size_of(Request)},
{base = raw_data(AUTH_ENTRY_MAGIC_COOKIE), len = len(AUTH_ENTRY_MAGIC_COOKIE)},
{base = raw_data(padding[:]), len = len(padding)},
{base = raw_data(auth_token[:]), len = len(auth_token)},
},
)
assert(err == .NONE)
assert(
n_sent ==
size_of(Request) + len(AUTH_ENTRY_MAGIC_COOKIE) + len(padding) + len(auth_token),
)
}
StaticResponse :: struct #packed {
success: u8,
pad1: u8,
major_version: u16,
minor_version: u16,
length: u16,
}
static_response := StaticResponse{}
{
n_recv, err := os.recv(socket, mem.ptr_to_bytes(&static_response), 0)
assert(err == os.ERROR_NONE)
assert(n_recv == size_of(StaticResponse))
assert(static_response.success == 1)
}
recv_buf: [1 << 15]u8 = {}
{
assert(len(recv_buf) >= cast(u32)static_response.length * 4)
n_recv, err := os.recv(socket, recv_buf[:], 0)
assert(err == os.ERROR_NONE)
assert(n_recv == cast(u32)static_response.length * 4)
}
DynamicResponse :: struct #packed {
release_number: u32,
resource_id_base: u32,
resource_id_mask: u32,
motion_buffer_size: u32,
vendor_length: u16,
maximum_request_length: u16,
screens_in_root_count: u8,
formats_count: u8,
image_byte_order: u8,
bitmap_format_bit_order: u8,
bitmap_format_scanline_unit: u8,
bitmap_format_scanline_pad: u8,
min_keycode: u8,
max_keycode: u8,
pad2: u32,
}
read_buffer := bytes.Buffer{}
bytes.buffer_init(&read_buffer, recv_buf[:])
dynamic_response := DynamicResponse{}
{
n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&dynamic_response))
assert(err == .None)
assert(n_read == size_of(DynamicResponse))
}
// Skip over the vendor information.
bytes.buffer_next(&read_buffer, cast(int)round_up_4(cast(u32)dynamic_response.vendor_length))
// Skip over the format information (each 8 bytes long).
bytes.buffer_next(&read_buffer, 8 * cast(int)dynamic_response.formats_count)
screen := Screen{}
{
n_read, err := bytes.buffer_read(&read_buffer, mem.ptr_to_bytes(&screen))
assert(err == .None)
assert(n_read == size_of(screen))
}
return (ConnectionInformation {
resource_id_base = dynamic_response.resource_id_base,
resource_id_mask = dynamic_response.resource_id_mask,
root_screen = screen,
})
}
next_x11_id :: proc(current_id: u32, info: ConnectionInformation) -> u32 {
return 1 + ((info.resource_id_mask & (current_id)) | info.resource_id_base)
}
x11_create_graphical_context :: proc(socket: os.Socket, gc_id: u32, root_id: u32) {
opcode: u8 : 55
FLAG_GC_BG: u32 : 8
BITMASK: u32 : FLAG_GC_BG
VALUE1: u32 : 0x00_00_ff_00
Request :: struct #packed {
opcode: u8,
pad1: u8,
length: u16,
id: u32,
drawable: u32,
bitmask: u32,
value1: u32,
}
request := Request {
opcode = opcode,
length = 5,
id = gc_id,
drawable = root_id,
bitmask = BITMASK,
value1 = VALUE1,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
x11_create_window :: proc(
socket: os.Socket,
window_id: u32,
parent_id: u32,
x: u16,
y: u16,
width: u16,
height: u16,
root_visual_id: u32,
) {
FLAG_WIN_BG_PIXEL: u32 : 2
FLAG_WIN_EVENT: u32 : 0x800
FLAG_COUNT: u16 : 2
EVENT_FLAG_EXPOSURE: u32 = 0x80_00
EVENT_FLAG_KEY_PRESS: u32 = 0x1
EVENT_FLAG_KEY_RELEASE: u32 = 0x2
EVENT_FLAG_BUTTON_PRESS: u32 = 0x4
EVENT_FLAG_BUTTON_RELEASE: u32 = 0x8
flags: u32 : FLAG_WIN_BG_PIXEL | FLAG_WIN_EVENT
depth: u8 : 24
border_width: u16 : 0
CLASS_INPUT_OUTPUT: u16 : 1
opcode: u8 : 1
BACKGROUND_PIXEL_COLOR: u32 : 0x00_ff_ff_00
Request :: struct #packed {
opcode: u8,
depth: u8,
request_length: u16,
window_id: u32,
parent_id: u32,
x: u16,
y: u16,
width: u16,
height: u16,
border_width: u16,
class: u16,
root_visual_id: u32,
bitmask: u32,
value1: u32,
value2: u32,
}
request := Request {
opcode = opcode,
depth = depth,
request_length = 8 + FLAG_COUNT,
window_id = window_id,
parent_id = parent_id,
x = x,
y = y,
width = width,
height = height,
border_width = border_width,
class = CLASS_INPUT_OUTPUT,
root_visual_id = root_visual_id,
bitmask = flags,
value1 = BACKGROUND_PIXEL_COLOR,
value2 = EVENT_FLAG_EXPOSURE | EVENT_FLAG_BUTTON_RELEASE | EVENT_FLAG_BUTTON_PRESS | EVENT_FLAG_KEY_PRESS | EVENT_FLAG_KEY_RELEASE,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
x11_map_window :: proc(socket: os.Socket, window_id: u32) {
opcode: u8 : 8
Request :: struct #packed {
opcode: u8,
pad1: u8,
request_length: u16,
window_id: u32,
}
request := Request {
opcode = opcode,
request_length = 2,
window_id = window_id,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
x11_put_image :: proc(
socket: os.Socket,
drawable_id: u32,
gc_id: u32,
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
depth: u8,
data: []u8,
) {
opcode: u8 : 72
Request :: struct #packed {
opcode: u8,
format: u8,
request_length: u16,
drawable_id: u32,
gc_id: u32,
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
left_pad: u8,
depth: u8,
pad1: u16,
}
data_length_padded := round_up_4(cast(u32)len(data))
request := Request {
opcode = opcode,
format = 2, // ZPixmap
request_length = cast(u16)(6 + data_length_padded / 4),
drawable_id = drawable_id,
gc_id = gc_id,
width = width,
height = height,
dst_x = dst_x,
dst_y = dst_y,
depth = depth,
}
{
padding_len := data_length_padded - cast(u32)len(data)
n_sent, err := linux.writev(
cast(linux.Fd)socket,
[]linux.IO_Vec {
{base = &request, len = size_of(Request)},
{base = raw_data(data), len = len(data)},
{base = raw_data(data), len = cast(uint)padding_len},
},
)
assert(err == .NONE)
assert(n_sent == size_of(Request) + len(data) + cast(int)padding_len)
}
}
render :: proc(socket: os.Socket, scene: ^Scene) {
for entity, i in scene.displayed_entities {
rect := ASSET_COORDINATES[entity]
row, column := idx_to_row_column(i)
x11_copy_area(
socket,
scene.sprite_pixmap_id,
scene.window_id,
scene.gc_id,
rect.x,
rect.y,
cast(u16)column * ENTITIES_WIDTH,
cast(u16)row * ENTITIES_HEIGHT,
ENTITIES_WIDTH,
ENTITIES_HEIGHT,
)
}
}
ENTITIES_ROW_COUNT :: 16
ENTITIES_COLUMN_COUNT :: 16
ENTITIES_WIDTH :: 16
ENTITIES_HEIGHT :: 16
Scene :: struct {
window_id: u32,
gc_id: u32,
sprite_pixmap_id: u32,
displayed_entities: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]Entity_kind,
// TODO: Bitfield?
mines: [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
}
wait_for_x11_events :: proc(socket: os.Socket, scene: ^Scene) {
GenericEvent :: struct #packed {
code: u8,
pad: [31]u8,
}
assert(size_of(GenericEvent) == 32)
KeyReleaseEvent :: struct #packed {
code: u8,
detail: u8,
sequence_number: u16,
time: u32,
root_id: u32,
event: u32,
child_id: u32,
root_x: u16,
root_y: u16,
event_x: u16,
event_y: u16,
state: u16,
same_screen: bool,
pad1: u8,
}
assert(size_of(KeyReleaseEvent) == 32)
ButtonReleaseEvent :: struct #packed {
code: u8,
detail: u8,
seq_number: u16,
timestamp: u32,
root: u32,
event: u32,
child: u32,
root_x: u16,
root_y: u16,
event_x: u16,
event_y: u16,
state: u16,
same_screen: bool,
pad1: u8,
}
assert(size_of(ButtonReleaseEvent) == 32)
EVENT_EXPOSURE: u8 : 0xc
EVENT_KEY_RELEASE: u8 : 0x3
EVENT_BUTTON_RELEASE: u8 : 0x5
KEYCODE_ENTER: u8 : 36
for {
generic_event := GenericEvent{}
n_recv, err := os.recv(socket, mem.ptr_to_bytes(&generic_event), 0)
if err == os.EPIPE || n_recv == 0 {
os.exit(0) // The end.
}
assert(err == os.ERROR_NONE)
assert(n_recv == size_of(GenericEvent))
switch generic_event.code {
case EVENT_EXPOSURE:
render(socket, scene)
case EVENT_KEY_RELEASE:
event := transmute(KeyReleaseEvent)generic_event
if event.detail == KEYCODE_ENTER {
reset(scene)
render(socket, scene)
}
case EVENT_BUTTON_RELEASE:
event := transmute(ButtonReleaseEvent)generic_event
on_cell_clicked(event.event_x, event.event_y, scene)
render(socket, scene)
}
}
}
reset :: proc(scene: ^Scene) {
for &entity in scene.displayed_entities {
entity = .Covered
}
for &mine in scene.mines {
mine = rand.choice([]bool{true, false, false, false})
}
}
x11_copy_area :: proc(
socket: os.Socket,
src_id: u32,
dst_id: u32,
gc_id: u32,
src_x: u16,
src_y: u16,
dst_x: u16,
dst_y: u16,
width: u16,
height: u16,
) {
opcode: u8 : 62
Request :: struct #packed {
opcode: u8,
pad1: u8,
request_length: u16,
src_id: u32,
dst_id: u32,
gc_id: u32,
src_x: u16,
src_y: u16,
dst_x: u16,
dst_y: u16,
width: u16,
height: u16,
}
request := Request {
opcode = opcode,
request_length = 7,
src_id = src_id,
dst_id = dst_id,
gc_id = gc_id,
src_x = src_x,
src_y = src_y,
dst_x = dst_x,
dst_y = dst_y,
width = width,
height = height,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
on_cell_clicked :: proc(x: u16, y: u16, scene: ^Scene) {
idx, row, column := locate_entity_by_coordinate(x, y)
mined := scene.mines[idx]
if mined {
scene.displayed_entities[idx] = .Mine_exploded
// Lose.
uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_exploded)
} else {
visited := [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool{}
uncover_cells_flood_fill(row, column, &scene.displayed_entities, &scene.mines, &visited)
// Win.
if count_remaining_goals(scene.displayed_entities, scene.mines) == 0 {
uncover_all_cells(&scene.displayed_entities, &scene.mines, .Mine_idle)
}
}
}
count_remaining_goals :: proc(
displayed_entities: [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
mines: [ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
) -> int {
covered := 0
for entity in displayed_entities {
covered += cast(int)(entity == .Covered)
}
mines_count := 0
for mine in mines {
mines_count += cast(int)mine
}
return covered - mines_count
}
uncover_all_cells :: proc(
displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
shown_mine: Entity_kind,
) {
for &entity, i in displayed_entities {
if mines[i] {
entity = shown_mine
} else {
row, column := idx_to_row_column(i)
mines_around_count := count_mines_around_cell(row, column, mines[:])
assert(mines_around_count <= 8)
entity = cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
}
}
}
uncover_cells_flood_fill :: proc(
row: int,
column: int,
displayed_entities: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]Entity_kind,
mines: ^[ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool,
visited: ^[ENTITIES_COLUMN_COUNT * ENTITIES_ROW_COUNT]bool,
) {
i := row_column_to_idx(row, column)
if visited[i] {return}
visited[i] = true
// Do not uncover covered mines.
if mines[i] {return}
if displayed_entities[i] != .Covered {return}
// Uncover cell.
mines_around_count := count_mines_around_cell(row, column, mines[:])
assert(mines_around_count <= 8)
displayed_entities[i] =
cast(Entity_kind)(cast(int)Entity_kind.Uncovered_0 + mines_around_count)
// Uncover neighbors.
// Up.
if !(row == 0) {
uncover_cells_flood_fill(row - 1, column, displayed_entities, mines, visited)
}
// Right
if !(column == (ENTITIES_COLUMN_COUNT - 1)) {
uncover_cells_flood_fill(row, column + 1, displayed_entities, mines, visited)
}
// Bottom.
if !(row == (ENTITIES_ROW_COUNT - 1)) {
uncover_cells_flood_fill(row + 1, column, displayed_entities, mines, visited)
}
// Left.
if !(column == 0) {
uncover_cells_flood_fill(row, column - 1, displayed_entities, mines, visited)
}
}
idx_to_row_column :: #force_inline proc(i: int) -> (int, int) {
column := i % ENTITIES_COLUMN_COUNT
row := i / ENTITIES_ROW_COUNT
return row, column
}
row_column_to_idx :: #force_inline proc(row: int, column: int) -> int {
return cast(int)row * ENTITIES_COLUMN_COUNT + cast(int)column
}
count_mines_around_cell :: proc(row: int, column: int, displayed_entities: []bool) -> int {
// TODO: Pad the border to elide all bound checks?
up_left :=
row == 0 || column == 0 \
? false \
: displayed_entities[row_column_to_idx(row - 1, column - 1)]
up := row == 0 ? false : displayed_entities[row_column_to_idx(row - 1, column)]
up_right :=
row == 0 || column == (ENTITIES_COLUMN_COUNT - 1) \
? false \
: displayed_entities[row_column_to_idx(row - 1, column + 1)]
right :=
column == (ENTITIES_COLUMN_COUNT - 1) \
? false \
: displayed_entities[row_column_to_idx(row, column + 1)]
bottom_right :=
row == (ENTITIES_ROW_COUNT - 1) || column == (ENTITIES_COLUMN_COUNT - 1) \
? false \
: displayed_entities[row_column_to_idx(row + 1, column + 1)]
bottom :=
row == (ENTITIES_ROW_COUNT - 1) \
? false \
: displayed_entities[row_column_to_idx(row + 1, column)]
bottom_left :=
column == 0 || row == (ENTITIES_COLUMN_COUNT - 1) \
? false \
: displayed_entities[row_column_to_idx(row + 1, column - 1)]
left := column == 0 ? false : displayed_entities[row_column_to_idx(row, column - 1)]
return(
cast(int)up_left +
cast(int)up +
cast(int)up_right +
cast(int)right +
cast(int)bottom_right +
cast(int)bottom +
cast(int)bottom_left +
cast(int)left \
)
}
locate_entity_by_coordinate :: proc(win_x: u16, win_y: u16) -> (idx: int, row: int, column: int) {
column = cast(int)win_x / ENTITIES_WIDTH
row = cast(int)win_y / ENTITIES_HEIGHT
idx = row_column_to_idx(row, column)
return idx, row, column
}
x11_create_pixmap :: proc(
socket: os.Socket,
window_id: u32,
pixmap_id: u32,
width: u16,
height: u16,
depth: u8,
) {
opcode: u8 : 53
Request :: struct #packed {
opcode: u8,
depth: u8,
request_length: u16,
pixmap_id: u32,
drawable_id: u32,
width: u16,
height: u16,
}
request := Request {
opcode = opcode,
depth = depth,
request_length = 4,
pixmap_id = pixmap_id,
drawable_id = window_id,
width = width,
height = height,
}
{
n_sent, err := os.send(socket, mem.ptr_to_bytes(&request), 0)
assert(err == os.ERROR_NONE)
assert(n_sent == size_of(Request))
}
}
main :: proc() {
png_data := #load("sprite.png")
sprite, err := png.load_from_bytes(png_data, {})
assert(err == nil)
sprite_data := make([]u8, sprite.height * sprite.width * 4)
// Convert the image format from the sprite (RGB) into the X11 image format (BGRX).
for i := 0; i < sprite.height * sprite.width - 3; i += 1 {
sprite_data[i * 4 + 0] = sprite.pixels.buf[i * 3 + 2] // R -> B
sprite_data[i * 4 + 1] = sprite.pixels.buf[i * 3 + 1] // G -> G
sprite_data[i * 4 + 2] = sprite.pixels.buf[i * 3 + 0] // B -> R
sprite_data[i * 4 + 3] = 0 // pad
}
auth_token, _ := load_x11_auth_token(context.temp_allocator)
socket := connect_x11_socket()
connection_information := x11_handshake(socket, &auth_token)
gc_id := next_x11_id(0, connection_information)
x11_create_graphical_context(socket, gc_id, connection_information.root_screen.id)
window_id := next_x11_id(gc_id, connection_information)
x11_create_window(
socket,
window_id,
connection_information.root_screen.id,
200,
200,
ENTITIES_COLUMN_COUNT * ENTITIES_WIDTH,
ENTITIES_ROW_COUNT * ENTITIES_HEIGHT,
connection_information.root_screen.root_visual_id,
)
img_depth: u8 = 24
pixmap_id := next_x11_id(window_id, connection_information)
x11_create_pixmap(
socket,
window_id,
pixmap_id,
cast(u16)sprite.width,
cast(u16)sprite.height,
img_depth,
)
scene := Scene {
window_id = window_id,
gc_id = gc_id,
sprite_pixmap_id = pixmap_id,
}
reset(&scene)
x11_put_image(
socket,
scene.sprite_pixmap_id,
scene.gc_id,
cast(u16)sprite.width,
cast(u16)sprite.height,
0,
0,
img_depth,
sprite_data,
)
x11_map_window(socket, window_id)
wait_for_x11_events(socket, &scene)
}
@(test)
test_round_up_4 :: proc(_: ^testing.T) {
assert(round_up_4(0) == 0)
assert(round_up_4(1) == 4)
assert(round_up_4(2) == 4)
assert(round_up_4(3) == 4)
assert(round_up_4(4) == 4)
assert(round_up_4(5) == 8)
assert(round_up_4(6) == 8)
assert(round_up_4(7) == 8)
assert(round_up_4(8) == 8)
}
@(test)
test_count_mines_around_cell :: proc(_: ^testing.T) {
{
mines := [ENTITIES_ROW_COUNT * ENTITIES_COLUMN_COUNT]bool{}
mines[row_column_to_idx(0, 0)] = true
mines[row_column_to_idx(0, 1)] = true
mines[row_column_to_idx(0, 2)] = true
mines[row_column_to_idx(1, 2)] = true
mines[row_column_to_idx(2, 2)] = true
mines[row_column_to_idx(2, 1)] = true
mines[row_column_to_idx(2, 0)] = true
mines[row_column_to_idx(1, 0)] = true
assert(count_mines_around_cell(1, 1, mines[:]) == 8)
}
}
Кстати, если вам нравится «Сапёр», есть ещё одна статья на эту тему. Kaboom: необычный сапёр. Спасибо за внимание! Ваш Cloud4Y.