Files
cell-sdl3/surface.c
2026-01-23 10:22:11 -06:00

1111 lines
35 KiB
C

#include "cell.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_surface.h>
#include <string.h>
#include <stdint.h>
#include "sdl.h"
// Stub implementations for stb_dxt functions
#define STB_DXT_HIGHQUAL 1
#define STB_DXT_NORMAL 0
void stb_compress_dxt_block(unsigned char *dest, const unsigned char *src, int alpha, int mode) {
// Stub implementation - just fill with zeros
memset(dest, 0, alpha ? 16 : 8);
}
void stb_compress_bc4_block(unsigned char *dest, const unsigned char *src) {
// Stub implementation - just fill with zeros
memset(dest, 0, 8);
}
void stb_compress_bc5_block(unsigned char *dest, const unsigned char *src) {
// Stub implementation - just fill with zeros
memset(dest, 0, 16);
}
irect js2irect(JSContext *js, JSValue v)
{
if (JS_IsNull(v)) return (irect){0,0,1,1};
irect rect;
JS_GETATOM(js,rect.x,v,x,number)
JS_GETATOM(js,rect.y,v,y,number)
JS_GETATOM(js,rect.w,v,width,number)
JS_GETATOM(js,rect.h,v,height,number)
float anchor_x, anchor_y;
JS_GETATOM(js, anchor_x, v, anchor_x, number)
JS_GETATOM(js, anchor_y, v, anchor_y, number)
rect.y -= anchor_y*rect.h;
rect.x -= anchor_x*rect.w;
return rect;
}
JSValue pixelformat2js(JSContext *js, SDL_PixelFormat fmt)
{
return SDL_PixelFormat2js(js, fmt);
}
SDL_PixelFormat js2pixelformat(JSContext *js, JSValue v)
{
return js2SDL_PixelFormat(js, v);
}
typedef struct { const char *name; SDL_ScaleMode mode; } scale_entry;
static const scale_entry k_scale_table[] = {
{ "nearest", SDL_SCALEMODE_NEAREST },
{ "linear", SDL_SCALEMODE_LINEAR },
{ NULL, SDL_SCALEMODE_LINEAR } /* fallback */
};
static JSValue scalemode2js(JSContext *js, SDL_ScaleMode mode){
const scale_entry *it;
for(it = k_scale_table; it->name; ++it)
if(it->mode == mode) break;
return JS_NewString(js, it->name ? it->name : "nearest");
}
SDL_ScaleMode js2SDL_ScaleMode(JSContext *js, JSValue v){
if(JS_IsNull(v)) return SDL_SCALEMODE_NEAREST;
const char *s = JS_ToCString(js, v);
if(!s) return SDL_SCALEMODE_NEAREST;
const scale_entry *it;
for(it = k_scale_table; it->name; ++it)
if(!strcmp(it->name, s)) break;
JS_FreeCString(js, s);
return it->mode;
}
// SDL_Surface free function
void SDL_Surface_free(JSRuntime *rt, SDL_Surface *s) {
if (s->flags & SDL_SURFACE_PREALLOCATED)
free(s->pixels);
SDL_DestroySurface(s);
}
// Class definition
QJSCLASS(SDL_Surface,)
JSValue make_surface(JSContext *js, SDL_Surface *s){
JSValue ret = SDL_Surface2js(js,s);
JS_SetPropertyStr(js, ret, "width", JS_NewInt32(js, s->w));
JS_SetPropertyStr(js, ret, "height", JS_NewInt32(js, s->h));
JS_SetPropertyStr(js, ret, "format", pixelformat2js(js, s->format));
JS_SetPropertyStr(js, ret, "pitch", JS_NewFloat64(js, s->pitch));
return JS_Stone(js, ret);
}
// SDL_Surface methods
JSC_CCALL(surface_blit,
SDL_Surface *dst = js2SDL_Surface(js, self);
irect dr = {0}, *pdr = NULL;
if(!JS_IsNull(argv[0])){ dr = js2irect(js, argv[0]); pdr = &dr; }
SDL_Surface *src = js2SDL_Surface(js, argv[1]);
if (!src)
return JS_ThrowReferenceError(js, "Argument must be a surface.");
irect sr = {0}, *psr = NULL;
if(!JS_IsNull(argv[2])){ sr = js2irect(js, argv[2]); psr = &sr; }
SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[3]);
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode);
)
JSC_CCALL(surface_scale,
SDL_Surface *src = js2SDL_Surface(js,self);
vec2 wh = js2vec2(js,argv[0]);
SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[1]);
SDL_Surface *new = SDL_ScaleSurface(src, wh.x, wh.y, mode);
ret = SDL_Surface2js(js,new);
)
static SDL_PixelFormatDetails pdetails = {
.format = SDL_PIXELFORMAT_RGBA8888, // Standard RGBA8888 format
.bits_per_pixel = 32, // 8 bits per channel, 4 channels = 32 bits
.bytes_per_pixel = 4, // 4 bytes per pixel
.padding = {0, 0}, // Unused padding
.Rmask = 0xFF000000, // Red mask
.Gmask = 0x00FF0000, // Green mask
.Bmask = 0x0000FF00, // Blue mask
.Amask = 0x000000FF, // Alpha mask
.Rbits = 8, // 8 bits for Red
.Gbits = 8, // 8 bits for Green
.Bbits = 8, // 8 bits for Blue
.Abits = 8, // 8 bits for Alpha
.Rshift = 24, // Red shift
.Gshift = 16, // Green shift
.Bshift = 8, // Blue shift
.Ashift = 0 // Alpha shift
};
JSC_CCALL(surface_fill,
SDL_Surface *src = js2SDL_Surface(js,self);
colorf color = js2color(js,argv[0]);
rect r = {
.x = 0,
.y = 0,
.w = src->w,
.h = src->h
};
SDL_FillSurfaceRect(src, &r, SDL_MapRGBA(&pdetails, NULL, color.x*255,color.y*255,color.z*255,color.w*255));
)
JSC_CCALL(surface_rect,
SDL_Surface *dst = js2SDL_Surface(js,self);
rect r = js2rect(js,argv[0]);
colorf color = js2color(js,argv[1]);
SDL_FillSurfaceRect(dst,&r,SDL_MapRGBA(&pdetails,NULL, color.x*255,color.y*255,color.z*255,color.w*255));
)
JSC_CCALL(surface_convert,
SDL_Surface *surf = js2SDL_Surface(js,self);
SDL_PixelFormat fmt = js2pixelformat(js, argv[0]);
SDL_Surface *dst = SDL_ConvertSurface(surf, fmt);
if (!dst) return JS_ThrowInternalError(js, "Convert failed: %s", SDL_GetError());
return SDL_Surface2js(js, dst);
)
JSC_CCALL(surface_dup,
SDL_Surface *surf = js2SDL_Surface(js,self);
SDL_Surface *conv = SDL_DuplicateSurface(surf);
if (!conv)
return JS_ThrowReferenceError(js, "could not blit to dup'd surface: %s", SDL_GetError());
return SDL_Surface2js(js,conv);
)
JSC_CCALL(surface_pixels,
SDL_Surface *surf = js2SDL_Surface(js, self);
int locked = 0;
if (SDL_MUSTLOCK(surf))
if (SDL_LockSurface(surf) < 0)
return JS_ThrowReferenceError(js, "Lock surface failed: %s", SDL_GetError());
size_t byte_size;
if (SDL_ISPIXELFORMAT_FOURCC(surf->format)) {
/* Planar/YUV formats: use BitsPerPixel to compute true size */
printf("FOURCC!!! Bits is %d\n", SDL_BYTESPERPIXEL(surf->format));
byte_size = (size_t)surf->pitch * surf->h * SDL_BYTESPERPIXEL(surf->format);
} else
byte_size = (size_t)surf->pitch * surf->h;
ret = js_new_blob_stoned_copy(js, surf->pixels, byte_size);
if (locked) SDL_UnlockSurface(surf);
)
JSValue js_surface_get_width(JSContext *js, JSValue self) {
SDL_Surface *s = js2SDL_Surface(js,self);
return JS_NewFloat64(js, s->w);
}
JSValue js_surface_get_height(JSContext *js, JSValue self) {
SDL_Surface *s = js2SDL_Surface(js,self);
return JS_NewFloat64(js, s->h);
}
JSValue js_surface_get_format(JSContext *js, JSValue self) {
SDL_Surface *s = js2SDL_Surface(js,self);
return pixelformat2js(js, s->format);
}
JSValue js_surface_get_pitch(JSContext *js, JSValue self) {
SDL_Surface *s = js2SDL_Surface(js,self);
return JS_NewFloat64(js, s->pitch);
}
JSC_CCALL(surface_toJSON,
SDL_Surface *surf = js2SDL_Surface(js,self);
// Create the result object
JSValue obj = JS_NewObject(js);
// Add width and height
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h));
// Add format
JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format));
// Add pitch
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch));
// Lock surface if needed
int locked = 0;
if (SDL_MUSTLOCK(surf)) {
if (SDL_LockSurface(surf) < 0) {
JS_FreeValue(js, obj);
return JS_ThrowInternalError(js, "Lock surface failed: %s", SDL_GetError());
}
locked = 1;
}
// Add pixels as ArrayBuffer
size_t byte_size = surf->pitch * surf->h;
JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size);
JS_SetPropertyStr(js, obj, "pixels", pixels);
// Unlock if we locked
if (locked)
SDL_UnlockSurface(surf);
return obj;
)
// Check for integer overflow in size calculations
static int check_size_overflow(size_t a, size_t b, size_t c, size_t *result)
{
if (a > SIZE_MAX / b) return 1;
size_t temp = a * b;
if (temp > SIZE_MAX / c) return 1;
*result = temp * c;
return 0;
}
// Helper function for BC1/BC3 compression
static JSValue compress_bc_common(JSContext *js, JSValueConst *argv, int argc, int alpha_mode, const char *format_name)
{
if (argc < 1)
return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name);
// Check if width/height properties exist
JSValue width_val = JS_GetPropertyStr(js, argv[0], "width");
JSValue height_val = JS_GetPropertyStr(js, argv[0], "height");
if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "compress_%s requires width and height properties", format_name);
}
int width, height;
if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "width and height must be numbers");
}
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
if (width < 1 || height < 1)
return JS_ThrowRangeError(js, "width and height must be at least 1");
if (width % 4 != 0 || height % 4 != 0)
return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression");
// Get pixel format
JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
SDL_PixelFormat format = js2pixelformat(js, format_val);
JS_FreeValue(js, format_val);
if (format == SDL_PIXELFORMAT_UNKNOWN)
return JS_ThrowTypeError(js, "Invalid or missing pixel format");
// Get pixels
JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
size_t pixel_len;
void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
if (!pixel_data || pixel_data == -1) {
JS_FreeValue(js, pixels_val);
return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer");
}
// Validate buffer size
int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
size_t required_size;
if (check_size_overflow(width, height, bytes_per_pixel, &required_size)) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Image dimensions too large");
}
if (pixel_len < required_size) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)",
width, height, required_size, pixel_len);
}
// Get high quality mode (default true)
int high_quality = 1;
if (argc > 1) {
high_quality = JS_ToBool(js, argv[1]);
}
int mode = high_quality ? STB_DXT_HIGHQUAL : STB_DXT_NORMAL;
// Calculate output size with overflow check
int blocks_x = width / 4;
int blocks_y = height / 4;
int bytes_per_block = (alpha_mode == 0) ? 8 : 16; // BC1=8, BC3=16
size_t output_size;
if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Output dimensions too large");
}
// Allocate output buffer
unsigned char *output = malloc(output_size);
if (!output) {
JS_FreeValue(js, pixels_val);
return JS_ThrowOutOfMemory(js);
}
// Allocate RGBA conversion buffer
size_t rgba_size;
if (check_size_overflow(width, height, 4, &rgba_size)) {
free(output);
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "RGBA buffer size too large");
}
unsigned char *rgba_data = malloc(rgba_size);
if (!rgba_data) {
free(output);
JS_FreeValue(js, pixels_val);
return JS_ThrowOutOfMemory(js);
}
// Convert to RGBA using SDL
int convert_result = SDL_ConvertPixels(
width, height,
format, pixel_data, width * bytes_per_pixel,
SDL_PIXELFORMAT_RGBA32, rgba_data, width * 4
);
JS_FreeValue(js, pixels_val);
if (convert_result != 0) {
free(output);
free(rgba_data);
return JS_ThrowInternalError(js, "Failed to convert pixels: %s", SDL_GetError());
}
// Compress blocks
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
unsigned char block[64]; // 4x4 RGBA = 64 bytes
// Extract 4x4 block
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
int src_x = bx * 4 + x;
int src_y = by * 4 + y;
int src_idx = (src_y * width + src_x) * 4;
int dst_idx = (y * 4 + x) * 4;
block[dst_idx + 0] = rgba_data[src_idx + 0];
block[dst_idx + 1] = rgba_data[src_idx + 1];
block[dst_idx + 2] = rgba_data[src_idx + 2];
block[dst_idx + 3] = rgba_data[src_idx + 3];
}
}
// Compress block
int output_idx = (by * blocks_x + bx) * bytes_per_block;
stb_compress_dxt_block(output + output_idx, block, alpha_mode, mode);
}
}
free(rgba_data);
// Create result object
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, height));
JS_SetPropertyStr(js, result, "format", JS_NewString(js, format_name));
JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block));
JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size);
free(output); // Free the output buffer after copying to blob
JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
return result;
}
// BC1/DXT1 compression
JSC_CCALL(surface_compress_bc1,
return compress_bc_common(js, argv, argc, 0, "bc1");
)
// BC3/DXT5 compression
JSC_CCALL(surface_compress_bc3,
return compress_bc_common(js, argv, argc, 1, "bc3");
)
// Generic helper for BC4/BC5 channel compression
static JSValue compress_bc_channels(JSContext *js, JSValueConst *argv, int argc,
int num_channels, const char *format_name,
void (*compress_func)(unsigned char *dest, const unsigned char *src))
{
if (argc < 1)
return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name);
// Check if width/height properties exist
JSValue width_val = JS_GetPropertyStr(js, argv[0], "width");
JSValue height_val = JS_GetPropertyStr(js, argv[0], "height");
if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "compress_%s requires width and height properties", format_name);
}
int width, height;
if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return JS_ThrowTypeError(js, "width and height must be numbers");
}
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
if (width < 1 || height < 1)
return JS_ThrowRangeError(js, "width and height must be at least 1");
if (width % 4 != 0 || height % 4 != 0)
return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression");
// Get pixel format
JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
SDL_PixelFormat format = js2pixelformat(js, format_val);
JS_FreeValue(js, format_val);
if (format == SDL_PIXELFORMAT_UNKNOWN)
return JS_ThrowTypeError(js, "Invalid or missing pixel format");
// Get pixels
JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
size_t pixel_len;
void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
if (!pixel_data || pixel_data == -1) {
JS_FreeValue(js, pixels_val);
return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer");
}
// Validate buffer size
int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
if (bytes_per_pixel < num_channels) {
JS_FreeValue(js, pixels_val);
return JS_ThrowTypeError(js, "%s compression requires a format with at least %d channel(s)",
format_name, num_channels);
}
size_t required_size;
if (check_size_overflow(width, height, bytes_per_pixel, &required_size)) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Image dimensions too large");
}
if (pixel_len < required_size) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)",
width, height, required_size, pixel_len);
}
// Calculate output size with overflow check
int blocks_x = width / 4;
int blocks_y = height / 4;
int bytes_per_block = (num_channels == 1) ? 8 : 16; // BC4=8, BC5=16
size_t output_size;
if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) {
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Output dimensions too large");
}
// Allocate output buffer
unsigned char *output = malloc(output_size);
if (!output) {
JS_FreeValue(js, pixels_val);
return JS_ThrowOutOfMemory(js);
}
// Allocate channel extraction buffer
size_t channel_size;
if (check_size_overflow(width, height, num_channels, &channel_size)) {
free(output);
JS_FreeValue(js, pixels_val);
return JS_ThrowRangeError(js, "Channel buffer size too large");
}
unsigned char *channel_data = malloc(channel_size);
if (!channel_data) {
free(output);
JS_FreeValue(js, pixels_val);
return JS_ThrowOutOfMemory(js);
}
// Extract channels
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int src_idx = (y * width + x) * bytes_per_pixel;
int dst_idx = (y * width + x) * num_channels;
// Extract first 'num_channels' channels
for (int c = 0; c < num_channels; c++) {
channel_data[dst_idx + c] = ((unsigned char*)pixel_data)[src_idx + c];
}
}
}
JS_FreeValue(js, pixels_val);
// Compress blocks
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
unsigned char block[32]; // Max 4x4 * 2 channels = 32 bytes
// Extract 4x4 block
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
int src_x = bx * 4 + x;
int src_y = by * 4 + y;
int src_idx = (src_y * width + src_x) * num_channels;
int dst_idx = (y * 4 + x) * num_channels;
for (int c = 0; c < num_channels; c++) {
block[dst_idx + c] = channel_data[src_idx + c];
}
}
}
// Compress block
int output_idx = (by * blocks_x + bx) * bytes_per_block;
compress_func(output + output_idx, block);
}
}
free(channel_data);
// Create result object
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, height));
JS_SetPropertyStr(js, result, "format", JS_NewString(js, format_name));
JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block));
JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size);
free(output); // Free the output buffer after copying to blob
JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
return result;
}
// BC4 compression (single channel)
JSC_CCALL(surface_compress_bc4,
return compress_bc_channels(js, argv, argc, 1, "bc4", stb_compress_bc4_block);
)
// BC5 compression (two channels)
JSC_CCALL(surface_compress_bc5,
return compress_bc_channels(js, argv, argc, 2, "bc5", stb_compress_bc5_block);
)
// Constructor for SDL_Surface
JSC_CCALL(surface_constructor,
if (argc < 1)
return JS_ThrowTypeError(js, "Surface constructor requires an object argument");
// Get width and height
int width, height;
JS_GETATOM(js, width, argv[0], width, number)
JS_GETATOM(js, height, argv[0], height, number)
if (!width || !height)
return JS_ThrowTypeError(js, "Surface constructor requires width and height properties");
// Check for pixel format
SDL_PixelFormat format = SDL_PIXELFORMAT_RGBA32; // default
JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
if (!JS_IsNull(format_val)) {
format = js2pixelformat(js, format_val);
}
JS_FreeValue(js, format_val);
// Check for pixel data
JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
if (!JS_IsNull(pixels_val)) {
// Create surface from pixel data
size_t len;
void *raw = js_get_blob_data(js, &len, pixels_val);
if (raw == -1) {
JS_FreeValue(js, pixels_val);
return JS_EXCEPTION;
}
if (!raw) {
JS_FreeValue(js, pixels_val);
return JS_ThrowTypeError(js, "no data");
}
int pitch;
JSValue pitch_val = JS_GetPropertyStr(js, argv[0], "pitch");
if (!JS_IsNull(pitch_val)) {
pitch = js2number(js, pitch_val);
JS_FreeValue(js, pitch_val);
} else {
// Calculate pitch based on format
int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
pitch = width * bytes_per_pixel;
}
// Copy the pixel data
void *pixels_copy = malloc(len);
if (!pixels_copy) {
JS_FreeValue(js, pixels_val);
return JS_ThrowOutOfMemory(js);
}
memcpy(pixels_copy, raw, len);
SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch);
if (!surface) {
free(pixels_copy);
JS_FreeValue(js, pixels_val);
return JS_ThrowInternalError(js, "Failed to create surface from pixels: %s", SDL_GetError());
}
JS_FreeValue(js, pixels_val);
ret = SDL_Surface2js(js, surface);
} else {
// Create blank surface
SDL_Surface *surface = SDL_CreateSurface(width, height, format);
if (!surface)
return JS_ThrowInternalError(js, "Failed to create surface: %s", SDL_GetError());
ret = SDL_Surface2js(js, surface);
}
)
static const JSCFunctionListEntry js_SDL_Surface_funcs[] = {
MIST_FUNC_DEF(surface, blit, 4),
MIST_FUNC_DEF(surface, scale, 1),
MIST_FUNC_DEF(surface, fill,1),
MIST_FUNC_DEF(surface, rect,2),
MIST_FUNC_DEF(surface, dup, 0),
MIST_FUNC_DEF(surface, pixels, 0),
MIST_FUNC_DEF(surface, convert, 1),
MIST_FUNC_DEF(surface, toJSON, 0),
};
// Helper function to create SDL_Surface from image object
static SDL_Surface* image_to_surface(JSContext *js, JSValue img_obj)
{
// Get width and height
JSValue width_val = JS_GetPropertyStr(js, img_obj, "width");
JSValue height_val = JS_GetPropertyStr(js, img_obj, "height");
if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return NULL;
}
int width, height;
if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
return NULL;
}
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
// Get format
JSValue format_val = JS_GetPropertyStr(js, img_obj, "format");
SDL_PixelFormat format = js2pixelformat(js, format_val);
JS_FreeValue(js, format_val);
if (format == SDL_PIXELFORMAT_UNKNOWN)
format = SDL_PIXELFORMAT_RGBA32; // default
// Get pixels
JSValue pixels_val = JS_GetPropertyStr(js, img_obj, "pixels");
size_t pixel_len;
void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
if (pixel_data == -1) {
JS_FreeValue(js, pixels_val);
return NULL;
}
if (!pixel_data) {
JS_FreeValue(js, pixels_val);
return NULL;
}
// Get pitch (optional)
int pitch;
JSValue pitch_val = JS_GetPropertyStr(js, img_obj, "pitch");
if (!JS_IsNull(pitch_val)) {
pitch = js2number(js, pitch_val);
JS_FreeValue(js, pitch_val);
} else {
pitch = width * SDL_BYTESPERPIXEL(format);
}
// Create a copy of pixel data since SDL_Surface will own it
void *pixels_copy = malloc(pixel_len);
if (!pixels_copy) {
JS_FreeValue(js, pixels_val);
return NULL;
}
memcpy(pixels_copy, pixel_data, pixel_len);
JS_FreeValue(js, pixels_val);
SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch);
if (!surface) {
free(pixels_copy);
return NULL;
}
return surface;
}
// Helper function to convert SDL_Surface back to image object
static JSValue surface_to_image(JSContext *js, SDL_Surface *surf)
{
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h));
JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format));
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch));
// Lock surface if needed
int locked = 0;
if (SDL_MUSTLOCK(surf)) {
if (SDL_LockSurface(surf) < 0) {
JS_FreeValue(js, obj);
return JS_NULL;
}
locked = 1;
}
// Add pixels as stoned blob
size_t byte_size = surf->pitch * surf->h;
JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size);
JS_SetPropertyStr(js, obj, "pixels", pixels);
// Unlock if we locked
if (locked)
SDL_UnlockSurface(surf);
// Add depth and hdr properties for completeness
JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(surf->format)));
JS_SetPropertyStr(js, obj, "hdr", JS_FALSE);
return obj;
}
// Scale function for image objects
JSC_CCALL(surface_scale_img,
if (argc < 2)
return JS_ThrowTypeError(js, "scale requires image and options objects");
SDL_Surface *src = image_to_surface(js, argv[0]);
if (!src)
return JS_ThrowTypeError(js, "First argument must be a valid image object");
// Get width and height from options
JSValue width_val = JS_GetPropertyStr(js, argv[1], "width");
JSValue height_val = JS_GetPropertyStr(js, argv[1], "height");
int new_width = src->w, new_height = src->h;
if (!JS_IsNull(width_val))
JS_ToInt32(js, &new_width, width_val);
if (!JS_IsNull(height_val))
JS_ToInt32(js, &new_height, height_val);
JS_FreeValue(js, width_val);
JS_FreeValue(js, height_val);
// Get scale mode
JSValue mode_val = JS_GetPropertyStr(js, argv[1], "mode");
SDL_ScaleMode mode = js2SDL_ScaleMode(js, mode_val);
JS_FreeValue(js, mode_val);
SDL_Surface *dst = SDL_ScaleSurface(src, new_width, new_height, mode);
SDL_DestroySurface(src);
if (!dst)
return JS_ThrowInternalError(js, "Scale failed: %s", SDL_GetError());
JSValue result = surface_to_image(js, dst);
SDL_DestroySurface(dst);
return result;
)
// Fill function for image objects
JSC_CCALL(surface_fill_img,
if (argc < 2)
return JS_ThrowTypeError(js, "fill requires image and color");
SDL_Surface *surf = image_to_surface(js, argv[0]);
if (!surf)
return JS_ThrowTypeError(js, "First argument must be a valid image object");
colorf color = js2color(js, argv[1]);
rect r = {
.x = 0,
.y = 0,
.w = surf->w,
.h = surf->h
};
SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.x*255, color.y*255, color.z*255, color.w*255));
JSValue result = surface_to_image(js, surf);
SDL_DestroySurface(surf);
return result;
)
// Rect function for image objects
JSC_CCALL(surface_rect_img,
if (argc < 3)
return JS_ThrowTypeError(js, "rect requires image, rectangle, and color");
SDL_Surface *surf = image_to_surface(js, argv[0]);
if (!surf)
return JS_ThrowTypeError(js, "First argument must be a valid image object");
rect r = js2rect(js, argv[1]);
colorf color = js2color(js, argv[2]);
SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.x*255, color.y*255, color.z*255, color.w*255));
JSValue result = surface_to_image(js, surf);
SDL_DestroySurface(surf);
return result;
)
// Blit function for image objects
JSC_CCALL(surface_blit_img,
if (argc < 2)
return JS_ThrowTypeError(js, "blit requires destination and source images");
SDL_Surface *dst = image_to_surface(js, argv[0]);
if (!dst)
return JS_ThrowTypeError(js, "First argument must be a valid destination image");
SDL_Surface *src = image_to_surface(js, argv[1]);
if (!src) {
SDL_DestroySurface(dst);
return JS_ThrowTypeError(js, "Second argument must be a valid source image");
}
irect dr = {0}, *pdr = NULL;
if (argc > 2 && !JS_IsNull(argv[2])) {
dr = js2irect(js, argv[2]);
pdr = &dr;
}
irect sr = {0}, *psr = NULL;
if (argc > 3 && !JS_IsNull(argv[3])) {
sr = js2irect(js, argv[3]);
psr = &sr;
}
SDL_ScaleMode mode = SDL_SCALEMODE_LINEAR;
if (argc > 4)
mode = js2SDL_ScaleMode(js, argv[4]);
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode);
SDL_DestroySurface(src);
JSValue result = surface_to_image(js, dst);
SDL_DestroySurface(dst);
return result;
)
// Duplicate function for image objects
JSC_CCALL(surface_dup_img,
if (argc < 1)
return JS_ThrowTypeError(js, "dup requires an image object");
SDL_Surface *surf = image_to_surface(js, argv[0]);
if (!surf)
return JS_ThrowTypeError(js, "Argument must be a valid image object");
SDL_Surface *dup = SDL_DuplicateSurface(surf);
SDL_DestroySurface(surf);
if (!dup)
return JS_ThrowInternalError(js, "Duplicate failed: %s", SDL_GetError());
JSValue result = surface_to_image(js, dup);
SDL_DestroySurface(dup);
return result;
)
// Generic convert function for pixel format/colorspace conversion
JSC_CCALL(surface_convert_generic,
if (argc < 2)
return JS_ThrowTypeError(js, "convert requires source and conversion objects");
// Parse source object
int src_width, src_height;
JS_GETATOM(js, src_width, argv[0], width, number)
JS_GETATOM(js, src_height, argv[0], height, number)
if (!src_width || !src_height)
return JS_ThrowTypeError(js, "source object requires width and height");
// Get source format
JSValue src_format_val = JS_GetPropertyStr(js, argv[0], "format");
SDL_PixelFormat src_format = js2pixelformat(js, src_format_val);
JS_FreeValue(js, src_format_val);
if (src_format == SDL_PIXELFORMAT_UNKNOWN)
return JS_ThrowTypeError(js, "source object requires valid format");
// Get source pixels
JSValue src_pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
size_t src_len;
void *src_pixels = js_get_blob_data(js, &src_len, src_pixels_val);
if (src_pixels == -1) {
JS_FreeValue(js, src_pixels_val);
return JS_EXCEPTION;
}
// Get source pitch (optional, calculate if not provided)
int src_pitch;
JSValue src_pitch_val = JS_GetPropertyStr(js, argv[0], "pitch");
if (!JS_IsNull(src_pitch_val)) {
src_pitch = js2number(js, src_pitch_val);
JS_FreeValue(js, src_pitch_val);
} else {
src_pitch = src_width * SDL_BYTESPERPIXEL(src_format);
}
// Get source colorspace (optional)
JSValue src_colorspace_val = JS_GetPropertyStr(js, argv[0], "colorspace");
SDL_Colorspace src_colorspace = SDL_COLORSPACE_SRGB; // default
if (!JS_IsNull(src_colorspace_val)) {
// For now, we'll use a simple numeric value for colorspace
int colorspace_num;
if (JS_ToInt32(js, &colorspace_num, src_colorspace_val) == 0) {
src_colorspace = (SDL_Colorspace)colorspace_num;
}
}
JS_FreeValue(js, src_colorspace_val);
// Parse conversion object
JSValue dst_format_val = JS_GetPropertyStr(js, argv[1], "format");
SDL_PixelFormat dst_format = js2pixelformat(js, dst_format_val);
JS_FreeValue(js, dst_format_val);
if (dst_format == SDL_PIXELFORMAT_UNKNOWN)
return JS_ThrowTypeError(js, "conversion object requires valid format");
// Get destination pitch (optional)
int dst_pitch;
JSValue dst_pitch_val = JS_GetPropertyStr(js, argv[1], "pitch");
if (!JS_IsNull(dst_pitch_val)) {
dst_pitch = js2number(js, dst_pitch_val);
JS_FreeValue(js, dst_pitch_val);
} else {
dst_pitch = src_width * SDL_BYTESPERPIXEL(dst_format);
}
// Get destination colorspace (optional)
JSValue dst_colorspace_val = JS_GetPropertyStr(js, argv[1], "colorspace");
SDL_Colorspace dst_colorspace = SDL_COLORSPACE_SRGB; // default
if (!JS_IsNull(dst_colorspace_val)) {
int colorspace_num;
if (JS_ToInt32(js, &colorspace_num, dst_colorspace_val) == 0) {
dst_colorspace = (SDL_Colorspace)colorspace_num;
}
}
JS_FreeValue(js, dst_colorspace_val);
// Calculate destination buffer size
size_t dst_size = dst_pitch * src_height;
void *dst_pixels = malloc(dst_size);
if (!dst_pixels) {
JS_FreeValue(js, src_pixels_val);
return JS_ThrowOutOfMemory(js);
}
// Check if we have colorspace info for both source and dest
bool has_src_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[0], "colorspace"));
bool has_dst_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[1], "colorspace"));
bool success;
if (has_src_colorspace || has_dst_colorspace) {
// Use SDL_ConvertPixelsAndColorspace
success = SDL_ConvertPixelsAndColorspace(
src_width, src_height,
src_format, src_colorspace, 0, src_pixels, src_pitch,
dst_format, dst_colorspace, 0, dst_pixels, dst_pitch
);
} else {
// Use SDL_ConvertPixels
success = SDL_ConvertPixels(
src_width, src_height,
src_format, src_pixels, src_pitch,
dst_format, dst_pixels, dst_pitch
);
}
JS_FreeValue(js, src_pixels_val);
if (!success) {
free(dst_pixels);
return JS_ThrowInternalError(js, "Pixel conversion failed: %s", SDL_GetError());
}
// Create result image object
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, src_width));
JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, src_height));
JS_SetPropertyStr(js, result, "format", pixelformat2js(js, dst_format));
JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, dst_pitch));
JSValue pixels = js_new_blob_stoned_copy(js, dst_pixels, dst_size);
free(dst_pixels);
JS_SetPropertyStr(js, result, "pixels", pixels);
// Add depth and hdr for consistency
JS_SetPropertyStr(js, result, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(dst_format)));
JS_SetPropertyStr(js, result, "hdr", JS_FALSE);
return result;
)
CELL_USE_INIT(
QJSCLASSPREP_FUNCS(SDL_Surface)
// Add the surface constructor
JSValue ctor = JS_NewCFunction2(js, js_surface_constructor, "surface", 1, JS_CFUNC_generic, 0);
// Add the generic convert function as a property on the constructor
JS_SetPropertyStr(js, ctor, "convert", JS_NewCFunction(js, js_surface_convert_generic, "convert", 2));
// Add the compression functions as static methods on the constructor
JS_SetPropertyStr(js, ctor, "compress_bc1", JS_NewCFunction(js, js_surface_compress_bc1, "compress_bc1", 2));
JS_SetPropertyStr(js, ctor, "compress_bc3", JS_NewCFunction(js, js_surface_compress_bc3, "compress_bc3", 2));
JS_SetPropertyStr(js, ctor, "compress_bc4", JS_NewCFunction(js, js_surface_compress_bc4, "compress_bc4", 1));
JS_SetPropertyStr(js, ctor, "compress_bc5", JS_NewCFunction(js, js_surface_compress_bc5, "compress_bc5", 1));
// Add standalone image manipulation functions
JS_SetPropertyStr(js, ctor, "scale", JS_NewCFunction(js, js_surface_scale_img, "scale", 2));
JS_SetPropertyStr(js, ctor, "fill", JS_NewCFunction(js, js_surface_fill_img, "fill", 2));
JS_SetPropertyStr(js, ctor, "rect", JS_NewCFunction(js, js_surface_rect_img, "rect", 3));
JS_SetPropertyStr(js, ctor, "blit", JS_NewCFunction(js, js_surface_blit_img, "blit", 5));
JS_SetPropertyStr(js, ctor, "dup", JS_NewCFunction(js, js_surface_dup_img, "dup", 1));
return ctor;
)