From 0587e41b3c8cb01f6fb5b1d648f46d27d53cd395 Mon Sep 17 00:00:00 2001 From: Romain Goyet Date: Fri, 11 Sep 2020 22:26:56 -0400 Subject: [PATCH] [ion/simulator] Support writing image files per-platform Linux uses libpng, macOS/iOS use CoreGraphics, Windows GDI+ --- ion/src/simulator/Makefile | 10 +- ion/src/simulator/linux/Makefile | 7 +- ion/src/simulator/linux/platform_images.cpp | 53 ++++++++- ion/src/simulator/macos/Makefile | 4 +- ion/src/simulator/shared/actions.cpp | 7 +- .../simulator/shared/apple/platform_images.mm | 104 +++++++++++++----- ion/src/simulator/shared/dummy/actions.cpp | 22 ---- .../simulator/shared/dummy/platform_files.cpp | 17 --- ion/src/simulator/shared/dummy/state_file.cpp | 95 ---------------- ion/src/simulator/shared/events_platform.cpp | 8 ++ .../{framebuffer_base.cpp => framebuffer.cpp} | 0 ion/src/simulator/shared/framebuffer.h | 1 - ion/src/simulator/shared/framebuffer_png.cpp | 57 ---------- ion/src/simulator/shared/main.cpp | 7 ++ ion/src/simulator/shared/platform.h | 6 + ion/src/simulator/web/Makefile | 1 - ion/src/simulator/windows/Makefile | 2 + ion/src/simulator/windows/platform_images.cpp | 72 +++++++++++- 18 files changed, 240 insertions(+), 233 deletions(-) delete mode 100644 ion/src/simulator/shared/dummy/actions.cpp delete mode 100644 ion/src/simulator/shared/dummy/platform_files.cpp delete mode 100644 ion/src/simulator/shared/dummy/state_file.cpp rename ion/src/simulator/shared/{framebuffer_base.cpp => framebuffer.cpp} (100%) delete mode 100644 ion/src/simulator/shared/framebuffer_png.cpp diff --git a/ion/src/simulator/Makefile b/ion/src/simulator/Makefile index 5a18da565..2cf5fbb82 100644 --- a/ion/src/simulator/Makefile +++ b/ion/src/simulator/Makefile @@ -23,7 +23,7 @@ ion_src += $(addprefix ion/src/simulator/shared/, \ display.cpp:-headless \ events.cpp \ events_platform.cpp:-headless \ - framebuffer_base.cpp \ + framebuffer.cpp \ framebuffer_png.cpp:+headless \ keyboard.cpp:-headless \ layout.cpp:-headless \ @@ -39,3 +39,11 @@ ion_simulator_assets_paths = $(add_prefix ion/src/simulator/assets/,$(ion_simula include ion/src/simulator/$(TARGET)/Makefile include ion/src/simulator/external/Makefile + +ifeq ($(ION_SIMULATOR_FILES),1) +ion_src += $(addprefix ion/src/simulator/shared/, \ + actions.cpp \ + state_file.cpp \ +) +SFLAGS += -DION_SIMULATOR_FILES=1 +endif diff --git a/ion/src/simulator/linux/Makefile b/ion/src/simulator/linux/Makefile index a52e873eb..449ef3651 100644 --- a/ion/src/simulator/linux/Makefile +++ b/ion/src/simulator/linux/Makefile @@ -1,5 +1,6 @@ -# The following lines allow us to use our own SDL_config.h +ION_SIMULATOR_FILES = 1 +# The following lines allow us to use our own SDL_config.h # First, make sure an error is raised if we ever use the standard SDL_config.h SFLAGS += -DUSING_GENERATED_CONFIG_H # Then use our very own include dir if either SDL.h or SDL_config.h are included @@ -33,7 +34,7 @@ ion_src += ion/src/simulator/shared/dummy/telemetry_init.cpp ion_src += ion/src/shared/telemetry_console.cpp endif -LDFLAGS += -ljpeg +LDFLAGS += -ljpeg -lpng $(eval $(call rule_for, \ INCBIN, \ @@ -46,4 +47,4 @@ $(eval $(call rule_for, \ $(call object_for,ion/src/simulator/linux/platform_images.cpp): $(BUILD_DIR)/ion/src/simulator/linux/platform_images.h # The header is refered to as so make sure it's findable this way -SFLAGS += -I$(BUILD_DIR) +$(call object_for,ion/src/simulator/linux/platform_images.cpp): SFLAGS += -I$(BUILD_DIR) diff --git a/ion/src/simulator/linux/platform_images.cpp b/ion/src/simulator/linux/platform_images.cpp index a3ec43760..0fb23867b 100644 --- a/ion/src/simulator/linux/platform_images.cpp +++ b/ion/src/simulator/linux/platform_images.cpp @@ -1,9 +1,10 @@ #include "../shared/platform.h" -#include +#include #include #include -#include +#include +#include #include @@ -166,6 +167,54 @@ SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { return texture; } +class RGB888Pixel { +public: + RGB888Pixel() {} + RGB888Pixel(KDColor c) : + m_red(c.red()), + m_green(c.green()), + m_blue(c.blue()) { + } +private: + uint8_t m_red; + uint8_t m_green; + uint8_t m_blue; +}; +static_assert(sizeof(RGB888Pixel) == 3, "RGB888Pixel shall be 3 bytes long"); + +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + FILE * file = fopen(path, "wb"); // Write in binary mode + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + png_infop info = png_create_info_struct(png); + png_init_io(png, file); + + png_set_IHDR(png, info, + width, height, + 8, // Number of bits per channel + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png, info); + + RGB888Pixel * row = new RGB888Pixel[3*width]; + for (int j=0;j(row)); + } + delete row; + + png_write_end(png, NULL); + + png_free_data(png, info, PNG_FREE_ALL, -1); // -1 = all items + png_destroy_write_struct(&png, nullptr); + fclose(file); +} + } } } diff --git a/ion/src/simulator/macos/Makefile b/ion/src/simulator/macos/Makefile index 8fc361b7e..a640a6571 100644 --- a/ion/src/simulator/macos/Makefile +++ b/ion/src/simulator/macos/Makefile @@ -1,3 +1,5 @@ +ION_SIMULATOR_FILES = 1 + ion_src += $(addprefix ion/src/simulator/macos/, \ platform_files.mm \ ) @@ -11,10 +13,8 @@ ion_src += $(addprefix ion/src/simulator/shared/, \ clipboard_helper.cpp \ collect_registers_x86_64.s \ collect_registers.cpp \ - actions.cpp \ haptics.cpp \ journal.cpp \ - state_file.cpp \ ) ifeq ($(EPSILON_TELEMETRY),1) diff --git a/ion/src/simulator/shared/actions.cpp b/ion/src/simulator/shared/actions.cpp index 81716d03f..ee8395f3f 100644 --- a/ion/src/simulator/shared/actions.cpp +++ b/ion/src/simulator/shared/actions.cpp @@ -1,4 +1,5 @@ #include "actions.h" +#include #include "framebuffer.h" #include "platform.h" #include "state_file.h" @@ -29,7 +30,11 @@ void loadState() { void takeScreenshot() { const char * path = Platform::filePathForWriting("png"); if (path != nullptr) { -// Framebuffer::writeToFile(path); + Platform::saveImage( + Framebuffer::address(), + Display::Width, Display::Height, + path + ); } } diff --git a/ion/src/simulator/shared/apple/platform_images.mm b/ion/src/simulator/shared/apple/platform_images.mm index 1b68a11f3..3d7338c04 100644 --- a/ion/src/simulator/shared/apple/platform_images.mm +++ b/ion/src/simulator/shared/apple/platform_images.mm @@ -1,4 +1,4 @@ -#include "../shared/platform.h" +#include "../platform.h" #include #include #if TARGET_OS_MAC @@ -11,42 +11,50 @@ namespace Ion { namespace Simulator { namespace Platform { +static CGContextRef createABGR8888Context(size_t width, size_t height) { + size_t bytesPerPixel = 4; + size_t bytesPerRow = bytesPerPixel * width; + size_t bitsPerComponent = 8; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate( + nullptr, // The context will allocate and take ownership of the bitmap buffer + width, height, + bitsPerComponent, bytesPerRow, colorSpace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big + ); + if (colorSpace) { + CFRelease(colorSpace); + } + return context; +} + SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { - CGImageRef cgImage = NULL; + CGImageRef image = nullptr; #if TARGET_OS_MAC //http://lists.libsdl.org/pipermail/commits-libsdl.org/2016-December/001235.html [[[NSApp windows] firstObject] setColorSpace:[NSColorSpace sRGBColorSpace]]; NSImage * nsImage = [NSImage imageNamed:[NSString stringWithUTF8String:identifier]]; - cgImage = [nsImage CGImageForProposedRect:NULL context:NULL hints:0]; + image = [nsImage CGImageForProposedRect:NULL context:NULL hints:0]; #else - cgImage = [[UIImage imageNamed:[NSString stringWithUTF8String:identifier]] CGImage]; + image = [[UIImage imageNamed:[NSString stringWithUTF8String:identifier]] CGImage]; #endif - if (cgImage == NULL) { - return NULL; + if (image == nullptr) { + return nullptr; } - size_t width = CGImageGetWidth(cgImage); - size_t height = CGImageGetHeight(cgImage); - size_t bytesPerPixel = 4; - size_t bytesPerRow = bytesPerPixel * width; - size_t bitsPerComponent = 8; + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); - size_t size = height * width * bytesPerPixel; - void * bitmapData = malloc(size); - memset(bitmapData, 0, size); + CGContextRef context = createABGR8888Context(width, height); + if (context == nullptr) { + return nullptr; + } - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate( - bitmapData, width, height, - bitsPerComponent, bytesPerRow, colorSpace, - kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big - ); + void * argb8888Pixels = CGBitmapContextGetData(context); - CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); - - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); SDL_Texture * texture = SDL_CreateTexture( renderer, @@ -56,20 +64,64 @@ SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { height ); + size_t bytesPerPixel = 4; + SDL_UpdateTexture( texture, NULL, - bitmapData, + argb8888Pixels, bytesPerPixel * width ); SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); - free(bitmapData); + CGContextRelease(context); return texture; } +class ABGR8888Pixel { +public: + ABGR8888Pixel(KDColor c) : + m_red(c.red()), + m_green(c.green()), + m_blue(c.blue()), + m_alpha(255) { + } +private: + uint8_t m_red; + uint8_t m_green; + uint8_t m_blue; + uint8_t m_alpha; +}; +static_assert(sizeof(ABGR8888Pixel) == 4, "ARGB8888Pixel shall be 4 bytes long"); + +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + CGContextRef context = createABGR8888Context(width, height); + if (context == nullptr) { + return; + } + ABGR8888Pixel * argb8888Pixels = static_cast(CGBitmapContextGetData(context)); + for (int i=0; i([NSURL fileURLWithPath:[NSString stringWithUTF8String:path]]); + + CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL); + CGImageDestinationAddImage(destination, image, nil); + CGImageDestinationFinalize(destination); + + if (destination) { + CFRelease(destination); + } + CGImageRelease(image); + CGContextRelease(context); +} + + } } } diff --git a/ion/src/simulator/shared/dummy/actions.cpp b/ion/src/simulator/shared/dummy/actions.cpp deleted file mode 100644 index 1e9055507..000000000 --- a/ion/src/simulator/shared/dummy/actions.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "../actions.h" - -namespace Ion { -namespace Simulator { -namespace Actions { - -bool actionForEvent(SDL_KeyboardEvent event) { - return false; -} - -void saveState() { -} - -void loadState() { -} - -void takeScreenshot() { -} - -} -} -} diff --git a/ion/src/simulator/shared/dummy/platform_files.cpp b/ion/src/simulator/shared/dummy/platform_files.cpp deleted file mode 100644 index fb73fbf22..000000000 --- a/ion/src/simulator/shared/dummy/platform_files.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "../platform.h" - -namespace Ion { -namespace Simulator { -namespace Platform { - -const char * filePathForReading(const char * extension) { - return nullptr; -} - -const char * filePathForWriting(const char * extension) { - return nullptr; -} - -} -} -} diff --git a/ion/src/simulator/shared/dummy/state_file.cpp b/ion/src/simulator/shared/dummy/state_file.cpp deleted file mode 100644 index 462f5a213..000000000 --- a/ion/src/simulator/shared/dummy/state_file.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include -#include -#include "journal.h" - -namespace Ion { -namespace Simulator { -namespace StateFile { - -static constexpr const char * sHeader = "NWSF"; -static constexpr int sHeaderLength = 4; -static constexpr int sVersionLength = 8; - -/* File format: * "NWSF" + "XXXXXXXX" (version) + EVENTS... */ - -static inline bool load(FILE * f) { - char buffer[sVersionLength+1]; - - // Header - buffer[sHeaderLength] = 0; - if (fread(buffer, sHeaderLength, 1, f) != 1) { - return false; - } - printf("READ\n"); - if (strcmp(buffer, sHeader) != 0) { - return false; - } - - // Software version - buffer[sVersionLength] = 0; - if (fread(buffer, sVersionLength, 1, f) != 1) { - return false; - } - if (strcmp(buffer, softwareVersion()) != 0) { - return false; - } - - // Events - Ion::Events::Journal * journal = Journal::replayJournal(); - while (fread(buffer, 1, 1, f) == 1) { - Ion::Events::Event e = Ion::Events::Event(buffer[0]); - journal->pushEvent(e); - } - Ion::Events::replayFrom(journal); - - return true; -} - -void load(const char * filename) { - FILE * f = nullptr; - if (strcmp(filename, "-") == 0) { - f = stdin; - } else { - f = fopen(filename, "rb"); - } - if (f == nullptr) { - return; - } - load(f); - fclose(f); -} - -static inline bool save(FILE * f) { - if (fwrite(sHeader, sHeaderLength, 1, f) != 1) { - return false; - } - if (fwrite(softwareVersion(), sVersionLength, 1, f) != 1) { - return false; - } - Ion::Events::Journal * journal = Journal::logJournal(); - Ion::Events::Event e; - while (!journal->isEmpty()) { - Ion::Events::Event e = journal->popEvent(); - uint8_t code = static_cast(e); - if (fwrite(&code, 1, 1, f) != 1) { - return false; - } - } - return true; -} - -void save(const char * filename) { - printf("Saving to \"%s\"\n", filename); - FILE * f = fopen(filename, "w"); - if (f == nullptr) { - return; - } - save(f); - fclose(f); -} - -} -} -} diff --git a/ion/src/simulator/shared/events_platform.cpp b/ion/src/simulator/shared/events_platform.cpp index c69bde84a..0cffa62e2 100644 --- a/ion/src/simulator/shared/events_platform.cpp +++ b/ion/src/simulator/shared/events_platform.cpp @@ -29,6 +29,14 @@ static inline Event eventFromSDLKeyboardEvent(SDL_KeyboardEvent event) { return Copy; case SDLK_v: return Paste; +#if ION_SIMULATOR_FILES + case SDLK_s: + Simulator::Actions::saveState(); + return None; + case SDLK_p: + Simulator::Actions::takeScreenshot(); + return None; +#endif } } if (event.keysym.mod & KMOD_ALT) { diff --git a/ion/src/simulator/shared/framebuffer_base.cpp b/ion/src/simulator/shared/framebuffer.cpp similarity index 100% rename from ion/src/simulator/shared/framebuffer_base.cpp rename to ion/src/simulator/shared/framebuffer.cpp diff --git a/ion/src/simulator/shared/framebuffer.h b/ion/src/simulator/shared/framebuffer.h index e9eed19e2..9ae419e8e 100644 --- a/ion/src/simulator/shared/framebuffer.h +++ b/ion/src/simulator/shared/framebuffer.h @@ -9,7 +9,6 @@ namespace Framebuffer { const KDColor * address(); void setActive(bool enabled); -void writeToFile(const char * filename); } } diff --git a/ion/src/simulator/shared/framebuffer_png.cpp b/ion/src/simulator/shared/framebuffer_png.cpp deleted file mode 100644 index 9582e48e1..000000000 --- a/ion/src/simulator/shared/framebuffer_png.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#if EPSILON_SIMULATOR_HAS_LIBPNG - -#include "framebuffer.h" -#include -#include -#include - -typedef struct { - uint8_t red; - uint8_t green; - uint8_t blue; -} pixel_t; - -void Ion::Simulator::Framebuffer::writeToFile(const char * filename) { - FILE * file = fopen(filename, "wb"); // Write in binary mode - //ENSURE(file != NULL, "Opening file %s", filename); - - png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - //ENSURE(png != NULL, "Allocating PNG write structure"); - - png_infop info = png_create_info_struct(png); - //ENSURE(info != NULL, "Allocating info structure"); - - png_init_io(png, file); - - png_set_IHDR(png, info, - Ion::Display::Width, Ion::Display::Height, - 8, // Number of bits per channel - PNG_COLOR_TYPE_RGB, - PNG_INTERLACE_NONE, - PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - - png_write_info(png, info); - - static_assert(sizeof(pixel_t) == 3, "pixel_t shall be 3 bytes long (RGB888 format)"); - pixel_t * row = (pixel_t *)malloc(3*Ion::Display::Width); - const KDColor * pixels = address(); - for (int j=0;j +#include namespace Ion { namespace Simulator { @@ -9,6 +10,11 @@ namespace Platform { SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier); const char * languageCode(); +#if ION_SIMULATOR_FILES +const char * filePathForReading(const char * extension); +const char * filePathForWriting(const char * extension); +void saveImage(const KDColor * pixels, int width, int height, const char * path); +#endif } } diff --git a/ion/src/simulator/web/Makefile b/ion/src/simulator/web/Makefile index 5e980c274..c79e47f99 100644 --- a/ion/src/simulator/web/Makefile +++ b/ion/src/simulator/web/Makefile @@ -21,7 +21,6 @@ ion_src += $(addprefix ion/src/simulator/web/, \ ) ion_src += $(addprefix ion/src/simulator/shared/, \ - dummy/actions.cpp \ dummy/language.cpp \ dummy/haptics_enabled.cpp \ haptics.cpp \ diff --git a/ion/src/simulator/windows/Makefile b/ion/src/simulator/windows/Makefile index 0ebf03945..b44499e02 100644 --- a/ion/src/simulator/windows/Makefile +++ b/ion/src/simulator/windows/Makefile @@ -1,3 +1,5 @@ +ION_SIMULATOR_FILES = 1 + ion_src += $(addprefix ion/src/simulator/windows/, \ platform_files.cpp \ platform_images.cpp \ diff --git a/ion/src/simulator/windows/platform_images.cpp b/ion/src/simulator/windows/platform_images.cpp index 648ddbe35..436cabc04 100644 --- a/ion/src/simulator/windows/platform_images.cpp +++ b/ion/src/simulator/windows/platform_images.cpp @@ -8,7 +8,7 @@ #include /* Loading images using GDI+ - * On Windows, we decompress JPEG images using GDI+ which is widely available. + * On Windows, we manipulate images using GDI+ which is widely available. * Note that this adds an extra runtime dependency (as compared to just SDL), * but this should not be an issue. */ @@ -36,14 +36,63 @@ static inline HRESULT CreateStreamOnResource(const char * name, LPSTREAM * strea return hr; } +// Helper class to init/shutdown Gdiplus using RAII +class GdiplusSession { +public: + GdiplusSession() { + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, nullptr); + } + ~GdiplusSession() { + Gdiplus::GdiplusShutdown(m_gdiplusToken); + } +private: + ULONG_PTR m_gdiplusToken; +}; + +// Helper function from MSDN +// https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-retrieving-the-class-identifier-for-an-encoder-use +int GetEncoderClsid(const WCHAR * format, CLSID * pClsid) { + UINT num = 0; // number of image encoders + UINT size = 0; // size of the image encoder array in bytes + + Gdiplus::ImageCodecInfo * pImageCodecInfo = nullptr; + Gdiplus::GetImageEncodersSize(&num, &size); + if (size == 0) { + return -1; + } + pImageCodecInfo = static_cast(malloc(size)); + if (pImageCodecInfo == nullptr) { + return -1; + } + + Gdiplus::GetImageEncoders(num, size, pImageCodecInfo); + + for (UINT i=0; iUnlockBits(bitmapData); delete bitmapData; delete image; - Gdiplus::GdiplusShutdown(gdiplusToken); return texture; } +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + static_assert(sizeof(KDColor) == 2, "KDColor expected to be RGB565"); + GdiplusSession session; + + Gdiplus::Bitmap bitmap(width, height, 2*width, PixelFormat16bppRGB565, reinterpret_cast(const_cast(pixels))); + + CLSID pngClsid; + if (GetEncoderClsid(L"image/png", &pngClsid) > 0) { + wchar_t * widePath = createWideCharArray(path); + bitmap.Save(widePath, &pngClsid, nullptr); + delete[] widePath; + } +} + } } }