skip to content
==> src/doom.c <==
1/* doom: a DDA raycaster (Lode Vandevenne's), one lane per screen column, 4
2 * tasks a frame (arg b: columns 32 b .. 32 b + 31). The pose lives in the
3 * view: the frame's arg-0 task drains the input queue (w s a d q e) into it,
4 * with wall collision, and publishes D_POSE_T = ticket + 1; the frame's other
5 * tasks (tickets t0 + k, consecutive within a publish) wait for it, bounded.
6 * The map comes from OP_INIT. Layout: @ajax/mesh-abi-view-doom. Split across
7 * machines (manifest `place = "split"`), every system but the owner renders
8 * its columns with OP_FOLLOW: meshd wrote the owner's pose for that frame into
9 * the view beforehand, so it neither waits nor reads input.
10 * v1's raycaster, kept as written: it mirrors the CPU reference (`h == h` is the NaN test).
11 * Ported from alpha-rust-mesh programs/doom/src/lib.rs. */
12#include "mesh/rt.h"
13
14static float doom_fabs(float x) { return mesh_f32_from_bits(mesh_f32_bits(x) & 0x7fffffffu); }
15/* f32 signum for non-NaN x (+0.0 gives 1.0, as the host Pose did). */
16static float doom_sign(float x) { return (mesh_f32_bits(x) >> 31) ? -1.0f : 1.0f; }
17
18static void doom_init(const mesh_ctx *ctx) {
19    for (uint32_t c = mesh_ctx_lane(ctx); c < 256; c += 32) mesh_ctx_set_u32(ctx, MESH_DOOM_D_MAP + 4 * c, mesh_doom_map_cell(c % 16, c / 16));
20    if (mesh_ctx_leader(ctx)) {
21        for (uint32_t i = 0; i < MESH_DOOM_START_LEN; i++) mesh_ctx_set_f32(ctx, MESH_DOOM_D_POS_X + 4 * i, MESH_DOOM_START[i]);
22        mesh_ctx_set_f32(ctx, MESH_DOOM_D_ROWS, MESH_DOOM_ROWS);
23        mesh_ctx_set_u32(ctx, MESH_DOOM_D_MAX_STEPS, MESH_DOOM_MAX_STEPS);
24        mesh_ctx_set_u32(ctx, MESH_DOOM_D_FRAME, 0);
25        mesh_ctx_set_u32(ctx, MESH_DOOM_D_POSE_T, 0);
26        mesh_ctx_set_u32(ctx, MESH_DOOM_D_KEYS, 0);
27    }
28}
29
30/* The pose: x y, dir x y, plane x y. */
31typedef struct {
32    float p[6];
33} doom_pose;
34
35static doom_pose doom_pose_read(mesh_view v) {
36    doom_pose o;
37    for (uint32_t i = 0; i < 6; i++) o.p[i] = mesh_view_f32(v, MESH_DOOM_D_POS_X + 4 * i);
38    return o;
39}
40static void doom_pose_write(const doom_pose *o, mesh_view v) {
41    for (uint32_t i = 0; i < 6; i++) mesh_view_set_f32(v, MESH_DOOM_D_POS_X + 4 * i, o->p[i]);
42}
43static bool doom_walkable(mesh_view v, float x, float y) {
44    return x >= 0.0f && y >= 0.0f && x < 16.0f && y < 16.0f &&
45           (mesh_view_u32(v, MESH_DOOM_D_MAP + 4 * (16 * (uint32_t)y + (uint32_t)x)) & 15u) == 0;
46}
47/* Moves by (dx, dy), each axis separately so the player slides along walls,
48 * keeping RADIUS from them. */
49static void doom_step(doom_pose *o, mesh_view v, float dx, float dy) {
50    if (doom_walkable(v, o->p[0] + dx + MESH_DOOM_RADIUS * doom_sign(dx), o->p[1])) o->p[0] += dx;
51    if (doom_walkable(v, o->p[0], o->p[1] + dy + MESH_DOOM_RADIUS * doom_sign(dy))) o->p[1] += dy;
52}
53static void doom_rotate(doom_pose *o, float s) {
54    float c = MESH_DOOM_TURN_COS;
55    float x = o->p[2], y = o->p[3];
56    o->p[2] = x * c - y * s;
57    o->p[3] = x * s + y * c;
58    x = o->p[4], y = o->p[5];
59    o->p[4] = x * c - y * s;
60    o->p[5] = x * s + y * c;
61}
62/* w/s forward/back, a/d turn left/right, q/e strafe (y points down the map). */
63static void doom_apply(doom_pose *o, mesh_view v, uint8_t key) {
64    float dx = o->p[2] * MESH_DOOM_MOVE, dy = o->p[3] * MESH_DOOM_MOVE;
65    float px = o->p[4], py = o->p[5];
66    switch (key) {
67    case 'w': doom_step(o, v, dx, dy); break;
68    case 's': doom_step(o, v, -dx, -dy); break;
69    case 'a': doom_rotate(o, -MESH_DOOM_TURN_SIN); break;
70    case 'd': doom_rotate(o, MESH_DOOM_TURN_SIN); break;
71    /* The camera plane points to the player's right. */
72    case 'q': doom_step(o, v, -px / MESH_DOOM_PLANE * MESH_DOOM_MOVE, -py / MESH_DOOM_PLANE * MESH_DOOM_MOVE); break;
73    case 'e': doom_step(o, v, px / MESH_DOOM_PLANE * MESH_DOOM_MOVE, py / MESH_DOOM_PLANE * MESH_DOOM_MOVE); break;
74    default: break;
75    }
76}
77
78/* One ray: the wall kind or 0, *side 0 x / 1 y, *dist the perpendicular distance or 1e30. */
79static inline __attribute__((always_inline)) uint32_t doom_cast(const uint32_t map[32], float px, float py, float dx, float dy,
80                                                                 float plx, float ply, uint32_t c, uint32_t max_steps,
81                                                                 uint32_t *side_out, float *dist_out) {
82    float cam = 2.0f * (float)c / (float)MESH_DOOM_WIDTH - 1.0f;
83    float rx = dx + plx * cam;
84    float ry = dy + ply * cam;
85    int32_t mx = (int32_t)px, my = (int32_t)py;
86    float ddx = rx == 0.0f ? 1e30f : doom_fabs(1.0f / rx);
87    float ddy = ry == 0.0f ? 1e30f : doom_fabs(1.0f / ry);
88    int32_t sx, sy;
89    float sdx, sdy;
90    if (rx < 0.0f) { sx = -1; sdx = (px - (float)mx) * ddx; }
91    else { sx = 1; sdx = ((float)mx + 1.0f - px) * ddx; }
92    if (ry < 0.0f) { sy = -1; sdy = (py - (float)my) * ddy; }
93    else { sy = 1; sdy = ((float)my + 1.0f - py) * ddy; }
94    uint32_t side = 0;
95    for (uint32_t i = 0; i < max_steps; i++) {
96        if (sdx < sdy) { sdx += ddx; mx += sx; side = 0; }
97        else { sdy += ddy; my += sy; side = 1; }
98        if (mx < 0 || mx > 15 || my < 0 || my > 15) break;
99        uint32_t cell = (uint32_t)(my * 16 + mx);
100        uint32_t k = (map[cell >> 3] >> (4 * (cell & 7))) & 15u;
101        if (k != 0) {
102            *side_out = side;
103            *dist_out = side == 0 ? sdx - ddx : sdy - ddy;
104            return k;
105        }
106    }
107    *side_out = side;
108    *dist_out = 1e30f;
109    return 0;
110}
111
112/* Renders columns 32 b .. 32 b + 31; returns how many hit a wall. */
113static uint32_t doom_render(const mesh_ctx *ctx, mesh_view v, const doom_pose *pose, uint32_t b) {
114    uint32_t l = mesh_ctx_lane(ctx);
115    /* The 16x16 map, 4 bits a cell: lane l packs cells 8l..8l+7, then every lane
116     * gathers all 32 words so the DDA loop never touches host memory. */
117    uint32_t packed = 0;
118    for (uint32_t j = 0; j < 8; j++) packed |= (mesh_view_u32(v, MESH_DOOM_D_MAP + 4 * (8 * l + j)) & 15u) << (4 * j);
119    uint32_t map[32];
120    for (uint32_t w = 0; w < 32; w++) map[w] = mesh_rt_shfl_idx(packed, w);
121
122    uint32_t c = b * 32 + l;
123    bool hit = false;
124    if (c < MESH_DOOM_WIDTH) {
125        float rows = mesh_view_f32(v, MESH_DOOM_D_ROWS);
126        uint32_t side;
127        float dist;
128        uint32_t kind = doom_cast(map, pose->p[0], pose->p[1], pose->p[2], pose->p[3], pose->p[4], pose->p[5], c,
129                                  mesh_view_u32(v, MESH_DOOM_D_MAX_STEPS), &side, &dist);
130        hit = kind != 0;
131        float height = 0.0f;
132        if (hit) {
133            float h = rows / dist;
134            height = (h > 4.0f * rows || !(h == h)) ? 4.0f * rows : h;
135        }
136        mesh_view_set_f32(v, MESH_DOOM_D_HEIGHT + 4 * c, height);
137        mesh_view_set_u32(v, MESH_DOOM_D_KIND + 4 * c, kind);
138        mesh_view_set_u32(v, MESH_DOOM_D_SIDE + 4 * c, side);
139        mesh_view_set_f32(v, MESH_DOOM_D_DIST + 4 * c, dist);
140    }
141    return (uint32_t)__builtin_popcount(mesh_rt_ballot(hit));
142}
143
144/* Arg 0: apply the queued keys (all lanes pop collectively and compute the
145 * same pose; at most one queue's worth a frame), then release the pose. */
146static doom_pose doom_begin_frame(const mesh_ctx *ctx, mesh_view v) {
147    doom_pose pose = doom_pose_read(v);
148    uint32_t n = 0;
149    while (n < MESH_DOOM_INPUT_BYTES) {
150        uint8_t k;
151        if (!mesh_ctx_input_pop(ctx, &k)) break;
152        doom_apply(&pose, v, k);
153        n++;
154    }
155    if (mesh_ctx_leader(ctx)) {
156        doom_pose_write(&pose, v);
157        mesh_view_set_u32(v, MESH_DOOM_D_FRAME, mesh_view_u32(v, MESH_DOOM_D_FRAME) + 1);
158        mesh_view_set_u32(v, MESH_DOOM_D_KEYS, mesh_view_u32(v, MESH_DOOM_D_KEYS) + n);
159        mesh_ctx_count(ctx, MESH_DOOM_C_FRAMES, 1);
160        if (n > 0) mesh_ctx_count(ctx, MESH_DOOM_C_KEYS, n);
161        mesh_rt_fence_acq_rel();
162        mesh_view w;
163        if (mesh_view_sub(v, MESH_DOOM_D_POSE_T, 4, &w)) mesh_rt_st_release(mesh_view_ptr(w), mesh_ctx_ticket(ctx) + 1);
164    }
165    mesh_rt_warp_sync();
166    return pose;
167}
168
169/* Args k > 0: wait until the arg-0 task of this publish (ticket t - k) has
170 * released the pose; a stray or lost arg-0 costs at most POSE_WAIT_NS and
171 * bumps C_STALLS. */
172static doom_pose doom_wait_pose(const mesh_ctx *ctx, mesh_view v, uint32_t k) {
173    if (mesh_ctx_leader(ctx)) {
174        mesh_view w;
175        if (mesh_view_sub(v, MESH_DOOM_D_POSE_T, 4, &w)) {
176            uint32_t want = mesh_ctx_ticket(ctx) + 1;
177            uint64_t t0 = mesh_rt_gtimer();
178            /* The arg-0 ticket lies in [t - k, t]: want - pose_t in [0, k]. */
179            while (want - mesh_rt_ld_acquire(mesh_view_ptr(w)) > k) {
180                if (mesh_rt_gtimer() - t0 > MESH_DOOM_POSE_WAIT_NS) {
181                    mesh_ctx_count(ctx, MESH_DOOM_C_STALLS, 1);
182                    break;
183                }
184                mesh_rt_nanosleep(200);
185            }
186        }
187    }
188    /* Lane 0's acquire, then the warp barrier, orders every lane's pose reads. */
189    mesh_rt_warp_sync();
190    return doom_pose_read(v);
191}
192
193static uint32_t doom_task(const mesh_ctx *ctx, uint32_t op, uint32_t arg) {
194    mesh_view v = mesh_ctx_view(ctx);
195    if (op == MESH_OP_FOLLOW && arg < MESH_DOOM_WIDTH / 32) {
196        if (mesh_ctx_leader(ctx)) mesh_ctx_count(ctx, MESH_DOOM_C_FOLLOWS, 1);
197        doom_pose pose = doom_pose_read(v);
198        return doom_render(ctx, v, &pose, arg);
199    }
200    if (op != MESH_OP_TASK) return MESH_R_BAD_OP;
201    doom_pose pose = arg == 0 ? doom_begin_frame(ctx, v) : doom_wait_pose(ctx, v, arg);
202    return doom_render(ctx, v, &pose, arg);
203}
204
205MESH_PROGRAM(doom_init, doom_task);