mirror of https://github.com/stella-emu/stella.git
507 lines
16 KiB
C++
507 lines
16 KiB
C++
//============================================================================
|
|
//
|
|
// SSSS tt lll lll
|
|
// SS SS tt ll ll
|
|
// SS tttttt eeee ll ll aaaa
|
|
// SSSS tt ee ee ll ll aa
|
|
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
|
|
// SS SS tt ee ll ll aa aa
|
|
// SSSS ttt eeeee llll llll aaaaa
|
|
//
|
|
// Copyright (c) 1995-2022 by Bradford W. Mott, Stephen Anthony
|
|
// and the Stella Team
|
|
//
|
|
// See the file "License.txt" for information on usage and redistribution of
|
|
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
|
|
//============================================================================
|
|
|
|
#ifdef IMAGE_SUPPORT
|
|
|
|
#include "OSystem.hxx"
|
|
#include "Console.hxx"
|
|
#include "FrameBuffer.hxx"
|
|
#include "FBSurface.hxx"
|
|
#include "Props.hxx"
|
|
#include "TIASurface.hxx"
|
|
#include "Version.hxx"
|
|
#include "PNGLibrary.hxx"
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
PNGLibrary::PNGLibrary(OSystem& osystem)
|
|
: myOSystem{osystem}
|
|
{
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::loadImage(const string& filename, FBSurface& surface, VariantList& metaData)
|
|
{
|
|
png_structp png_ptr{nullptr};
|
|
png_infop info_ptr{nullptr};
|
|
png_uint_32 iwidth{0}, iheight{0};
|
|
int bit_depth{0}, color_type{0}, interlace_type{0};
|
|
|
|
const auto loadImageERROR = [&](string_view s) {
|
|
if(png_ptr)
|
|
png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr : nullptr, nullptr);
|
|
throw runtime_error(string{s});
|
|
};
|
|
|
|
std::ifstream in(filename, std::ios_base::binary);
|
|
if(!in.is_open())
|
|
loadImageERROR("No image found");
|
|
|
|
// Create the PNG loading context structure
|
|
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr,
|
|
png_user_error, png_user_warn);
|
|
if(png_ptr == nullptr)
|
|
loadImageERROR("Couldn't allocate memory for PNG image");
|
|
|
|
// Allocate/initialize the memory for image information. REQUIRED.
|
|
info_ptr = png_create_info_struct(png_ptr);
|
|
if(info_ptr == nullptr)
|
|
loadImageERROR("Couldn't create image information for PNG image");
|
|
|
|
// Set up the input control
|
|
png_set_read_fn(png_ptr, &in, png_read_data);
|
|
|
|
// Read PNG header info
|
|
png_read_info(png_ptr, info_ptr);
|
|
png_get_IHDR(png_ptr, info_ptr, &iwidth, &iheight, &bit_depth,
|
|
&color_type, &interlace_type, nullptr, nullptr);
|
|
|
|
// Tell libpng to strip 16 bit/color files down to 8 bits/color
|
|
png_set_strip_16(png_ptr);
|
|
|
|
// Extract multiple pixels with bit depths of 1, 2, and 4 from a single
|
|
// byte into separate bytes (useful for paletted and grayscale images).
|
|
png_set_packing(png_ptr);
|
|
|
|
// Only normal RBG(A) images are supported (without the alpha channel)
|
|
if(color_type == PNG_COLOR_TYPE_RGBA)
|
|
{
|
|
png_set_strip_alpha(png_ptr);
|
|
}
|
|
else if(color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
|
|
{
|
|
png_set_gray_to_rgb(png_ptr);
|
|
}
|
|
else if(color_type == PNG_COLOR_TYPE_PALETTE)
|
|
{
|
|
png_set_palette_to_rgb(png_ptr);
|
|
}
|
|
else if(color_type != PNG_COLOR_TYPE_RGB)
|
|
{
|
|
loadImageERROR("Unknown format in PNG image");
|
|
}
|
|
|
|
// Create/initialize storage area for the current image
|
|
if(!allocateStorage(iwidth, iheight))
|
|
loadImageERROR("Not enough memory to read PNG image");
|
|
|
|
// The PNG read function expects an array of rows, not a single 1-D array
|
|
for(uInt32 irow = 0, offset = 0; irow < ReadInfo.height; ++irow, offset += ReadInfo.pitch)
|
|
ReadInfo.row_pointers[irow] = ReadInfo.buffer.data() + offset;
|
|
|
|
// Read the entire image in one go
|
|
png_read_image(png_ptr, ReadInfo.row_pointers.data());
|
|
|
|
// We're finished reading
|
|
png_read_end(png_ptr, info_ptr);
|
|
|
|
// Read the meta data we got
|
|
readMetaData(png_ptr, info_ptr, metaData);
|
|
|
|
// Load image into the surface, setting the correct dimensions
|
|
loadImagetoSurface(surface);
|
|
|
|
// Cleanup
|
|
if(png_ptr)
|
|
png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr : nullptr, nullptr);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::saveImage(const string& filename, const VariantList& metaData)
|
|
{
|
|
std::ofstream out(filename, std::ios_base::binary);
|
|
if(!out.is_open())
|
|
throw runtime_error("ERROR: Couldn't create snapshot file");
|
|
|
|
const FrameBuffer& fb = myOSystem.frameBuffer();
|
|
|
|
const Common::Rect& rectUnscaled = fb.imageRect();
|
|
const Common::Rect rect(
|
|
Common::Point(fb.scaleX(rectUnscaled.x()), fb.scaleY(rectUnscaled.y())),
|
|
fb.scaleX(rectUnscaled.w()), fb.scaleY(rectUnscaled.h())
|
|
);
|
|
|
|
const size_t width = rect.w(), height = rect.h();
|
|
|
|
// Get framebuffer pixel data (we get ABGR format)
|
|
vector<png_byte> buffer(width * height * 4);
|
|
fb.readPixels(buffer.data(), width * 4, rect);
|
|
|
|
// Set up pointers into "buffer" byte array
|
|
vector<png_bytep> rows(height);
|
|
for(size_t k = 0; k < height; ++k)
|
|
rows[k] = buffer.data() + k * width * 4;
|
|
|
|
// And save the image
|
|
saveImageToDisk(out, rows, width, height, metaData);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::saveImage(const string& filename, const FBSurface& surface,
|
|
const Common::Rect& rect, const VariantList& metaData)
|
|
{
|
|
std::ofstream out(filename, std::ios_base::binary);
|
|
if(!out.is_open())
|
|
throw runtime_error("ERROR: Couldn't create snapshot file");
|
|
|
|
// Do we want the entire surface or just a section?
|
|
size_t width = rect.w(), height = rect.h();
|
|
if(rect.empty())
|
|
{
|
|
width = surface.width();
|
|
height = surface.height();
|
|
}
|
|
|
|
// Get the surface pixel data (we get ABGR format)
|
|
vector<png_byte> buffer(width * height * 4);
|
|
surface.readPixels(buffer.data(), static_cast<uInt32>(width), rect);
|
|
|
|
// Set up pointers into "buffer" byte array
|
|
vector<png_bytep> rows(height);
|
|
for(size_t k = 0; k < height; ++k)
|
|
rows[k] = buffer.data() + k * width * 4;
|
|
|
|
// And save the image
|
|
saveImageToDisk(out, rows, width, height, metaData);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::saveImageToDisk(std::ofstream& out, const vector<png_bytep>& rows,
|
|
size_t width, size_t height, const VariantList& metaData)
|
|
{
|
|
png_structp png_ptr{nullptr};
|
|
png_infop info_ptr{nullptr};
|
|
|
|
const auto saveImageERROR = [&](string_view s) {
|
|
if(png_ptr)
|
|
png_destroy_write_struct(&png_ptr, &info_ptr);
|
|
throw runtime_error(string{s});
|
|
};
|
|
|
|
// Create the PNG saving context structure
|
|
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr,
|
|
png_user_error, png_user_warn);
|
|
if(png_ptr == nullptr)
|
|
saveImageERROR("Couldn't allocate memory for PNG file");
|
|
|
|
// Allocate/initialize the memory for image information. REQUIRED.
|
|
info_ptr = png_create_info_struct(png_ptr);
|
|
if(info_ptr == nullptr)
|
|
saveImageERROR("Couldn't create image information for PNG file");
|
|
|
|
// Set up the output control
|
|
png_set_write_fn(png_ptr, &out, png_write_data, png_io_flush);
|
|
|
|
// Write PNG header info
|
|
png_set_IHDR(png_ptr, info_ptr,
|
|
static_cast<png_uint_32>(width), static_cast<png_uint_32>(height), 8,
|
|
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
|
|
PNG_FILTER_TYPE_DEFAULT);
|
|
|
|
// Write meta data
|
|
writeMetaData(png_ptr, info_ptr, metaData);
|
|
|
|
// Write the file header information. REQUIRED
|
|
png_write_info(png_ptr, info_ptr);
|
|
|
|
// Pack pixels into bytes
|
|
png_set_packing(png_ptr);
|
|
|
|
// Swap location of alpha bytes from ARGB to RGBA
|
|
png_set_swap_alpha(png_ptr);
|
|
|
|
// Pack ARGB into RGB
|
|
png_set_filler(png_ptr, 0, PNG_FILLER_AFTER);
|
|
|
|
// Flip BGR pixels to RGB
|
|
png_set_bgr(png_ptr);
|
|
|
|
// Write the entire image in one go
|
|
png_write_image(png_ptr, const_cast<png_bytep*>(rows.data()));
|
|
|
|
// We're finished writing
|
|
png_write_end(png_ptr, info_ptr);
|
|
|
|
// Cleanup
|
|
if(png_ptr)
|
|
png_destroy_write_struct(&png_ptr, &info_ptr);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::updateTime(uInt64 time)
|
|
{
|
|
if(++mySnapCounter % mySnapInterval == 0)
|
|
takeSnapshot(static_cast<uInt32>(time) >> 10); // not quite milliseconds, but close enough
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::toggleContinuousSnapshots(bool perFrame)
|
|
{
|
|
if(mySnapInterval == 0)
|
|
{
|
|
ostringstream buf;
|
|
uInt32 interval = myOSystem.settings().getInt("ssinterval");
|
|
if(perFrame)
|
|
{
|
|
buf << "Enabling snapshots every frame";
|
|
interval = 1;
|
|
}
|
|
else
|
|
{
|
|
buf << "Enabling snapshots in " << interval << " second intervals";
|
|
interval *= static_cast<uInt32>(myOSystem.frameRate());
|
|
}
|
|
myOSystem.frameBuffer().showTextMessage(buf.str());
|
|
setContinuousSnapInterval(interval);
|
|
}
|
|
else
|
|
{
|
|
ostringstream buf;
|
|
buf << "Disabling snapshots, generated "
|
|
<< (mySnapCounter / mySnapInterval)
|
|
<< " files";
|
|
myOSystem.frameBuffer().showTextMessage(buf.str());
|
|
setContinuousSnapInterval(0);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::setContinuousSnapInterval(uInt32 interval)
|
|
{
|
|
mySnapInterval = interval;
|
|
mySnapCounter = 0;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::takeSnapshot(uInt32 number)
|
|
{
|
|
if(!myOSystem.hasConsole())
|
|
return;
|
|
|
|
// Figure out the correct snapshot name
|
|
string filename;
|
|
const string sspath = myOSystem.snapshotSaveDir().getPath() +
|
|
(myOSystem.settings().getString("snapname") != "int"
|
|
? myOSystem.romFile().getNameWithExt("")
|
|
: myOSystem.console().properties().get(PropType::Cart_Name));
|
|
|
|
// Check whether we want multiple snapshots created
|
|
if(number > 0)
|
|
{
|
|
ostringstream buf;
|
|
buf << sspath << "_" << std::hex << std::setw(8) << std::setfill('0')
|
|
<< number << ".png";
|
|
filename = buf.str();
|
|
}
|
|
else if(!myOSystem.settings().getBool("sssingle"))
|
|
{
|
|
// Determine if the file already exists, checking each successive filename
|
|
// until one doesn't exist
|
|
filename = sspath + ".png";
|
|
const FSNode node(filename);
|
|
if(node.exists())
|
|
{
|
|
ostringstream buf;
|
|
for(uInt32 i = 1; ;++i)
|
|
{
|
|
buf.str("");
|
|
buf << sspath << "_" << i << ".png";
|
|
const FSNode next(buf.str());
|
|
if(!next.exists())
|
|
break;
|
|
}
|
|
filename = buf.str();
|
|
}
|
|
}
|
|
else
|
|
filename = sspath + ".png";
|
|
|
|
// Some text fields to add to the PNG snapshot
|
|
VariantList metaData;
|
|
ostringstream version;
|
|
VarList::push_back(metaData, "Title", "Snapshot");
|
|
version << "Stella " << STELLA_VERSION << " (Build " << STELLA_BUILD << ") ["
|
|
<< BSPF::ARCH << "]";
|
|
VarList::push_back(metaData, "Software", version.str());
|
|
const string& name = (myOSystem.settings().getString("snapname") == "int")
|
|
? myOSystem.console().properties().get(PropType::Cart_Name)
|
|
: myOSystem.romFile().getName();
|
|
VarList::push_back(metaData, "ROM Name", name);
|
|
VarList::push_back(metaData, "ROM MD5", myOSystem.console().properties().get(PropType::Cart_MD5));
|
|
VarList::push_back(metaData, "TV Effects", myOSystem.frameBuffer().tiaSurface().effectsInfo());
|
|
|
|
// Now create a PNG snapshot
|
|
string message = "Snapshot saved";
|
|
if(myOSystem.settings().getBool("ss1x"))
|
|
{
|
|
try
|
|
{
|
|
Common::Rect rect;
|
|
const FBSurface& surface = myOSystem.frameBuffer().tiaSurface().baseSurface(rect);
|
|
myOSystem.png().saveImage(filename, surface, rect, metaData);
|
|
}
|
|
catch(const runtime_error& e)
|
|
{
|
|
message = e.what();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Make sure we have a 'clean' image, with no onscreen messages
|
|
myOSystem.frameBuffer().enableMessages(false);
|
|
myOSystem.frameBuffer().tiaSurface().renderForSnapshot();
|
|
|
|
try
|
|
{
|
|
myOSystem.png().saveImage(filename, metaData);
|
|
}
|
|
catch(const runtime_error& e)
|
|
{
|
|
message = e.what();
|
|
}
|
|
|
|
// Re-enable old messages
|
|
myOSystem.frameBuffer().enableMessages(true);
|
|
}
|
|
myOSystem.frameBuffer().showTextMessage(message);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
bool PNGLibrary::allocateStorage(size_t width, size_t height)
|
|
{
|
|
// Create space for the entire image (3 bytes per pixel in RGB format)
|
|
const size_t req_buffer_size = width * height * 3;
|
|
if(req_buffer_size > ReadInfo.buffer.capacity())
|
|
ReadInfo.buffer.reserve(req_buffer_size * 1.5);
|
|
|
|
const size_t req_row_size = height;
|
|
if(req_row_size > ReadInfo.row_pointers.capacity())
|
|
ReadInfo.row_pointers.reserve(req_row_size * 1.5);
|
|
|
|
ReadInfo.width = static_cast<png_uint_32>(width);
|
|
ReadInfo.height = static_cast<png_uint_32>(height);
|
|
ReadInfo.pitch = static_cast<png_uint_32>(width * 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::loadImagetoSurface(FBSurface& surface)
|
|
{
|
|
// First determine if we need to resize the surface
|
|
const uInt32 iw = ReadInfo.width, ih = ReadInfo.height;
|
|
if(iw > surface.width() || ih > surface.height())
|
|
surface.resize(iw, ih);
|
|
|
|
// The source dimensions are set here; the destination dimensions are
|
|
// set by whoever owns the surface
|
|
surface.setSrcPos(0, 0);
|
|
surface.setSrcSize(iw, ih);
|
|
|
|
// Convert RGB triples into pixels and store in the surface
|
|
uInt32 *s_buf{nullptr}, s_pitch{0};
|
|
surface.basePtr(s_buf, s_pitch);
|
|
const uInt8* i_buf = ReadInfo.buffer.data();
|
|
const uInt32 i_pitch = ReadInfo.pitch;
|
|
|
|
const FrameBuffer& fb = myOSystem.frameBuffer();
|
|
for(uInt32 irow = 0; irow < ih; ++irow, i_buf += i_pitch, s_buf += s_pitch)
|
|
{
|
|
const uInt8* i_ptr = i_buf;
|
|
uInt32* s_ptr = s_buf;
|
|
for(uInt32 icol = 0; icol < ReadInfo.width; ++icol, i_ptr += 3)
|
|
*s_ptr++ = fb.mapRGB(*i_ptr, *(i_ptr+1), *(i_ptr+2));
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::writeMetaData(
|
|
const png_structp png_ptr, png_infop info_ptr, // NOLINT
|
|
const VariantList& metaData)
|
|
{
|
|
const size_t numMetaData = metaData.size();
|
|
if(numMetaData == 0)
|
|
return;
|
|
|
|
vector<png_text> text_ptr(numMetaData);
|
|
for(size_t i = 0; i < numMetaData; ++i)
|
|
{
|
|
text_ptr[i].key = const_cast<char*>(metaData[i].first.c_str());
|
|
text_ptr[i].text = const_cast<char*>(metaData[i].second.toCString());
|
|
text_ptr[i].compression = PNG_TEXT_COMPRESSION_NONE;
|
|
text_ptr[i].text_length = 0;
|
|
}
|
|
png_set_text(png_ptr, info_ptr, text_ptr.data(), static_cast<int>(numMetaData));
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::readMetaData(
|
|
const png_structp png_ptr, png_infop info_ptr, // NOLINT
|
|
VariantList& metaData)
|
|
{
|
|
png_textp text_ptr{nullptr};
|
|
int numMetaData{0};
|
|
|
|
png_get_text(png_ptr, info_ptr, &text_ptr, &numMetaData);
|
|
|
|
metaData.clear();
|
|
for(int i = 0; i < numMetaData; ++i)
|
|
{
|
|
VarList::push_back(metaData, text_ptr[i].key, text_ptr[i].text);
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::png_read_data(const png_structp ctx, // NOLINT
|
|
png_bytep area, png_size_t size)
|
|
{
|
|
(static_cast<std::ifstream*>(png_get_io_ptr(ctx)))->read(
|
|
reinterpret_cast<char *>(area), size);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::png_write_data(const png_structp ctx, // NOLINT
|
|
png_bytep area, png_size_t size)
|
|
{
|
|
(static_cast<std::ofstream*>(png_get_io_ptr(ctx)))->write(
|
|
reinterpret_cast<const char *>(area), size);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::png_io_flush(const png_structp ctx) // NOLINT
|
|
{
|
|
(static_cast<std::ofstream*>(png_get_io_ptr(ctx)))->flush();
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::png_user_warn(const png_structp ctx, // NOLINT
|
|
png_const_charp str)
|
|
{
|
|
throw runtime_error(string("PNGLibrary warning: ") + str);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
void PNGLibrary::png_user_error(const png_structp ctx, // NOLINT
|
|
png_const_charp str)
|
|
{
|
|
throw runtime_error(string("PNGLibrary error: ") + str);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
PNGLibrary::ReadInfoType PNGLibrary::ReadInfo;
|
|
|
|
#endif // IMAGE_SUPPORT
|