rtrun.c

C source for Real-time ray tracing on a round ESP32 display, 91 lines. Raw file

// rtrun.c - browser front-end for the raytrace2 renderer.
//
// It compiles the device's real raytrace2.c to WebAssembly unchanged. Two
// things the device supplies are stubbed here: the microsecond timer, and the
// FreeRTOS second-core worker. On the board the frame is split across the two
// Xtensa cores (a worker task does half of each phase); in the browser there is
// one thread, so the "worker" is run inline by a tiny cooperative shim built on
// setjmp/longjmp - each time the renderer notifies the worker and then waits, we
// run exactly one phase of the worker on the spot and return. The halves are
// disjoint, so running them in sequence gives the same picture.

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
#include <emscripten.h>

#include "raytrace2.h"
#include "freertos/task.h"

// ---- esp_timer ----
int64_t esp_timer_get_time(void) { return (int64_t)(emscripten_get_now() * 1000.0); }

// ---- cooperative single-thread FreeRTOS shim ----
#define MAIN_H ((TaskHandle_t)1)
#define WORK_H ((TaskHandle_t)2)
static TaskFunction_t s_wfn;
static void          *s_wparam;
static int            s_role;       // 0 = main, 1 = worker
static int            s_pending;    // worker has been notified
static jmp_buf        s_yield;

BaseType_t xTaskCreatePinnedToCore(TaskFunction_t fn, const char *name, uint32_t stack,
                                   void *param, UBaseType_t prio, TaskHandle_t *handle,
                                   BaseType_t core)
{
    (void)name; (void)stack; (void)prio; (void)core;
    s_wfn = fn; s_wparam = param;
    if (handle) *handle = WORK_H;
    return pdPASS;
}

TaskHandle_t xTaskGetCurrentTaskHandle(void) { return MAIN_H; }

void xTaskNotifyGive(TaskHandle_t h) { if (h == WORK_H) s_pending = 1; }

uint32_t ulTaskNotifyTake(BaseType_t clear_on_exit, TickType_t wait)
{
    (void)clear_on_exit; (void)wait;
    if (s_role == 1) {                      // we are inside the worker
        if (s_pending) { s_pending = 0; return 1; }   // consume -> run this phase
        longjmp(s_yield, 1);                          // nothing more -> yield to main
    }
    if (s_pending) {                        // main waiting for the worker's half
        if (setjmp(s_yield) == 0) { s_role = 1; s_wfn(s_wparam); }
        s_role = 0;
    }
    return 1;
}

// ---- harness ----
static raytrace2_t fx;
static uint8_t     s_rgba[RT2_W * RT2_H * 4];

EMSCRIPTEN_KEEPALIVE int  rt_init(void)              { return raytrace2_init(&fx); }
EMSCRIPTEN_KEEPALIVE int  rt_w(void)                 { return RT2_W; }
EMSCRIPTEN_KEEPALIVE int  rt_h(void)                 { return RT2_H; }
EMSCRIPTEN_KEEPALIVE void rt_orbit(float dx, float dy){ raytrace2_orbit(&fx, dx, dy); }
EMSCRIPTEN_KEEPALIVE uint32_t rt_fps(void)           { return fx.fps; }
EMSCRIPTEN_KEEPALIVE void rt_frame(void)             { raytrace2_frame(&fx); raytrace2_draw_fps(&fx); }

// Read the panel's byte-swapped RGB565 frame memory out to RGBA, round-masked.
EMSCRIPTEN_KEEPALIVE uint8_t *rt_rgba(void)
{
    const int cx = RT2_W / 2, cy = RT2_H / 2, r2 = (RT2_W / 2) * (RT2_W / 2);
    const uint16_t *fb = fx.pixels;
    for (int y = 0; y < RT2_H; y++) {
        for (int x = 0; x < RT2_W; x++) {
            int o = (y * RT2_W + x) * 4, dx = x - cx, dy = y - cy;
            if (dx * dx + dy * dy > r2) { s_rgba[o] = s_rgba[o+1] = s_rgba[o+2] = 0; s_rgba[o+3] = 255; continue; }
            uint16_t v = fb[y * RT2_W + x];
            uint16_t c = (uint16_t)((v << 8) | (v >> 8));
            uint8_t r5 = (c >> 11) & 0x1F, g6 = (c >> 5) & 0x3F, b5 = c & 0x1F;
            s_rgba[o]   = (uint8_t)((r5 << 3) | (r5 >> 2));
            s_rgba[o+1] = (uint8_t)((g6 << 2) | (g6 >> 4));
            s_rgba[o+2] = (uint8_t)((b5 << 3) | (b5 >> 2));
            s_rgba[o+3] = 255;
        }
    }
    return s_rgba;
}