diff --git a/OCEmu.desktop b/OCEmu.desktop new file mode 100755 index 0000000..d5d0b09 --- /dev/null +++ b/OCEmu.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Exec=/usr/bin/lua $OCEMU_PATH/src/boot.lua +GenericName=OCEmu +Icon=$OCEMU_PATH/icon.png +Name=OCEmu +Path=$OCEMU_PATH/src +StartupNotify=true +Terminal=false +Type=Application diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..51acc41 Binary files /dev/null and b/icon.png differ diff --git a/src/component/filesystem.lua b/src/component/filesystem.lua index 85a6b64..892d81a 100644 --- a/src/component/filesystem.lua +++ b/src/component/filesystem.lua @@ -1,4 +1,6 @@ -local address, _, directory, label, readonly, speed = ... +local address, _, directory, label, readonly, speed, size = ... +size = size or math.huge +local usedSize = 0 compCheckArg(1,directory,"string","nil") compCheckArg(2,label,"string","nil") compCheckArg(3,readonly,"boolean") @@ -49,6 +51,32 @@ local writeCosts = {1/1, 1/2, 1/3, 1/4, 1/5, 1/6} local mai = {} local obj = {} +local function getAllFiles(dirPath, tab) + tab = tab or {} + local items = elsa.filesystem.getDirectoryItems(directory .. dirPath) + for k, v in pairs(items) do + if elsa.filesystem.isDirectory(directory .. dirPath .. "/" .. v) then + getAllFiles(dirPath .. "/" .. v, tab) + else + table.insert(tab, directory .. dirPath .. "/" .. v) + end + end + return tab +end + +local function calcUsedSpace() + local files = getAllFiles("/") + usedSize = 0 + for k, v in pairs(files) do + local path = v + usedSize = usedSize + 512 -- default OC emulation of "file info" + usedSize = usedSize + elsa.filesystem.getSize(v) + end + return usedSize +end + +calcUsedSpace() -- get used space + mai.read = {direct = true, limit = 15, doc = "function(handle:number, count:number):string or nil -- Reads up to the specified amount of data from an open file descriptor with the specified handle. Returns nil when EOF is reached."} function obj.read(handle, count) --TODO @@ -79,9 +107,8 @@ end mai.spaceUsed = {direct = true, doc = "function():number -- The currently used capacity of the file system, in bytes."} function obj.spaceUsed() - --STUB cprint("filesystem.spaceUsed") - return 0 + return usedSize end mai.rename = {doc = "function(from:string, to:string):boolean -- Renames/moves an object from the first specified absolute path in the file system to the second."} @@ -131,6 +158,11 @@ function obj.write(handle, value) if handles[handle] == nil or (handles[handle][2] ~= "w" and handles[handle][2] ~= "a") then return nil, "bad file descriptor" end + local len = value:len() + if usedSize + len > size then + return nil, "not enough space" -- todo use OC error message + end + usedSize = usedSize + len -- if sucedded, add to used space. handles[handle][1]:write(value) return true end @@ -176,9 +208,8 @@ end mai.spaceTotal = {direct = true, doc = "function():number -- The overall capacity of the file system, in bytes."} function obj.spaceTotal() - --STUB cprint("filesystem.spaceTotal") - return math.huge + return size end mai.getLabel = {direct = true, doc = "function():string -- Get the current label of the file system."} diff --git a/src/component/gpu.lua b/src/component/gpu.lua index 4e4f930..3bb42a9 100644 --- a/src/component/gpu.lua +++ b/src/component/gpu.lua @@ -16,11 +16,203 @@ local setPaletteColorCosts = {1/2, 1/8, 1/16} local setCosts = {1/64, 1/128, 1/256} local copyCosts = {1/16, 1/32, 1/64} local fillCosts = {1/32, 1/64, 1/128} +local bitbltCost = 0.5 * math.pow(2, maxtier) -- gpu component local mai = {} local obj = {} +local activeBufferIdx = 0 -- 0 = screen +local buffers = {} +local totalMemory = maxwidth*maxheight*maxtier +local usedMemory = 0 + +local function bufferSet(buf, x, y, char, fg, bg) + if x > buf.width or y > buf.height or x < 1 or y < 1 then + return false + end + local pos = (y-1) * buf.width + x + fg = fg or buf.fg + bg = bg or buf.bg + buf.foreground[pos] = fg + buf.background[pos] = bg + local before = utf8.sub(buf.text, 1, pos-1) + local after = utf8.sub(buf.text, pos+1) + buf.text = before .. char .. after + buf.dirty = true + return true +end + +local function bufferGet(buf, x, y) + local pos = (y-1) * buf.width + x + local char = utf8.sub(buf.text, pos, pos) + local fg = buf.foreground[pos] or 0xFFFFFF + local bg = buf.background[pos] or 0 + return char, fg, bg +end + +local function consumeGraphicCallBudget(cost) + if activeBufferIdx == 0 then + return machine.consumeCallBudget(cost) + else + return true + end +end + +mai.allocateBuffer = {direct = true, doc = "function([width: number, height: number]): number -- allocates a new buffer with dimensions width*height (defaults to max resolution) and appends it to the buffer list. Returns the index of the new buffer and returns nil with an error message on failure. A buffer can be allocated even when there is no screen bound to this gpu. Index 0 is always reserved for the screen and thus the lowest index of an allocated buffer is always 1."} +function obj.allocateBuffer(width, height) + cprint("gpu.allocateBuffer", width, height) + width = width or maxwidth + height = height or maxheight + + if width <= 0 or height <= 0 then + return false, "invalid page dimensions: must be greater than zero" + end + + local size = width*height + if usedMemory+size > totalMemory then + return false, "not enough video memory" + end + local buffer = { + text = (" "):rep(width*height), + foreground = {}, + background = {}, + width = width, + height = height, + size = width*height, + dirty = true, + fg = 0xFFFFFF, + bg = 0x000000, + bufferGet = bufferGet -- exposure of API for screen_sdl2 + } + usedMemory = usedMemory + size + table.insert(buffers, buffer) + return #buffers +end + +mai.freeBuffer = {direct = true, doc = "function(index: number): boolean -- Closes buffer at `index`. Returns true if a buffer closed. If the current buffer is closed, index moves to 0"} +function obj.freeBuffer(idx) + cprint("gpu.freeBuffer", idx) + if not buffers[idx] then + return false, "no buffer at index" + else + usedMemory = usedMemory - buffers[idx].size + buffers[idx] = nil + if idx == activeBufferIdx then + idx = 0 + end + return true + end +end + +mai.freeAllBuffers = {direct = true, doc = "function(): number -- Closes all buffers and returns the count. If the active buffer is closed, index moves to 0"} +function obj.freeAllBuffers() + local count = #buffers + activeBufferIdx = 0 + buffers = {} + usedMemory = 0 + return count +end + +mai.buffers = {direct = true, doc = "function(): number -- Returns an array of indexes of the allocated buffers"} +function obj.buffers() + local array = {} + for k, v in pairs(buffers) do + table.insert(array, k) + end + return array +end + +mai.getActiveBuffer = {direct = true, doc = "function(): number -- returns the index of the currently selected buffer. 0 is reserved for the screen. Can return 0 even when there is no screen"} +function obj.getActiveBuffer() + return activeBufferIdx +end + +mai.setActiveBuffer = {direct = true, doc = "function(index: number): number -- Sets the active buffer to `index`. 1 is the first vram buffer and 0 is reserved for the screen. returns nil for invalid index (0 is always valid)"} +function obj.setActiveBuffer(idx) + cprint("gpu.setActiveBuffer", idx) + if idx ~= 0 and not buffers[idx] then + return nil + else + activeBufferIdx = idx + end +end + +mai.freeMemory = {direct = true, doc = "function(): number -- returns the total free memory not allocated to buffers. This does not include the screen."} +function obj.freeMemory() + return totalMemory - usedMemory +end + +mai.totalMemory = {direct = true, doc = "function(): number -- returns the total memory size of the gpu vram. This does not include the screen."} +function obj.totalMemory() + return totalMemory +end + +mai.getBufferSize = {direct = true, doc = "function(index: number): number, number -- returns the buffer size at index. Returns the screen resolution for index 0. returns nil for invalid indexes"} +function obj.getBufferSize(idx) + if idx == 0 then + return obj.getResolution() + else + local buf = buffers[idx] + if buf then + return buf.width, buf.height + else + return nil + end + end +end + +local function determineBitbltBudgetCost(src, dst) + if dst ~= "screen" then -- write to buffer from buffer/screen are free + return 0 + elseif src == "screen" then + return 0 + elseif src.dirty then + return bitbltCost * (src.width * src.height) / (maxwidth * maxheight) + elseif not src.dirty then + return 0.001 + end +end + +mai.bitblt = {direct = true, doc = "function([dst: number, col: number, row: number, width: number, height: number, src: number, fromCol: number, fromRow: number]):boolean -- bitblt from buffer to screen. All parameters are optional. Writes to `dst` page in rectangle `x, y, width, height`, defaults to the bound screen and its viewport. Reads data from `src` page at `fx, fy`, default is the active page from position 1, 1"} +function obj.bitblt(dst, col, row, width, height, src, fromCol, fromRow) + cprint("gpu.bitblt", dst, col, row, width, height, src, fromCol, fromRow) + dst = dst or 0 + col = col or 1 + row = row or 1 + src = src or activeBufferIdx + fromCol = fromCol or 1 + fromRow = fromRow or 1 + + if dst == 0 then + if bindaddress == nil then + return nil, "no screen" + end + if not width or not height then + local rw, rh = component.cecinvoke(bindaddress, "getResolution") + width = width or rw + height = height or rh + end + + -- TODO consume call budget + if src == 0 then + -- TODO act as copy() + else + local buf = buffers[src] + if not buf then + return nil + end + local cost = determineBitbltBudgetCost(buf, "screen") + if not machine.consumeCallBudget(cost) then return end + buf.dirty = false + width, height = math.min(buf.width, width), math.min(buf.height, height) + component.cecinvoke(bindaddress, "bitblt", buf, col, row, width, height, fromCol, fromRow) + end + else + + end +end + mai.bind = {doc = "function(address:string):boolean -- Binds the GPU to the screen with the specified address."} function obj.bind(address, reset) cprint("gpu.bind", address, reset) @@ -42,6 +234,9 @@ function obj.bind(address, reset) component.cecinvoke(bindaddress, "setDepth", math.min(component.cecinvoke(bindaddress, "maxDepth"), maxtier)) component.cecinvoke(bindaddress, "setForeground", 0xFFFFFF) component.cecinvoke(bindaddress, "setBackground", 0x000000) + buffers = {} + usedMemory = 0 + activeBufferIdx = 0 end end @@ -51,14 +246,18 @@ function obj.getForeground() if bindaddress == nil then return nil, "no screen" end - return component.cecinvoke(bindaddress, "getForeground") + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "getForeground") + else + return buffers[activeBufferIdx].fg + end end mai.setForeground = {direct = true, doc = "function(value:number[, palette:boolean]):number, number or nil -- Sets the foreground color to the specified value. Optionally takes an explicit palette index. Returns the old value and if it was from the palette its palette index."} function obj.setForeground(value, palette) cprint("gpu.setForeground", value, palette) - if not machine.consumeCallBudget(setForegroundCosts[maxtier]) then return end + if not consumeGraphicCallBudget(setForegroundCosts[maxtier]) then return end compCheckArg(1,value,"number") compCheckArg(2,palette,"boolean","nil") if bindaddress == nil then @@ -70,7 +269,11 @@ function obj.setForeground(value, palette) if palette == true and (value < 0 or value > 15) then error("invalid palette index", 0) end - return component.cecinvoke(bindaddress, "setForeground", value, palette) + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "setForeground", value, palette) + else + buffers[activeBufferIdx].fg = value + end end mai.getBackground = {direct = true, doc = "function():number, boolean -- Get the current background color and whether it's from the palette or not."} @@ -79,13 +282,17 @@ function obj.getBackground() if bindaddress == nil then return nil, "no screen" end - return component.cecinvoke(bindaddress, "getBackground") + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "getBackground") + else + return buffers[activeBufferIdx].bg + end end mai.setBackground = {direct = true, doc = "function(value:number[, palette:boolean]):number, number or nil -- Sets the background color to the specified value. Optionally takes an explicit palette index. Returns the old value and if it was from the palette its palette index."} function obj.setBackground(value, palette) cprint("gpu.setBackground", value, palette) - if not machine.consumeCallBudget(setBackgroundCosts[maxtier]) then return end + if not consumeGraphicCallBudget(setBackgroundCosts[maxtier]) then return end compCheckArg(1,value,"number") compCheckArg(2,palette,"boolean","nil") if bindaddress == nil then @@ -98,7 +305,11 @@ function obj.setBackground(value, palette) if palette and (value < 0 or value > 15) then error("invalid palette index", 0) end - return component.cecinvoke(bindaddress, "setBackground", value, palette) + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "setBackground", value, palette) + else + buffers[activeBufferIdx].bg = value + end end mai.getDepth = {direct = true, doc = "function():number -- Returns the currently set color depth."} @@ -133,7 +344,7 @@ end mai.fill = {direct = true, doc = "function(x:number, y:number, width:number, height:number, char:string):boolean -- Fills a portion of the screen at the specified position with the specified size with the specified character."} function obj.fill(x, y, width, height, char) cprint("gpu.fill", x, y, width, height, char) - if not machine.consumeCallBudget(fillCosts[maxtier]) then return end + if not consumeGraphicCallBudget(fillCosts[maxtier]) then return end compCheckArg(1,x,"number") compCheckArg(2,y,"number") compCheckArg(3,width,"number") @@ -145,7 +356,17 @@ function obj.fill(x, y, width, height, char) if utf8.len(char) ~= 1 then return nil, "invalid fill value" end - return component.cecinvoke(bindaddress, "fill", x, y, width, height, char) + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "fill", x, y, width, height, char) + else + local buf = buffers[activeBufferIdx] + for dx=0, width-1 do + for dy=0, height-1 do + bufferSet(buf, x+dx, y+dy, char) + end + end + return true + end end mai.getScreen = {direct = true, doc = "function():string -- Get the address of the screen the GPU is currently bound to."} @@ -260,17 +481,21 @@ function obj.get(x, y) if bindaddress == nil then return nil, "no screen" end - local w,h = component.cecinvoke(bindaddress, "getResolution") + local w,h = obj.getResolution() if x < 1 or x >= w+1 or y < 1 or y >= h+1 then error("index out of bounds", 0) end - return component.cecinvoke(bindaddress, "get", x, y) + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "get", x, y) + else + return bufferGet(buffers[activeBufferIdx], x, y), nil, nil + end end mai.set = {direct = true, doc = "function(x:number, y:number, value:string[, vertical:boolean]):boolean -- Plots a string value to the screen at the specified position. Optionally writes the string vertically."} function obj.set(x, y, value, vertical) cprint("gpu.set", x, y, value, vertical) - if not machine.consumeCallBudget(setCosts[maxtier]) then return end + if not consumeGraphicCallBudget(setCosts[maxtier]) then return end compCheckArg(1,x,"number") compCheckArg(2,y,"number") compCheckArg(3,value,"string") @@ -278,7 +503,19 @@ function obj.set(x, y, value, vertical) if bindaddress == nil then return nil, "no screen" end - return component.cecinvoke(bindaddress, "set", x, y, value, vertical) + if activeBufferIdx == 0 then + return component.cecinvoke(bindaddress, "set", x, y, value, vertical) + else + for i=1, utf8.len(value) do + local ch = utf8.sub(value, i, i) + if vertical then + bufferSet(buffers[activeBufferIdx], x, y+i-1, ch) + else + bufferSet(buffers[activeBufferIdx], x+i-1, y, ch) + end + end + return true + end end mai.copy = {direct = true, doc = "function(x:number, y:number, width:number, height:number, tx:number, ty:number):boolean -- Copies a portion of the screen from the specified location with the specified size by the specified translation."} diff --git a/src/component/screen_sdl2.lua b/src/component/screen_sdl2.lua index 2799161..e10af08 100644 --- a/src/component/screen_sdl2.lua +++ b/src/component/screen_sdl2.lua @@ -15,6 +15,9 @@ local scrrfc, scrrbc = scrfgc, scrbgc local palcol = {} local precise = false +local obj = {type="screen"} + + t3pal = {} for i = 0,15 do t3pal[i] = (i+1)*0x0F0F0F @@ -84,6 +87,11 @@ function elsa.mousebuttonup(event) if bttndown and buttons[mbevent.button] then table.insert(machine.signals,{"drop",address,lx,ly,buttons[mbevent.button]}) bttndown = nil + else + if elsa.SDL.hasClipboardText() then + local text = ffi.string(elsa.SDL.getClipboardText()) + table.insert(machine.signals, {"clipboard", obj.getKeyboards()[1], text}) + end end end @@ -328,7 +336,6 @@ local touchinvert = false -- screen component local mai = {} -local obj = {type="screen"} mai.isTouchModeInverted = {doc = "function():boolean -- Whether touch mode is inverted (sneak-activate opens GUI, instead of normal activate)."} function obj.isTouchModeInverted() @@ -497,6 +504,25 @@ function cec.fill(x1, y1, w, h, char) -- Fills a portion of the screen at the sp end return true end +function cec.bitblt(buf, col, row, w, h, fromCol, fromRow) + cprint("(cec) screen.bitblt", tostring(buf), col, row, w, h, fromCol, fromRow) + local oldFg = srcfgc + local oldBg = srcbgc + for y=0, h-1 do + for x=0, w-1 do + local char, fg, bg = buf:bufferGet(x+fromCol, y+fromRow) + local dx = x+col + local dy = y+row + if dx >= 1 and dx <= width and dy >= 1 and dy <= height then + srcfgc = fg + srcbgc = bg + setPos(dx, dy, utf8.byte(char), fg, bg) + end + end + end + srcfgc = oldFg + srcbgc = oldBg +end function cec.getResolution() -- Get the current screen resolution. cprint("(cec) screen.getResolution") return width, height diff --git a/src/config.lua b/src/config.lua index 8b0e819..d4ca025 100644 --- a/src/config.lua +++ b/src/config.lua @@ -21,6 +21,7 @@ local comments = { ["filesystem.floppySize"]="The size of writable floppy disks, in kilobytes.", ["filesystem.hddPlatterCounts"]="Number of physical platters to pretend a disk has in unmanaged mode. This controls seek times, in how it emulates sectors overlapping (thus sharing a common head position for access).", ["filesystem.hddSizes"]="The sizes of the three tiers of hard drives, in kilobytes. This list must contain exactly three entries, or it will be ignored.", +["emulator.profiler"]="Whether to enable real-time profiler or not.", ["filesystem.maxReadBuffer"]="The maximum block size that can be read in one 'read' call on a file system. This is used to limit the amount of memory a call from a user program can cause to be allocated on the host side: when 'read' is, called a byte array with the specified size has to be allocated. So if this weren't limited, a Lua program could trigger massive memory allocations regardless of the amount of RAM installed in the computer it runs on. As a side effect this pretty much determines the read performance of file systems.", ["internet.enableHttp"]="Whether to allow HTTP requests via internet cards. When enabled, the `request` method on internet card components becomes available.", ["internet.enableTcp"]="Whether to allow TCP connections via internet cards. When enabled, the `connect` method on internet card components becomes available.", diff --git a/src/main.lua b/src/main.lua index 16f2372..b9b3c79 100644 --- a/src/main.lua +++ b/src/main.lua @@ -96,6 +96,21 @@ if settings.components == nil then config.set("emulator.components",settings.components) end +--[[local memoryUsages = {} + +local profilerHook = function(event) + local func = debug.getinfo(2) + if func == nil then return end + local name = func.name + if name ~= nil then + if event == "call" or event == "tail call" then + memoryUsages[name] = collectgarbage("count") + elseif event == "return" then + memoryUsages[name] = collectgarbage("count") - memoryUsages[name] + end + end +end]] + local maxCallBudget = (1.5 + 1.5 + 1.5) / 3 -- T3 CPU and 2 T3+ memory machine = { @@ -211,7 +226,13 @@ local env = { }, collectgarbage = collectgarbage, coroutine = { - create = coroutine.create, + create = function(...) + local c = coroutine.create(...) + if settings.profiler then + debug.sethook(c, profilerHook, "cr") + end + return c + end, resume = coroutine.resume, running = coroutine.running, status = coroutine.status, @@ -227,7 +248,18 @@ local env = { getregistry = debug.getregistry, getupvalue = debug.getupvalue, getuservalue = debug.getuservalue, - sethook = debug.sethook, + sethook = function(...) + if not select(1, ...) then + if settings.profiler then + cprint("attempt to clear hooks") + debug.sethook() + cprint("adding profiler hook") + debug.sethook(profilerHook, "cr") + end + else + debug.sethook(...) + end + end, setlocal = debug.setlocal, setmetatable = debug.setmetatable, setupvalue = debug.setupvalue, @@ -384,6 +416,10 @@ function boot_machine() error("Failed to parse machine.lua\n\t" .. tostring(err)) end machine.thread = coroutine.create(machine_fn) + if settings.profiler then + print("hook profiler to machine thread") + debug.sethook(machine.thread, profilerHook, "cr") + end local results = { coroutine.resume(machine.thread) } if results[1] then if #results ~= 1 then