Index: source/lib/file/common/real_directory.h =================================================================== --- source/lib/file/common/real_directory.h +++ source/lib/file/common/real_directory.h @@ -52,6 +52,7 @@ virtual Status Load(const OsPath& name, const shared_ptr& buf, size_t size) const; Status Store(const OsPath& name, const shared_ptr& fileContents, size_t size); + Status Preallocate(const OsPath& name, size_t size); void Watch(); Index: source/lib/file/common/real_directory.cpp =================================================================== --- source/lib/file/common/real_directory.cpp +++ source/lib/file/common/real_directory.cpp @@ -58,6 +58,12 @@ } +Status RealDirectory::Preallocate(const OsPath& name, size_t size) +{ + return io::Preallocate(m_path / name, size); +} + + void RealDirectory::Watch() { if(!m_watch) Index: source/lib/file/io/io.h =================================================================== --- source/lib/file/io/io.h +++ source/lib/file/io/io.h @@ -38,6 +38,8 @@ #include "lib/allocators/unique_range.h" +#include + namespace ERR { const Status IO = -110301; @@ -291,6 +293,37 @@ //----------------------------------------------------------------------------- // Store +static inline Status Preallocate(const OsPath& pathname, size_t size, const Parameters& p = Parameters()) +{ +#if OS_WIN + File file; + int oflag = O_WRONLY; + if (p.queueDepth != 1) + oflag |= O_DIRECT; + RETURN_STATUS_IF_ERR(file.Open(pathname, oflag)); + if (waio_Preallocate(file.Descriptor(), (off_t)size) != INFO::OK) + return ERR::FAIL; + file.Close(); // (required by wtruncate) + RETURN_STATUS_IF_ERR(wtruncate(pathname, size)); + return INFO::OK; +#else + // TODO: implement faster preallocation on non-Windows platforms. + std::ofstream out(OsString(pathname).c_str(), std::ios::binary | std::ios::out); + if (out.fail()) + return ERR::FAIL; + const size_t BUFFER_SIZE = 128 * KiB; + void* buffer = malloc(BUFFER_SIZE); + for (size_t offset = 0; offset < size; offset += BUFFER_SIZE) + { + out.write(static_cast(buffer), std::min(BUFFER_SIZE, size - offset)); + if (out.fail()) + return ERR::FAIL; + } + free(buffer); + return INFO::OK; +#endif +} + // efficient writing requires preallocation; the resulting file is // padded to the sector size and needs to be truncated afterwards. // this function takes care of both. Index: source/lib/file/vfs/vfs.h =================================================================== --- source/lib/file/vfs/vfs.h +++ source/lib/file/vfs/vfs.h @@ -144,6 +144,14 @@ virtual Status CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; /** + * Create a preallocated file with the given size. + * @param pathname + * @param size [bytes] of the required preallocation. + * @return Status. + **/ + virtual Status CreatePreallocatedFile(const VfsPath& pathname, size_t size) = 0; + + /** * Replace a file with the given contents. * * @see CreateFile Index: source/lib/file/vfs/vfs.cpp =================================================================== --- source/lib/file/vfs/vfs.cpp +++ source/lib/file/vfs/vfs.cpp @@ -146,6 +146,28 @@ return INFO::OK; } + virtual Status CreatePreallocatedFile(const VfsPath& pathname, size_t size) + { + std::lock_guard lock(vfs_mutex); + VfsDirectory* directory; + Status st; + st = vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD | VFS_LOOKUP_CREATE | VFS_LOOKUP_CREATE_ALWAYS); + if (st == ERR::FILE_ACCESS) + return ERR::FILE_ACCESS; + + WARN_RETURN_STATUS_IF_ERR(st); + + const PRealDirectory& realDirectory = directory->AssociatedDirectory(); + const OsPath name = pathname.Filename(); + RETURN_STATUS_IF_ERR(realDirectory->Preallocate(name, size)); + + const VfsFile file(name, size, time(0), realDirectory->Priority(), realDirectory); + directory->AddFile(file); + + m_trace->NotifyStore(pathname, size); + return INFO::OK; + } + virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) { std::unique_lock lock(vfs_mutex); Index: source/lib/tex/bmp_helper.h =================================================================== --- source/lib/tex/bmp_helper.h +++ source/lib/tex/bmp_helper.h @@ -0,0 +1,60 @@ +/* Copyright (C) 2019 Wildfire Games. +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "lib/byte_order.h" +#include "lib/types.h" + +#pragma pack(push, 1) +struct BmpHeader +{ + // BITMAPFILEHEADER + // "BM" + u16 bfType; + // Image file size + u32 bfSize; + u16 bfReserved1; + u16 bfReserved2; + // Offset to image data + u32 bfOffBits; + + // BITMAPINFOHEADER + u32 biSize; + i32 biWidth; + i32 biHeight; + u16 biPlanes; + u16 biBitCount; + u32 biCompression; + u32 biSizeImage; + // The following are unused and zeroed when writing: + i32 biXPelsPerMeter; + i32 biYPelsPerMeter; + u32 biClrUsed; + u32 biClrImportant; +}; +#pragma pack(pop) + +BmpHeader CreateBMPHeader( + const u32 image_size, + const i32 image_width, + const i32 image_height, + const u16 bpp, + const u32 compression = 0); Index: source/lib/tex/bmp_helper.cpp =================================================================== --- source/lib/tex/bmp_helper.cpp +++ source/lib/tex/bmp_helper.cpp @@ -0,0 +1,59 @@ +/* Copyright (C) 2019 Wildfire Games. +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be included +* in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "precompiled.h" + +#include "lib/tex/bmp_helper.h" + +BmpHeader CreateBMPHeader(const u32 image_size, const i32 image_width, const i32 image_height, const u16 bpp, const u32 compression) +{ + const u32 header_size = sizeof(BmpHeader); + const u32 file_size = header_size + image_size; + const BmpHeader hdr = + { + // BITMAPFILEHEADER + // bfType = 'B','M' + 0x4D42, + // bfSize + file_size, + // bfReserved1,2 + 0, 0, + // bfOffBits + header_size, + + // BITMAPINFOHEADER + // biSize = sizeof(BITMAPINFOHEADER) + 40, + image_width, + image_height, + // biPlanes + 1, + bpp, + // biCompression + compression, + // biSizeImage + image_size, + // unused (bi?PelsPerMeter, biClr*) + 0, 0, 0, 0 + }; + return hdr; +} Index: source/lib/tex/tex_bmp.cpp =================================================================== --- source/lib/tex/tex_bmp.cpp +++ source/lib/tex/tex_bmp.cpp @@ -27,35 +27,9 @@ #include "precompiled.h" #include "lib/byte_order.h" -#include "tex_codec.h" +#include "lib/tex/bmp_helper.h" +#include "lib/tex/tex_codec.h" -#pragma pack(push, 1) - -struct BmpHeader -{ - // BITMAPFILEHEADER - u16 bfType; // "BM" - u32 bfSize; // of file - u16 bfReserved1; - u16 bfReserved2; - u32 bfOffBits; // offset to image data - - // BITMAPINFOHEADER - u32 biSize; - i32 biWidth; - i32 biHeight; - u16 biPlanes; - u16 biBitCount; - u32 biCompression; - u32 biSizeImage; - // the following are unused and zeroed when writing: - i32 biXPelsPerMeter; - i32 biYPelsPerMeter; - u32 biClrUsed; - u32 biClrImportant; -}; - -#pragma pack(pop) #define BI_RGB 0 // biCompression @@ -128,30 +102,12 @@ { const size_t hdr_size = sizeof(BmpHeader); // needed for BITMAPFILEHEADER const size_t img_size = t->img_size(); - const size_t file_size = hdr_size + img_size; const i32 h = (t->m_Flags & TEX_TOP_DOWN)? -(i32)t->m_Height : (i32)t->m_Height; size_t transforms = t->m_Flags; transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. transforms ^= TEX_BGR; // BMP is native BGR. - const BmpHeader hdr = - { - // BITMAPFILEHEADER - 0x4D42, // bfType = 'B','M' - (u32)file_size, // bfSize - 0, 0, // bfReserved1,2 - hdr_size, // bfOffBits - - // BITMAPINFOHEADER - 40, // biSize = sizeof(BITMAPINFOHEADER) - (i32)t->m_Width, - h, - 1, // biPlanes - (u16)t->m_Bpp, - BI_RGB, // biCompression - (u32)img_size, // biSizeImage - 0, 0, 0, 0 // unused (bi?PelsPerMeter, biClr*) - }; + const BmpHeader hdr = CreateBMPHeader((u32)img_size, (i32)t->m_Width, h, (u16)t->m_Bpp, BI_RGB); return tex_codec_write(t, transforms, &hdr, hdr_size, da); } Index: source/ps/BMPBlockWriter.h =================================================================== --- source/ps/BMPBlockWriter.h +++ source/ps/BMPBlockWriter.h @@ -0,0 +1,51 @@ +/* Copyright (C) 2019 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#ifndef INCLUDED_BMPBLOCKWRITER +#define INCLUDED_BMPBLOCKWRITER + +#include "lib/file/vfs/vfs.h" + +#include +#include + +// Allows to write to a BMP file by blocks. Sample usage is to write tiles as +// you get them from renderer. Removes the opened file in destructor of any +// failed operation. Closes the file on success. +// Currently works only 24bit blocks in BGR format. +class CBMPBlockWriter +{ +public: + CBMPBlockWriter(const size_t width, const size_t height, const size_t blockWidth, const size_t blockHeight); + ~CBMPBlockWriter(); + + bool OpenAndPreallocate(const VfsPath& path); + + /** + * Saves data block to the file. Data size should be at least blockWidth * blockHeight * 24bit. + */ + bool WriteBlock(void* data, const size_t x, const size_t y); +private: + const size_t m_Width, m_Height; + const size_t m_BlockWidth, m_BlockHeight; + const size_t m_HeaderSize; + + OsPath m_RealPath; + std::ofstream m_File; +}; + +#endif // INCLUDED_BMPBLOCKWRITER Index: source/ps/BMPBlockWriter.cpp =================================================================== --- source/ps/BMPBlockWriter.cpp +++ source/ps/BMPBlockWriter.cpp @@ -0,0 +1,70 @@ +/* Copyright (C) 2019 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#include "precompiled.h" + +#include "lib/tex/bmp_helper.h" +#include "lib/tex/tex.h" +#include "ps/BMPBlockWriter.h" +#include "ps/Filesystem.h" + +CBMPBlockWriter::CBMPBlockWriter(const size_t width, const size_t height, const size_t blockWidth, const size_t blockHeight) + : m_Width(width), + m_Height(height), + m_BlockWidth(blockWidth), + m_BlockHeight(blockHeight), + m_HeaderSize(tex_hdr_size(L".bmp")) +{ +} + +CBMPBlockWriter::~CBMPBlockWriter() +{ + if (!m_File.is_open() || m_File.fail()) + wunlink(m_RealPath); +} + +bool CBMPBlockWriter::OpenAndPreallocate(const VfsPath& path) +{ + const size_t bpp = 24; + const size_t imageSize = m_Width * m_Height * bpp / 8; + if (g_VFS->CreatePreallocatedFile(path, m_HeaderSize + imageSize) != INFO::OK) + return false; + if (!VfsFileExists(path) || g_VFS->GetRealPath(path, m_RealPath) != INFO::OK) + return false; + m_File = std::ofstream(OsString(m_RealPath).c_str(), std::ios::binary | std::ios::out | std::ios::in); + if (m_File.fail()) + return false; + BmpHeader header = CreateBMPHeader(imageSize, m_Width, m_Height, bpp, 0 /* we shouldn't use compression */); + m_File.seekp(0, std::ios::beg); + m_File.write(reinterpret_cast(&header), sizeof(BmpHeader)); + return !m_File.fail(); +} + +bool CBMPBlockWriter::WriteBlock(void* data, const size_t x, const size_t y) +{ + // TODO: account BMP padding. + const size_t rowSize = m_Width * 3; + for (size_t shiftY = 0; shiftY < m_BlockHeight; ++shiftY) + { + const size_t offset = m_HeaderSize + (y + shiftY) * rowSize + x * 3; + m_File.seekp(offset, std::ios::beg); + m_File.write(static_cast(data) + shiftY * m_BlockWidth * 3, m_BlockWidth * 3); + if (m_File.fail()) + return false; + } + return !m_File.fail(); +} Index: source/ps/Util.cpp =================================================================== --- source/ps/Util.cpp +++ source/ps/Util.cpp @@ -19,12 +19,13 @@ #include "ps/Util.h" -#include "lib/posix/posix_utsname.h" +#include "graphics/GameView.h" +#include "i18n/L10n.h" +#include "lib/allocators/shared_ptr.h" +#include "lib/bits.h" // round_up #include "lib/ogl.h" +#include "lib/posix/posix_utsname.h" #include "lib/snd.h" -#include "lib/timer.h" -#include "lib/bits.h" // round_up -#include "lib/allocators/shared_ptr.h" #include "lib/sysdep/sysdep.h" // sys_OpenFile #include "lib/sysdep/gfx.h" #include "lib/sysdep/cpu.h" @@ -34,19 +35,17 @@ #endif #include "lib/sysdep/smbios.h" #include "lib/tex/tex.h" - -#include "i18n/L10n.h" +#include "lib/timer.h" #include "lib/utf8.h" - +#include "maths/MathUtil.h" +#include "ps/BMPBlockWriter.h" +#include "ps/CLogger.h" +#include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Game.h" -#include "ps/CLogger.h" -#include "ps/Filesystem.h" #include "ps/VideoMode.h" #include "renderer/Renderer.h" -#include "maths/MathUtil.h" -#include "graphics/GameView.h" #include #include @@ -286,13 +285,111 @@ LOGERROR("Error writing screenshot to '%s'", filename.string8()); } +void WriteBigBMPScreenshotByBlocks(const VfsPath& path, int tiles) +{ + // Slightly ugly and inflexible: Always draw 640*480 tiles onto the screen, and + // hope the screen is actually large enough for that. + const int tile_w = 640, tile_h = 480; + ENSURE(g_xres >= tile_w && g_yres >= tile_h); + const int img_w = tile_w * tiles, img_h = tile_h * tiles; + CBMPBlockWriter bmpWriter(img_w, img_h, tile_w, tile_h); + if (!bmpWriter.OpenAndPreallocate(path)) + return; + + GLenum fmt = GL_BGR; + int flags = TEX_BOTTOM_UP | TEX_BGR; + + const size_t bpp = 24; + const size_t tile_size = tile_w * tile_h * bpp / 8; + void* tile_data = malloc(tile_size); + if (!tile_data) + { + WARN_IF_ERR(ERR::NO_MEM); + return; + } + + CCamera oldCamera = *g_Game->GetView()->GetCamera(); + + // Resize various things so that the sizes and aspect ratios are correct + { + g_Renderer.Resize(tile_w, tile_h); + SViewPort vp = { 0, 0, tile_w, tile_h }; + g_Game->GetView()->SetViewport(vp); + } + +#if !CONFIG2_GLES + // Temporarily move everything onto the front buffer, so the user can + // see the exciting progress as it renders (and can tell when it's finished). + // (It doesn't just use SwapBuffers, because it doesn't know whether to + // call the SDL version or the Atlas version.) + GLint oldReadBuffer, oldDrawBuffer; + glGetIntegerv(GL_READ_BUFFER, &oldReadBuffer); + glGetIntegerv(GL_DRAW_BUFFER, &oldDrawBuffer); + glDrawBuffer(GL_FRONT); + glReadBuffer(GL_FRONT); +#endif + + ogl_WarnIfError(); + + // Hide the cursor + CStrW oldCursor = g_CursorName; + g_CursorName = L""; + + // Render each tile + CMatrix3D projection; + projection.SetIdentity(); + for (int tile_y = 0; tile_y < tiles; ++tile_y) + { + for (int tile_x = 0; tile_x < tiles; ++tile_x) + { + // Adjust the camera to render the appropriate region + if (oldCamera.GetProjectionType() == CCamera::PERSPECTIVE) + projection.SetPerspectiveTile(oldCamera.GetFOV(), oldCamera.GetAspectRatio(), oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tile_x, tile_y); + g_Game->GetView()->GetCamera()->SetProjection(projection); + + RenderLogger(false); + RenderGui(false); + Render(); + RenderGui(true); + RenderLogger(true); + + // Copy the tile pixels into the main image + glReadPixels(0, 0, tile_w, tile_h, fmt, GL_UNSIGNED_BYTE, tile_data); + bmpWriter.WriteBlock(tile_data, tile_x * tile_w, tile_y * tile_h); + } + } + + ogl_WarnIfError(); + + // Restore the old cursor + g_CursorName = oldCursor; + +#if !CONFIG2_GLES + // Restore the buffer settings + glDrawBuffer(oldDrawBuffer); + glReadBuffer(oldReadBuffer); +#endif + // Restore the viewport settings + { + g_Renderer.Resize(g_xres, g_yres); + SViewPort vp = { 0, 0, g_xres, g_yres }; + g_Game->GetView()->SetViewport(vp); + g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera); + } + + free(tile_data); +} // Similar to WriteScreenshot, but generates an image of size 640*tiles x 480*tiles. void WriteBigScreenshot(const VfsPath& extension, int tiles) { // If the game hasn't started yet then use WriteScreenshot to generate the image. - if(g_Game == NULL){ WriteScreenshot(L".bmp"); return; } + if (g_Game == nullptr) + { + WriteScreenshot(L".bmp"); + return; + } // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. @@ -305,9 +402,19 @@ // hope the screen is actually large enough for that. const int tile_w = 640, tile_h = 480; ENSURE(g_xres >= tile_w && g_yres >= tile_h); - - const int img_w = tile_w*tiles, img_h = tile_h*tiles; + const int img_w = tile_w * tiles, img_h = tile_h * tiles; const int bpp = 24; + const size_t img_size = img_w * img_h * bpp / 8; + + // Not all GLES support BGR. +#if !CONFIG2_GLES + if (extension == L".bmp" && img_size > 10 * MiB) + { + WriteBigBMPScreenshotByBlocks(filename, tiles); + return; + } +#endif + GLenum fmt = GL_RGB; int flags = TEX_BOTTOM_UP; // we want writing BMP to be as fast as possible, @@ -320,7 +427,6 @@ #endif } - const size_t img_size = img_w * img_h * bpp/8; const size_t tile_size = tile_w * tile_h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); void* tile_data = malloc(tile_size);