Files
gw-basic-2026/src/graphics.c
Eremey Valetov 0bacfcef6c Implement VIEW/WINDOW/PALETTE, PMAP, fix MBF float format, update to v0.13.0
Graphics viewport and coordinate mapping:
- VIEW [[SCREEN] (x1,y1)-(x2,y2) [,[fill][,border]]] with clipping
- WINDOW [[SCREEN] (x1,y1)-(x2,y2)] with Cartesian/screen modes
- PALETTE [attribute, color] with CGA 16-color remapping
- PMAP(coord, func) for logical/physical coordinate conversion
- All graphics statements (PSET, LINE, CIRCLE, PAINT, GET/PUT) respect
  viewport clipping and WINDOW coordinate mapping

MBF (Microsoft Binary Format) float support:
- CVS/CVD now interpret bytes as MBF format (compatible with real GW-BASIC)
- MKS$/MKD$ now produce MBF-encoded bytes
- Fixed shift errors in MBF↔IEEE conversion routines (single: 1→0, double: 4→3)
- Random-access file I/O now byte-compatible with original GWBASIC.EXE

66 tests (2 new), 61 compat matches (up from 58).
2026-03-10 22:20:58 -04:00

845 lines
24 KiB
C

#include "graphics.h"
#include "hal.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <ctype.h>
static uint8_t *framebuf;
static int fb_width, fb_height;
static int screen_mode;
static int current_color = 1;
static int last_x, last_y;
/* CGA default palette (RGBI) */
static const uint32_t default_palette[16] = {
0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF,
};
/* Active palette: palette_map[attr] gives the physical color index */
static int palette_map[16];
static uint32_t palette[16];
/* Viewport state (VIEW) */
static bool vp_active;
static bool vp_screen; /* VIEW SCREEN flag */
static int vp_x1, vp_y1, vp_x2, vp_y2;
/* Window state (WINDOW) */
static bool win_active;
static bool win_screen; /* WINDOW SCREEN flag */
static double win_x1, win_y1, win_x2, win_y2;
static void reset_palette(void)
{
for (int i = 0; i < 16; i++) {
palette_map[i] = i;
palette[i] = default_palette[i];
}
}
bool gfx_active(void) { return framebuf != NULL; }
int gfx_get_mode(void) { return screen_mode; }
void gfx_set_color(int c) { current_color = c; }
int gfx_get_color(void) { return current_color; }
void gfx_get_last(int *x, int *y) { *x = last_x; *y = last_y; }
void gfx_set_last(int x, int y) { last_x = x; last_y = y; }
int gfx_get_width(void) { return fb_width; }
int gfx_get_height(void) { return fb_height; }
void gfx_init(int mode)
{
gfx_shutdown();
screen_mode = mode;
switch (mode) {
case 1: fb_width = 320; fb_height = 200; break;
case 2: fb_width = 640; fb_height = 200; break;
default: return; /* text mode, no framebuffer */
}
framebuf = calloc(fb_width * fb_height, 1);
current_color = (mode == 2) ? 1 : 3;
last_x = 0;
last_y = 0;
vp_active = false;
vp_x1 = 0; vp_y1 = 0;
vp_x2 = fb_width - 1; vp_y2 = fb_height - 1;
win_active = false;
reset_palette();
}
void gfx_shutdown(void)
{
free(framebuf);
framebuf = NULL;
fb_width = 0;
fb_height = 0;
screen_mode = 0;
vp_active = false;
win_active = false;
reset_palette();
}
void gfx_cls(void)
{
if (!framebuf) return;
if (vp_active) {
for (int y = vp_y1; y <= vp_y2; y++)
for (int x = vp_x1; x <= vp_x2; x++)
framebuf[y * fb_width + x] = 0;
} else {
memset(framebuf, 0, fb_width * fb_height);
}
}
/* Clip bounds: viewport when active, otherwise full framebuffer */
static inline void clip_bounds(int *cx1, int *cy1, int *cx2, int *cy2)
{
if (vp_active) {
*cx1 = vp_x1; *cy1 = vp_y1;
*cx2 = vp_x2; *cy2 = vp_y2;
} else {
*cx1 = 0; *cy1 = 0;
*cx2 = fb_width - 1; *cy2 = fb_height - 1;
}
}
static inline void set_pixel(int x, int y, int color)
{
int cx1, cy1, cx2, cy2;
clip_bounds(&cx1, &cy1, &cx2, &cy2);
if (x >= cx1 && x <= cx2 && y >= cy1 && y <= cy2)
framebuf[y * fb_width + x] = color;
}
static inline int get_pixel(int x, int y)
{
if (x >= 0 && x < fb_width && y >= 0 && y < fb_height)
return framebuf[y * fb_width + x];
return 0;
}
void gfx_pset(int x, int y, int color)
{
set_pixel(x, y, color);
last_x = x;
last_y = y;
}
int gfx_point(int x, int y)
{
return get_pixel(x, y);
}
/* Bresenham line */
static void draw_line(int x0, int y0, int x1, int y1, int color)
{
int dx = abs(x1 - x0);
int dy = -abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
for (;;) {
set_pixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
void gfx_line(int x1, int y1, int x2, int y2, int color, int style)
{
if (style == GFX_BOXF) {
/* Filled box */
int xlo = x1 < x2 ? x1 : x2;
int xhi = x1 > x2 ? x1 : x2;
int ylo = y1 < y2 ? y1 : y2;
int yhi = y1 > y2 ? y1 : y2;
for (int y = ylo; y <= yhi; y++)
for (int x = xlo; x <= xhi; x++)
set_pixel(x, y, color);
} else if (style == GFX_BOX) {
/* Box outline */
draw_line(x1, y1, x2, y1, color);
draw_line(x2, y1, x2, y2, color);
draw_line(x2, y2, x1, y2, color);
draw_line(x1, y2, x1, y1, color);
} else {
draw_line(x1, y1, x2, y2, color);
}
last_x = x2;
last_y = y2;
}
/* Midpoint circle with aspect ratio */
void gfx_circle(int cx, int cy, int r, int color,
double start, double end, double aspect)
{
if (r <= 0) return;
if (start == 0.0 && end == 0.0) {
/* Full circle */
double ax = (aspect > 0) ? aspect : (5.0 / 6.0); /* CGA aspect */
int x = 0, y = r;
int d = 1 - r;
while (x <= y) {
int px, py;
/* 8 octants with aspect adjustment */
px = (int)(x * ax); py = y;
set_pixel(cx + px, cy + py, color);
set_pixel(cx - px, cy + py, color);
set_pixel(cx + px, cy - py, color);
set_pixel(cx - px, cy - py, color);
px = (int)(y * ax); py = x;
set_pixel(cx + px, cy + py, color);
set_pixel(cx - px, cy + py, color);
set_pixel(cx + px, cy - py, color);
set_pixel(cx - px, cy - py, color);
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
}
x++;
}
} else {
/* Arc: parametric with aspect */
double ax = (aspect > 0) ? aspect : (5.0 / 6.0);
double step = 1.0 / r;
if (step > 0.05) step = 0.05;
for (double a = start; a <= end; a += step) {
int px = cx + (int)(r * cos(a) * ax);
int py = cy - (int)(r * sin(a));
set_pixel(px, py, color);
}
}
last_x = cx;
last_y = cy;
}
/* Flood fill (stack-based) */
void gfx_paint(int x, int y, int fill_color, int border_color)
{
if (!framebuf) return;
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
int start = get_pixel(x, y);
if (start == fill_color || start == border_color) return;
/* Simple stack-based flood fill */
int stack_cap = 4096;
int *stack = malloc(stack_cap * 2 * sizeof(int));
if (!stack) return;
int sp = 0;
#define PUSH(px, py) do { \
if (sp < stack_cap) { stack[sp*2] = (px); stack[sp*2+1] = (py); sp++; } \
} while(0)
#define POP(px, py) do { sp--; (px) = stack[sp*2]; (py) = stack[sp*2+1]; } while(0)
PUSH(x, y);
while (sp > 0) {
int cx, cy;
POP(cx, cy);
if (cx < 0 || cx >= fb_width || cy < 0 || cy >= fb_height) continue;
int c = get_pixel(cx, cy);
if (c == fill_color || c == border_color) continue;
set_pixel(cx, cy, fill_color);
/* Grow stack if needed */
if (sp + 4 >= stack_cap) {
stack_cap *= 2;
int *ns = realloc(stack, stack_cap * 2 * sizeof(int));
if (!ns) break;
stack = ns;
}
PUSH(cx + 1, cy);
PUSH(cx - 1, cy);
PUSH(cx, cy + 1);
PUSH(cx, cy - 1);
}
#undef PUSH
#undef POP
free(stack);
}
/* DRAW mini-language parser */
void gfx_draw(const char *cmd)
{
if (!framebuf) return;
int x = last_x, y = last_y;
int draw_color = current_color;
int scale = 4; /* default scale factor (C4) */
const char *p = cmd;
while (*p) {
while (*p == ' ') p++;
if (!*p) break;
int blind = 0, no_update = 0;
/* Prefix modifiers */
while (*p == 'B' || *p == 'b' || *p == 'N' || *p == 'n') {
if (*p == 'B' || *p == 'b') blind = 1;
if (*p == 'N' || *p == 'n') no_update = 1;
p++;
}
char ch = toupper(*p);
if (!ch) break;
p++;
/* Parse optional numeric argument */
int has_arg = 0, arg = 0;
if (*p == '-' || isdigit((unsigned char)*p)) {
int neg = 0;
if (*p == '-') { neg = 1; p++; }
while (isdigit((unsigned char)*p)) {
arg = arg * 10 + (*p - '0');
p++;
}
if (neg) arg = -arg;
has_arg = 1;
}
int dist = has_arg ? arg : scale;
int nx = x, ny = y;
switch (ch) {
case 'U': ny = y - dist; break;
case 'D': ny = y + dist; break;
case 'L': nx = x - dist; break;
case 'R': nx = x + dist; break;
case 'E': nx = x + dist; ny = y - dist; break;
case 'F': nx = x + dist; ny = y + dist; break;
case 'G': nx = x - dist; ny = y + dist; break;
case 'H': nx = x - dist; ny = y - dist; break;
case 'M': {
/* M x,y or M +/-x, +/-y */
int relative = 0;
while (*p == ' ') p++;
if (*p == '+' || *p == '-') relative = 1;
int mx = 0, my = 0;
int mneg = 0;
if (*p == '-') { mneg = 1; p++; }
else if (*p == '+') p++;
while (isdigit((unsigned char)*p)) { mx = mx * 10 + (*p - '0'); p++; }
if (mneg) mx = -mx;
while (*p == ' ' || *p == ',') p++;
mneg = 0;
if (*p == '-') { mneg = 1; p++; }
else if (*p == '+') p++;
while (isdigit((unsigned char)*p)) { my = my * 10 + (*p - '0'); p++; }
if (mneg) my = -my;
if (relative) { nx = x + mx; ny = y + my; }
else { nx = mx; ny = my; }
break;
}
case 'C':
draw_color = has_arg ? arg : 1;
continue;
case 'S':
scale = has_arg ? arg : 4;
continue;
case 'A':
/* Angle: 0-3, we ignore rotation for simplicity */
continue;
case ';':
continue;
default:
continue;
}
if (!blind)
draw_line(x, y, nx, ny, draw_color);
if (!no_update) {
x = nx;
y = ny;
}
while (*p == ' ' || *p == ';') p++;
}
last_x = x;
last_y = y;
}
/* Sixel output encoder */
void gfx_flush(void)
{
if (!framebuf || !gw_hal) return;
/* Determine which colors are used */
int color_used[16] = {0};
for (int i = 0; i < fb_width * fb_height; i++)
color_used[framebuf[i] & 0x0F] = 1;
/* Build Sixel output in a buffer */
int buf_cap = 1024 * 64;
char *buf = malloc(buf_cap);
if (!buf) return;
int pos = 0;
#define EMIT(s) do { \
int _l = strlen(s); \
if (pos + _l >= buf_cap) { \
buf_cap *= 2; \
char *nb = realloc(buf, buf_cap); \
if (!nb) { free(buf); return; } \
buf = nb; \
} \
memcpy(buf + pos, s, _l); pos += _l; \
} while(0)
/* DCS q - enter Sixel mode */
EMIT("\033Pq");
/* Define palette (using palette mapping) */
for (int c = 0; c < 16; c++) {
if (!color_used[c]) continue;
uint32_t rgb = default_palette[palette_map[c]];
int r = ((rgb >> 16) & 0xFF) * 100 / 255;
int g = ((rgb >> 8) & 0xFF) * 100 / 255;
int b = (rgb & 0xFF) * 100 / 255;
char pal[32];
snprintf(pal, sizeof(pal), "#%d;2;%d;%d;%d", c, r, g, b);
EMIT(pal);
}
/* Output Sixel bands (6 rows each) */
for (int band = 0; band < fb_height; band += 6) {
int band_h = (band + 6 <= fb_height) ? 6 : fb_height - band;
for (int c = 0; c < 16; c++) {
if (!color_used[c]) continue;
/* Check if this color appears in this band */
int has_color = 0;
for (int y2 = band; y2 < band + band_h && !has_color; y2++)
for (int x2 = 0; x2 < fb_width && !has_color; x2++)
if (framebuf[y2 * fb_width + x2] == c)
has_color = 1;
if (!has_color) continue;
/* Emit color selector */
char cs[8];
snprintf(cs, sizeof(cs), "#%d", c);
EMIT(cs);
/* Emit Sixel data for this color in this band */
/* Use RLE for runs of same byte */
int prev_sixel = -1;
int run_len = 0;
for (int x2 = 0; x2 < fb_width; x2++) {
int sixel = 0;
for (int bit = 0; bit < band_h; bit++) {
int y2 = band + bit;
if (framebuf[y2 * fb_width + x2] == c)
sixel |= (1 << bit);
}
if (sixel == prev_sixel) {
run_len++;
} else {
/* Flush previous run */
if (prev_sixel >= 0) {
char sc[16];
if (run_len > 3) {
snprintf(sc, sizeof(sc), "!%d%c", run_len, prev_sixel + 63);
} else {
for (int r2 = 0; r2 < run_len; r2++) {
sc[0] = prev_sixel + 63;
sc[1] = '\0';
EMIT(sc);
}
sc[0] = '\0';
}
EMIT(sc);
}
prev_sixel = sixel;
run_len = 1;
}
}
/* Flush last run */
if (prev_sixel >= 0) {
char sc[16];
if (run_len > 3) {
snprintf(sc, sizeof(sc), "!%d%c", run_len, prev_sixel + 63);
} else {
for (int r2 = 0; r2 < run_len; r2++) {
sc[0] = prev_sixel + 63;
sc[1] = '\0';
EMIT(sc);
}
sc[0] = '\0';
}
EMIT(sc);
}
/* GNL ($/- selectors): $ = CR (reuse row), - = newline */
/* If more colors in this band, use $; else use - for next band */
/* Check if there's another color to render in this band */
int more = 0;
for (int nc = c + 1; nc < 16; nc++) {
if (!color_used[nc]) continue;
for (int y2 = band; y2 < band + band_h && !more; y2++)
for (int x2 = 0; x2 < fb_width && !more; x2++)
if (framebuf[y2 * fb_width + x2] == nc)
more = 1;
if (more) break;
}
if (more) {
EMIT("$"); /* CR - overlay next color on same band */
}
}
EMIT("-"); /* LF - advance to next band */
}
/* ST - string terminator */
EMIT("\033\\");
#undef EMIT
/* Write out via HAL raw output */
gw_hal->write_raw(buf, pos);
free(buf);
}
/* ================================================================
* VIEW / WINDOW / PALETTE
* ================================================================ */
void gfx_view(bool screen_flag, int x1, int y1, int x2, int y2,
int fill, bool has_fill, int border, bool has_border)
{
if (!framebuf) return;
/* Normalize */
if (x1 > x2) { int t = x1; x1 = x2; x2 = t; }
if (y1 > y2) { int t = y1; y1 = y2; y2 = t; }
/* Clamp to screen */
if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0;
if (x2 >= fb_width) x2 = fb_width - 1;
if (y2 >= fb_height) y2 = fb_height - 1;
vp_active = true;
vp_screen = screen_flag;
vp_x1 = x1; vp_y1 = y1;
vp_x2 = x2; vp_y2 = y2;
/* Draw border (outside viewport, on full framebuffer) */
if (has_border) {
bool save = vp_active;
vp_active = false;
if (x1 > 0) draw_line(x1 - 1, y1 > 0 ? y1 - 1 : y1, x1 - 1, y2 < fb_height - 1 ? y2 + 1 : y2, border);
if (x2 < fb_width - 1) draw_line(x2 + 1, y1 > 0 ? y1 - 1 : y1, x2 + 1, y2 < fb_height - 1 ? y2 + 1 : y2, border);
if (y1 > 0) draw_line(x1 > 0 ? x1 - 1 : x1, y1 - 1, x2 < fb_width - 1 ? x2 + 1 : x2, y1 - 1, border);
if (y2 < fb_height - 1) draw_line(x1 > 0 ? x1 - 1 : x1, y2 + 1, x2 < fb_width - 1 ? x2 + 1 : x2, y2 + 1, border);
vp_active = save;
}
/* Fill viewport */
if (has_fill) {
for (int y = vp_y1; y <= vp_y2; y++)
for (int x = vp_x1; x <= vp_x2; x++)
framebuf[y * fb_width + x] = fill;
}
/* Reset last position to viewport origin */
last_x = vp_x1;
last_y = vp_y1;
/* Reset WINDOW when VIEW changes */
win_active = false;
}
void gfx_view_reset(void)
{
vp_active = false;
if (framebuf) {
vp_x1 = 0; vp_y1 = 0;
vp_x2 = fb_width - 1; vp_y2 = fb_height - 1;
}
win_active = false;
last_x = 0;
last_y = 0;
}
void gfx_window(bool screen_flag, double x1, double y1, double x2, double y2)
{
win_active = true;
win_screen = screen_flag;
win_x1 = x1; win_y1 = y1;
win_x2 = x2; win_y2 = y2;
}
void gfx_window_reset(void)
{
win_active = false;
}
bool gfx_has_window(void)
{
return win_active;
}
int gfx_map_x(double x)
{
int left = vp_active ? vp_x1 : 0;
int right = vp_active ? vp_x2 : (fb_width - 1);
if (win_active) {
if (win_x2 == win_x1) return left;
return left + (int)((x - win_x1) / (win_x2 - win_x1) * (right - left) + 0.5);
}
if (vp_active && !vp_screen)
return left + (int)x;
return (int)x;
}
int gfx_map_y(double y)
{
int top = vp_active ? vp_y1 : 0;
int bottom = vp_active ? vp_y2 : (fb_height - 1);
if (win_active) {
if (win_y2 == win_y1) return top;
if (win_screen) {
/* WINDOW SCREEN: Y increases downward */
return top + (int)((y - win_y1) / (win_y2 - win_y1) * (bottom - top) + 0.5);
} else {
/* WINDOW (Cartesian): Y increases upward */
return bottom - (int)((y - win_y1) / (win_y2 - win_y1) * (bottom - top) + 0.5);
}
}
if (vp_active && !vp_screen)
return top + (int)y;
return (int)y;
}
double gfx_pmap(double coord, int func)
{
int left = vp_active ? vp_x1 : 0;
int right = vp_active ? vp_x2 : (fb_width - 1);
int top = vp_active ? vp_y1 : 0;
int bottom = vp_active ? vp_y2 : (fb_height - 1);
if (!win_active) return coord;
switch (func) {
case 0: /* logical X → physical X */
if (win_x2 == win_x1) return left;
return left + (coord - win_x1) / (win_x2 - win_x1) * (right - left);
case 1: /* logical Y → physical Y */
if (win_y2 == win_y1) return top;
if (win_screen)
return top + (coord - win_y1) / (win_y2 - win_y1) * (bottom - top);
else
return bottom - (coord - win_y1) / (win_y2 - win_y1) * (bottom - top);
case 2: /* physical X → logical X */
if (right == left) return win_x1;
return win_x1 + (coord - left) / (double)(right - left) * (win_x2 - win_x1);
case 3: /* physical Y → logical Y */
if (bottom == top) return win_y1;
if (win_screen)
return win_y1 + (coord - top) / (double)(bottom - top) * (win_y2 - win_y1);
else
return win_y1 + (bottom - coord) / (double)(bottom - top) * (win_y2 - win_y1);
default:
return coord;
}
}
void gfx_palette_set(int attr, int color)
{
if (attr < 0 || attr > 15 || color < 0 || color > 15) return;
palette_map[attr] = color;
palette[attr] = default_palette[color];
}
void gfx_palette_reset(void)
{
reset_palette();
}
/*
* CGA framebuffer PEEK/POKE.
*
* CGA interlaced layout:
* Even rows: offset 0x0000, odd rows: offset 0x2000
* Each row = 80 bytes
* SCREEN 1: 2 bpp (4 pixels/byte, MSB first)
* SCREEN 2: 1 bpp (8 pixels/byte, MSB first)
*/
static void cga_offset_to_pixels(uint16_t offset, int *row, int *col_byte)
{
int bank = (offset >= 0x2000) ? 1 : 0;
uint16_t linear = offset - bank * 0x2000;
*row = (linear / 80) * 2 + bank;
*col_byte = linear % 80;
}
uint8_t gfx_cga_peek(uint16_t offset)
{
if (!framebuf) return 0;
int row, col_byte;
cga_offset_to_pixels(offset, &row, &col_byte);
if (row < 0 || row >= fb_height) return 0;
int bpp = (screen_mode == 2) ? 1 : 2;
int ppb = 8 / bpp;
int base_x = col_byte * ppb;
uint8_t val = 0;
for (int i = 0; i < ppb; i++) {
int x = base_x + i;
if (x >= fb_width) break;
int px = get_pixel(x, row) & ((1 << bpp) - 1);
val |= px << (8 - bpp - i * bpp);
}
return val;
}
void gfx_cga_poke(uint16_t offset, uint8_t val)
{
if (!framebuf) return;
int row, col_byte;
cga_offset_to_pixels(offset, &row, &col_byte);
if (row < 0 || row >= fb_height) return;
int bpp = (screen_mode == 2) ? 1 : 2;
int ppb = 8 / bpp;
int base_x = col_byte * ppb;
int mask = (1 << bpp) - 1;
for (int i = 0; i < ppb; i++) {
int x = base_x + i;
if (x >= fb_width) break;
int px = (val >> (8 - bpp - i * bpp)) & mask;
set_pixel(x, row, px);
}
}
/*
* GET/PUT sprite support.
*
* Sprites are stored in integer arrays using the CGA packed format:
* Word 0: width in bits (pixel_width * bits_per_pixel)
* Word 1: height in scan lines
* Remaining: packed pixel data, row by row, padded to word boundary
*
* SCREEN 1: 2 bits/pixel (4 pixels/byte)
* SCREEN 2: 1 bit/pixel (8 pixels/byte)
*/
static int bits_per_pixel(void)
{
return (screen_mode == 2) ? 1 : 2;
}
int gfx_sprite_size(int x1, int y1, int x2, int y2)
{
if (!framebuf) return 0;
int w = abs(x2 - x1) + 1;
int h = abs(y2 - y1) + 1;
int bpp = bits_per_pixel();
int bytes_per_row = (w * bpp + 7) / 8;
if (bytes_per_row & 1) bytes_per_row++; /* pad to word boundary */
int data_bytes = bytes_per_row * h;
return 2 + (data_bytes + 1) / 2; /* 2 header words + data words */
}
int gfx_sprite_get(int x1, int y1, int x2, int y2, int16_t *buf, int bufsize)
{
if (!framebuf) return 0;
/* Normalize coordinates */
if (x1 > x2) { int t = x1; x1 = x2; x2 = t; }
if (y1 > y2) { int t = y1; y1 = y2; y2 = t; }
int w = x2 - x1 + 1;
int h = y2 - y1 + 1;
int bpp = bits_per_pixel();
int width_bits = w * bpp;
int bytes_per_row = (width_bits + 7) / 8;
if (bytes_per_row & 1) bytes_per_row++; /* word-align */
int data_bytes = bytes_per_row * h;
int required = 2 + (data_bytes + 1) / 2;
if (bufsize < required) return required;
buf[0] = (int16_t)width_bits;
buf[1] = (int16_t)h;
/* Pack pixels into CGA format */
uint8_t *out = (uint8_t *)&buf[2];
memset(out, 0, data_bytes);
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
int px = get_pixel(x1 + col, y1 + row);
int bit_offset = col * bpp;
int byte_idx = bit_offset / 8;
int bit_pos = 8 - bpp - (bit_offset % 8); /* MSB first */
out[row * bytes_per_row + byte_idx] |=
(px & ((1 << bpp) - 1)) << bit_pos;
}
}
return required;
}
void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action)
{
if (!framebuf || bufsize < 2) return;
int bpp = bits_per_pixel();
int width_bits = (uint16_t)buf[0];
int h = (uint16_t)buf[1];
int w = width_bits / bpp;
if (w <= 0 || h <= 0) return;
int bytes_per_row = (width_bits + 7) / 8;
if (bytes_per_row & 1) bytes_per_row++;
const uint8_t *src = (const uint8_t *)&buf[2];
int mask = (1 << bpp) - 1;
for (int row = 0; row < h; row++) {
int sy = y + row;
if (sy < 0 || sy >= fb_height) continue;
for (int col = 0; col < w; col++) {
int sx = x + col;
if (sx < 0 || sx >= fb_width) continue;
int bit_offset = col * bpp;
int byte_idx = bit_offset / 8;
int bit_pos = 8 - bpp - (bit_offset % 8);
int sprite_px = (src[row * bytes_per_row + byte_idx] >> bit_pos) & mask;
int old_px = get_pixel(sx, sy);
int new_px;
switch (action) {
case GFX_ACTION_PSET: new_px = sprite_px; break;
case GFX_ACTION_PRESET: new_px = (~sprite_px) & mask; break;
case GFX_ACTION_AND: new_px = old_px & sprite_px; break;
case GFX_ACTION_OR: new_px = old_px | sprite_px; break;
default: new_px = old_px ^ sprite_px; break; /* XOR */
}
set_pixel(sx, sy, new_px);
}
}
}