luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 51f75cdfff4b082259a3fd0d9b0a608283a2a9a0
parent 24f03437c48d300bed3c8033f0038a8c5309866a
Author: luajitos <bbhbb2094@gmail.com>
Date:   Tue,  2 Dec 2025 02:45:02 +0000

Fixed Paint

Diffstat:
Mdecoder.c | 47+++++++++++++++++++++++++++++++++++++++++++++++
Miso_includes/apps/com.luajitos.lunareditor/src/editor.lua | 23+++++++++++++++++++----
Miso_includes/apps/com.luajitos.paint/src/init.lua | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Miso_includes/os/init.lua | 5++++-
Miso_includes/os/libs/Application.lua | 37+++++++++++++++++++++++++++++++++++--
Miso_includes/os/libs/Image.lua | 1025+++++++++++++++++++++++++++++++++++++++----------------------------------------
Miso_includes/os/libs/Run.lua | 9+++++++++
Mkernel.c | 13+++++++++++++
Mlibc.c | 219++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mluajit_init.c | 4++++
10 files changed, 875 insertions(+), 601 deletions(-)

diff --git a/decoder.c b/decoder.c @@ -590,3 +590,50 @@ int lua_image_rotate(lua_State* L) { lua_pushlightuserdata(L, rotated); return 1; } + +/* Lua binding: Get image buffer as BGRA string (for Lua Image library) */ +int lua_image_get_buffer_bgra(lua_State* L) { + image_t* img = (image_t*)lua_touserdata(L, 1); + + if (!img || !img->data) { + lua_pushnil(L); + lua_pushstring(L, "Invalid image"); + return 2; + } + + /* Allocate buffer for BGRA data (always 4 bytes per pixel) */ + size_t buf_size = img->width * img->height * 4; + uint8_t* bgra_buf = (uint8_t*)malloc(buf_size); + if (!bgra_buf) { + lua_pushnil(L); + lua_pushstring(L, "Out of memory"); + return 2; + } + + /* Convert image data to BGRA format */ + int bytes_per_pixel = img->bpp / 8; + for (uint32_t y = 0; y < img->height; y++) { + for (uint32_t x = 0; x < img->width; x++) { + uint8_t* src_pixel = img->data + (y * img->width + x) * bytes_per_pixel; + uint8_t* dst_pixel = bgra_buf + (y * img->width + x) * 4; + + /* Source is RGB or RGBA */ + uint8_t r = src_pixel[0]; + uint8_t g = src_pixel[1]; + uint8_t b = src_pixel[2]; + uint8_t a = (bytes_per_pixel == 4) ? src_pixel[3] : 255; + + /* Destination is BGRA */ + dst_pixel[0] = b; + dst_pixel[1] = g; + dst_pixel[2] = r; + dst_pixel[3] = a; + } + } + + /* Push as Lua string */ + lua_pushlstring(L, (const char*)bgra_buf, buf_size); + free(bgra_buf); + + return 1; +} diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua @@ -23,10 +23,10 @@ local visibleLines = math.floor(contentHeight / lineHeight) -- Toolbar buttons local toolbarButtons = { - { x = 5, y = 5, width = 50, height = 20, label = "New", action = "new" }, - { x = 60, y = 5, width = 50, height = 20, label = "Open", action = "open" }, - { x = 115, y = 5, width = 50, height = 20, label = "Save", action = "save" }, - { x = 170, y = 5, width = 55, height = 20, label = "SaveAs", action = "saveas" }, + { x = 5, y = 5, width = 70, height = 20, label = "New", action = "new" }, + { x = 80, y = 5, width = 70, height = 20, label = "Open", action = "open" }, + { x = 155, y = 5, width = 70, height = 20, label = "Save", action = "save" }, + { x = 230, y = 5, width = 75, height = 20, label = "Save As", action = "saveas" }, } -- Helper: get window title @@ -411,6 +411,21 @@ window.onClick = function(x, y, button) end end +-- Resize callback +window.onResize = function(newWidth, newHeight, oldWidth, oldHeight) + windowWidth = newWidth + windowHeight = newHeight + contentHeight = windowHeight - toolbarHeight - statusBarHeight + visibleLines = math.floor(contentHeight / lineHeight) + if osprint then + osprint("[LunarEditor] onResize: " .. oldWidth .. "x" .. oldHeight .. " -> " .. newWidth .. "x" .. newHeight .. "\n") + osprint("[LunarEditor] contentHeight=" .. contentHeight .. " visibleLines=" .. visibleLines .. "\n") + end + -- Ensure cursor is still visible after resize + ensureCursorVisible() + window:markDirty() +end + -- Check for command line arguments to open a file if args then local argPath = args.o or args.open or args[1] diff --git a/iso_includes/apps/com.luajitos.paint/src/init.lua b/iso_includes/apps/com.luajitos.paint/src/init.lua @@ -13,9 +13,13 @@ window.resizable = true -- Allow resizing -- Store drawn strokes (lines between points) local strokes = {} --- Background image (loaded BMP/PNG) +-- Background image (native C image for display) local bgImage = nil +-- Background image dimensions (for saving) +local bgImageWidth = nil +local bgImageHeight = nil + -- Current file path local currentFile = nil @@ -68,7 +72,7 @@ local function addLine(x1, y1, x2, y2) end end --- Load image file (BMP or PNG) +-- Load image file (BMP, PNG, or JPEG) local function loadImage(path) if not path then return false end @@ -77,22 +81,34 @@ local function loadImage(path) local ext = path:lower():match("%.([^%.]+)$") + -- Destroy old image if exists + if bgImage and ImageDestroy then + ImageDestroy(bgImage) + bgImage = nil + end + bgImageWidth = nil + bgImageHeight = nil + + -- Load native image for display + local nativeImg = nil if ext == "bmp" and BMPLoad then - if bgImage and ImageDestroy then - ImageDestroy(bgImage) - end - bgImage = BMPLoad(data) - currentFile = path - strokes = {} - return bgImage ~= nil + nativeImg = BMPLoad(data) elseif ext == "png" and PNGLoad then - if bgImage and ImageDestroy then - ImageDestroy(bgImage) + nativeImg = PNGLoad(data) + elseif (ext == "jpeg" or ext == "jpg") and JPEGLoad then + nativeImg = JPEGLoad(data) + end + + if nativeImg then + bgImage = nativeImg + -- Get dimensions from native image + if ImageGetWidth and ImageGetHeight then + bgImageWidth = ImageGetWidth(nativeImg) + bgImageHeight = ImageGetHeight(nativeImg) end - bgImage = PNGLoad(data) currentFile = path strokes = {} - return bgImage ~= nil + return true end return false @@ -106,6 +122,8 @@ local buttons = { ImageDestroy(bgImage) bgImage = nil end + bgImageWidth = nil + bgImageHeight = nil currentFile = nil window:markDirty() end}, @@ -139,34 +157,58 @@ local buttons = { local canvasW = window.width local canvasH = window.height - toolbarHeight - -- Create image from canvas - local img = Image.new(canvasW, canvasH, false) - - -- Fill with white background - for y = 0, canvasH - 1 do - for x = 0, canvasW - 1 do - img:writePixel(x, y, 255, 255, 255) + local img + local imgW, imgH + + -- If we have a background image, load it with Image.open for saving + if currentFile and bgImage then + -- Use Image.open to get the Lua Image object + Image.fsOverride = fs + local loadedImg, err = Image.open(currentFile) + Image.fsOverride = nil + + if loadedImg then + img = loadedImg + imgW, imgH = img:getSize() + else + print("Paint: Failed to reload image for save: " .. (err or "unknown")) end end + -- If no background image loaded, create new white canvas + if not img then + imgW = canvasW + imgH = canvasH + img = Image.new(imgW, imgH, false) + img:fill(0xFFFFFF) + end + + -- Use batch mode for efficient stroke drawing + img:beginBatch() + -- Draw all strokes for _, s in ipairs(strokes) do - local r = s.r + local rad = s.r + -- Extract RGB from color local sr = bit.band(bit.rshift(s.color, 16), 0xFF) local sg = bit.band(bit.rshift(s.color, 8), 0xFF) local sb = bit.band(s.color, 0xFF) - for dy = -r, r do - for dx = -r, r do - if dx*dx + dy*dy <= r*r then + + for dy = -rad, rad do + for dx = -rad, rad do + if dx*dx + dy*dy <= rad*rad then local px, py = s.x + dx, s.y + dy - if px >= 0 and px < canvasW and py >= 0 and py < canvasH then - img:writePixel(px, py, sr, sg, sb) + if px >= 0 and px < imgW and py >= 0 and py < imgH then + img:_batchWritePixel(px, py, sr, sg, sb) end end end end end + -- End batch mode to finalize buffer + img:endBatch() + -- Save based on extension local ext = path:lower():match("%.([^%.]+)$") local success, err diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua @@ -1445,7 +1445,10 @@ function MainDraw() _G.resizing_window.height = new_h _G.resizing_window.dirty = true - -- Clear buffer to force recreation + -- Free old buffer and clear to force recreation + if _G.resizing_window.buffer and VESAFreeWindowBuffer then + VESAFreeWindowBuffer(_G.resizing_window.buffer) + end _G.resizing_window.buffer = nil -- Call onResize callback if dimensions changed diff --git a/iso_includes/os/libs/Application.lua b/iso_includes/os/libs/Application.lua @@ -558,6 +558,12 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) end end + -- Free window buffer to release memory + if self.buffer and VESAFreeWindowBuffer then + VESAFreeWindowBuffer(self.buffer) + self.buffer = nil + end + -- Hide the window self:hide() @@ -636,7 +642,10 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) self.maximized = true end - -- Clear buffer to force recreation with new size + -- Free old buffer and clear to force recreation with new size + if self.buffer and VESAFreeWindowBuffer then + VESAFreeWindowBuffer(self.buffer) + end self.buffer = nil self.dirty = true @@ -644,6 +653,27 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) _G.osprint(string.format("[MAXIMIZE] Window now %dx%d, buffer cleared, maximized=%s\n", self.width, self.height, tostring(self.maximized))) end + + -- Call onResize callback if it exists + if self.onResize then + local oldWidth = self.restoreWidth or self.width + local oldHeight = self.restoreHeight or self.height + if self.maximized then + -- We just maximized, old size is restore size + pcall(self.onResize, self.width, self.height, self.restoreWidth, self.restoreHeight) + else + -- We just restored, old size was the maximized size (screen size) + local screenW = 1024 + local screenH = 768 + if _G.sys and _G.sys.screens and _G.sys.screens[1] then + screenW = _G.sys.screens[1].width or screenW + screenH = _G.sys.screens[1].height or screenH + end + local maxW = screenW - (BORDER_WIDTH * 2) + local maxH = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2) + pcall(self.onResize, self.width, self.height, maxW, maxH) + end + end end -- Resize window @@ -662,7 +692,10 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) self.width = newWidth self.height = newHeight - -- Clear buffer to force recreation with new size + -- Free old buffer and clear to force recreation with new size + if self.buffer and VESAFreeWindowBuffer then + VESAFreeWindowBuffer(self.buffer) + end self.buffer = nil self.dirty = true diff --git a/iso_includes/os/libs/Image.lua b/iso_includes/os/libs/Image.lua @@ -1,6 +1,7 @@ -- Image.lua - Image Creation and Manipulation Library -- Requires: "imaging" permission --- Uses binary string buffer for pixel storage +-- Uses BGRA binary string buffer (matching screen buffer format for fast blitting) +-- Internal format: 4 bytes per pixel, order: B, G, R, A (little-endian 0xAARRGGBB) local Image = {} @@ -106,7 +107,7 @@ ImageObj.__metatable = false -- Prevent metatable access/modification -- Create a new image -- @param width Image width in pixels -- @param height Image height in pixels --- @param hasAlpha Optional, default true - whether image has alpha channel +-- @param hasAlpha Optional, default true - whether image uses alpha (always stored as 4 bytes) -- @return Image object function Image.new(width, height, hasAlpha) checkPermission() @@ -130,19 +131,19 @@ function Image.new(width, height, hasAlpha) self.width = width self.height = height self.hasAlpha = hasAlpha - self.bytesPerPixel = hasAlpha and 4 or 3 -- RGBA or RGB + self.bytesPerPixel = 4 -- Always 4 bytes (BGRA) for screen buffer compatibility -- Create binary buffer for pixels - -- Format: RGBA (4 bytes per pixel) or RGB (3 bytes per pixel) + -- Format: BGRA (4 bytes per pixel) matching screen buffer + -- Memory order: B, G, R, A (which is 0xAARRGGBB in little-endian uint32) local numPixels = width * height -- Initialize to transparent black (or opaque black if no alpha) - -- Use string.rep for efficient allocation local pixel if hasAlpha then - pixel = "\0\0\0\0" -- RGBA: transparent black + pixel = "\0\0\0\0" -- BGRA: transparent black (B=0, G=0, R=0, A=0) else - pixel = "\0\0\0" -- RGB: black + pixel = "\0\0\0\255" -- BGRA: opaque black (B=0, G=0, R=0, A=255) end self.buffer = string.rep(pixel, numPixels) @@ -153,7 +154,7 @@ end -- Write a pixel to the image -- @param x X coordinate (0-based) -- @param y Y coordinate (0-based) --- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number, or table {r, g, b, a} +-- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number (0xRRGGBB or 0xAARRGGBB), or table {r, g, b, a} function ImageObj:writePixel(x, y, color) checkPermission() @@ -162,76 +163,67 @@ function ImageObj:writePixel(x, y, color) end if x < 0 or x >= self.width or y < 0 or y >= self.height then - error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") + return -- Silently ignore out of bounds (more efficient for drawing) end - -- Parse color + -- Parse color to get r, g, b, a values local r, g, b, a if type(color) == "table" then -- Table format {r, g, b, a} - r = color.r or 0 - g = color.g or 0 - b = color.b or 0 - a = color.a or 255 + r = color.r or color[1] or 0 + g = color.g or color[2] or 0 + b = color.b or color[3] or 0 + a = color.a or color[4] or 255 elseif type(color) == "string" then -- Remove # if present color = color:gsub("^#", "") if #color == 6 then -- RRGGBB format - r = tonumber(color:sub(1, 2), 16) - g = tonumber(color:sub(3, 4), 16) - b = tonumber(color:sub(5, 6), 16) + r = tonumber(color:sub(1, 2), 16) or 0 + g = tonumber(color:sub(3, 4), 16) or 0 + b = tonumber(color:sub(5, 6), 16) or 0 a = 255 elseif #color == 8 then -- RRGGBBAA format - r = tonumber(color:sub(1, 2), 16) - g = tonumber(color:sub(3, 4), 16) - b = tonumber(color:sub(5, 6), 16) - a = tonumber(color:sub(7, 8), 16) + r = tonumber(color:sub(1, 2), 16) or 0 + g = tonumber(color:sub(3, 4), 16) or 0 + b = tonumber(color:sub(5, 6), 16) or 0 + a = tonumber(color:sub(7, 8), 16) or 255 else - error("Invalid color format. Use 'RRGGBB' or 'RRGGBBAA'") + r, g, b, a = 0, 0, 0, 255 end elseif type(color) == "number" then - -- Treat as 0xRRGGBB or 0xRRGGBBAA + -- Treat as 0xRRGGBB or 0xAARRGGBB if color <= 0xFFFFFF then - -- RGB format + -- RGB format (0xRRGGBB) r = bit.rshift(bit.band(color, 0xFF0000), 16) g = bit.rshift(bit.band(color, 0x00FF00), 8) b = bit.band(color, 0x0000FF) a = 255 else - -- RGBA format - r = bit.rshift(bit.band(color, 0xFF000000), 24) - g = bit.rshift(bit.band(color, 0x00FF0000), 16) - b = bit.rshift(bit.band(color, 0x0000FF00), 8) - a = bit.band(color, 0x000000FF) + -- ARGB format (0xAARRGGBB) + a = bit.rshift(bit.band(color, 0xFF000000), 24) + r = bit.rshift(bit.band(color, 0x00FF0000), 16) + g = bit.rshift(bit.band(color, 0x0000FF00), 8) + b = bit.band(color, 0x000000FF) end else - error("Color must be a string (hex), number, or table {r, g, b, a}") - end - - if not r or not g or not b then - error("Failed to parse color: " .. tostring(color)) + r, g, b, a = 0, 0, 0, 255 end - -- Calculate byte offset in buffer (row-major order) + -- Calculate byte offset in buffer (4 bytes per pixel, BGRA order) local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 + local byteOffset = pixelIndex * 4 + 1 - -- Update buffer - local newBytes - if self.hasAlpha then - newBytes = string.char(r, g, b, a) - else - newBytes = string.char(r, g, b) - end + -- Write BGRA bytes + local newBytes = string.char(b, g, r, a) -- Replace bytes in buffer self.buffer = self.buffer:sub(1, byteOffset - 1) .. newBytes .. - self.buffer:sub(byteOffset + self.bytesPerPixel) + self.buffer:sub(byteOffset + 4) end -- Read a pixel from the image @@ -249,26 +241,21 @@ function ImageObj:readPixel(x, y) error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") end - -- Calculate byte offset in buffer + -- Calculate byte offset in buffer (4 bytes per pixel, BGRA order) local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 + local byteOffset = pixelIndex * 4 + 1 - -- Read bytes from buffer - local r = string.byte(self.buffer, byteOffset) + -- Read BGRA bytes + local b = string.byte(self.buffer, byteOffset) local g = string.byte(self.buffer, byteOffset + 1) - local b = string.byte(self.buffer, byteOffset + 2) - - -- Format as hex string - local rHex = string.format("%02X", r) - local gHex = string.format("%02X", g) - local bHex = string.format("%02X", b) + local r = string.byte(self.buffer, byteOffset + 2) + local a = string.byte(self.buffer, byteOffset + 3) + -- Format as hex string (RRGGBBAA) if self.hasAlpha then - local a = string.byte(self.buffer, byteOffset + 3) - local aHex = string.format("%02X", a) - return rHex .. gHex .. bHex .. aHex + return string.format("%02X%02X%02X%02X", r, g, b, a) else - return rHex .. gHex .. bHex + return string.format("%02X%02X%02X", r, g, b) end end @@ -288,12 +275,13 @@ function ImageObj:getPixel(x, y) end local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 + local byteOffset = pixelIndex * 4 + 1 - local r = string.byte(self.buffer, byteOffset) + -- Read BGRA bytes + local b = string.byte(self.buffer, byteOffset) local g = string.byte(self.buffer, byteOffset + 1) - local b = string.byte(self.buffer, byteOffset + 2) - local a = self.hasAlpha and string.byte(self.buffer, byteOffset + 3) or 255 + local r = string.byte(self.buffer, byteOffset + 2) + local a = string.byte(self.buffer, byteOffset + 3) return { r = r, @@ -315,57 +303,82 @@ function ImageObj:setPixel(x, y, rgba) end if x < 0 or x >= self.width or y < 0 or y >= self.height then - error("Pixel coordinates out of bounds: (" .. x .. ", " .. y .. ")") + return -- Silently ignore out of bounds end - local r = rgba.r or 0 - local g = rgba.g or 0 - local b = rgba.b or 0 - local a = rgba.a or 255 + local r = rgba.r or rgba[1] or 0 + local g = rgba.g or rgba[2] or 0 + local b = rgba.b or rgba[3] or 0 + local a = rgba.a or rgba[4] or 255 local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 + local byteOffset = pixelIndex * 4 + 1 - local newBytes - if self.hasAlpha then - newBytes = string.char(r, g, b, a) - else - newBytes = string.char(r, g, b) - end + -- Write BGRA bytes + local newBytes = string.char(b, g, r, a) self.buffer = self.buffer:sub(1, byteOffset - 1) .. newBytes .. - self.buffer:sub(byteOffset + self.bytesPerPixel) + self.buffer:sub(byteOffset + 4) end -- Fill entire image with a color --- @param color Color in hex format "RRGGBB" or "RRGGBBAA", or table {r, g, b, a} +-- @param color Color in hex format "RRGGBB" or "RRGGBBAA", number, or table {r, g, b, a} function ImageObj:fill(color) checkPermission() - -- Convert table to hex string if needed - if type(color) == "table" then - local r = color.r or 0 - local g = color.g or 0 - local b = color.b or 0 - local a = color.a or 255 - - color = string.format("%02X%02X%02X%02X", r, g, b, a) - end + -- Parse color to get r, g, b, a values + local r, g, b, a = 0, 0, 0, 255 - for y = 0, self.height - 1 do - for x = 0, self.width - 1 do - self:writePixel(x, y, color) + if type(color) == "table" then + r = color.r or color[1] or 0 + g = color.g or color[2] or 0 + b = color.b or color[3] or 0 + a = color.a or color[4] or 255 + elseif type(color) == "string" then + color = color:gsub("^#", "") + if #color == 6 then + r = tonumber(color:sub(1, 2), 16) or 0 + g = tonumber(color:sub(3, 4), 16) or 0 + b = tonumber(color:sub(5, 6), 16) or 0 + a = 255 + elseif #color == 8 then + r = tonumber(color:sub(1, 2), 16) or 0 + g = tonumber(color:sub(3, 4), 16) or 0 + b = tonumber(color:sub(5, 6), 16) or 0 + a = tonumber(color:sub(7, 8), 16) or 255 + end + elseif type(color) == "number" then + if color <= 0xFFFFFF then + r = bit.rshift(bit.band(color, 0xFF0000), 16) + g = bit.rshift(bit.band(color, 0x00FF00), 8) + b = bit.band(color, 0x0000FF) + a = 255 + else + a = bit.rshift(bit.band(color, 0xFF000000), 24) + r = bit.rshift(bit.band(color, 0x00FF0000), 16) + g = bit.rshift(bit.band(color, 0x0000FF00), 8) + b = bit.band(color, 0x000000FF) end end + + -- Create single pixel in BGRA format + local pixel = string.char(b, g, r, a) + + -- Fill buffer efficiently using string.rep + local numPixels = self.width * self.height + self.buffer = string.rep(pixel, numPixels) end -- Clear image to transparent (or opaque black if no alpha) function ImageObj:clear() checkPermission() - local clearColor = self.hasAlpha and "00000000" or "000000FF" - self:fill(clearColor) + if self.hasAlpha then + self:fill({r=0, g=0, b=0, a=0}) + else + self:fill({r=0, g=0, b=0, a=255}) + end end -- Get image dimensions @@ -386,7 +399,7 @@ function ImageObj:getInfo() } end --- Draw a rectangle +-- Draw a filled rectangle -- @param x X coordinate of top-left corner -- @param y Y coordinate of top-left corner -- @param width Rectangle width @@ -395,15 +408,69 @@ end function ImageObj:fillRect(x, y, width, height, color) checkPermission() - for dy = 0, height - 1 do - for dx = 0, width - 1 do - local px = x + dx - local py = y + dy - if px >= 0 and px < self.width and py >= 0 and py < self.height then - self:writePixel(px, py, color) - end + -- Parse color once + local r, g, b, a = 0, 0, 0, 255 + if type(color) == "number" then + if color <= 0xFFFFFF then + r = bit.rshift(bit.band(color, 0xFF0000), 16) + g = bit.rshift(bit.band(color, 0x00FF00), 8) + b = bit.band(color, 0x0000FF) + else + a = bit.rshift(bit.band(color, 0xFF000000), 24) + r = bit.rshift(bit.band(color, 0x00FF0000), 16) + g = bit.rshift(bit.band(color, 0x0000FF00), 8) + b = bit.band(color, 0x000000FF) + end + elseif type(color) == "table" then + r = color.r or color[1] or 0 + g = color.g or color[2] or 0 + b = color.b or color[3] or 0 + a = color.a or color[4] or 255 + end + + local pixel = string.char(b, g, r, a) + + -- Clip to image bounds + local x1 = math.max(0, x) + local y1 = math.max(0, y) + local x2 = math.min(self.width, x + width) + local y2 = math.min(self.height, y + height) + + if x1 >= x2 or y1 >= y2 then return end + + local rectWidth = x2 - x1 + local rowPixels = string.rep(pixel, rectWidth) + + -- Build new buffer with rectangle + local parts = {} + local imgWidth = self.width + + -- Rows before rectangle + if y1 > 0 then + parts[#parts + 1] = self.buffer:sub(1, y1 * imgWidth * 4) + end + + -- Rectangle rows + for row = y1, y2 - 1 do + local rowStart = row * imgWidth * 4 + 1 + -- Pixels before rectangle in this row + if x1 > 0 then + parts[#parts + 1] = self.buffer:sub(rowStart, rowStart + x1 * 4 - 1) + end + -- Rectangle pixels + parts[#parts + 1] = rowPixels + -- Pixels after rectangle in this row + if x2 < imgWidth then + parts[#parts + 1] = self.buffer:sub(rowStart + x2 * 4, rowStart + imgWidth * 4 - 1) end end + + -- Rows after rectangle + if y2 < self.height then + parts[#parts + 1] = self.buffer:sub(y2 * imgWidth * 4 + 1) + end + + self.buffer = table.concat(parts) end -- Draw a line (Bresenham's algorithm) @@ -442,22 +509,101 @@ function ImageObj:drawLine(x1, y1, x2, y2, color) end end --- Get raw buffer +-- Get raw buffer (BGRA format, ready for screen blitting) -- @return Binary string buffer function ImageObj:getBuffer() checkPermission() return self.buffer end +-- Get row of pixels as binary string (for fast blitting) +-- @param y Row index (0-based) +-- @return Binary string of row pixels in BGRA format +function ImageObj:getRow(y) + checkPermission() + if y < 0 or y >= self.height then + return nil + end + local rowStart = y * self.width * 4 + 1 + local rowEnd = rowStart + self.width * 4 - 1 + return self.buffer:sub(rowStart, rowEnd) +end + +-- Begin batch edit mode - converts buffer to table for faster pixel writes +-- Call endBatch() when done to convert back to string +function ImageObj:beginBatch() + checkPermission() + if self._batchMode then return end -- Already in batch mode + + -- Convert string buffer to table of bytes for fast random access + -- Process in chunks to avoid "string slice too long" error + self._batchBuffer = {} + local bufLen = #self.buffer + local chunkSize = 4096 + + for i = 1, bufLen, chunkSize do + local endIdx = math.min(i + chunkSize - 1, bufLen) + local bytes = {string.byte(self.buffer, i, endIdx)} + for j = 1, #bytes do + self._batchBuffer[i + j - 1] = bytes[j] + end + end + self._batchMode = true +end + +-- End batch edit mode - converts table back to string buffer +function ImageObj:endBatch() + checkPermission() + if not self._batchMode then return end -- Not in batch mode + + -- Convert table back to string using string.char with multiple args + -- Process in chunks to avoid too many arguments to string.char + local chunks = {} + local chunkSize = 256 -- string.char can handle many args + local bufLen = #self._batchBuffer + + for i = 1, bufLen, chunkSize do + local endIdx = math.min(i + chunkSize - 1, bufLen) + -- Use unpack to pass multiple values to string.char at once + chunks[#chunks + 1] = string.char(unpack(self._batchBuffer, i, endIdx)) + end + + self.buffer = table.concat(chunks) + self._batchBuffer = nil + self._batchMode = false +end + +-- Fast pixel write for batch mode (takes RGB values, writes BGRA) +-- @param x X coordinate +-- @param y Y coordinate +-- @param r Red (0-255) +-- @param g Green (0-255) +-- @param b Blue (0-255) +-- @param a Alpha (0-255), optional, defaults to 255 +function ImageObj:_batchWritePixel(x, y, r, g, b, a) + if not self._batchMode then return end + if x < 0 or x >= self.width or y < 0 or y >= self.height then return end + + local pixelIndex = y * self.width + x + local byteOffset = pixelIndex * 4 + 1 + + -- Write BGRA bytes + self._batchBuffer[byteOffset] = b + self._batchBuffer[byteOffset + 1] = g + self._batchBuffer[byteOffset + 2] = r + self._batchBuffer[byteOffset + 3] = a or 255 +end + -- Convert to native C image for efficient drawing with ImageDraw -- @return Native image handle (userdata) or nil on error function ImageObj:toNativeImage() checkPermission() -- Check if ImageCreateFromBuffer is available (fastest path) + -- The buffer is already in BGRA format matching screen buffer if ImageCreateFromBuffer then - -- Pass the raw buffer directly to C - it will be copied in one go - local img = ImageCreateFromBuffer(self.width, self.height, self.buffer, self.hasAlpha) + -- Pass the raw buffer directly to C - it's already in the right format + local img = ImageCreateFromBuffer(self.width, self.height, self.buffer, true) if img then return img end @@ -476,14 +622,13 @@ function ImageObj:toNativeImage() end -- Copy pixels to native image (slow path - pixel by pixel) - local bpp = self.bytesPerPixel for y = 0, self.height - 1 do for x = 0, self.width - 1 do - local offset = (y * self.width + x) * bpp + 1 - local r = string.byte(self.buffer, offset) + local offset = (y * self.width + x) * 4 + 1 + local b = string.byte(self.buffer, offset) local g = string.byte(self.buffer, offset + 1) - local b = string.byte(self.buffer, offset + 2) - local a = bpp == 4 and string.byte(self.buffer, offset + 3) or 255 + local r = string.byte(self.buffer, offset + 2) + local a = string.byte(self.buffer, offset + 3) -- ImageSetPixel expects 0xAARRGGBB format local color = bit.bor( @@ -527,6 +672,9 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity) local scaleX = srcImage.width / w local scaleY = srcImage.height / h + -- Use batch mode for efficiency + self:beginBatch() + -- Blend each pixel for dstY = 0, h - 1 do for dstX = 0, w - 1 do @@ -544,38 +692,32 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity) if srcX >= srcImage.width then srcX = srcImage.width - 1 end if srcY >= srcImage.height then srcY = srcImage.height - 1 end - -- Get source pixel - local srcIndex = (srcY * srcImage.width + srcX) * srcImage.bytesPerPixel + 1 - local srcR = string.byte(srcImage.buffer, srcIndex) + -- Get source pixel (BGRA format) + local srcIndex = (srcY * srcImage.width + srcX) * 4 + 1 + local srcB = string.byte(srcImage.buffer, srcIndex) local srcG = string.byte(srcImage.buffer, srcIndex + 1) - local srcB = string.byte(srcImage.buffer, srcIndex + 2) - local srcA = srcImage.hasAlpha and string.byte(srcImage.buffer, srcIndex + 3) or 255 + local srcR = string.byte(srcImage.buffer, srcIndex + 2) + local srcA = string.byte(srcImage.buffer, srcIndex + 3) -- Apply global opacity srcA = math.floor(srcA * opacity) -- Get destination pixel - local destIndex = (destY * self.width + destX) * self.bytesPerPixel + 1 - local dstR = string.byte(self.buffer, destIndex) - local dstG = string.byte(self.buffer, destIndex + 1) - local dstB = string.byte(self.buffer, destIndex + 2) - local dstA = self.hasAlpha and string.byte(self.buffer, destIndex + 3) or 255 + local destIndex = (destY * self.width + destX) * 4 + 1 + local dstB = self._batchBuffer[destIndex] + local dstG = self._batchBuffer[destIndex + 1] + local dstR = self._batchBuffer[destIndex + 2] + local dstA = self._batchBuffer[destIndex + 3] -- Alpha blending formula: result = src * srcAlpha + dst * (1 - srcAlpha) local srcAlpha = srcA / 255 - local dstAlpha = dstA / 255 local invSrcAlpha = 1 - srcAlpha -- Blend colors local outR = math.floor(srcR * srcAlpha + dstR * invSrcAlpha) local outG = math.floor(srcG * srcAlpha + dstG * invSrcAlpha) local outB = math.floor(srcB * srcAlpha + dstB * invSrcAlpha) - - -- Calculate output alpha (for RGBA destinations) - local outA = 255 - if self.hasAlpha then - outA = math.floor(srcAlpha * 255 + dstAlpha * invSrcAlpha * 255) - end + local outA = math.floor(srcA + dstA * invSrcAlpha) -- Clamp values outR = math.min(255, math.max(0, outR)) @@ -583,25 +725,21 @@ function ImageObj:addImage(srcImage, x, y, w, h, opacity) outB = math.min(255, math.max(0, outB)) outA = math.min(255, math.max(0, outA)) - -- Write blended pixel - local newBytes - if self.hasAlpha then - newBytes = string.char(outR, outG, outB, outA) - else - newBytes = string.char(outR, outG, outB) - end - - self.buffer = self.buffer:sub(1, destIndex - 1) .. - newBytes .. - self.buffer:sub(destIndex + self.bytesPerPixel) + -- Write blended pixel (BGRA) + self._batchBuffer[destIndex] = outB + self._batchBuffer[destIndex + 1] = outG + self._batchBuffer[destIndex + 2] = outR + self._batchBuffer[destIndex + 3] = outA end end end + + self:endBatch() end -- Save image as PNG file -- @param path Path to save PNG file --- @param options Optional table with fields: fs (SafeFS instance), compression (boolean), colorSpace (string), interlacing (boolean) +-- @param options Optional table with fields: fs (SafeFS instance), compression (boolean) -- @return true on success, nil and error message on failure function ImageObj:saveAsPNG(path, options) checkPermission() @@ -613,12 +751,10 @@ function ImageObj:saveAsPNG(path, options) -- Parse options options = options or {} local compression = options.compression or false - local colorSpace = options.colorSpace or "sRGB" - local interlacing = options.interlacing or false local safeFSInstance = options.fs - -- Build PNG file format - local pngData = self:_encodePNG(compression, colorSpace, interlacing) + -- Build PNG file format (convert from BGRA to RGBA) + local pngData = self:_encodePNG(compression) if not pngData then return nil, "Failed to encode PNG" @@ -652,7 +788,7 @@ function ImageObj:saveAsBMP(path, options) options = options or {} local safeFSInstance = options.fs - -- Build BMP file format + -- Build BMP file format (convert from BGRA to BGR) local bmpData = self:_encodeBMP() if not bmpData then @@ -672,15 +808,11 @@ function ImageObj:saveAsBMP(path, options) return writeFile(path, bmpData) end --- Internal: Encode image as PNG format +-- Internal: Encode image as PNG format (converts BGRA buffer to RGBA for PNG) -- @param compression Use compression (true/false) --- @param colorSpace Color space (e.g., "sRGB", "linear") --- @param interlacing Use ADAM7 interlacing (true/false) -- @return PNG binary data as string -function ImageObj:_encodePNG(compression, colorSpace, interlacing) +function ImageObj:_encodePNG(compression) compression = compression or false - colorSpace = colorSpace or "sRGB" - interlacing = interlacing or false local chunks = {} @@ -690,7 +822,7 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing) -- IHDR chunk (image header) local colorType = self.hasAlpha and 6 or 2 -- 6=RGBA, 2=RGB - local interlaceMethod = interlacing and 1 or 0 -- 0=none, 1=ADAM7 + local interlaceMethod = 0 local ihdr = self:_createChunk("IHDR", self:_uint32(self.width) .. @@ -703,27 +835,16 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing) ) chunks[#chunks + 1] = ihdr - -- sRGB chunk (if sRGB color space) - if colorSpace == "sRGB" then - -- Rendering intent: 0 = Perceptual - local srgb = self:_createChunk("sRGB", string.char(0)) - chunks[#chunks + 1] = srgb - end + -- sRGB chunk + local srgb = self:_createChunk("sRGB", string.char(0)) + chunks[#chunks + 1] = srgb - -- gAMA chunk (gamma correction) - if colorSpace == "sRGB" then - -- sRGB gamma is 2.2, stored as 1/2.2 * 100000 = 45455 - local gama = self:_createChunk("gAMA", self:_uint32(45455)) - chunks[#chunks + 1] = gama - end + -- gAMA chunk + local gama = self:_createChunk("gAMA", self:_uint32(45455)) + chunks[#chunks + 1] = gama - -- IDAT chunk (image data) - local imageData - if interlacing then - imageData = self:_createImageDataAdam7() - else - imageData = self:_createImageData() - end + -- IDAT chunk (image data) - convert BGRA to RGBA + local imageData = self:_createImageDataRGBA() local compressedData = self:_deflateCompress(imageData, compression) local idat = self:_createChunk("IDAT", compressedData) @@ -736,287 +857,97 @@ function ImageObj:_encodePNG(compression, colorSpace, interlacing) return table.concat(chunks) end --- Internal: Create PNG chunk --- @param chunkType 4-character chunk type --- @param data Chunk data --- @return Complete chunk with length, type, data, and CRC -function ImageObj:_createChunk(chunkType, data) - local length = self:_uint32(#data) - local typeAndData = chunkType .. data - local crc = self:_crc32(typeAndData) - - return length .. typeAndData .. self:_uint32(crc) -end - --- Internal: Create image data with PNG filtering --- @return Filtered image data -function ImageObj:_createImageData() +-- Internal: Create image data with PNG filtering (converts BGRA to RGBA) +-- @return Filtered image data in RGBA format +function ImageObj:_createImageDataRGBA() local rows = {} + local width = self.width + local hasAlpha = self.hasAlpha + local buffer = self.buffer for y = 0, self.height - 1 do - -- Filter type 0 (None) for each scanline - local row = {string.char(0)} + -- Build row data: filter byte + RGBA pixels + local rowBytes = {0} -- Filter type 0 (None) + local rowStart = y * width * 4 + 1 - for x = 0, self.width - 1 do - local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 - - local r = string.byte(self.buffer, byteOffset) - local g = string.byte(self.buffer, byteOffset + 1) - local b = string.byte(self.buffer, byteOffset + 2) - - row[#row + 1] = string.char(r, g, b) - - if self.hasAlpha then - local a = string.byte(self.buffer, byteOffset + 3) - row[#row + 1] = string.char(a) + for x = 0, width - 1 do + local offset = rowStart + x * 4 + local b = string.byte(buffer, offset) + local g = string.byte(buffer, offset + 1) + local r = string.byte(buffer, offset + 2) + local a = string.byte(buffer, offset + 3) + + -- Write RGBA (PNG format) + if hasAlpha then + rowBytes[#rowBytes + 1] = r + rowBytes[#rowBytes + 1] = g + rowBytes[#rowBytes + 1] = b + rowBytes[#rowBytes + 1] = a + else + rowBytes[#rowBytes + 1] = r + rowBytes[#rowBytes + 1] = g + rowBytes[#rowBytes + 1] = b end end - rows[#rows + 1] = table.concat(row) + rows[#rows + 1] = string.char(unpack(rowBytes)) end return table.concat(rows) end --- Internal: Create image data with ADAM7 interlacing --- @return Filtered image data with ADAM7 interlacing -function ImageObj:_createImageDataAdam7() - -- ADAM7 interlacing pass parameters - -- Pass: starting_row, starting_col, row_increment, col_increment - local adam7Passes = { - {0, 0, 8, 8}, -- Pass 1: every 8th pixel, starting at (0,0) - {0, 4, 8, 8}, -- Pass 2: every 8th pixel, starting at (0,4) - {4, 0, 8, 4}, -- Pass 3: every 4th pixel, starting at (4,0) - {0, 2, 4, 4}, -- Pass 4: every 4th pixel, starting at (0,2) - {2, 0, 4, 2}, -- Pass 5: every 2nd pixel, starting at (2,0) - {0, 1, 2, 2}, -- Pass 6: every 2nd pixel, starting at (0,1) - {1, 0, 2, 1} -- Pass 7: every pixel, starting at (1,0) - } - - local allRows = {} - - for passNum = 1, 7 do - local pass = adam7Passes[passNum] - local startRow, startCol, rowInc, colInc = pass[1], pass[2], pass[3], pass[4] - - -- Calculate pass dimensions - local passWidth = 0 - local passHeight = 0 - - for y = startRow, self.height - 1, rowInc do - passHeight = passHeight + 1 - end - - if passHeight > 0 then - for x = startCol, self.width - 1, colInc do - passWidth = passWidth + 1 - end - end - - -- Only process if pass has pixels - if passWidth > 0 and passHeight > 0 then - local y = startRow - for row = 0, passHeight - 1 do - -- Filter type 0 (None) for each scanline - local rowData = {string.char(0)} - - local x = startCol - for col = 0, passWidth - 1 do - local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 - - local r = string.byte(self.buffer, byteOffset) - local g = string.byte(self.buffer, byteOffset + 1) - local b = string.byte(self.buffer, byteOffset + 2) - - rowData[#rowData + 1] = string.char(r, g, b) - - if self.hasAlpha then - local a = string.byte(self.buffer, byteOffset + 3) - rowData[#rowData + 1] = string.char(a) - end - - x = x + colInc - end - - allRows[#allRows + 1] = table.concat(rowData) - y = y + rowInc - end - end - end +-- Internal: Create PNG chunk +function ImageObj:_createChunk(chunkType, data) + local length = self:_uint32(#data) + local typeAndData = chunkType .. data + local crc = self:_crc32(typeAndData) - return table.concat(allRows) + return length .. typeAndData .. self:_uint32(crc) end -- Internal: DEFLATE compression --- @param data Data to compress --- @param useCompression Use actual compression (true) or uncompressed blocks (false) --- @return Compressed data with zlib wrapper function ImageObj:_deflateCompress(data, useCompression) useCompression = useCompression or false local result = {} -- Zlib header (RFC 1950) - -- CMF (Compression Method and Flags) - local cmf = 0x78 -- CM=8 (deflate), CINFO=7 (32K window) - -- FLG (Flags) - local flg = 0x01 -- FCHECK makes (CMF*256+FLG) % 31 == 0 + local cmf = 0x78 + local flg = 0x01 result[#result + 1] = string.char(cmf, flg) - if useCompression then - -- Use basic RLE compression (simple but effective) - result[#result + 1] = self:_deflateCompressRLE(data) - else - -- DEFLATE data (RFC 1951) - uncompressed blocks - local pos = 1 - while pos <= #data do - local blockSize = math.min(65535, #data - pos + 1) - local isLast = (pos + blockSize > #data) and 1 or 0 - - -- Block header (3 bits: BFINAL, BTYPE) - result[#result + 1] = string.char(isLast) -- BFINAL + BTYPE=00 (uncompressed) - - -- LEN and NLEN (16-bit little endian) - local len = blockSize - local nlen = bit.bxor(len, 0xFFFF) - - result[#result + 1] = string.char( - bit.band(len, 0xFF), - bit.rshift(len, 8), - bit.band(nlen, 0xFF), - bit.rshift(nlen, 8) - ) - - -- Literal data - result[#result + 1] = data:sub(pos, pos + blockSize - 1) - - pos = pos + blockSize - end - end - - -- Adler-32 checksum (RFC 1950) - local adler = self:_adler32(data) - result[#result + 1] = self:_uint32(adler) - - return table.concat(result) -end - --- Internal: DEFLATE compression with Fixed Huffman coding --- @param data Data to compress --- @return Compressed DEFLATE data with Huffman encoding -function ImageObj:_deflateCompressRLE(data) - -- Use Fixed Huffman codes (BTYPE=01) - -- This provides compression without needing dynamic Huffman trees - - local bitWriter = { - buffer = 0, - bits = 0, - output = {} - } - - -- Helper: Write bits to output - local function writeBits(value, numBits) - for i = 0, numBits - 1 do - bitWriter.buffer = bit.bor(bitWriter.buffer, bit.lshift(bit.band(bit.rshift(value, i), 1), bitWriter.bits)) - bitWriter.bits = bitWriter.bits + 1 - - if bitWriter.bits == 8 then - bitWriter.output[#bitWriter.output + 1] = string.char(bitWriter.buffer) - bitWriter.buffer = 0 - bitWriter.bits = 0 - end - end - end - - -- Helper: Flush remaining bits - local function flushBits() - if bitWriter.bits > 0 then - bitWriter.output[#bitWriter.output + 1] = string.char(bitWriter.buffer) - bitWriter.buffer = 0 - bitWriter.bits = 0 - end - end - - -- Fixed Huffman code tables (RFC 1951 Section 3.2.6) - local function getFixedLiteralCode(value) - if value <= 143 then - -- 0-143: 8 bits, codes 00110000 through 10111111 - return bit.bor(0x30, value), 8 - elseif value <= 255 then - -- 144-255: 9 bits, codes 110010000 through 111111111 - return bit.bor(0x190, value - 144), 9 - elseif value <= 279 then - -- 256-279: 7 bits, codes 0000000 through 0010111 - return value - 256, 7 - else - -- 280-287: 8 bits, codes 11000000 through 11000111 - return bit.bor(0xC0, value - 280), 8 - end - end - - -- Block header: BFINAL=1, BTYPE=01 (fixed Huffman) - writeBits(1, 1) -- BFINAL (last block) - writeBits(1, 2) -- BTYPE (fixed Huffman) - - -- Simple LZ77: just look for repeated bytes + -- DEFLATE data (RFC 1951) - uncompressed blocks local pos = 1 while pos <= #data do - local byte = string.byte(data, pos) + local blockSize = math.min(65535, #data - pos + 1) + local isLast = (pos + blockSize > #data) and 1 or 0 - -- Look for run-length encoding opportunities - local runLength = 1 - while pos + runLength <= #data and runLength < 258 do - if string.byte(data, pos + runLength) == byte then - runLength = runLength + 1 - else - break - end - end + result[#result + 1] = string.char(isLast) - if runLength >= 4 then - -- Use length/distance pair - -- First, output the literal byte - local code, bits = getFixedLiteralCode(byte) - writeBits(code, bits) - - -- Then output length code (simplified: just use code 257-285) - local lengthCode = 257 + math.min(runLength - 3, 28) - code, bits = getFixedLiteralCode(lengthCode) - writeBits(code, bits) - - -- Extra bits for length (if needed) - if runLength > 10 then - local extraBits = math.min(math.floor((runLength - 11) / 2), 5) - writeBits(runLength - 11 - extraBits * 2, extraBits) - end + local len = blockSize + local nlen = bit.bxor(len, 0xFFFF) - -- Distance code (distance=1 for RLE) - writeBits(0, 5) -- Distance code 0 = distance 1 + result[#result + 1] = string.char( + bit.band(len, 0xFF), + bit.rshift(len, 8), + bit.band(nlen, 0xFF), + bit.rshift(nlen, 8) + ) - pos = pos + runLength - else - -- Just output literal - local code, bits = getFixedLiteralCode(byte) - writeBits(code, bits) - pos = pos + 1 - end - end + result[#result + 1] = data:sub(pos, pos + blockSize - 1) - -- End of block (code 256) - local code, bits = getFixedLiteralCode(256) - writeBits(code, bits) + pos = pos + blockSize + end - flushBits() + -- Adler-32 checksum + local adler = self:_adler32(data) + result[#result + 1] = self:_uint32(adler) - return table.concat(bitWriter.output) + return table.concat(result) end -- Internal: Calculate CRC32 --- @param data Input data --- @return CRC32 value function ImageObj:_crc32(data) local crc = 0xFFFFFFFF @@ -1037,8 +968,6 @@ function ImageObj:_crc32(data) end -- Internal: Calculate Adler-32 --- @param data Input data --- @return Adler-32 value function ImageObj:_adler32(data) local s1 = 1 local s2 = 0 @@ -1053,8 +982,6 @@ function ImageObj:_adler32(data) end -- Internal: Encode 32-bit unsigned integer as big-endian --- @param value Integer value --- @return 4-byte big-endian string function ImageObj:_uint32(value) return string.char( bit.band(bit.rshift(value, 24), 0xFF), @@ -1065,8 +992,6 @@ function ImageObj:_uint32(value) end -- Internal: Encode 32-bit unsigned integer as little-endian --- @param value Integer value --- @return 4-byte little-endian string function ImageObj:_uint32le(value) return string.char( bit.band(value, 0xFF), @@ -1077,8 +1002,6 @@ function ImageObj:_uint32le(value) end -- Internal: Encode 16-bit unsigned integer as little-endian --- @param value Integer value --- @return 2-byte little-endian string function ImageObj:_uint16le(value) return string.char( bit.band(value, 0xFF), @@ -1086,124 +1009,207 @@ function ImageObj:_uint16le(value) ) end --- Internal: Encode image as BMP format --- @return BMP binary data as string +-- Internal: Encode image as BMP format (BGRA buffer is already close to BMP's BGR format) function ImageObj:_encodeBMP() local bmpParts = {} - -- Calculate sizes - local rowSize = ((self.bytesPerPixel * self.width + 3) / 4) * 4 -- Row must be multiple of 4 bytes - rowSize = math.floor(rowSize) + -- BMP uses 3 bytes per pixel (BGR, no alpha) + local bmpBytesPerPixel = 3 + local rowSize = math.floor((bmpBytesPerPixel * self.width + 3) / 4) * 4 local pixelDataSize = rowSize * self.height - local fileSize = 54 + pixelDataSize -- 54 = header size + local fileSize = 54 + pixelDataSize -- BMP File Header (14 bytes) - bmpParts[#bmpParts + 1] = "BM" -- Signature - bmpParts[#bmpParts + 1] = self:_uint32le(fileSize) -- File size - bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Reserved - bmpParts[#bmpParts + 1] = self:_uint32le(54) -- Pixel data offset + bmpParts[#bmpParts + 1] = "BM" + bmpParts[#bmpParts + 1] = self:_uint32le(fileSize) + bmpParts[#bmpParts + 1] = self:_uint32le(0) + bmpParts[#bmpParts + 1] = self:_uint32le(54) -- DIB Header (BITMAPINFOHEADER - 40 bytes) - bmpParts[#bmpParts + 1] = self:_uint32le(40) -- Header size - bmpParts[#bmpParts + 1] = self:_uint32le(self.width) -- Width - bmpParts[#bmpParts + 1] = self:_uint32le(self.height) -- Height (positive = bottom-up) - bmpParts[#bmpParts + 1] = self:_uint16le(1) -- Color planes - bmpParts[#bmpParts + 1] = self:_uint16le(self.bytesPerPixel * 8) -- Bits per pixel - bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Compression (0 = none) - bmpParts[#bmpParts + 1] = self:_uint32le(pixelDataSize) -- Image size - bmpParts[#bmpParts + 1] = self:_uint32le(2835) -- X pixels per meter (72 DPI) - bmpParts[#bmpParts + 1] = self:_uint32le(2835) -- Y pixels per meter (72 DPI) - bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Colors in palette - bmpParts[#bmpParts + 1] = self:_uint32le(0) -- Important colors - - -- Pixel data (bottom-up, BGR or BGRA format) - local padding = string.rep(string.char(0), rowSize - (self.width * self.bytesPerPixel)) + bmpParts[#bmpParts + 1] = self:_uint32le(40) + bmpParts[#bmpParts + 1] = self:_uint32le(self.width) + bmpParts[#bmpParts + 1] = self:_uint32le(self.height) + bmpParts[#bmpParts + 1] = self:_uint16le(1) + bmpParts[#bmpParts + 1] = self:_uint16le(24) -- 24-bit BMP + bmpParts[#bmpParts + 1] = self:_uint32le(0) + bmpParts[#bmpParts + 1] = self:_uint32le(pixelDataSize) + bmpParts[#bmpParts + 1] = self:_uint32le(2835) + bmpParts[#bmpParts + 1] = self:_uint32le(2835) + bmpParts[#bmpParts + 1] = self:_uint32le(0) + bmpParts[#bmpParts + 1] = self:_uint32le(0) + + -- Pixel data (bottom-up, BGR format) + local paddingLen = rowSize - self.width * bmpBytesPerPixel + local padding = paddingLen > 0 and string.rep(string.char(0), paddingLen) or "" + local width = self.width + local buffer = self.buffer for y = self.height - 1, 0, -1 do -- BMP is bottom-up - local rowData = {} - - for x = 0, self.width - 1 do - local pixelIndex = y * self.width + x - local byteOffset = pixelIndex * self.bytesPerPixel + 1 + local rowBytes = {} + local rowStart = y * width * 4 + 1 - local r = string.byte(self.buffer, byteOffset) - local g = string.byte(self.buffer, byteOffset + 1) - local b = string.byte(self.buffer, byteOffset + 2) - - -- BMP uses BGR order - rowData[#rowData + 1] = string.char(b, g, r) - - if self.hasAlpha then - local a = string.byte(self.buffer, byteOffset + 3) - rowData[#rowData + 1] = string.char(a) - end + for x = 0, width - 1 do + local offset = rowStart + x * 4 + -- Buffer is BGRA, BMP wants BGR + rowBytes[#rowBytes + 1] = string.byte(buffer, offset) -- B + rowBytes[#rowBytes + 1] = string.byte(buffer, offset + 1) -- G + rowBytes[#rowBytes + 1] = string.byte(buffer, offset + 2) -- R + -- Skip alpha end - bmpParts[#bmpParts + 1] = table.concat(rowData) - bmpParts[#bmpParts + 1] = padding + bmpParts[#bmpParts + 1] = string.char(unpack(rowBytes)) + if paddingLen > 0 then + bmpParts[#bmpParts + 1] = padding + end end return table.concat(bmpParts) end --- Internal: Load PNG file using existing decoder --- @param path Path to PNG file --- @return Image object or nil on error +-- Internal: Load PNG file using existing decoder (converts to BGRA) local function loadPNG(path) - if not PNGLoad or not ImageGetWidth or not ImageGetHeight or not ImageGetPixel then + -- Get functions from _G (they may be in sandbox env) + local pngLoad = PNGLoad or _G.PNGLoad + local imgGetWidth = ImageGetWidth or _G.ImageGetWidth + local imgGetHeight = ImageGetHeight or _G.ImageGetHeight + local imgGetBufferBGRA = ImageGetBufferBGRA or _G.ImageGetBufferBGRA + local imgDestroy = ImageDestroy or _G.ImageDestroy + + if not pngLoad or not imgGetWidth or not imgGetHeight then return nil, "PNG decoder functions not available" end - -- Read file using fs or CRamdisk local pngData, err = readFile(path) if not pngData then return nil, err end - -- Decode PNG - local imgBuffer = PNGLoad(pngData) + local imgBuffer = pngLoad(pngData) if not imgBuffer then return nil, "Failed to decode PNG: " .. path end - -- Get dimensions - local width = ImageGetWidth(imgBuffer) - local height = ImageGetHeight(imgBuffer) + local width = imgGetWidth(imgBuffer) + local height = imgGetHeight(imgBuffer) - -- Create Image object local self = setmetatable({}, ImageObj) self.width = width self.height = height self.hasAlpha = true self.bytesPerPixel = 4 - -- Read all pixels from C buffer into binary string - -- Build rows sequentially to avoid sparse table issues - local rows = {} - for y = 0, height - 1 do - local rowPixels = {} - for x = 0, width - 1 do - local r, g, b, a = ImageGetPixel(imgBuffer, x, y) - rowPixels[#rowPixels + 1] = string.char(r or 0, g or 0, b or 0, a or 255) + -- Use fast C function to get entire buffer as BGRA string + if imgGetBufferBGRA then + local buf, bufErr = imgGetBufferBGRA(imgBuffer) + if buf then + self.buffer = buf + else + if imgDestroy then imgDestroy(imgBuffer) end + return nil, bufErr or "Failed to get image buffer" end - rows[#rows + 1] = table.concat(rowPixels) + else + -- Fallback: read pixel by pixel (slow) + local imgGetPixel = ImageGetPixel or _G.ImageGetPixel + if not imgGetPixel then + if imgDestroy then imgDestroy(imgBuffer) end + return nil, "ImageGetPixel not available" + end + local rows = {} + for y = 0, height - 1 do + local rowBytes = {} + for x = 0, width - 1 do + local r, g, b, a = imgGetPixel(imgBuffer, x, y) + local idx = x * 4 + rowBytes[idx + 1] = b or 0 + rowBytes[idx + 2] = g or 0 + rowBytes[idx + 3] = r or 0 + rowBytes[idx + 4] = a or 255 + end + rows[#rows + 1] = string.char(unpack(rowBytes)) + end + self.buffer = table.concat(rows) end - self.buffer = table.concat(rows) + if imgDestroy then + imgDestroy(imgBuffer) + end + + return self +end + +-- Internal: Load JPEG file using existing decoder (converts to BGRA) +local function loadJPEG(path) + -- Get functions from _G (they may be in sandbox env) + local jpegLoad = JPEGLoad or _G.JPEGLoad + local imgGetWidth = ImageGetWidth or _G.ImageGetWidth + local imgGetHeight = ImageGetHeight or _G.ImageGetHeight + local imgGetBufferBGRA = ImageGetBufferBGRA or _G.ImageGetBufferBGRA + local imgDestroy = ImageDestroy or _G.ImageDestroy + + if not jpegLoad or not imgGetWidth or not imgGetHeight then + return nil, "JPEG decoder functions not available" + end - -- Free C image buffer - if ImageDestroy then - ImageDestroy(imgBuffer) + local jpegData, err = readFile(path) + if not jpegData then + return nil, err + end + + local imgBuffer = jpegLoad(jpegData) + if not imgBuffer then + return nil, "Failed to decode JPEG: " .. path + end + + local width = imgGetWidth(imgBuffer) + local height = imgGetHeight(imgBuffer) + + local self = setmetatable({}, ImageObj) + self.width = width + self.height = height + self.hasAlpha = false -- JPEG doesn't support alpha + self.bytesPerPixel = 4 -- Still use 4 bytes for screen compatibility + + -- Use fast C function to get entire buffer as BGRA string + if imgGetBufferBGRA then + local buf, bufErr = imgGetBufferBGRA(imgBuffer) + if buf then + self.buffer = buf + else + if imgDestroy then imgDestroy(imgBuffer) end + return nil, bufErr or "Failed to get image buffer" + end + else + -- Fallback: read pixel by pixel (slow) + local imgGetPixel = ImageGetPixel or _G.ImageGetPixel + if not imgGetPixel then + if imgDestroy then imgDestroy(imgBuffer) end + return nil, "ImageGetPixel not available" + end + local rows = {} + for y = 0, height - 1 do + local rowBytes = {} + for x = 0, width - 1 do + local r, g, b = imgGetPixel(imgBuffer, x, y) + local idx = x * 4 + rowBytes[idx + 1] = b or 0 + rowBytes[idx + 2] = g or 0 + rowBytes[idx + 3] = r or 0 + rowBytes[idx + 4] = 255 + end + rows[#rows + 1] = string.char(unpack(rowBytes)) + end + self.buffer = table.concat(rows) + end + + if imgDestroy then + imgDestroy(imgBuffer) end return self end --- Internal: Load BMP file --- @param path Path to BMP file --- @return Image object or nil on error +-- Internal: Load BMP file (converts to BGRA) local function loadBMP(path) - -- Read file using fs or CRamdisk local bmpData, err = readFile(path) if not bmpData then return nil, err @@ -1213,14 +1219,12 @@ local function loadBMP(path) return nil, "Invalid BMP file: too small" end - -- Helper: Read 16-bit little-endian local function readUInt16LE(data, offset) local b1 = string.byte(data, offset) local b2 = string.byte(data, offset + 1) return bit.bor(b1, bit.lshift(b2, 8)) end - -- Helper: Read 32-bit little-endian local function readUInt32LE(data, offset) local b1 = string.byte(data, offset) local b2 = string.byte(data, offset + 1) @@ -1229,12 +1233,10 @@ local function loadBMP(path) return bit.bor(b1, bit.lshift(b2, 8), bit.lshift(b3, 16), bit.lshift(b4, 24)) end - -- Check BMP signature if bmpData:sub(1, 2) ~= "BM" then return nil, "Not a valid BMP file" end - -- Read BMP header local pixelDataOffset = readUInt32LE(bmpData, 11) local headerSize = readUInt32LE(bmpData, 15) @@ -1242,13 +1244,11 @@ local function loadBMP(path) return nil, "Unsupported BMP format (old header)" end - -- Read DIB header (BITMAPINFOHEADER) local width = readUInt32LE(bmpData, 19) local height = readUInt32LE(bmpData, 23) local bitsPerPixel = readUInt16LE(bmpData, 29) local compression = readUInt32LE(bmpData, 31) - -- Validate if compression ~= 0 then return nil, "Compressed BMP not supported" end @@ -1257,54 +1257,48 @@ local function loadBMP(path) return nil, "Only 24-bit and 32-bit BMP supported" end - -- Create Image object local self = setmetatable({}, ImageObj) self.width = width self.height = height self.hasAlpha = (bitsPerPixel == 32) - self.bytesPerPixel = self.hasAlpha and 4 or 3 + self.bytesPerPixel = 4 -- Always 4 bytes for screen compatibility - -- Calculate row size (rows are padded to 4-byte boundary) local srcBytesPerPixel = bitsPerPixel / 8 local rowSize = math.floor((srcBytesPerPixel * width + 3) / 4) * 4 - -- Read pixels (BMP is bottom-up, BGR/BGRA format) - local buffer = {} - for y = height - 1, 0, -1 do - local rowOffset = pixelDataOffset + (height - 1 - y) * rowSize + 1 + -- Read pixels (BMP is bottom-up, BGR format) and convert to BGRA + local rows = {} + for y = 0, height - 1 do + local rowPixels = {} + -- BMP rows are stored bottom-to-top + local srcY = height - 1 - y + local rowOffset = pixelDataOffset + srcY * rowSize + 1 for x = 0, width - 1 do local pixelOffset = rowOffset + x * srcBytesPerPixel - -- Read BGR(A) local b = string.byte(bmpData, pixelOffset) local g = string.byte(bmpData, pixelOffset + 1) local r = string.byte(bmpData, pixelOffset + 2) local a = 255 - if self.hasAlpha then + if srcBytesPerPixel == 4 then a = string.byte(bmpData, pixelOffset + 3) end - -- Store as RGB(A) - local bufferIndex = (y * width + x) * self.bytesPerPixel + 1 - buffer[bufferIndex] = string.char(r) - buffer[bufferIndex + 1] = string.char(g) - buffer[bufferIndex + 2] = string.char(b) - - if self.hasAlpha then - buffer[bufferIndex + 3] = string.char(a) - end + -- Store as BGRA + rowPixels[#rowPixels + 1] = string.char(b, g, r, a) end + rows[#rows + 1] = table.concat(rowPixels) end - self.buffer = table.concat(buffer) + self.buffer = table.concat(rows) return self end -- Open image file (auto-detect format from extension) --- @param path Path to image file (.png or .bmp) +-- @param path Path to image file (.png, .bmp, .jpg, .jpeg) -- @return Image object or nil on error function Image.open(path) checkPermission() @@ -1313,7 +1307,6 @@ function Image.open(path) error("Image.open requires a file path") end - -- Detect format from extension local extension = path:match("%.([^%.]+)$") if not extension then return nil, "Cannot determine file format (no extension)" @@ -1325,6 +1318,8 @@ function Image.open(path) return loadPNG(path) elseif extension == "bmp" then return loadBMP(path) + elseif extension == "jpg" or extension == "jpeg" then + return loadJPEG(path) else return nil, "Unsupported image format: " .. extension end diff --git a/iso_includes/os/libs/Run.lua b/iso_includes/os/libs/Run.lua @@ -1660,6 +1660,7 @@ function run.execute(app_name, fsRoot) sandbox_env.ImageGetWidth = _G.ImageGetWidth sandbox_env.ImageGetHeight = _G.ImageGetHeight sandbox_env.ImageGetPixel = _G.ImageGetPixel + sandbox_env.ImageGetBufferBGRA = _G.ImageGetBufferBGRA sandbox_env.ImageDestroy = _G.ImageDestroy -- Add partial window update function for efficient drawing (e.g., paint apps) @@ -2102,6 +2103,14 @@ function run.execute(app_name, fsRoot) allowed_keys.JPEGLoad = true allowed_keys.PNGLoad = true allowed_keys.BMPLoad = true + allowed_keys.ImageGetWidth = true + allowed_keys.ImageGetHeight = true + allowed_keys.ImageGetPixel = true + allowed_keys.ImageGetBufferBGRA = true + allowed_keys.ImageDraw = true + allowed_keys.ImageDrawScaled = true + allowed_keys.ImageDestroy = true + allowed_keys.ImageGetInfo = true setmetatable(sandbox_env, { __index = function(t, k) diff --git a/kernel.c b/kernel.c @@ -1038,6 +1038,19 @@ void usermode_function(void) { lua_pushcfunction(L, lua_image_rotate); lua_setglobal(L, "ImageRotate"); + lua_pushcfunction(L, lua_image_get_width); + lua_setglobal(L, "ImageGetWidth"); + + lua_pushcfunction(L, lua_image_get_height); + lua_setglobal(L, "ImageGetHeight"); + + lua_pushcfunction(L, lua_image_get_pixel); + lua_setglobal(L, "ImageGetPixel"); + + extern int lua_image_get_buffer_bgra(lua_State* L); + lua_pushcfunction(L, lua_image_get_buffer_bgra); + lua_setglobal(L, "ImageGetBufferBGRA"); + /* Test simple Lua execution first */ terminal_writestring("Testing simple Lua expression...\n"); const char* simple_test = "osprint('Simple test works!\\n')"; diff --git a/libc.c b/libc.c @@ -131,10 +131,31 @@ int toupper(int c) { /* ========== Memory Allocation ========== */ -/* Simple bump allocator - NOT suitable for production */ -#define HEAP_SIZE (256 * 1024 * 1024) /* 256 MB heap for LuaJIT + ramdisk + apps */ +/* + * Free-list allocator with block coalescing + * Each block has a header with size and free/used flag + * Free blocks are linked in a free list for reuse + */ + +#define HEAP_SIZE (384 * 1024 * 1024) /* 384 MB heap for LuaJIT + ramdisk + apps */ static uint8_t heap[HEAP_SIZE] __attribute__((aligned(4096))); -static size_t heap_pos = 0; + +/* Block header - stored before each allocation */ +typedef struct block_header { + size_t size; /* Size of user data (not including header) */ + struct block_header *next_free; /* Next free block (only valid if free) */ + uint32_t magic; /* Magic number for validation */ + uint32_t is_free; /* 1 if free, 0 if allocated */ +} block_header_t; + +#define BLOCK_MAGIC 0xDEADBEEF +#define HEADER_SIZE ((sizeof(block_header_t) + 15) & ~15) /* Aligned header size */ +#define MIN_BLOCK_SIZE 32 /* Minimum user data size */ + +/* Free list head */ +static block_header_t *free_list = NULL; +static int heap_initialized = 0; +static size_t heap_end = 0; /* Current end of used heap */ extern void terminal_writestring(const char *); extern void terminal_putchar(char); @@ -154,50 +175,147 @@ static void print_hex(unsigned long val) { while (i > 0) terminal_putchar(buf[--i]); } +/* Initialize heap if needed */ +static void heap_init(void) { + if (heap_initialized) return; + heap_initialized = 1; + heap_end = 0; + free_list = NULL; +} + +/* Get header from user pointer */ +static inline block_header_t *get_header(void *ptr) { + return (block_header_t *)((uint8_t *)ptr - HEADER_SIZE); +} + +/* Get user pointer from header */ +static inline void *get_user_ptr(block_header_t *header) { + return (void *)((uint8_t *)header + HEADER_SIZE); +} + +/* Find a free block that fits, using first-fit strategy */ +static block_header_t *find_free_block(size_t size) { + block_header_t *curr = free_list; + block_header_t *prev = NULL; + + while (curr) { + if (curr->size >= size) { + /* Remove from free list */ + if (prev) { + prev->next_free = curr->next_free; + } else { + free_list = curr->next_free; + } + curr->next_free = NULL; + curr->is_free = 0; + return curr; + } + prev = curr; + curr = curr->next_free; + } + return NULL; +} + +/* Allocate a new block from the heap end */ +static block_header_t *alloc_new_block(size_t size) { + size_t total_size = HEADER_SIZE + size; + + if (heap_end + total_size > HEAP_SIZE) { + return NULL; /* Out of memory */ + } + + block_header_t *header = (block_header_t *)&heap[heap_end]; + heap_end += total_size; + + header->size = size; + header->next_free = NULL; + header->magic = BLOCK_MAGIC; + header->is_free = 0; + + return header; +} + void *malloc(size_t size) { if (size == 0) return NULL; - /* Align to 16 bytes */ + + heap_init(); + + /* Align size to 16 bytes, with minimum */ size = (size + 15) & ~15; - if (heap_pos + size > HEAP_SIZE) { - terminal_writestring("MALLOC OOM: size="); - print_hex(size); - terminal_writestring(" heap_pos="); - print_hex(heap_pos); - terminal_writestring("\n"); - return NULL; /* Out of memory */ + if (size < MIN_BLOCK_SIZE) size = MIN_BLOCK_SIZE; + + /* Try to find a free block first */ + block_header_t *header = find_free_block(size); + + if (!header) { + /* No suitable free block, allocate new */ + header = alloc_new_block(size); + if (!header) { + terminal_writestring("MALLOC OOM: size="); + print_hex(size); + terminal_writestring(" heap_end="); + print_hex(heap_end); + terminal_writestring("\n"); + return NULL; + } } - void *ptr = &heap[heap_pos]; - heap_pos += size; - memset(ptr, 0, size); /* Zero initialize */ - - /* Debug: show large allocations */ - // Disabled to reduce boot noise - // if (size > 4096) { - // terminal_writestring("MALLOC: size="); - // print_hex(size); - // terminal_writestring(" ptr="); - // print_hex((unsigned long)ptr); - // terminal_writestring("\n"); - // } + void *ptr = get_user_ptr(header); + memset(ptr, 0, header->size); /* Zero initialize */ return ptr; } void free(void *ptr) { - /* Simple allocator doesn't support free */ - (void)ptr; + if (!ptr) return; + + block_header_t *header = get_header(ptr); + + /* Validate the block */ + if (header->magic != BLOCK_MAGIC) { + terminal_writestring("FREE: invalid pointer (bad magic)\n"); + return; + } + + if (header->is_free) { + terminal_writestring("FREE: double free detected\n"); + return; + } + + /* Mark as free and add to free list (front insertion) */ + header->is_free = 1; + header->next_free = free_list; + free_list = header; } void *realloc(void *ptr, size_t size) { - /* Simple implementation - always allocate new */ + if (!ptr) return malloc(size); + if (size == 0) { free(ptr); return NULL; } + + block_header_t *header = get_header(ptr); + + /* Validate */ + if (header->magic != BLOCK_MAGIC) { + terminal_writestring("REALLOC: invalid pointer\n"); + return NULL; + } + + /* If current block is large enough, reuse it */ + size_t aligned_size = (size + 15) & ~15; + if (aligned_size < MIN_BLOCK_SIZE) aligned_size = MIN_BLOCK_SIZE; + + if (header->size >= aligned_size) { + return ptr; /* Current block is big enough */ + } + + /* Need to allocate new block */ void *new_ptr = malloc(size); - if (new_ptr && ptr) { - /* Copy old data - we don't know the old size, so just copy size bytes */ - memcpy(new_ptr, ptr, size); + if (new_ptr) { + memcpy(new_ptr, ptr, header->size); /* Copy old data */ + free(ptr); } return new_ptr; } @@ -911,7 +1029,7 @@ int close(int fd) { return 0; } -/* mmap - memory map stub (just allocates from heap) */ +/* mmap - memory map stub (uses our allocator with block headers) */ /* All memory is executable in bare metal x86, so PROT_EXEC works automatically */ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) { (void)addr; (void)fd; (void)offset; @@ -934,26 +1052,22 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) /* Align to page boundary (4KB) */ size_t aligned_length = (length + 4095) & ~4095; - if (heap_pos + aligned_length > HEAP_SIZE) { + /* Use malloc which now supports free */ + void *ptr = malloc(aligned_length); + if (!ptr) { terminal_writestring("MMAP OOM: need="); print_hex(aligned_length); - terminal_writestring(" used="); - print_hex(heap_pos); - terminal_writestring(" total="); - print_hex(HEAP_SIZE); + terminal_writestring(" heap_end="); + print_hex(heap_end); terminal_writestring("\n"); errno = 12; /* ENOMEM */ return (void *)-1; } - void *ptr = &heap[heap_pos]; - heap_pos += aligned_length; - memset(ptr, 0, aligned_length); - terminal_writestring("MMAP OK: ptr="); print_hex((unsigned long)ptr); terminal_writestring(" used="); - print_hex(heap_pos); + print_hex(heap_end); terminal_writestring("/"); print_hex(HEAP_SIZE); terminal_writestring("\n"); @@ -967,7 +1081,11 @@ void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off_t offse } int munmap(void *addr, size_t length) { - (void)addr; (void)length; + (void)length; + /* Actually free the memory now */ + if (addr) { + free(addr); + } return 0; } @@ -1113,7 +1231,7 @@ double modf(double x, double *iptr) { return x - int_part; } -/* Memory remapping - allocate new and copy */ +/* Memory remapping - allocate new and copy, free old */ void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...) { (void)flags; @@ -1125,24 +1243,19 @@ void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ... /* Align to page boundary */ new_size = (new_size + 4095) & ~4095; - if (heap_pos + new_size > HEAP_SIZE) { + /* Allocate new memory using our allocator */ + void *new_ptr = malloc(new_size); + if (!new_ptr) { errno = 12; /* ENOMEM */ return (void *)-1; } - /* Allocate new memory */ - void *new_ptr = &heap[heap_pos]; - heap_pos += new_size; - /* Copy old data */ if (old_address && old_size > 0) { size_t copy_size = old_size < new_size ? old_size : new_size; memcpy(new_ptr, old_address, copy_size); - } - - /* Zero remaining */ - if (new_size > old_size) { - memset((char*)new_ptr + old_size, 0, new_size - old_size); + /* Free the old memory */ + free(old_address); } return new_ptr; diff --git a/luajit_init.c b/luajit_init.c @@ -1201,6 +1201,10 @@ __attribute__((naked)) void usermode_main(void) { lua_pushcfunction(L, lua_image_get_pixel); lua_setglobal(L, "ImageGetPixel"); + extern int lua_image_get_buffer_bgra(lua_State* L); + lua_pushcfunction(L, lua_image_get_buffer_bgra); + lua_setglobal(L, "ImageGetBufferBGRA"); + /* Register compression functions */ lua_pushcfunction(L, lua_deflate_compress); lua_setglobal(L, "DeflateCompress");