#include #include #include #include #include #include #include #include #include #include EYN_CMDMETA_V1("Open an REI, BMP image, REIV video, REIS audio, or WAV audio viewer.", "view /images/picture.rei"); #define REI_MAGIC 0x52454900u #define REI_DEPTH_MONO 1u #define REI_DEPTH_RGB 3u #define REI_DEPTH_RGBA 4u #define REI_COMP_NONE 0x0u #define REI_COMP_RLE 0x1u #define REI_COMP_MASK 0x0Fu #define REIV_MAGIC 0x52455600u #define REIV_VERSION_V1 1u #define REIV_VERSION_V2 2u #define REIV_VERSION_V3 3u #define REIV_PIXFMT_RGB565LE 1u #define REIV_FLAG_LOOP_DEFAULT 0x01u #define REIV_FRAME_FLAG_RLE565 0x00000001u #define REIV_FRAME_FLAG_RLE8 0x00000002u #define REIV_FRAME_FLAG_DELTA_XOR_PREV 0x00000004u /* ---- REIS (audio) constants ---- */ #define REIS_MAGIC 0x52454953u #define REIS_VERSION 1u #define REIS_COMP_NONE 0x0u #define REIS_COMP_RLE 0x1u #define REIS_COMP_MASK 0x0Fu #define REIS_HEADER_SIZE 32u /* ---- WAV constants ---- */ #define WAV_RIFF_MAGIC 0x46464952u /* 'RIFF' LE */ #define WAV_WAVE_MAGIC 0x45564157u /* 'WAVE' LE */ #define WAV_FMT_MAGIC 0x20746D66u /* 'fmt ' LE */ #define WAV_DATA_MAGIC 0x61746164u /* 'data' LE */ /* BMP / DIB magic: first two bytes are always 'B','M' (0x42, 0x4D). */ #define BMP_MAGIC_B0 0x42u #define BMP_MAGIC_B1 0x4Du #define BMP_BI_RGB 0u /* uncompressed */ #define BMP_BI_BITFIELDS 3u /* channel masks in header */ #define BMP_FILEHEADER_SIZE 14u #define BMP_INFOHEADER_MIN 40u /* AC97 output format: 48 kHz stereo 16-bit signed LE */ #define AC97_RATE 48000u #define AC97_CHANNELS 2u #define AC97_BITS 16u #define AC97_DMA_BUF 4096u #define VIEW_STATUS_H 18 #define VIEW_BG_R GUI_PAL_BG_R #define VIEW_BG_G GUI_PAL_BG_G #define VIEW_BG_B GUI_PAL_BG_B typedef struct { uint32_t magic; uint16_t width; uint16_t height; uint8_t depth; uint8_t reserved1; uint16_t reserved2; } rei_header_t; typedef struct { uint32_t magic; uint16_t width; uint16_t height; uint8_t pixfmt; uint8_t flags; uint16_t version; uint32_t frame_count; uint32_t fps_num; uint32_t fps_den; uint32_t frames_offset; } reiv_header_t; typedef struct { uint32_t offset; uint32_t size; uint32_t flags; } reiv_frame_entry_t; typedef struct { int loaded; int width; int height; uint16_t* pixels; } rei_image_t; typedef struct { int is_video; int width; int height; int fd; char path[256]; reiv_header_t header; reiv_frame_entry_t* index; uint16_t* frame; uint16_t* prev_frame; uint8_t* payload_buf; size_t payload_cap; uint8_t* delta_buf; size_t delta_cap; uint32_t frame_size_bytes; uint32_t next_frame; uint32_t stream_payload_off; uint32_t stream_data_base; int playing; unsigned frame_delay_us; } reiv_stream_t; /* ---- REIS on-disk header ---- */ typedef struct __attribute__((packed)) { uint32_t magic; uint16_t version; uint8_t channels; uint8_t bits; uint32_t sample_rate; uint32_t frame_count; uint32_t data_offset; uint32_t data_size; uint32_t flags; uint32_t reserved; } reis_header_t; /* ---- Audio playback state ---- */ /* * Read-ahead buffer size: how many source bytes we read from disk in one * batch. Larger = fewer disk syscalls = less stutter, but more stack. * * 32 768 bytes at 44100 Hz stereo 16-bit = ~186 ms of source audio per * disk read. At 48000 Hz stereo 16-bit = exactly 8 full AC97 DMA buffers * (8 × 4096 = 32 768). Chosen to fill roughly half the AC97 ring in one * disk read, which gives enough headroom to hide I/O latency. * * This lives on the stack inside audio_pump, so keep it below ~48 KB to * stay within the 64 KB userland stack. */ #define AUDIO_READAHEAD 32768u typedef struct { int is_audio; /* 1 if this is an audio file */ int playing; int audio_inited; /* 1 after AC97 init succeeds */ int volume_percent; /* software gain applied before AC97 write */ uint32_t play_started_ms; uint32_t played_ms; /* * Source PCM -- two modes: * Buffered: pcm != NULL, entire decoded audio in memory. * Streaming: pcm == NULL, src_fd >= 0, frames read from disk * on demand sequentially (no lseek in the hot loop). * Streaming is used for uncompressed REIS and WAV to avoid * multi-megabyte heap allocations in the 1 MB userland heap. */ uint8_t* pcm; /* buffered PCM (NULL when streaming) */ size_t pcm_size; /* byte size of pcm buffer */ /* Streaming fields (used when pcm == NULL) */ int src_fd; /* open file descriptor; -1 if not streaming */ uint32_t src_data_offset; /* byte offset of first PCM frame in file */ uint32_t src_frame_count; /* total source frames available */ /* Resampled 48 kHz stereo s16le for AC97 */ int16_t* out_buf; /* pre-resampled buffer (NULL -- not used) */ size_t out_frames; /* total output frames at AC97_RATE */ size_t out_size; /* byte size of out_buf */ /* Source format info */ uint32_t src_rate; uint8_t src_channels; uint8_t src_bits; /* Current playback cursor (in resampled output frames at AC97_RATE) */ size_t cursor; /* Duration tracking */ uint32_t duration_ms; /* * Read-ahead buffer for streaming path. * * We read AUDIO_READAHEAD bytes from disk in one batch, hold them here, * and feed the AC97 ring directly from this buffer. This amortizes * disk-read syscall overhead across many DMA buffer fills and keeps * the ring fed even when individual VFS reads are slow. * * ra_buf -- raw source bytes * ra_len -- bytes currently valid in ra_buf * ra_pos -- read cursor within ra_buf (bytes consumed so far) * ra_error -- Bresenham fractional error carried between refills */ uint8_t ra_buf[AUDIO_READAHEAD]; size_t ra_len; /* valid bytes in ra_buf */ size_t ra_pos; /* bytes consumed from ra_buf */ uint32_t ra_error; /* Bresenham error (persists across refills) */ } audio_state_t; typedef struct { int handle; int running; int content_w; int content_h; int viewport_w; int viewport_h; /* * Blit dimensions: capped at the kernel gui_blit_rgb565 hard limit of * 320×200 pixels. viewport_w/h may be larger (the full content area); * blit_w/blit_h are the actual framebuffer dimensions handed to the * kernel. render_framebuffer maps blit coords → viewport coords → * source image coords so that zoom/pan still operates in the full * viewport space. The kernel upscales blit_w×blit_h → viewport_w×viewport_h * via the dst_w/dst_h fields of gui_blit_rgb565_t. */ int blit_w; int blit_h; uint16_t* viewbuf; size_t viewbuf_cap; int zoom_permille; int origin_x; int origin_y; int prev_left_down; int dragging; int drag_last_x; int drag_last_y; int64_t frame_counter; uint32_t audio_ui_last_redraw_ms; int audio_ui_dirty; rei_image_t image; reiv_stream_t video; audio_state_t audio; } app_t; static uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { return (uint16_t)(((uint16_t)(r >> 3) << 11) | ((uint16_t)(g >> 2) << 5) | (uint16_t)(b >> 3)); } static int clampi(int value, int lo, int hi) { if (value < lo) return lo; if (value > hi) return hi; return value; } static int str_ends_with(const char* s, const char* suffix) { size_t a = s ? strlen(s) : 0; size_t b = suffix ? strlen(suffix) : 0; if (a < b) return 0; return strcmp(s + a - b, suffix) == 0; } static void str_copy(char* dst, int cap, const char* src) { if (!dst || cap <= 0) return; int i = 0; if (!src) { dst[0] = '\0'; return; } while (src[i] && i < cap - 1) { dst[i] = src[i]; ++i; } dst[i] = '\0'; } static void str_append(char* dst, int cap, const char* src) { if (!dst || cap <= 0 || !src) return; int n = (int)strlen(dst); int i = 0; if (n >= cap - 1) return; while (src[i] && (n + i) < cap - 1) { dst[n + i] = src[i]; ++i; } dst[n + i] = '\0'; } static void str_append_uint(char* dst, int cap, uint32_t value) { char tmp[16]; int pos = 0; if (value == 0u) { str_append(dst, cap, "0"); return; } while (value > 0u && pos < (int)sizeof(tmp)) { tmp[pos++] = (char)('0' + (value % 10u)); value /= 10u; } while (pos > 0) { char c[2]; c[0] = tmp[--pos]; c[1] = '\0'; str_append(dst, cap, c); } } static int read_exact_fd(int fd, void* out_buf, size_t len) { uint8_t* out = (uint8_t*)out_buf; size_t total = 0; while (total < len) { ssize_t got = read(fd, out + total, len - total); if (got <= 0) return -1; total += (size_t)got; } return 0; } /* * Read the first 4 bytes of a file as a little-endian uint32 to identify the * format magic, then close. Returns 0 if the file cannot be read. */ static uint32_t probe_file_magic(const char* path) { int fd = open(path, O_RDONLY, 0); if (fd < 0) return 0; uint8_t buf[4]; ssize_t n = read(fd, buf, 4); close(fd); if (n != 4) return 0; return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24); } static int skip_fd_bytes(int fd, size_t len) { uint8_t scratch[256]; size_t left = len; while (left > 0) { size_t want = left > sizeof(scratch) ? sizeof(scratch) : left; ssize_t got = read(fd, scratch, want); if (got <= 0) return -1; left -= (size_t)got; } return 0; } static int rle_decode_packbits(const uint8_t* in, size_t in_len, uint8_t* out, size_t out_len, int pixel_size) { size_t ip = 0; size_t op = 0; if (!in || !out || pixel_size <= 0) return -1; while (ip < in_len && op < out_len) { int8_t n = (int8_t)in[ip++]; if (n >= 0) { size_t count = (size_t)n + 1u; size_t bytes = count * (size_t)pixel_size; if (ip + bytes > in_len) return -1; if (op + bytes > out_len) return -1; memcpy(out + op, in + ip, bytes); ip += bytes; op += bytes; } else if (n != -128) { size_t count = (size_t)(1 - n); size_t bytes = count * (size_t)pixel_size; if (ip + (size_t)pixel_size > in_len) return -1; if (op + bytes > out_len) return -1; const uint8_t* px = in + ip; ip += (size_t)pixel_size; for (size_t i = 0; i < count; ++i) { memcpy(out + op, px, (size_t)pixel_size); op += (size_t)pixel_size; } } } return (op == out_len) ? 0 : -1; } /* Backends are split into .view modules and compiled into this frontend. */ #include #include #include /* ==== Audio support: REIS and WAV loading + resampling ==== */ static void audio_free(audio_state_t* a) { if (!a) return; if (a->pcm) free(a->pcm); if (a->out_buf) free(a->out_buf); if (a->src_fd >= 0) close(a->src_fd); memset(a, 0, sizeof(*a)); a->src_fd = -1; } static uint32_t audio_now_ms(void) { return (uint32_t)eyn_syscall0(EYN_SYSCALL_GET_TICKS_MS); } static uint32_t audio_playback_ms(const audio_state_t* a) { if (!a) return 0; /* * Cursor-based elapsed time: tracks how much audio has been delivered * to the AC97 ring. This is more accurate than wall-clock timing * because it pauses naturally when the CPU cannot keep up with disk * I/O, rather than advancing past the actual playback position. */ uint32_t pos_ms = (uint32_t)((uint64_t)a->cursor * 1000u / AC97_RATE); if (a->duration_ms > 0 && pos_ms > a->duration_ms) pos_ms = a->duration_ms; return pos_ms; } static int16_t audio_apply_volume(int16_t sample, int volume_percent) { long scaled; if (volume_percent <= 0) return 0; if (volume_percent == 100) return sample; scaled = ((long)sample * (long)volume_percent) / 100L; if (scaled > 32767L) return 32767; if (scaled < -32768L) return -32768; return (int16_t)scaled; } /* * Resample arbitrary-format PCM to 48 kHz stereo s16le for AC97 output. * Nearest-neighbour resampling (good enough for a simple player). */ static int audio_resample_to_ac97(audio_state_t* a) { if (!a) return -1; if (a->src_rate == 0 || a->src_channels == 0 || a->src_bits == 0) return -1; uint32_t src_frame_bytes = (uint32_t)a->src_channels * ((uint32_t)a->src_bits / 8u); if (src_frame_bytes == 0) return -1; /* * Determine total source frames: streaming uses src_frame_count directly; * buffered uses the pcm buffer size. */ uint32_t src_frames; if (a->src_fd >= 0 && !a->pcm) { src_frames = a->src_frame_count; } else { if (!a->pcm || a->pcm_size == 0) return -1; src_frames = (uint32_t)(a->pcm_size / src_frame_bytes); } if (src_frames == 0) return -1; /* Calculate output frame count for progress/duration tracking only. */ uint64_t out_frames64 = ((uint64_t)src_frames * AC97_RATE + a->src_rate - 1u) / a->src_rate; if (out_frames64 == 0) return -1; a->out_frames = (size_t)out_frames64; a->out_size = a->out_frames * AC97_CHANNELS * sizeof(int16_t); /* out_buf is never pre-allocated; pump resamples into a stack buffer. */ a->out_buf = NULL; a->duration_ms = (uint32_t)((uint64_t)a->out_frames * 1000u / AC97_RATE); /* Diagnostic: show computed audio parameters */ printf("audio: %u Hz %u-bit %uch -> %u output frames, duration %u ms\n", (unsigned)a->src_rate, (unsigned)a->src_bits, (unsigned)a->src_channels, (unsigned)a->out_frames, (unsigned)a->duration_ms); return 0; } #include #include /* * Submit the next chunk of resampled audio to the AC97 driver. * Returns 0 if more data remains, 1 if playback finished, -1 on error. */ static int audio_pump(audio_state_t* a) { if (!a || !a->is_audio || !a->playing) return -1; if (!a->pcm && a->src_fd < 0) return -1; uint32_t src_frame_bytes = (uint32_t)a->src_channels * ((uint32_t)a->src_bits / 8u); if (src_frame_bytes == 0) return -1; uint32_t src_frames = a->pcm ? (uint32_t)(a->pcm_size / src_frame_bytes) : a->src_frame_count; size_t frames_per_buf = AC97_DMA_BUF / (AC97_CHANNELS * sizeof(int16_t)); /* * ---- Streaming fast path: native 48 kHz stereo 16-bit 100 % volume ---- * * The data in ra_buf is already in AC97 output format, so we can * submit it directly without copying through an intermediate * chunk_buf. One AUDIO_WRITE_BULK syscall queues up to 8 DMA * buffers from ra_buf in a single ring-3/ring-0 transition. */ if (a->src_fd >= 0 && !a->pcm && a->src_rate == AC97_RATE && a->src_channels == 2u && a->src_bits == 16u && a->volume_percent == 100) { if (a->cursor >= a->out_frames) return 1; /* Refill ra_buf if less than one DMA buffer remains. */ size_t avail = (a->ra_len > a->ra_pos) ? (a->ra_len - a->ra_pos) : 0; if (avail < AC97_DMA_BUF) { size_t leftover = avail; if (leftover > 0) memmove(a->ra_buf, a->ra_buf + a->ra_pos, leftover); a->ra_pos = 0; a->ra_len = leftover; size_t want = AUDIO_READAHEAD - a->ra_len; int got = (int)read(a->src_fd, a->ra_buf + a->ra_len, (int)want); if (got > 0) a->ra_len += (size_t)got; avail = a->ra_len; } /* * Determine how many bytes to submit. Align down to 4 KB * boundaries so each DMA buffer is exactly full (the kernel * zero-pads short chunks, but we avoid unnecessary silence). */ size_t frames_left = a->out_frames - a->cursor; size_t bytes_left = frames_left * AC97_CHANNELS * sizeof(int16_t); size_t submit = avail < bytes_left ? avail : bytes_left; size_t aligned = submit & ~(size_t)(AC97_DMA_BUF - 1u); /* * If we have a partial tail (less than 4 KB remaining in the * file), include it -- the kernel will zero-pad the last DMA * buffer. This avoids dropping the final fraction of audio. */ if (aligned == 0 && submit > 0) aligned = submit; if (aligned == 0) return 1; /* nothing left */ int count = eyn_sys_audio_write_bulk( a->ra_buf + a->ra_pos, (int)aligned); if (count <= 0) return 0; /* ring full -- retry next frame */ size_t bytes_queued = (size_t)count * AC97_DMA_BUF; if (bytes_queued > aligned) bytes_queued = aligned; size_t frames_queued = bytes_queued / (AC97_CHANNELS * sizeof(int16_t)); a->ra_pos += bytes_queued; a->cursor += frames_queued; return (a->cursor >= a->out_frames) ? 1 : 0; } /* ---- General path: per-buffer write with resample support ---- */ while (1) { if (a->cursor >= a->out_frames) return 1; /* playback complete */ size_t remaining = a->out_frames - a->cursor; size_t to_send = remaining < frames_per_buf ? remaining : frames_per_buf; if (to_send == 0) return 1; int16_t chunk_buf[AC97_DMA_BUF / sizeof(int16_t)]; /* * ra_advance: how many bytes to commit from ra_buf after a * successful write. Set during the chunk-build step below, * applied only after eyn_sys_audio_write succeeds. This * prevents ra_pos from advancing when the ring is full -- * which previously caused the audio to skip forward because * ra_buf moved ahead of cursor. * * new_ra_error: similarly, the updated Bresenham error is only * committed to a->ra_error after a successful write. */ size_t ra_advance = 0; uint32_t new_ra_error = a->ra_error; if (a->src_fd >= 0 && !a->pcm) { /* * Streaming resample path: compute how many source bytes we * need for this output chunk, then serve from ra_buf. * (The native-48 kHz fast path is handled above via bulk * write and never reaches here.) */ uint64_t src_last64 = (uint64_t)(a->cursor + to_send - 1u) * a->src_rate; uint32_t src_last = (uint32_t)(src_last64 / AC97_RATE); uint64_t src_start64 = (uint64_t)a->cursor * a->src_rate; uint32_t src_start = (uint32_t)(src_start64 / AC97_RATE); uint32_t src_count = src_last - src_start + 1u; size_t src_bytes = (size_t)src_count * src_frame_bytes; /* Clamp to read-ahead buffer size. */ if (src_bytes > AUDIO_READAHEAD) return -1; /* Refill if not enough source bytes available. */ if (a->ra_pos + src_bytes > a->ra_len) { size_t leftover = (a->ra_len > a->ra_pos) ? (a->ra_len - a->ra_pos) : 0; if (leftover > 0) memmove(a->ra_buf, a->ra_buf + a->ra_pos, leftover); a->ra_pos = 0; a->ra_len = leftover; size_t want = AUDIO_READAHEAD - a->ra_len; int got = (int)read(a->src_fd, a->ra_buf + a->ra_len, (int)want); if (got > 0) a->ra_len += (size_t)got; } const uint8_t* src_ptr = a->ra_buf + a->ra_pos; size_t avail = (a->ra_len > a->ra_pos) ? (a->ra_len - a->ra_pos) : 0; size_t si = 0; uint32_t error = a->ra_error; /* local copy; committed below */ for (size_t i = 0; i < to_send; ++i) { size_t byte_off = si * src_frame_bytes; int16_t left = 0, right = 0; if (byte_off + src_frame_bytes <= avail) { if (a->src_bits == 16) { const int16_t* fp = (const int16_t*)(src_ptr + byte_off); left = fp[0]; right = (a->src_channels >= 2u) ? fp[1] : left; } else { const uint8_t* fp = src_ptr + byte_off; left = (int16_t)((int)fp[0] - 128) * 256; right = (a->src_channels >= 2u) ? (int16_t)((int)fp[1] - 128) * 256 : left; } } left = audio_apply_volume(left, a->volume_percent); right = audio_apply_volume(right, a->volume_percent); chunk_buf[i * 2 + 0] = left; chunk_buf[i * 2 + 1] = right; error += a->src_rate; while (error >= AC97_RATE) { error -= AC97_RATE; si++; } } /* Record advance amounts -- applied only after successful write. */ size_t src_consumed = si * src_frame_bytes; if (src_consumed > avail) src_consumed = avail; ra_advance = src_consumed; new_ra_error = error; } else { /* * Buffered path: entire source PCM is in a->pcm. * Bresenham-style resampling from memory. */ uint64_t src_pos64 = (uint64_t)a->cursor * a->src_rate; uint32_t src_idx = (uint32_t)(src_pos64 / AC97_RATE); uint32_t error = (uint32_t)(src_pos64 % AC97_RATE); /* Fast path: native 48kHz stereo 16-bit, 100% volume. */ if (a->src_rate == AC97_RATE && a->src_channels == 2u && a->src_bits == 16u && a->volume_percent == 100) { memcpy(chunk_buf, a->pcm + a->cursor * (AC97_CHANNELS * sizeof(int16_t)), to_send * AC97_CHANNELS * sizeof(int16_t)); } else { for (size_t i = 0; i < to_send; ++i) { if (src_idx >= src_frames) src_idx = src_frames - 1u; int16_t left = 0, right = 0; if (a->src_bits == 16) { const int16_t* fp = (const int16_t*)(a->pcm + src_idx * src_frame_bytes); left = fp[0]; right = (a->src_channels >= 2u) ? fp[1] : left; } else { const uint8_t* fp = a->pcm + src_idx * src_frame_bytes; left = (int16_t)((int)fp[0] - 128) * 256; right = (a->src_channels >= 2u) ? (int16_t)((int)fp[1] - 128) * 256 : left; } left = audio_apply_volume(left, a->volume_percent); right = audio_apply_volume(right, a->volume_percent); chunk_buf[i * 2 + 0] = left; chunk_buf[i * 2 + 1] = right; error += a->src_rate; while (error >= AC97_RATE) { error -= AC97_RATE; src_idx++; } } } } size_t bytes = to_send * AC97_CHANNELS * sizeof(int16_t); int rc = eyn_sys_audio_write((const void*)chunk_buf, (int)bytes); if (rc < 0) return 0; /* ring full -- come back next render frame */ /* * Write succeeded: commit read-ahead state. These are only * advanced here so that a full ring (rc < 0 above) leaves ra_pos * and ra_error unchanged, and the next call retries the same * chunk instead of jumping forward in the file. */ a->ra_pos += ra_advance; a->ra_error = new_ra_error; a->cursor += to_send; } } static int ensure_viewbuf(app_t* app) { if (!app) return -1; if (app->blit_w <= 0 || app->blit_h <= 0) return -1; size_t need_px = (size_t)app->blit_w * (size_t)app->blit_h; if (need_px <= app->viewbuf_cap && app->viewbuf) return 0; uint16_t* grown = (uint16_t*)realloc(app->viewbuf, need_px * sizeof(uint16_t)); if (!grown) return -1; app->viewbuf = grown; app->viewbuf_cap = need_px; return 0; } static void reset_view_transform(app_t* app, int src_w, int src_h) { if (!app || src_w <= 0 || src_h <= 0 || app->viewport_w <= 0 || app->viewport_h <= 0) return; int zoom_w = (app->viewport_w * 1000) / src_w; int zoom_h = (app->viewport_h * 1000) / src_h; int zoom = zoom_w < zoom_h ? zoom_w : zoom_h; if (zoom < 50) zoom = 50; if (zoom > 6000) zoom = 6000; app->zoom_permille = zoom; int scaled_w = (src_w * zoom + 999) / 1000; int scaled_h = (src_h * zoom + 999) / 1000; app->origin_x = (app->viewport_w - scaled_w) / 2; app->origin_y = (app->viewport_h - scaled_h) / 2; } static void zoom_view(app_t* app, int src_w, int src_h, int zoom_in) { if (!app || src_w <= 0 || src_h <= 0) return; int old_zoom = app->zoom_permille; int next_zoom = zoom_in ? (old_zoom * 5) / 4 : (old_zoom * 4) / 5; next_zoom = clampi(next_zoom, 50, 8000); if (next_zoom == old_zoom) return; int cx = app->viewport_w / 2; int cy = app->viewport_h / 2; int src_cx = ((cx - app->origin_x) * 1000) / old_zoom; int src_cy = ((cy - app->origin_y) * 1000) / old_zoom; app->zoom_permille = next_zoom; app->origin_x = cx - (src_cx * next_zoom) / 1000; app->origin_y = cy - (src_cy * next_zoom) / 1000; } static void render_framebuffer(app_t* app, const uint16_t* src, int src_w, int src_h) { if (!app || !src || !app->viewbuf) return; if (app->blit_w <= 0 || app->blit_h <= 0) return; /* * Render into the blit buffer (blit_w × blit_h, ≤320×200). * Each blit pixel (bx, by) is first mapped to a viewport pixel * (vpx, vpy) by scaling blit dims → viewport dims. The viewport * coordinate is then mapped to a source image pixel via zoom/origin. * The kernel upscales the blit buffer back to viewport dimensions via * the dst_w/dst_h fields of gui_blit_rgb565_t. */ uint16_t bg = rgb565(VIEW_BG_R, VIEW_BG_G, VIEW_BG_B); size_t total = (size_t)app->blit_w * (size_t)app->blit_h; for (size_t i = 0; i < total; ++i) app->viewbuf[i] = bg; int zoom = app->zoom_permille; if (zoom <= 0) zoom = 1000; /* * Precompute source-X for each blit column once, outside the row loop. * The inner loop previously did two integer divisions per pixel: one for * the blit→viewport column scale and one for the viewport→source zoom. * Both depend only on bx (not by), so they can be hoisted entirely. * blit_w is ≤320 by construction; sx_table is indexed [0, blit_w). */ int sx_table[320]; int ncols = (app->blit_w < 320) ? app->blit_w : 320; for (int bx = 0; bx < ncols; ++bx) { int vpx = (app->blit_w == app->viewport_w) ? bx : (bx * app->viewport_w / app->blit_w); int sx = ((vpx - app->origin_x) * 1000) / zoom; sx_table[bx] = (sx >= 0 && sx < src_w) ? sx : -1; } for (int by = 0; by < app->blit_h; ++by) { /* Map blit row → viewport row (integer nearest-neighbour downscale) */ int vpy = (app->blit_h == app->viewport_h) ? by : (by * app->viewport_h / app->blit_h); int sy = ((vpy - app->origin_y) * 1000) / zoom; if (sy < 0 || sy >= src_h) continue; size_t row_off = (size_t)by * (size_t)app->blit_w; size_t src_row = (size_t)sy * (size_t)src_w; for (int bx = 0; bx < ncols; ++bx) { int sx = sx_table[bx]; if (sx < 0) continue; app->viewbuf[row_off + (size_t)bx] = src[src_row + (size_t)sx]; } } } static void draw_status(app_t* app, const char* path) { gui_rect_t status_bg = { .x = 0, .y = app->viewport_h, .w = app->content_w, .h = VIEW_STATUS_H, .r = GUI_PAL_STATUS_R, .g = GUI_PAL_STATUS_G, .b = GUI_PAL_STATUS_B, ._pad = 0 }; (void)gui_fill_rect(app->handle, &status_bg); char left[128]; char right[128]; left[0] = '\0'; right[0] = '\0'; if (app->video.is_video) { uint32_t shown = app->video.next_frame ? app->video.next_frame : 1u; str_copy(left, (int)sizeof(left), path); str_append(left, (int)sizeof(left), " | frame "); str_append_uint(left, (int)sizeof(left), shown); str_append(left, (int)sizeof(left), "/"); str_append_uint(left, (int)sizeof(left), app->video.header.frame_count); str_append(left, (int)sizeof(left), " | "); str_append(left, (int)sizeof(left), app->video.playing ? "playing" : "paused"); str_copy(right, (int)sizeof(right), "Space play/pause +/- zoom wheel zoom arrows/mouse pan Esc quit"); } else if (app->audio.is_audio) { uint32_t pos_ms = audio_playback_ms(&app->audio); uint32_t dur_ms = app->audio.duration_ms; str_copy(left, (int)sizeof(left), path); str_append(left, (int)sizeof(left), " | "); str_append_uint(left, (int)sizeof(left), pos_ms / 1000u); str_append(left, (int)sizeof(left), "."); str_append_uint(left, (int)sizeof(left), (pos_ms / 100u) % 10u); str_append(left, (int)sizeof(left), "s / "); str_append_uint(left, (int)sizeof(left), dur_ms / 1000u); str_append(left, (int)sizeof(left), "."); str_append_uint(left, (int)sizeof(left), (dur_ms / 100u) % 10u); str_append(left, (int)sizeof(left), "s | "); str_append_uint(left, (int)sizeof(left), (uint32_t)app->audio.volume_percent); str_append(left, (int)sizeof(left), "% | "); str_append(left, (int)sizeof(left), app->audio.playing ? "playing" : "paused"); str_copy(right, (int)sizeof(right), "Space play/pause +/- volume Esc quit"); } else { str_copy(left, (int)sizeof(left), path); str_append(left, (int)sizeof(left), " | zoom "); str_append_uint(left, (int)sizeof(left), (uint32_t)(app->zoom_permille / 10)); str_append(left, (int)sizeof(left), "%"); str_copy(right, (int)sizeof(right), "+/- zoom wheel zoom arrows/mouse pan Esc quit"); } gui_text_t t1 = { .x = 6, .y = app->viewport_h + 5, .r = GUI_PAL_TEXT_R, .g = GUI_PAL_TEXT_G, .b = GUI_PAL_TEXT_B, ._pad = 0, .text = left }; gui_text_t t2 = { .x = app->content_w / 2, .y = app->viewport_h + 5, .r = GUI_PAL_DIM_R, .g = GUI_PAL_DIM_G, .b = GUI_PAL_DIM_B, ._pad = 0, .text = right }; (void)gui_draw_text(app->handle, &t1); (void)gui_draw_text(app->handle, &t2); } static void render_app(app_t* app, const char* path, const uint16_t* src_pixels, int src_w, int src_h) { if (!app || !src_pixels) return; if (ensure_viewbuf(app) != 0) return; render_framebuffer(app, src_pixels, src_w, src_h); /* * Do NOT call gui_clear here. The kernel processes the blit buffer * first, then draw-commands on top. Calling gui_clear after * gui_blit_rgb565 would paint a solid background over the image. * render_framebuffer already fills every viewbuf pixel with the * background colour before drawing the image. */ (void)gui_begin(app->handle); gui_blit_rgb565_t blit = { /* * src_w/src_h: the actual pixels in viewbuf (≤320×200 kernel limit). * dst_w/dst_h: the full viewport area the kernel scales the blit into. */ .src_w = app->blit_w, .src_h = app->blit_h, .pixels = app->viewbuf, .dst_w = app->viewport_w, .dst_h = app->viewport_h, }; (void)gui_blit_rgb565(app->handle, &blit); draw_status(app, path); (void)gui_present(app->handle); } static int key_is_zoom_in(int key) { unsigned ch = (unsigned)key & 0xFFu; return ch == '+' || ch == '=' || key == 0x2102; } static int key_is_zoom_out(int key) { unsigned ch = (unsigned)key & 0xFFu; return ch == '-' || ch == '_' || key == 0x2103; } typedef enum { VIEW_BACKEND_NONE = 0, VIEW_BACKEND_REI, VIEW_BACKEND_REIV, VIEW_BACKEND_REIS, VIEW_BACKEND_WAV, VIEW_BACKEND_BMP, } view_backend_id_t; typedef struct { int dir_available; int rei; int reiv; int reis; int wav; int bmp; } view_backend_registry_t; static const char* view_backend_name(view_backend_id_t id) { switch (id) { case VIEW_BACKEND_REI: return "rei"; case VIEW_BACKEND_REIV: return "reiv"; case VIEW_BACKEND_REIS: return "reis"; case VIEW_BACKEND_WAV: return "wav"; case VIEW_BACKEND_BMP: return "bmp"; default: return "none"; } } static void view_scan_backends(view_backend_registry_t* reg) { if (!reg) return; memset(reg, 0, sizeof(*reg)); int fd = open("/.view", O_RDONLY, 0); if (fd < 0) return; reg->dir_available = 1; typedef struct { uint8_t is_dir; uint8_t _pad[3]; uint32_t size; char name[56]; } view_dirent_t; view_dirent_t entries[16]; for (;;) { int bytes = getdents(fd, (void*)entries, sizeof(entries)); if (bytes <= 0) break; int count = bytes / (int)sizeof(view_dirent_t); for (int i = 0; i < count; ++i) { if (entries[i].is_dir || !entries[i].name[0]) continue; if (strcmp(entries[i].name, "rei.backend") == 0) reg->rei = 1; else if (strcmp(entries[i].name, "reiv.backend") == 0) reg->reiv = 1; else if (strcmp(entries[i].name, "reis.backend") == 0) reg->reis = 1; else if (strcmp(entries[i].name, "wav.backend") == 0) reg->wav = 1; else if (strcmp(entries[i].name, "bmp.backend") == 0) reg->bmp = 1; } } close(fd); } static void view_print_available_backends(const view_backend_registry_t* reg) { if (!reg || !reg->dir_available) { puts("view: backend directory /.view not found"); return; } puts("view: installed backends in /.view:"); if (reg->rei) puts(" - rei"); if (reg->reiv) puts(" - reiv"); if (reg->reis) puts(" - reis"); if (reg->wav) puts(" - wav"); if (reg->bmp) puts(" - bmp"); } static view_backend_id_t view_select_backend(const char* path, const view_backend_registry_t* reg) { if (!path || !reg) return VIEW_BACKEND_NONE; uint32_t magic = probe_file_magic(path); int ext_rei = str_ends_with(path, ".rei"); int ext_reiv = str_ends_with(path, ".reiv"); int ext_reis = str_ends_with(path, ".reis"); int ext_wav = str_ends_with(path, ".wav"); int ext_bmp = str_ends_with(path, ".bmp"); if (reg->reis && (magic == REIS_MAGIC || ext_reis)) return VIEW_BACKEND_REIS; if (reg->wav && ((magic == WAV_RIFF_MAGIC && probe_is_wav(path)) || ext_wav)) return VIEW_BACKEND_WAV; if (reg->reiv && (magic == REIV_MAGIC || ext_reiv)) return VIEW_BACKEND_REIV; if (reg->bmp && (ext_bmp || probe_is_bmp(path))) return VIEW_BACKEND_BMP; if (reg->rei && (magic == REI_MAGIC || ext_rei)) return VIEW_BACKEND_REI; return VIEW_BACKEND_NONE; } static void usage(void) { puts("Usage: view "); } int main(int argc, char** argv) { if (argc < 2 || !argv[1] || !argv[1][0]) { usage(); return 1; } const char* path = argv[1]; /* * app_t is declared static to keep the 32KB read-ahead buffer (ra_buf) * and other large fields off the stack. The userland stack on EYN-OS * is 64 KB; putting a 32 KB+ struct on it would overflow it immediately. */ static app_t app; memset(&app, 0, sizeof(app)); app.video.fd = -1; app.audio.src_fd = -1; view_backend_registry_t backend_reg; view_scan_backends(&backend_reg); view_backend_id_t backend = view_select_backend(path, &backend_reg); if (backend == VIEW_BACKEND_NONE) { puts("view: no backend in /.view supports this file"); view_print_available_backends(&backend_reg); return 1; } if (backend == VIEW_BACKEND_REIS || backend == VIEW_BACKEND_WAV) { int load_rc = (backend == VIEW_BACKEND_REIS) ? audio_load_reis(path, &app.audio) : audio_load_wav(path, &app.audio); if (load_rc != 0) { printf("view: backend '%s' failed to load audio\n", view_backend_name(backend)); return 1; } /* Probe and init the AC97 audio controller */ if (eyn_sys_audio_probe() != 0) { puts("view: no audio hardware detected"); audio_free(&app.audio); return 1; } if (eyn_sys_audio_init() != 0) { puts("view: failed to initialise audio hardware"); audio_free(&app.audio); return 1; } app.audio.audio_inited = 1; app.audio.playing = 1; /* auto-play */ app.audio.play_started_ms = audio_now_ms(); app.audio.played_ms = 0; app.audio_ui_last_redraw_ms = 0; app.audio_ui_dirty = 1; } else if (backend == VIEW_BACKEND_REIV) { if (reiv_open(path, &app.video) != 0) { puts("view: backend 'reiv' failed to open video"); return 1; } if (reiv_decode_next(&app.video) != 0) { puts("view: backend 'reiv' failed to decode first frame"); reiv_close(&app.video); return 1; } } else if (backend == VIEW_BACKEND_BMP) { if (bmp_load_file(path, &app.image) != 0) { puts("view: backend 'bmp' failed to load image"); return 1; } } else if (backend == VIEW_BACKEND_REI) { if (rei_load_file(path, &app.image) != 0) { puts("view: backend 'rei' failed to load image"); return 1; } } else { puts("view: backend selected but no handler is implemented"); view_print_available_backends(&backend_reg); return 1; } app.handle = gui_create("View", "Esc quit | +/- zoom | wheel zoom | arrows/mouse pan"); if (app.handle < 0) { reiv_close(&app.video); rei_free(&app.image); audio_free(&app.audio); puts("view: gui_create failed"); return 1; } (void)gui_set_continuous_redraw(app.handle, 1); app.running = 1; /* * Deadline-based video frame pacing. * * Instead of sleeping a fixed frame_delay_us after every frame (which * accumulates work_time as drift), we record when playback began and * compute the wall-clock deadline for each frame. Sleep only the * remaining time to hit that deadline. If we're already past it * (decode + render took too long), skip the sleep entirely. * * On loop/rewind (next_frame wraps back), reset the base so the * new cycle starts fresh. */ uint32_t video_base_ms = audio_now_ms(); uint32_t video_last_frame = 0; while (app.running) { gui_size_t sz = {0, 0}; (void)gui_get_content_size(app.handle, &sz); if (sz.w <= 0) sz.w = 920; if (sz.h <= 0) sz.h = 560; app.content_w = sz.w; app.content_h = sz.h; app.viewport_w = sz.w; app.viewport_h = sz.h - VIEW_STATUS_H; if (app.viewport_h < 40) app.viewport_h = 40; /* Blit buffer is capped at the kernel gui_blit_rgb565 hard limit. */ app.blit_w = app.viewport_w > 320 ? 320 : app.viewport_w; app.blit_h = app.viewport_h > 200 ? 200 : app.viewport_h; int src_w = 0; int src_h = 0; const uint16_t* src_pixels = NULL; if (app.audio.is_audio) { /* Audio mode: no image to display, use a 1x1 dummy */ src_w = 1; src_h = 1; src_pixels = NULL; } else { src_w = app.video.is_video ? app.video.width : app.image.width; src_h = app.video.is_video ? app.video.height : app.image.height; src_pixels = app.video.is_video ? app.video.frame : app.image.pixels; } if (!app.audio.is_audio && (app.frame_counter == 0 || app.zoom_permille <= 0)) { reset_view_transform(&app, src_w, src_h); } gui_event_t ev; while (gui_poll_event(app.handle, &ev) > 0) { if (ev.type == GUI_EVENT_CLOSE) { app.running = 0; break; } if (ev.type == GUI_EVENT_KEY) { int base = ev.a & 0x0FFF; unsigned ch = (unsigned)ev.a & 0xFFu; if (ev.a == 27 || ch == 27u || ch == 'q' || ch == 'Q') { app.running = 0; break; } if (app.audio.is_audio && ch == ' ') { if (app.audio.playing) { app.audio.played_ms = audio_playback_ms(&app.audio); app.audio.playing = 0; eyn_sys_audio_stop(); } else { app.audio.play_started_ms = audio_now_ms(); app.audio.playing = 1; } app.audio_ui_dirty = 1; } else if (app.audio.is_audio && key_is_zoom_in(ev.a)) { if (app.audio.volume_percent < 400) app.audio.volume_percent += 10; app.audio_ui_dirty = 1; } else if (app.audio.is_audio && key_is_zoom_out(ev.a)) { if (app.audio.volume_percent > 0) app.audio.volume_percent -= 10; app.audio_ui_dirty = 1; } else if (app.video.is_video && ch == ' ') { app.video.playing = !app.video.playing; } else if (!app.audio.is_audio && key_is_zoom_in(ev.a)) { zoom_view(&app, src_w, src_h, 1); } else if (!app.audio.is_audio && key_is_zoom_out(ev.a)) { zoom_view(&app, src_w, src_h, 0); } else if (!app.audio.is_audio && (ev.a == 0x1001 || base == 0x1001)) { app.origin_y += 20; } else if (!app.audio.is_audio && (ev.a == 0x1002 || base == 0x1002)) { app.origin_y -= 20; } else if (!app.audio.is_audio && (ev.a == 0x1003 || base == 0x1003)) { app.origin_x += 20; } else if (!app.audio.is_audio && (ev.a == 0x1004 || base == 0x1004)) { app.origin_x -= 20; } else if (!app.audio.is_audio && ch == '0') { reset_view_transform(&app, src_w, src_h); } } else if (ev.type == GUI_EVENT_MOUSE && !app.audio.is_audio) { int left_down = (ev.c & 0x1) != 0; int press_edge = left_down && !app.prev_left_down; int release_edge = (!left_down) && app.prev_left_down; if (ev.d > 0) { zoom_view(&app, src_w, src_h, 1); } else if (ev.d < 0) { zoom_view(&app, src_w, src_h, 0); } if (press_edge) { app.dragging = 1; app.drag_last_x = ev.a; app.drag_last_y = ev.b; } else if (left_down && app.dragging) { int dx = ev.a - app.drag_last_x; int dy = ev.b - app.drag_last_y; app.origin_x += dx; app.origin_y += dy; app.drag_last_x = ev.a; app.drag_last_y = ev.b; } else if (release_edge) { app.dragging = 0; } app.prev_left_down = left_down; } } /* ---- Audio pumping ---- */ if (app.audio.is_audio && app.audio.playing && app.running) { int arc = audio_pump(&app.audio); if (arc == 1) { /* Finished playback -- clean stop. */ app.audio.played_ms = app.audio.duration_ms; app.audio.playing = 0; eyn_sys_audio_stop(); } /* arc == 0: ring full or transient -- retry next frame (normal). */ /* arc < 0: guard failed -- do not kill playing; let pump retry. */ } /* ---- Video frame advance ---- */ if (app.video.is_video && app.video.playing && app.running) { int rc = reiv_decode_next(&app.video); if (rc < 0) { app.running = 0; } } /* ---- Render ---- */ if (app.audio.is_audio) { uint32_t now_ms = audio_now_ms(); if (app.audio_ui_dirty || app.audio_ui_last_redraw_ms == 0 || (now_ms - app.audio_ui_last_redraw_ms) >= 100u) { /* Audio player UI: dark background + progress bar + status */ (void)gui_begin(app.handle); gui_rgb_t bg = { .r = VIEW_BG_R, .g = VIEW_BG_G, .b = VIEW_BG_B, ._pad = 0 }; (void)gui_clear(app.handle, &bg); /* Draw a progress bar in the centre of the viewport */ int bar_w = app.viewport_w - 40; int bar_h = 16; int bar_x = 20; int bar_y = app.viewport_h / 2 - bar_h / 2; if (bar_w < 20) bar_w = 20; /* Background bar */ gui_rect_t bar_bg = { .x = bar_x, .y = bar_y, .w = bar_w, .h = bar_h, .r = GUI_PAL_SURFACE_R, .g = GUI_PAL_SURFACE_G, .b = GUI_PAL_SURFACE_B, ._pad = 0 }; (void)gui_fill_rect(app.handle, &bar_bg); /* Filled portion */ int fill_w = 0; if (app.audio.duration_ms > 0) { uint32_t pos_ms = audio_playback_ms(&app.audio); fill_w = (int)((uint64_t)pos_ms * (uint64_t)bar_w / app.audio.duration_ms); } if (fill_w > bar_w) fill_w = bar_w; if (fill_w > 0) { gui_rect_t bar_fill = { .x = bar_x, .y = bar_y, .w = fill_w, .h = bar_h, .r = GUI_PAL_ACCENT_R, .g = GUI_PAL_ACCENT_G, .b = GUI_PAL_ACCENT_B, ._pad = 0 }; (void)gui_fill_rect(app.handle, &bar_fill); } /* Audio info label */ char info[128]; info[0] = '\0'; str_copy(info, (int)sizeof(info), "Audio: "); str_append_uint(info, (int)sizeof(info), (uint32_t)app.audio.src_rate); str_append(info, (int)sizeof(info), " Hz, "); str_append_uint(info, (int)sizeof(info), (uint32_t)app.audio.src_bits); str_append(info, (int)sizeof(info), "-bit, "); str_append(info, (int)sizeof(info), app.audio.src_channels == 1 ? "mono" : "stereo"); str_append(info, (int)sizeof(info), " | vol "); str_append_uint(info, (int)sizeof(info), (uint32_t)app.audio.volume_percent); str_append(info, (int)sizeof(info), "%"); gui_text_t info_text = { .x = bar_x, .y = bar_y - 20, .r = GUI_PAL_TEXT_R, .g = GUI_PAL_TEXT_G, .b = GUI_PAL_TEXT_B, ._pad = 0, .text = info }; (void)gui_draw_text(app.handle, &info_text); draw_status(&app, path); (void)gui_present(app.handle); app.audio_ui_last_redraw_ms = now_ms; app.audio_ui_dirty = 0; } } else { render_app(&app, path, src_pixels, src_w, src_h); } app.frame_counter += 1; if (app.video.is_video) { uint32_t cur_frame = app.video.next_frame; if (cur_frame < video_last_frame) { /* Rewind occurred -- reset deadline base. */ video_base_ms = audio_now_ms(); } video_last_frame = cur_frame; uint32_t target_ms = video_base_ms + (uint32_t)(((uint64_t)cur_frame * (uint64_t)app.video.frame_delay_us) / 1000u); uint32_t now = audio_now_ms(); if (now < target_ms) { usleep((target_ms - now) * 1000u); } } else if (app.audio.is_audio) { usleep(10000); /* ~10 ms polling interval for audio */ } else { usleep(16000); } } if (app.audio.is_audio && app.audio.audio_inited) { eyn_sys_audio_stop(); } (void)gui_set_continuous_redraw(app.handle, 0); if (app.viewbuf) free(app.viewbuf); reiv_close(&app.video); rei_free(&app.image); audio_free(&app.audio); return 0; }