[ion/simulator] Support writing image files per-platform

Linux uses libpng, macOS/iOS use CoreGraphics, Windows GDI+
This commit is contained in:
Romain Goyet
2020-09-11 22:26:56 -04:00
committed by Léa Saviot
parent ab1df4fbef
commit 0587e41b3c
18 changed files with 240 additions and 233 deletions

View File

@@ -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

View File

@@ -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 <ion/src/simulator/linux/platform_images.h> 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)

View File

@@ -1,9 +1,10 @@
#include "../shared/platform.h"
#include <SDL.h>
#include <assert.h>
#include <jpeglib.h>
#include <png.h>
#include <assert.h>
#include <SDL.h>
#include <stdlib.h>
#include <ion/src/simulator/linux/platform_images.h>
@@ -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<height;j++) {
for (int i=0; i<width; i++) {
row[i] = RGB888Pixel(pixels[i+width*j]);
}
png_write_row(png, reinterpret_cast<png_bytep>(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);
}
}
}
}

View File

@@ -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)

View File

@@ -1,4 +1,5 @@
#include "actions.h"
#include <ion/display.h>
#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
);
}
}

View File

@@ -1,4 +1,4 @@
#include "../shared/platform.h"
#include "../platform.h"
#include <SDL.h>
#include <TargetConditionals.h>
#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<ABGR8888Pixel *>(CGBitmapContextGetData(context));
for (int i=0; i<width*height; i++) {
argb8888Pixels[i] = ABGR8888Pixel(pixels[i]);
}
CGImageRef image = CGBitmapContextCreateImage(context);
CFURLRef url = static_cast<CFURLRef>([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);
}
}
}
}

View File

@@ -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() {
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -1,95 +0,0 @@
#include <ion.h>
#include <ion/events.h>
#include <stdio.h>
#include <string.h>
#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<uint8_t>(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);
}
}
}
}

View File

@@ -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) {

View File

@@ -9,7 +9,6 @@ namespace Framebuffer {
const KDColor * address();
void setActive(bool enabled);
void writeToFile(const char * filename);
}
}

View File

@@ -1,57 +0,0 @@
#if EPSILON_SIMULATOR_HAS_LIBPNG
#include "framebuffer.h"
#include <ion/display.h>
#include <stdlib.h>
#include <png.h>
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<Ion::Display::Height;j++) {
for (int i=0; i<Ion::Display::Width; i++) {
KDColor c = pixels[i+Ion::Display::Width*j];
row[i].red = c.red();
row[i].green = c.green();
row[i].blue = c.blue();
}
png_write_row(png, (png_bytep)row);
}
free(row);
png_write_end(png, NULL);
png_free_data(png, info, PNG_FREE_ALL, -1); // -1 = all items
png_destroy_write_struct(&png, (png_infopp)NULL);
fclose(file);
}
#endif

View File

@@ -94,6 +94,13 @@ int main(int argc, char * argv[]) {
bool headless = args.popFlag("--headless");
#if ION_SIMULATOR_FILES
const char * stateFile = args.pop("--load-state-file");
if (stateFile) {
StateFile::load(stateFile);
}
#endif
Random::init();
if (!headless) {
Journal::init();

View File

@@ -2,6 +2,7 @@
#define ION_SIMULATOR_PLATFORM_H
#include <SDL.h>
#include <kandinsky/color.h>
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
}
}

View File

@@ -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 \

View File

@@ -1,3 +1,5 @@
ION_SIMULATOR_FILES = 1
ion_src += $(addprefix ion/src/simulator/windows/, \
platform_files.cpp \
platform_images.cpp \

View File

@@ -8,7 +8,7 @@
#include <assert.h>
/* 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<Gdiplus::ImageCodecInfo *>(malloc(size));
if (pImageCodecInfo == nullptr) {
return -1;
}
Gdiplus::GetImageEncoders(num, size, pImageCodecInfo);
for (UINT i=0; i<num; i++) {
if (wcscmp(pImageCodecInfo[i].MimeType, format) == 0) {
*pClsid = pImageCodecInfo[i].Clsid;
free(pImageCodecInfo);
return i; // Success
}
}
free(pImageCodecInfo);
return -1;
}
static wchar_t * createWideCharArray(const char * src) {
int wchars_num = MultiByteToWideChar(CP_UTF8, 0, src, -1, NULL, 0);
wchar_t * wstr = new wchar_t[wchars_num];
MultiByteToWideChar(CP_UTF8, 0, src, -1, wstr, wchars_num);
return wstr;
}
namespace Ion {
namespace Simulator {
namespace Platform {
SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) {
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
GdiplusSession session;
LPSTREAM stream;
int resourceID = -1;
@@ -87,11 +136,24 @@ SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) {
image->UnlockBits(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<BYTE *>(const_cast<KDColor *>(pixels)));
CLSID pngClsid;
if (GetEncoderClsid(L"image/png", &pngClsid) > 0) {
wchar_t * widePath = createWideCharArray(path);
bitmap.Save(widePath, &pngClsid, nullptr);
delete[] widePath;
}
}
}
}
}