Print images to Simulator console

Maybe it would only work in Simulator, not a tethered device? Maybe it would save where screenshots go, but in a subfolder that auto-deletes when the Simulator quits or clears the console? Maybe images would look janky and scroll laggily because they are a hacky overlay on the text window?

I don't care... any limited means of print(image) integrated with the console's text flow would be amazing!

I'm always spending time on awkward ways to check the contents of image variables relating to other variables, often interfering with the very game I'm trying to test. If I could dump an image into the console amid the stream of other text, I'd be in heaven!

If it obeyed the "use device colors" setting, that would allow transparency to be visible in both light and dark mode. Or, fill in transparency with a color.

Save or drag included images? Optional--not worth skipping the feature over such niceties!

(stdout could omit them or just dump some asterisks or something.)

1 Like

Did I say "awkward ways to check image variables"? FWIW, here's my current awkward method--the images go to the Playdate screen of course, but with some console text for reference:

I started work on an image viewer that would allow you to print() an image variable and it would open it in a small window. You could also select an image from the Memory window and view it. However, it's been side-lined for now due to more pressing things. I should get back to it. Would this sort of thing work for you?

3 Likes

That sounds very useful! Especially if it didn't show just the latest image "printed," but a history of multiple (forward/back, scrolling, grid, whatever's easiest to make!)

Ideally it would be nice to tie it in some way to console output--or to be able to include a string right in the image viewer--so you know WHAT you are looking at in context. Or even simply output a marker into the console with a number, and then number images in the viewer window.

Or maybe images could pop up from clickable "links" in the console? Just thinking out loud.

1 Like

Apologies for the necromancy on an old thread.

I have a trivial function I used to peek at images in the simulator console:

