Reworked the PNG save code a little, making it more abstract and not

having to know about TIA, Properties, etc.  Basically, it now saves
a snapshot of either the FrameBuffer or an FBSurface, and adds 
text comments passed into it.  The contents of the surface and comments
are no longer calculated (or known) by the PNG code.

This is in preparation for saving FBSurface from anywhere, which will
help in the debugger for taking snapshots.


git-svn-id: svn://svn.code.sf.net/p/stella/code/trunk@2928 8b62c5a3-ac7e-4cc8-8f21-d9a121418aba
This commit is contained in:
stephena 2014-06-19 16:45:07 +00:00
parent 2d32f3d39a
commit 78f5a2acef
13 changed files with 209 additions and 157 deletions

View File

@ -117,7 +117,7 @@ bool FrameBufferSDL2::setVideoMode(const string& title, const VideoMode& mode,
uInt32 pos = myOSystem.settings().getBool("center")
? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED;
uInt32 flags = mode.fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
uInt32 flags = mode.fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
// OSX seems to have issues with destroying the window, and wants to keep
// the same handle
@ -259,8 +259,8 @@ FBSurface* FrameBufferSDL2::createSurface(uInt32 w, uInt32 h,
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FrameBufferSDL2::readPixels(const GUI::Rect& rect,
uInt8* pixels, uInt32 pitch) const
void FrameBufferSDL2::readPixels(uInt8* pixels, uInt32 pitch,
const GUI::Rect& rect) const
{
SDL_Rect r;
r.x = rect.x(); r.y = rect.y();

View File

@ -96,16 +96,16 @@ class FrameBufferSDL2 : public FrameBuffer
bool isDoubleBuffered() const { return myDblBufferedFlag; }
/**
This method is called to get the specified ARGB data from the viewable
FrameBuffer area. Note that this isn't the same as any internal
surfaces that may be in use; it should return the actual data as it
is currently seen onscreen.
This method is called to get a copy of the specified ARGB data from the
viewable FrameBuffer area. Note that this isn't the same as any
internal surfaces that may be in use; it should return the actual data
as it is currently seen onscreen.
@param rect The bounding rectangle for the buffer
@param buffer The actual pixel data in ARGB8888 format
@param buffer A copy of the pixel data in ARGB8888 format
@param pitch The pitch (in bytes) for the pixel data
@param rect The bounding rectangle for the buffer
*/
void readPixels(const GUI::Rect& rect, uInt8* buffer, uInt32 pitch) const;
void readPixels(uInt8* buffer, uInt32 pitch, const GUI::Rect& rect) const;
protected:
//////////////////////////////////////////////////////////////////////

View File

@ -26,7 +26,6 @@
#include "FrameBuffer.hxx"
#include "Props.hxx"
#include "TIA.hxx"
#include "Version.hxx"
#include "PNGLibrary.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -129,22 +128,67 @@ done:
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::saveImage(const string& filename, const Properties& props)
void PNGLibrary::saveImage(const string& filename, const VariantList& comments)
{
ofstream out(filename.c_str(), ios_base::binary);
if(!out.is_open())
throw "ERROR: Couldn't create snapshot file";
const GUI::Rect& rect = myFB.imageRect();
png_uint_32 width = rect.width(), height = rect.height();
// Get framebuffer pixel data (we get ABGR format)
png_bytep buffer = new png_byte[width * height * 4];
myFB.readPixels(buffer, width*4, rect);
// Set up pointers into "buffer" byte array
png_bytep* rows = new png_bytep[height];
for(png_uint_32 k = 0; k < height; ++k)
rows[k] = (png_bytep) (buffer + k*width*4);
// And save the image
saveImage(out, buffer, rows, width, height, comments);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::saveImage(const string& filename, const FBSurface& surface,
const GUI::Rect& rect, const VariantList& comments)
{
ofstream out(filename.c_str(), ios_base::binary);
if(!out.is_open())
throw "ERROR: Couldn't create snapshot file";
// Do we want the entire surface or just a section?
png_uint_32 width = rect.width(), height = rect.height();
if(rect.isEmpty())
{
width = surface.width();
height = surface.height();
}
// Get the surface pixel data (we get ABGR format)
png_bytep buffer = new png_byte[width * height * 4];
surface.readPixels(buffer, width, rect);
// Set up pointers into "buffer" byte array
png_bytep* rows = new png_bytep[height];
for(png_uint_32 k = 0; k < height; ++k)
rows[k] = (png_bytep) (buffer + k*width*4);
// And save the image
saveImage(out, buffer, rows, width, height, comments);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::saveImage(ofstream& out, png_bytep& buffer, png_bytep*& rows,
png_uint_32 width, png_uint_32 height, const VariantList& comments)
{
#define saveImageERROR(s) { err_message = s; goto done; }
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
const char* err_message = NULL;
const GUI::Rect& imageR = myFB.imageRect();
png_uint_32 width = imageR.width(), height = imageR.height();
png_bytep image = new png_byte[width * height * 4];
png_bytep* row_pointers = new png_bytep[height];
ofstream out(filename.c_str(), ios_base::binary);
if(!out.is_open())
saveImageERROR("ERROR: Couldn't create snapshot file");
// Create the PNG saving context structure
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL,
png_user_error, png_user_warn);
@ -165,7 +209,7 @@ void PNGLibrary::saveImage(const string& filename, const Properties& props)
PNG_FILTER_TYPE_DEFAULT);
// Write comments
writeComments(png_ptr, info_ptr, props);
writeComments(png_ptr, info_ptr, comments);
// Write the file header information. REQUIRED
png_write_info(png_ptr, info_ptr);
@ -182,15 +226,8 @@ void PNGLibrary::saveImage(const string& filename, const Properties& props)
// Flip BGR pixels to RGB
png_set_bgr(png_ptr);
// Get framebuffer surface pixel data (we get ABGR format)
myFB.readPixels(imageR, image, width*4);
// Set up pointers into "image" byte array
for(png_uint_32 k = 0; k < height; ++k)
row_pointers[k] = (png_bytep) (image + k*width*4);
// Write the entire image in one go
png_write_image(png_ptr, row_pointers);
png_write_image(png_ptr, rows);
// We're finished writing
png_write_end(png_ptr, info_ptr);
@ -199,94 +236,14 @@ void PNGLibrary::saveImage(const string& filename, const Properties& props)
done:
if(png_ptr)
png_destroy_write_struct(&png_ptr, &info_ptr);
if(image)
delete[] image;
if (row_pointers)
delete[] row_pointers;
if(buffer)
delete[] buffer;
if (rows)
delete[] rows;
if(err_message)
throw err_message;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::saveImage(const string& filename, const TIA& tia,
const Properties& props)
{
#define saveImageERROR(s) { err_message = s; goto done; }
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
const char* err_message = NULL;
png_uint_32 tiaw = tia.width(), width = tiaw*2, height = tia.height();
png_bytep image = new png_byte[width * height * 3];
png_bytep* row_pointers = new png_bytep[height];
uInt8 r, g, b;
uInt8* buf_ptr = image;
ofstream out(filename.c_str(), ios_base::binary);
if(!out.is_open())
saveImageERROR("ERROR: Couldn't create snapshot file");
// Create the PNG saving context structure
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL,
png_user_error, png_user_warn);
if(png_ptr == NULL)
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 == NULL)
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, width, height, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
// Write comments
writeComments(png_ptr, info_ptr, props);
// Write the file header information. REQUIRED
png_write_info(png_ptr, info_ptr);
// Fill the buffer with pixels from the tia, scaled 2x horizontally
for(uInt32 y = 0; y < height; ++y)
{
for(uInt32 x = 0; x < tiaw; ++x)
{
uInt32 pixel = myFB.tiaSurface().pixel(y*tiaw+x);
myFB.getRGB(pixel, &r, &g, &b);
*buf_ptr++ = r;
*buf_ptr++ = g;
*buf_ptr++ = b;
*buf_ptr++ = r;
*buf_ptr++ = g;
*buf_ptr++ = b;
}
// Set up pointers into "image" byte array
buf_ptr = row_pointers[y] = image + y*width*3;
}
// Write the entire image in one go
png_write_image(png_ptr, row_pointers);
// We're finished writing
png_write_end(png_ptr, info_ptr);
// Cleanup
done:
if(png_ptr)
png_destroy_write_struct(&png_ptr, &info_ptr);
if(image)
delete[] image;
if (row_pointers)
delete[] row_pointers;
if (err_message)
throw err_message;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool PNGLibrary::allocateStorage(png_uint_32 w, png_uint_32 h)
{
@ -382,29 +339,21 @@ void PNGLibrary::scaleImagetoSurface(FBSurface& surface)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void PNGLibrary::writeComments(png_structp png_ptr, png_infop info_ptr,
const Properties& props)
const VariantList& comments)
{
// Pre-processor voodoo to make the code shorter
#define CONVERT_TO_PNGTEXT(_idx, _key, _text) \
char key##_idx[] = _key; \
char text##_idx[256]; \
strncpy(text##_idx, _text.c_str(), 255); \
text_ptr[_idx].key = key##_idx; \
text_ptr[_idx].text = text##_idx; \
text_ptr[_idx].compression = PNG_TEXT_COMPRESSION_NONE; \
text_ptr[_idx].text_length = 0;
uInt32 numComments = comments.size();
if(numComments == 0)
return;
ostringstream version;
version << "Stella " << STELLA_VERSION << " (Build " << STELLA_BUILD << ") ["
<< BSPF_ARCH << "]";
png_text text_ptr[4];
CONVERT_TO_PNGTEXT(0, "Software", version.str());
CONVERT_TO_PNGTEXT(1, "ROM Name", props.get(Cartridge_Name));
CONVERT_TO_PNGTEXT(2, "ROM MD5", props.get(Cartridge_MD5));
CONVERT_TO_PNGTEXT(3, "TV Effects", myFB.tiaSurface().effectsInfo());
png_set_text(png_ptr, info_ptr, text_ptr, 4);
png_text text_ptr[numComments];
for(uInt32 i = 0; i < numComments; ++i)
{
text_ptr[i].key = (char*) comments[i].first.c_str();
text_ptr[i].text = (char*) comments[i].second.toString().c_str();
text_ptr[i].compression = PNG_TEXT_COMPRESSION_NONE;
text_ptr[i].text_length = 0;
}
png_set_text(png_ptr, info_ptr, text_ptr, numComments);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View File

@ -61,27 +61,30 @@ class PNGLibrary
this will be a TIA image, but it could actually be used for *any* mode.
@param filename The filename to save the PNG image
@param props The properties object containing info about the ROM
@param comments The text comments to add to the PNG image
@return On success, the PNG file has been saved to 'filename',
otherwise a const char* exception is thrown containing a
more detailed error message.
*/
void saveImage(const string& filename, const Properties& props);
void saveImage(const string& filename,
const VariantList& comments = EmptyVarList);
/**
Save the current TIA image to a PNG file using data directly from
the internal TIA buffer. No filtering or scaling will be included.
Save the given surface to a PNG file.
@param filename The filename to save the PNG image
@param tia Source of the raw TIA data
@param props The properties object containing info about the ROM
@param filename The filename to save the PNG image
@param surface The surface data for the PNG image
@param rect The area of the surface to use
@param comments The text comments to add to the PNG image
@return On success, the PNG file has been saved to 'filename',
otherwise a const char* exception is thrown containing a
more detailed error message.
*/
void saveImage(const string& filename, const TIA& tia, const Properties& props);
void saveImage(const string& filename, const FBSurface& surface,
const GUI::Rect& rect = GUI::EmptyRect,
const VariantList& comments = EmptyVarList);
private:
const FrameBuffer& myFB;
@ -111,6 +114,19 @@ class PNGLibrary
*/
bool allocateStorage(png_uint_32 iwidth, png_uint_32 iheight);
/** The actual method which saves a PNG image.
@param out The output stream for writing PNG data
@param buffer Actual PNG RGB data
@param rows Pointer into 'buffer' for each row
@param width The width of the PNG image
@param height The height of the PNG image
@param comments The text comments to add to the PNG image
*/
void saveImage(ofstream& out, png_bytep& buffer, png_bytep*& rows,
png_uint_32 width, png_uint_32 height,
const VariantList& comments);
/**
Scale the PNG data from 'ReadInfo' into the FBSurface. For now, scaling
is done on integer boundaries only (ie, 1x, 2x, etc up or down).
@ -123,7 +139,7 @@ class PNGLibrary
Write PNG tEXt chunks to the image.
*/
void writeComments(png_structp png_ptr, png_infop info_ptr,
const Properties& props);
const VariantList& comments);
/** PNG library callback functions */
static void png_read_data(png_structp ctx, png_bytep area, png_size_t size);

View File

@ -82,6 +82,8 @@ static const Variant EmptyVariant("");
class VariantList : public Common::Array< pair<string,Variant> >
{
public:
VariantList() { }
void push_back(const Variant& name, const Variant& tag = EmptyVariant)
{
ensureCapacity(_size + 1);
@ -89,4 +91,7 @@ class VariantList : public Common::Array< pair<string,Variant> >
}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const VariantList EmptyVarList;
#endif

View File

@ -42,6 +42,7 @@
#include "StateManager.hxx"
#include "M6532.hxx"
#include "MouseControl.hxx"
#include "Version.hxx"
#include "EventHandler.hxx"
@ -1904,20 +1905,30 @@ void EventHandler::takeSnapshot(uInt32 number)
else
filename = sspath + ".png";
// Some text fields to add to the PNG snapshot
VariantList comments;
ostringstream version;
version << "Stella " << STELLA_VERSION << " (Build " << STELLA_BUILD << ") ["
<< BSPF_ARCH << "]";
comments.push_back("Software", version.str());
comments.push_back("ROM Name", myOSystem.console().properties().get(Cartridge_Name));
comments.push_back("ROM MD5", myOSystem.console().properties().get(Cartridge_MD5));
comments.push_back("TV Effects", myOSystem.frameBuffer().tiaSurface().effectsInfo());
// Now create a PNG snapshot
if(myOSystem.settings().getBool("ss1x"))
{
string message = "Snapshot saved";
try
{
myOSystem.png().saveImage(filename, myOSystem.console().tia(),
myOSystem.console().properties());
GUI::Rect rect;
const FBSurface& surface = myOSystem.frameBuffer().tiaSurface().baseSurface(rect);
myOSystem.png().saveImage(filename, surface, rect, comments);
}
catch(const char* msg)
{
message = msg;
}
if(showmessage)
myOSystem.frameBuffer().showMessage(message);
}
@ -1929,7 +1940,7 @@ void EventHandler::takeSnapshot(uInt32 number)
string message = "Snapshot saved";
try
{
myOSystem.png().saveImage(filename, myOSystem.console().properties());
myOSystem.png().saveImage(filename, comments);
}
catch(const char* msg)
{

View File

@ -34,6 +34,29 @@ FBSurface::FBSurface()
myAttributes.blendalpha = 100;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::readPixels(uInt8* buffer, uInt32 pitch, const GUI::Rect& rect) const
{
uInt8* src = (uInt8*) myPixels + rect.y() * myPitch + rect.x();
if(rect.isEmpty())
memcpy(buffer, src, width() * height() * 4);
else
{
uInt32 w = BSPF_min(rect.width(), width());
uInt32 h = BSPF_min(rect.height(), height());
// Copy 'height' lines of width 'pitch' (in bytes for both)
uInt8* dst = buffer;
while(h--)
{
memcpy(dst, src, w * 4);
src += myPitch * 4;
dst += pitch * 4;
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FBSurface::hLine(uInt32 x, uInt32 y, uInt32 x2, uInt32 color)
{

View File

@ -71,12 +71,22 @@ class FBSurface
This method returns the surface pixel pointer and pitch, which are
used when one wishes to modify the surface pixels directly.
*/
inline void basePtr(uInt32*& pixels, uInt32& pitch)
inline void basePtr(uInt32*& pixels, uInt32& pitch) const
{
pixels = myPixels;
pitch = myPitch;
}
/**
This method is called to get a copy of the specified ARGB data from
the behind-the-scenes surface.
@param buffer A copy of the pixel data in ARGB8888 format
@param pitch The pitch (in bytes) for the pixel data
@param rect The bounding rectangle for the buffer
*/
void readPixels(uInt8* buffer, uInt32 pitch, const GUI::Rect& rect) const;
//////////////////////////////////////////////////////////////////////////
// Note: The drawing primitives below will work, but do not take
// advantage of any acceleration whatsoever. The methods are

View File

@ -160,7 +160,7 @@ bool FrameBuffer::initialize()
FBSurface::setPalette(myPalette);
// Create a TIA surface; we need it for rendering TIA images
myTIASurface = new TIASurface(*this, myOSystem);
myTIASurface = new TIASurface(myOSystem);
return true;
}

View File

@ -357,11 +357,11 @@ class FrameBuffer
surfaces that may be in use; it should return the actual data as it
is currently seen onscreen.
@param rect The bounding rectangle for the buffer
@param buffer The actual pixel data in ARGB8888 format
@param pitch The pitch (in bytes) for the pixel data
@param rect The bounding rectangle for the buffer
*/
virtual void readPixels(const GUI::Rect& rect, uInt8* buffer, uInt32 pitch) const = 0;
virtual void readPixels(uInt8* buffer, uInt32 pitch, const GUI::Rect& rect) const = 0;
protected:
/**

View File

@ -26,12 +26,13 @@
#include "TIASurface.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TIASurface::TIASurface(FrameBuffer& buffer, OSystem& system)
: myFB(buffer),
myOSystem(system),
TIASurface::TIASurface(OSystem& system)
: myOSystem(system),
myFB(system.frameBuffer()),
myTIA(NULL),
myTiaSurface(NULL),
mySLineSurface(NULL),
myBaseTiaSurface(NULL),
myFilterType(kNormal),
myUsePhosphor(false),
myPhosphorBlend(77),
@ -53,6 +54,10 @@ TIASurface::TIASurface(FrameBuffer& buffer, OSystem& system)
}
uInt32 scanID = myFB.allocateSurface(1, kScanH, scanData);
mySLineSurface = myFB.surface(scanID);
// Base TIA surface for use in taking snapshots in 1x mode
uInt32 baseID = myFB.allocateSurface(kTIAW*2, kTIAH);
myBaseTiaSurface = myFB.surface(baseID);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -125,6 +130,30 @@ void TIASurface::setPalette(const uInt32* tia_palette, const uInt32* rgb_palette
myNTSCFilter.setTIAPalette(*this, rgb_palette);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const FBSurface& TIASurface::baseSurface(GUI::Rect& rect)
{
uInt32 tiaw = myTIA->width(), width = tiaw*2, height = myTIA->height();
rect.setBounds(0, 0, width, height);
// Fill the surface with pixels from the TIA, scaled 2x horizontally
uInt32 *buffer, pitch;
myBaseTiaSurface->basePtr(buffer, pitch);
uInt32* buf_ptr = buffer;
for(uInt32 y = 0; y < height; ++y)
{
for(uInt32 x = 0; x < tiaw; ++x)
{
uInt32 pixel = myFB.tiaSurface().pixel(y*tiaw+x);
*buf_ptr++ = pixel;
*buf_ptr++ = pixel;
}
}
return *myBaseTiaSurface;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 TIASurface::pixel(uInt32 idx, uInt8 shift) const
{

View File

@ -27,6 +27,7 @@ class FrameBuffer;
class FBSurface;
class VideoMode;
#include "Rect.hxx"
#include "NTSCFilter.hxx"
#include "bspf.hxx"
@ -47,7 +48,7 @@ class TIASurface
/**
Creates a new TIASurface object
*/
TIASurface(FrameBuffer& buffer, OSystem& system);
TIASurface(OSystem& system);
/**
Destructor
@ -72,6 +73,11 @@ class TIASurface
*/
void setPalette(const uInt32* tia_palette, const uInt32* rgb_palette);
/**
Get the TIA base surface for use in saving to a PNG image.
*/
const FBSurface& baseSurface(GUI::Rect& rect);
/**
Get the TIA pixel associated with the given TIA buffer index,
shifting by the given offset (for greyscale values).
@ -136,11 +142,11 @@ class TIASurface
void render();
private:
FrameBuffer& myFB;
OSystem& myOSystem;
FrameBuffer& myFB;
TIA* myTIA;
FBSurface *myTiaSurface, *mySLineSurface;
FBSurface *myTiaSurface, *mySLineSurface, *myBaseTiaSurface;
// Enumeration created such that phosphor off/on is in LSB,
// and Blargg off/on is in MSB

View File

@ -159,6 +159,9 @@ struct Rect
}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const Rect EmptyRect;
} // End of namespace GUI
#endif