--- Takes a playdate image and returns a multiline string for printing.
function image_str(img)
    local black <const> = playdate.graphics.kColorBlack
    local white <const> = playdate.graphics.kColorClear
    local out = {}
    local w, h = img:getSize()
    for y = 0,h-1 do
        local s = {}
        for x = 0,w-1 do
            local p = img:sample(x,y)
            if p == black then
                s[#s+1] = "⬛"
            elseif p == white then
                s[#s+1] = "⬜"
            else
                s[#s+1] = "🟨"
            end
        end
        out[#out+1] = table.concat(s)
    end
    return table.concat(out, "\n")
end

This generates output like the following (112x96pixel)

For fun, tonight I wrote a version that dithers to unicode block characters:

--- Dithers a playdate image to a string half the size of the original
function img_str4(img)
    local black <const> = playdate.graphics.kColorBlack
    local map = {
        ["⬛⬜⬜⬜"] = "▘", ["⬜⬛⬜⬜"] = "▝", ["⬜⬜⬛⬜"] = "▖", ["⬜⬜⬜⬛"] = "▗",
        ["⬛⬛⬜⬜"] = "▀", ["⬜⬛⬜⬛"] = "▐", ["⬜⬜⬛⬛"] = "▄", ["⬛⬜⬛⬜"] = "▌",
        ["⬜⬜⬜⬜"] = " ", ["⬛⬛⬛⬛"] = "█", ["⬛⬜⬜⬛"] = "▚", ["⬜⬛⬛⬜"] = "▞",
        ["⬛⬛⬛⬜"] = "▛", ["⬛⬛⬜⬛"] = "▜", ["⬛⬜⬛⬛"] = "▙", ["⬜⬛⬛⬛"] = "▟",
    }
    local out = {}
    local w, h = img:getSize()
    for y = 0,h-1,2 do
        local l = {}
        for x = 0,w-1,2 do
            local s = table.concat({
                img:sample(x,y)     == black and "⬛" or "⬜",
                img:sample(x+1,y)   == black and "⬛" or "⬜",
                img:sample(x,y+1)   == black and "⬛" or "⬜",
                img:sample(x+1,y+1) == black and "⬛" or "⬜",
            })
            l[#l+1] = map[s]
        end
        out[#out+1] = table.concat(l)
    end
    return table.concat(out, "\n")
end

Alternatively you can directly launch file:// urls of images you write to the Playdate disk which will open the gif file in Preview.

function preview_image(img, username, filename)
    filename = filename or "img.gif"
    username = username or "peter"
    playdate.datastore.writeImage(img, filename)
    local file_path = string.format(
        "/Users/%s/Developer/PlaydateSDK/Disk/Data/%s/images/%s",
        username, playdate.metadata.bundleID, filename
    )
    playdate.simulator.openURL(string.format("file://%s", file_path))
    print("Preview image at", file_path)
end
7 Likes

That's a cool approach!

On the simulator, you can also take a "shared memory" approach.

In your code, something like below (C++ for Linux, but self-explanatory):

#if __has_include(<sys/mman.h>) && __has_include(<sys/stat.h>) 
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <string>
#include <cassert>

class shared_memory {
public:
    void *shmp = nullptr;

    shared_memory(const shared_memory&) = delete;
    shared_memory(shared_memory&&) = delete;
    shared_memory *operator=(const shared_memory&) = delete;
    shared_memory *operator=(shared_memory&&) = delete;

    shared_memory(std::string_view name, std::size_t size): name(name) {
        const int fd = ::shm_open(this->name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600);
        assert(fd != -1);
        const int r = ::ftruncate(fd, static_cast<off_t>(size));
        assert(r != -1);
        shmp = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        assert(shmp != MAP_FAILED);
    }

    ~shared_memory() {
        if(!name.empty())
            ::shm_unlink(name.c_str());
    }

private:
    std::string name;
};

#endif // __has_include(<sys/mman.h>) && __has_include(<sys/stat.h>) 

And have a quick client in python

#!/usr/bin/env python
import pygame as pg
from multiprocessing.shared_memory import SharedMemory
from contextlib import closing, contextmanager
from multiprocessing.resource_tracker import unregister

@contextmanager
def shared_memoryview(name: str) -> memoryview:
    with closing(SharedMemory(name=name)) as result:
        unregister('/' + name, "shared_memory")
        yield result.buf

def frame_index(x, y):
    return (239 - y) * 400 + x

def main():
    pg.init()

    pg.display.set_mode((400, 240))
    surface = pg.Surface((400, 240))

    pg.display.flip()

    with (shared_memoryview('xbuffer') as sm_x,
         shared_memoryview('ybuffer') as sm_y,
         shared_memoryview('zbuffer') as sm_z,
         shared_memoryview('cbuffer') as sm_c):
        with sm_x.cast('f') as buf_x, sm_y.cast('f') as buf_y, sm_z.cast('f') as buf_z:

            def blit_on_surface():
                with pg.PixelArray(surface) as ar:
                    for y in range(240):
                        for x in range(400):
                            i = frame_index(x, y)
                            #r, g, b = sm.buf[(y * 400 + x) * 3:(y * 400 + x) * 3 + 3]
                            #r, g, b = (buf_x[i] / 4, buf_y[i] / 4, buf_z[i] / 4)
                            r, g, b = (sm_c[i] * 3, sm_c[i] * 3, sm_c[i] * 3)
                            ar[x, y] = (r, g , b)

            def blit_on_screen():
                screen = pg.display.get_surface()
                screen.blit(surface, (0, 0))
                pg.display.flip()

            last_blit = 0
            while True:
                now = pg.time.get_ticks()
                if now - last_blit > 1000:
                    last_blit = now
                    blit_on_surface()
                    blit_on_screen()
                
                for event in pg.event.get():
                    if event.type == pg.MOUSEMOTION:
                        mouse_x, mouse_y = pg.mouse.get_pos()
                        i = frame_index(mouse_x, mouse_y)
                        print(mouse_x, mouse_y)
                        print(buf_x[i],buf_y[i],buf_z[i])
                    if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
                        pg.quit()
                        raise SystemExit
                    if event.type == pg.QUIT:
                        pg.quit()
                        raise SystemExit


if __name__ == "__main__":
    main()

This is something I hacked for a specific bug but I hope it could help

1 Like

Hehe, that's pretty neat (-: Might be worth a small dedicated "tips & tricks" post.

Next step: Braille charset! (-; E.g., via the first arbitrary link found on the web:

⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢀⣀⣀⣀⣀⣀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣠⣾⣿⡿⠿⠿⠿⠿⢿⣿⣶⣤⣤⣶⣶⣶⣿⣷⣶⣦⣄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣰⣿⡟⠁⠄⠄⠄⠄⠄⠄⠄⠉⠛⠛⠉⠉⠁⠄⠈⠉⠙⠿⣿⣦⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢰⣿⡟⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⢿⣿⣦⣴⣶⣶⣶⣦⣀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣾⣿⠃⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⢿⠿⠟⠛⠛⠛⠿⣿⣿⣦⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢠⣿⡏⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣠⣶⡆⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⠿⣿⣦⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣸⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⠄⢠⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⠄⣠⣤⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⠻⣿⣦⣀⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢠⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣼⣿⡟⠄⠄⠄⠄⠄⠄⠄⠄⢰⣿⣿⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⠻⢿⣷⣄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣾⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⠃⠄⠄⠄⠄⠄⠄⠄⢀⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⠄⣀⡀⠄⠄⠄⠄⠄⠉⢿⣷⡄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢸⣿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⠄⢰⣿⡏⠄⠄⠄⠄⠄⠄⠄⠄⣸⣿⡿⠄⠄⠄⠄⠄⠄⠄⠄⣼⣿⡿⠄⠄⠄⠄⠄⠄⠈⣿⣿⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⢀⣿⣿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⢀⣾⣿⠁⠄⠄⠄⠄⠄⠄⠄⢠⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⢀⣿⣿⠃⠄⠄⠄⠄⠄⠄⠄⣿⣿⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⣸⣿⢿⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⣸⣿⡏⠄⠄⠄⠄⠄⠄⠄⠄⣾⣿⡇⠄⠄⠄⠄⠄⠄⠄⠄⣼⣿⠇⠄⠄⠄⠄⠄⠄⠄⢀⣿⡿⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⢠⣿⡟⠸⣿⣧⠄⠄⠄⠄⠄⠄⠄⢀⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⢰⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⢰⣿⡟⠄⠄⠄⠄⠄⠄⠄⠄⣸⣿⠇⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⣾⣿⠁⠄⠻⣿⣧⣀⠄⠄⠄⠄⣀⣸⣿⡿⠄⠄⠄⠄⠄⠄⠄⢀⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⢀⣾⣿⠁⠄⠄⠄⠄⠄⠄⠄⢠⣿⡟⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⠄⠄⠄⠈⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣤⣄⡀⢸⣿⡟⠄⠄⠄⠄⠄⠄⠄⠄⣼⣿⡿⠄⠄⠄⠄⠄⠄⠄⢀⣿⡿⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⡀⠄⠄⢰⣿⣿⠿⠟⠛⠛⠋⠉⠉⠉⠉⠛⠛⠻⠿⣿⣿⣿⣿⡁⠄⠄⠄⠄⠄⠄⠄⢰⣿⣿⠃⠄⠄⠄⠄⠄⠄⢠⣾⡿⠁⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠸⣿⣧⠄⠄⠄⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠉⢿⣿⣷⣦⣄⣀⣀⣀⣀⣠⣿⣿⡏⠄⠄⠄⠄⠄⢀⣴⣿⡟⠁⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠹⣿⣆⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⢿⣿⡟⠿⠿⠿⠿⠿⠿⠻⣿⣷⣶⣤⣤⣤⣴⣿⡿⠏⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠘⢿⣿⣦⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣠⣾⣿⡇⠄⠄⠄⠄⠄⠄⠄⠈⠙⠛⠻⢿⣿⣿⠟⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⠻⢿⣷⣤⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣤⣤⣴⣶⡿⠿⠛⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢀⣠⣿⣿⠋⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⠛⠛⠻⠿⠿⠿⠿⠟⠻⢿⣿⣿⣿⣇⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣀⣤⣶⣿⡟⠋⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⠙⠛⠿⠿⣿⣿⣷⣶⣶⣶⣶⣶⣿⣿⠿⠟⠛⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄
2 Likes