Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -166,6 +166,14 @@ ;;;;;;;;;;;;;;;;;;;;;;;; +[videorendering] +enabled = true +fps = 60 +path = "C:/Trailer/" +start = 0 +format = ".jpeg" +jpeg_quality = 100 + [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. Index: build/premake/extern_libs5.lua =================================================================== --- build/premake/extern_libs5.lua +++ build/premake/extern_libs5.lua @@ -431,6 +431,22 @@ }) end, }, + libjpg = { + compile_settings = function() + if os.istarget("windows") or os.istarget("macosx") then + add_default_include_paths("libjpg") + end + end, + link_settings = function() + if os.istarget("windows") or os.istarget("macosx") then + add_default_lib_paths("libjpg") + end + add_default_links({ + win_names = { "jpeg-6b" }, + unix_names = { "jpeg" }, + }) + end, + }, libpng = { compile_settings = function() if os.istarget("windows") then Index: build/premake/premake5.lua =================================================================== --- build/premake/premake5.lua +++ build/premake/premake5.lua @@ -876,6 +876,7 @@ "sdl", "openal", "libpng", + "libjpg", "zlib", "valgrind", "cxxtest", @@ -994,6 +995,7 @@ "sdl", "libpng", + "libjpg", "zlib", "spidermonkey", Index: source/graphics/HeightMipmap.cpp =================================================================== --- source/graphics/HeightMipmap.cpp +++ source/graphics/HeightMipmap.cpp @@ -253,7 +253,7 @@ } DynArray da; - WARN_IF_ERR(t.encode(filename.Extension(), &da)); + WARN_IF_ERR(t.encode(filename.Extension(), &da, 100)); g_VFS->CreateFile(filename, DummySharedPtr(da.base), da.pos); ignore_result(da_free(&da)); } Index: source/lib/external_libraries/libjpeg.h =================================================================== --- source/lib/external_libraries/libjpeg.h +++ source/lib/external_libraries/libjpeg.h @@ -0,0 +1,24 @@ +/* Copyright (c) 2022 Wildfire Games + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: © 2022 Wildfire Games + */ + +#ifndef INCLUDED_LIBJPEG +#define INCLUDED_LIBJPEG + +extern "C" { +// we are not a core library module, so don't define JPEG_INTERNALS +#include +#include +} + +// automatically link against the required library +#if MSC_VERSION +# ifdef NDEBUG +# pragma comment(lib, "jpeg-6b.lib") +# else +# pragma comment(lib, "jpeg-6bd.lib") +# endif // #ifdef NDEBUG +#endif // #ifdef MSC_VERSION + +#endif // #ifndef INCLUDED_LIBJPEG Index: source/lib/tex/tex.h =================================================================== --- source/lib/tex/tex.h +++ source/lib/tex/tex.h @@ -281,7 +281,7 @@ * when no longer needed. Invalid unless function succeeds. * @return Status **/ - Status encode(const OsPath& extension, DynArray* da); + Status encode(const OsPath& extension, DynArray* da, int quality); /** * store the given image data into a Tex object; this will be as if Index: source/lib/tex/tex.cpp =================================================================== --- source/lib/tex/tex.cpp +++ source/lib/tex/tex.cpp @@ -747,7 +747,7 @@ } -Status Tex::encode(const OsPath& extension, DynArray* da) +Status Tex::encode(const OsPath& extension, DynArray* da, int quality) { CHECK_TEX(this); WARN_RETURN_STATUS_IF_ERR(tex_validate_plain_format(m_Bpp, m_Flags)); @@ -764,7 +764,7 @@ WARN_RETURN_STATUS_IF_ERR(tex_codec_for_filename(extension, &c)); // encode into - Status err = c->encode(this, da); + Status err = c->encode(this, da, quality); if(err < 0) { (void)da_free(da); Index: source/lib/tex/tex_bmp.cpp =================================================================== --- source/lib/tex/tex_bmp.cpp +++ source/lib/tex/tex_bmp.cpp @@ -124,7 +124,7 @@ } -Status TexCodecBmp::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const +Status TexCodecBmp::encode(Tex* RESTRICT t, DynArray* RESTRICT da, int UNUSED(quality)) const { const size_t hdr_size = sizeof(BmpHeader); // needed for BITMAPFILEHEADER const size_t img_size = t->img_size(); Index: source/lib/tex/tex_codec.h =================================================================== --- source/lib/tex/tex_codec.h +++ source/lib/tex/tex_codec.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -63,7 +63,7 @@ * by the caller. * @return Status **/ - virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da) const = 0; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const = 0; /** * transform the texture's pixel format. @@ -120,7 +120,7 @@ class TexCodecPng:ITexCodec { public: virtual Status decode(u8* data, size_t size, Tex* RESTRICT t) const; - virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da) const; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const; virtual Status transform(Tex* t, size_t transforms) const; virtual bool is_hdr(const u8* file) const; virtual bool is_ext(const OsPath& extension) const; @@ -131,10 +131,24 @@ }; }; +class TexCodecJpg:ITexCodec { +public: + virtual Status decode(u8* data, size_t size, Tex* RESTRICT t) const; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const; + virtual Status transform(Tex* t, size_t transforms) const; + virtual bool is_hdr(const u8* file) const; + virtual bool is_ext(const OsPath& extension) const; + virtual size_t hdr_size(const u8* file) const; + virtual const wchar_t* get_name() const { + static const wchar_t *name = L"jpg"; + return name; + }; +}; + class TexCodecDds:ITexCodec { public: virtual Status decode(u8* data, size_t size, Tex* RESTRICT t) const; - virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da) const; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const; virtual Status transform(Tex* t, size_t transforms) const; virtual bool is_hdr(const u8* file) const; virtual bool is_ext(const OsPath& extension) const; @@ -148,7 +162,7 @@ class TexCodecTga:ITexCodec { public: virtual Status decode(u8* data, size_t size, Tex* RESTRICT t) const; - virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da) const; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const; virtual Status transform(Tex* t, size_t transforms) const; virtual bool is_hdr(const u8* file) const; virtual bool is_ext(const OsPath& extension) const; @@ -162,7 +176,7 @@ class TexCodecBmp:ITexCodec { public: virtual Status decode(u8* data, size_t size, Tex* RESTRICT t) const; - virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da) const; + virtual Status encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const; virtual Status transform(Tex* t, size_t transforms) const; virtual bool is_hdr(const u8* file) const; virtual bool is_ext(const OsPath& extension) const; Index: source/lib/tex/tex_codec.cpp =================================================================== --- source/lib/tex/tex_codec.cpp +++ source/lib/tex/tex_codec.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2014 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -36,11 +36,12 @@ // Statically allocate all of the codecs... TexCodecDds DdsCodec; TexCodecPng PngCodec; +TexCodecJpg JpgCodec; TexCodecTga TgaCodec; TexCodecBmp BmpCodec; // Codecs will be searched in this order static const ITexCodec *codecs[] = {(ITexCodec *)&DdsCodec, (ITexCodec *)&PngCodec, - (ITexCodec *)&TgaCodec, (ITexCodec *)&BmpCodec}; + (ITexCodec *)&JpgCodec, (ITexCodec *)&TgaCodec, (ITexCodec *)&BmpCodec}; static const int codecs_len = sizeof(codecs) / sizeof(ITexCodec*); // find codec that recognizes the desired output file extension, Index: source/lib/tex/tex_dds.cpp =================================================================== --- source/lib/tex/tex_dds.cpp +++ source/lib/tex/tex_dds.cpp @@ -617,7 +617,7 @@ } -Status TexCodecDds::encode(Tex* RESTRICT UNUSED(t), DynArray* RESTRICT UNUSED(da)) const +Status TexCodecDds::encode(Tex* RESTRICT UNUSED(t), DynArray* RESTRICT UNUSED(da), int UNUSED(quality)) const { // note: do not return ERR::NOT_SUPPORTED et al. because that would // break tex_write (which assumes either this, 0 or errors are returned). Index: source/lib/tex/tex_jpg.cpp =================================================================== --- source/lib/tex/tex_jpg.cpp +++ source/lib/tex/tex_jpg.cpp @@ -0,0 +1,599 @@ +/* Copyright (c) 2022 Wildfire Games + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: © 2022 Wildfire Games + */ + +#include "precompiled.h" + +#include + +#include "lib/external_libraries/libjpeg.h" +#include "lib/allocators/shared_ptr.h" + +#include "tex_codec.h" + + +// squelch "dtor / setjmp interaction" warnings. +// all attempts to resolve the underlying problem failed; apparently +// the warning is generated if setjmp is used at all in C++ mode. +// (jpg_*code have no code that would trigger ctors/dtors, nor are any +// called in their prolog/epilog code). +#if MSC_VERSION +# pragma warning(disable: 4611) +#endif + + +/* IMPORTANT: we assume that JOCTET is 8 bits. */ +cassert(sizeof(JOCTET) == 1 && CHAR_BIT == 8); + +//----------------------------------------------------------------------------- +// mem source manager +//----------------------------------------------------------------------------- + + +/* Expanded data source object for memory input */ +typedef struct +{ + struct jpeg_source_mgr pub; /* public fields */ + DynArray* da; +} +SrcMgr; +typedef SrcMgr* SrcPtr; + + +/* +* Initialize source --- called by jpeg_read_header +* before any data is actually read. +*/ + +METHODDEF(void) src_init(j_decompress_ptr UNUSED(cinfo)) +{ +} + + +/* +* Fill the input buffer --- called whenever buffer is emptied. +* +* In typical applications, this should read fresh data into the buffer +* (ignoring the current state of next_input_byte & bytes_in_buffer), +* reset the pointer & count to the start of the buffer, and return TRUE +* indicating that the buffer has been reloaded. It is not necessary to +* fill the buffer entirely, only to obtain at least one more byte. +* +* There is no such thing as an EOF return. If the end of the file has been +* reached, the routine has a choice of ERREXIT() or inserting fake data into +* the buffer. In most cases, generating a warning message and inserting a +* fake EOI marker is the best course of action --- this will allow the +* decompressor to output however much of the image is there. However, +* the resulting error message is misleading if the real problem is an empty +* input file, so we handle that case specially. +*/ + +METHODDEF(boolean) src_fill_buffer(j_decompress_ptr cinfo) +{ + SrcPtr src = (SrcPtr)cinfo->src; + static const JOCTET eoi[2] = { 0xFF, JPEG_EOI }; + + /* + * since jpeg_mem_src fills the buffer with everything we've got, + * jpeg is trying to read beyond end of buffer. return a fake EOI marker. + * note: don't modify input buffer: it might be read-only. + */ + + WARNMS(cinfo, JWRN_JPEG_EOF); + + src->pub.next_input_byte = eoi; + src->pub.bytes_in_buffer = 2; + return TRUE; +} + + +/* +* Skip data --- used to skip over a potentially large amount of +* uninteresting data (such as an APPn marker). +*/ + +METHODDEF(void) src_skip_data(j_decompress_ptr cinfo, long num_bytes) +{ + SrcPtr src = (SrcPtr)cinfo->src; + size_t skip_count = (size_t)num_bytes; + + /* docs say non-positive num_byte skips should be ignored */ + if(num_bytes <= 0) + return; + + /* + * just subtract bytes available in buffer, + * making sure we don't underflow the size_t. + * note: if we skip to or beyond end of buffer, + * bytes_in_buffer = 0 => fill_input_buffer called => abort. + */ + if(skip_count > src->pub.bytes_in_buffer) + skip_count = src->pub.bytes_in_buffer; + + src->pub.bytes_in_buffer -= skip_count; + src->pub.next_input_byte += skip_count; +} + + +/* +* An additional method that can be provided by data source modules is the +* resync_to_restart method for error recovery in the presence of RST markers. +* For the moment, this source module just uses the default resync method +* provided by the JPEG library. That method assumes that no backtracking +* is possible. +*/ + + +/* +* Terminate source --- called by jpeg_finish_decompress +* after all data has been read. Often a no-op. +* +* NB: *not* called by jpeg_abort or jpeg_destroy; surrounding +* application must deal with any cleanup that should happen even +* for error exit. +*/ + +METHODDEF(void) src_term(j_decompress_ptr UNUSED(cinfo)) +{ + /* + * no-op (we don't own the buffer and shouldn't, + * to make possible multiple images in a source). + */ +} + + +/* +* Prepare for input from a buffer. +* The caller is responsible for freeing it after finishing decompression. +*/ + +GLOBAL(void) src_prepare(j_decompress_ptr cinfo, u8* RESTRICT data, size_t size) +{ + SrcPtr src; + + /* Treat 0-length buffer as fatal error */ + if(size == 0) + ERREXIT(cinfo, JERR_INPUT_EMPTY); + + /* + * The source object is made permanent so that + * a series of JPEG images can be read from the same file + * by calling jpeg_mem_src only before the first one. + * This makes it unsafe to use this manager and a different source + * manager serially with the same JPEG object. Caveat programmer. + */ + + /* first time for this JPEG object? */ + if(!cinfo->src) + cinfo->src = (struct jpeg_source_mgr*) + (*cinfo->mem->alloc_small) ((j_common_ptr)cinfo, JPOOL_PERMANENT, + sizeof(SrcMgr)); + /* (takes care of raising error if out of memory) */ + + src = (SrcPtr)cinfo->src; + src->pub.init_source = src_init; + src->pub.fill_input_buffer = src_fill_buffer; + src->pub.skip_input_data = src_skip_data; + src->pub.resync_to_restart = jpeg_resync_to_restart; /* default */ + src->pub.term_source = src_term; + + /* + * fill buffer with everything we have. + * if fill_input_buffer is called, the buffer was overrun. + */ + src->pub.bytes_in_buffer = size; + src->pub.next_input_byte = (JOCTET*)data; +} + + +//----------------------------------------------------------------------------- +// mem destination manager +//----------------------------------------------------------------------------- + +/* Expanded data destination object for memory output */ +typedef struct { + struct jpeg_destination_mgr pub; /* public fields */ + DynArray* da; +} DstMgr; + +typedef DstMgr* DstPtr; + +// this affects how often dst_empty_output_buffer is called (which +// efficiently expands the DynArray) and how much tail memory we waste +// (not an issue because it is freed immediately after compression). +#define OUTPUT_BUF_SIZE 64*KiB /* choose an efficiently writeable size */ + +// note: can't call dst_empty_output_buffer from dst_init or vice versa +// because only the former must advance da->pos. +static void make_room_in_buffer(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + DynArray* da = dst->da; + + void* start = da->base + da->cur_size; + + if(da_set_size(da, da->cur_size+OUTPUT_BUF_SIZE) != 0) + ERREXIT(cinfo, JERR_FILE_WRITE); + + dst->pub.next_output_byte = (JOCTET*)start; + dst->pub.free_in_buffer = OUTPUT_BUF_SIZE; +} + + +/* +* Initialize destination --- called by jpeg_start_compress +* before any data is actually written. +*/ +METHODDEF(void) dst_init(j_compress_ptr cinfo) +{ + make_room_in_buffer(cinfo); +} + + +/* +* Empty the output buffer --- called whenever buffer fills up. +* +* In typical applications, this should write the entire output buffer +* (ignoring the current state of next_output_byte & free_in_buffer), +* reset the pointer & count to the start of the buffer, and return TRUE +* indicating that the buffer has been dumped. +* +* +*/ +METHODDEF(boolean) dst_empty_output_buffer(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + DynArray* da = dst->da; + + // writing out OUTPUT_BUF_SIZE-dst->pub.free_in_buffer bytes + // sounds reasonable, but makes for broken output. + da->pos += OUTPUT_BUF_SIZE; + + make_room_in_buffer(cinfo); + + return TRUE; // not suspended +} + + +/* +* Terminate destination --- called by jpeg_finish_compress +* after all data has been written. Usually needs to flush buffer. +* +* NB: *not* called by jpeg_abort or jpeg_destroy; surrounding +* application must deal with any cleanup that should happen even +* for error exit. +*/ +METHODDEF(void) dst_term(j_compress_ptr cinfo) +{ + DstPtr dst = (DstPtr)cinfo->dest; + DynArray* da = dst->da; + + // account for nbytes left in buffer + da->pos += OUTPUT_BUF_SIZE - dst->pub.free_in_buffer; +} + + +/* +* Prepare for output to a buffer. +* The caller is responsible for allocating and writing out to disk after +* compression is complete. +*/ + +GLOBAL(void) dst_prepare(j_compress_ptr cinfo, DynArray* da) +{ + /* The destination object is made permanent so that multiple JPEG images + * can be written to the same file without re-executing dst_prepare. + * This makes it dangerous to use this manager and a different destination + * manager serially with the same JPEG object, because their private object + * sizes may be different. Caveat programmer. + */ + if (cinfo->dest == NULL) { /* first time for this JPEG object? */ + cinfo->dest = (struct jpeg_destination_mgr*)(*cinfo->mem->alloc_small) + ((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(DstMgr)); + } + + DstPtr dst = (DstPtr)cinfo->dest; + dst->pub.init_destination = dst_init; + dst->pub.empty_output_buffer = dst_empty_output_buffer; + dst->pub.term_destination = dst_term; + dst->da = da; +} + + +//----------------------------------------------------------------------------- +// error handler, shared by jpg_(en|de)code +//----------------------------------------------------------------------------- + +// the JPEG library's standard error handler (jerror.c) is divided into +// several "methods" which we can override individually. This allows +// adjusting the behavior without duplicating a lot of code, which may +// have to be updated with each future release. +// +// we here override error_exit to return control to the library's caller +// (i.e. jpg_(de|en)code) when a fatal error occurs, rather than calling exit. +// +// the replacement error_exit does a longjmp back to the caller's +// setjmp return point. it needs access to the jmp_buf, +// so we store it in a "subclass" of jpeg_error_mgr. + +struct JpgErrorMgr +{ + struct jpeg_error_mgr pub; // "public" fields + + // jump here (back to JPEG lib caller) on error + jmp_buf call_site; + + // description of first error encountered; must store in JPEG context + // for thread safety. initialized in setup_err_mgr. + char msg[JMSG_LENGTH_MAX]; + + JpgErrorMgr(jpeg_compress_struct& cinfo); + JpgErrorMgr(jpeg_decompress_struct& cinfo); +private: + void init(); +}; + + +METHODDEF(void) err_error_exit(j_common_ptr cinfo) +{ + // get subclass + JpgErrorMgr* err_mgr = (JpgErrorMgr*)cinfo->err; + + // "output" error message (i.e. store in JpgErrorMgr; + // call_site is responsible for displaying it via debug_printf) + (*cinfo->err->output_message)(cinfo); + + // jump back to call site, i.e. jpg_(de|en)code + longjmp(err_mgr->call_site, 1); +} + + +// stores message in JpgErrorMgr for later output by jpg_(de|en)code. +// note: don't display message here, so the caller can +// add some context (whether encoding or decoding, and filename). +METHODDEF(void) err_output_message(j_common_ptr cinfo) +{ + // get subclass + JpgErrorMgr* err_mgr = (JpgErrorMgr*)cinfo->err; + + // this context already had an error message; don't overwrite it. + // (subsequent errors probably aren't related to the real problem). + // note: was set to '\0' by ctor. + if(err_mgr->msg[0] != '\0') + return; + + // generate the message and store it + (*cinfo->err->format_message)(cinfo, err_mgr->msg); +} + + +void JpgErrorMgr::init() +{ + // fill in pub fields + jpeg_std_error(&pub); + // .. and override some methods: + pub.error_exit = err_error_exit; + pub.output_message = err_output_message; + + // required for "already have message" check in err_output_message + msg[0] = '\0'; +} + +JpgErrorMgr::JpgErrorMgr(jpeg_compress_struct& cinfo) +{ + init(); + // hack: register this error manager with cinfo. + // must be done before jpeg_create_* in case that fails + // (unlikely, but possible if out of memory). + cinfo.err = &pub; +} + +JpgErrorMgr::JpgErrorMgr(jpeg_decompress_struct& cinfo) +{ + init(); + // hack: register this error manager with cinfo. + // must be done before jpeg_create_* in case that fails + // (unlikely, but possible if out of memory). + cinfo.err = &pub; +} + + +//----------------------------------------------------------------------------- + + +Status TexCodecJpg::transform(Tex* UNUSED(t), size_t UNUSED(transforms)) const +{ + return INFO::TEX_CODEC_CANNOT_HANDLE; +} + + +// note: jpg_encode and jpg_decode cannot be combined due to +// libjpg interface differences. +// we do split them up into interface and impl to simplify +// resource cleanup and avoid "dtor / setjmp interaction" warnings. +// +// rationale for row array: jpeg won't output more than a few +// scanlines at a time, so we need an output loop anyway. however, +// passing at least 2..4 rows is more efficient in low-quality modes +// due to less copying. + + +static Status jpg_decode_impl(u8* RESTRICT data, size_t size, jpeg_decompress_struct* cinfo, Tex* t) +{ + src_prepare(cinfo, data, size); + + // ignore return value since: + // - suspension is not possible with the mem data source + // - we passed TRUE to raise an error if table-only JPEG file + (void)jpeg_read_header(cinfo, TRUE); + + // set libjpg output format. we cannot go with the default because + // Photoshop writes non-standard CMYK files that must be converted to RGB. + size_t flags = 0; + cinfo->out_color_space = JCS_RGB; + if(cinfo->num_components == 1) + { + flags |= TEX_GREY; + cinfo->out_color_space = JCS_GRAYSCALE; + } + + // lower quality, but faster + cinfo->dct_method = JDCT_IFAST; + cinfo->do_fancy_upsampling = FALSE; + + // ignore return value since suspension is not possible with the + // mem data source. + // note: since we've set out_color_space, JPEG will always + // return an acceptable image format; no need to check. + (void)jpeg_start_decompress(cinfo); + + // scaled output image dimensions and final bpp are now available. + int w = cinfo->output_width; + int h = cinfo->output_height; + int bpp = cinfo->output_components * 8; + + // alloc destination buffer + const size_t pitch = w * bpp / 8; + const size_t imgSize = pitch * h; // for allow_rows + std::shared_ptr img; + AllocateAligned(img, imgSize, g_PageSize); + + // read rows + std::vector rows = tex_codec_alloc_rows(img.get(), h, pitch, TEX_TOP_DOWN, 0); + // could use cinfo->output_scanline to keep track of progress, + // but we need to count lines_left anyway (paranoia). + JSAMPARRAY row = (JSAMPARRAY)&rows[0]; + JDIMENSION lines_left = h; + while(lines_left != 0) + { + JDIMENSION lines_read = jpeg_read_scanlines(cinfo, row, lines_left); + row += lines_read; + lines_left -= lines_read; + + // we've decoded in-place; no need to further process + } + + // ignore return value since suspension is not possible with the + // mem data source. + (void)jpeg_finish_decompress(cinfo); + + Status ret = INFO::OK; + if(cinfo->err->num_warnings != 0) + ret = WARN::TEX_INVALID_DATA; + + // store image info and validate + return ret | t->wrap(w,h,bpp,flags,img,0); +} + + +static Status jpg_encode_impl(Tex* t, jpeg_compress_struct* cinfo, DynArray* da, int quality) +{ + dst_prepare(cinfo, da); + + // describe image format + // required: + cinfo->image_width = (JDIMENSION)t->m_Width; + cinfo->image_height = (JDIMENSION)t->m_Height; + cinfo->input_components = (int)t->m_Bpp / 8; + cinfo->in_color_space = (t->m_Bpp == 8)? JCS_GRAYSCALE : JCS_RGB; + // defaults depend on cinfo->in_color_space already having been set! + jpeg_set_defaults(cinfo); + + // additional settings + jpeg_set_quality(cinfo, quality, TRUE); + + // TRUE ensures that we will write a complete interchange-JPEG file. + // don't change unless you are very sure of what you're doing. + jpeg_start_compress(cinfo, TRUE); + + // if BGR, convert to RGB. + WARN_IF_ERR(t->transform_to(t->m_Flags & ~TEX_BGR)); + + const size_t pitch = t->m_Width * t->m_Bpp / 8; + u8* data = t->get_data(); + std::vector rows = tex_codec_alloc_rows(data, t->m_Height, pitch, t->m_Flags, TEX_TOP_DOWN); + + // could use cinfo->output_scanline to keep track of progress, + // but we need to count lines_left anyway (paranoia). + JSAMPARRAY row = (JSAMPARRAY)&rows[0]; + JDIMENSION lines_left = (JDIMENSION)t->m_Height; + while(lines_left != 0) + { + JDIMENSION lines_read = jpeg_write_scanlines(cinfo, row, lines_left); + row += lines_read; + lines_left -= lines_read; + + // we've decoded in-place; no need to further process + } + + jpeg_finish_compress(cinfo); + + Status ret = INFO::OK; + if(cinfo->err->num_warnings != 0) + ret = WARN::TEX_INVALID_DATA; + + return ret; +} + + + +bool TexCodecJpg::is_hdr(const u8* file) const +{ + // JFIF requires SOI marker at start of stream. + // we compare single bytes to be endian-safe. + return (file[0] == 0xff && file[1] == 0xd8); +} + + +bool TexCodecJpg::is_ext(const OsPath& extension) const +{ + return extension == L".jpg" || extension == L".jpeg"; +} + + +size_t TexCodecJpg::hdr_size(const u8* UNUSED(file)) const +{ + return 0; // libjpg returns decoded image data; no header +} + + +Status TexCodecJpg::decode(u8* RESTRICT data, size_t size, Tex* RESTRICT t) const +{ + // contains the JPEG decompression parameters and pointers to + // working space (allocated as needed by the JPEG library). + struct jpeg_decompress_struct cinfo; + + JpgErrorMgr jerr(cinfo); + if(setjmp(jerr.call_site)) + return ERR::FAIL; + + jpeg_create_decompress(&cinfo); + + Status ret = jpg_decode_impl(data, size, &cinfo, t); + + jpeg_destroy_decompress(&cinfo); // releases a "good deal" of memory + + return ret; +} + + +// limitation: palette images aren't supported +Status TexCodecJpg::encode(Tex* RESTRICT t, DynArray* RESTRICT da, int quality) const +{ + // contains the JPEG compression parameters and pointers to + // working space (allocated as needed by the JPEG library). + struct jpeg_compress_struct cinfo; + + JpgErrorMgr jerr(cinfo); + if(setjmp(jerr.call_site)) + WARN_RETURN(ERR::FAIL); + + jpeg_create_compress(&cinfo); + + Status ret = jpg_encode_impl(t, &cinfo, da, quality); + + jpeg_destroy_compress(&cinfo); // releases a "good deal" of memory + + return ret; +} Index: source/lib/tex/tex_png.cpp =================================================================== --- source/lib/tex/tex_png.cpp +++ source/lib/tex/tex_png.cpp @@ -271,7 +271,7 @@ // limitation: palette images aren't supported Status TexCodecPng::decode(u8* RESTRICT data, size_t size, Tex* RESTRICT t) const { - TIMER_ACCRUE(tc_png_decode); +TIMER_ACCRUE(tc_png_decode); png_infop info_ptr = 0; @@ -304,7 +304,7 @@ // limitation: palette images aren't supported -Status TexCodecPng::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const +Status TexCodecPng::encode(Tex* RESTRICT t, DynArray* RESTRICT da, int UNUSED(quality)) const { png_infop info_ptr = 0; Index: source/lib/tex/tex_tga.cpp =================================================================== --- source/lib/tex/tex_tga.cpp +++ source/lib/tex/tex_tga.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -144,7 +144,7 @@ } -Status TexCodecTga::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const +Status TexCodecTga::encode(Tex* RESTRICT t, DynArray* RESTRICT da, int UNUSED(quality)) const { u8 img_desc = 0; if(t->m_Flags & TEX_TOP_DOWN) Index: source/main.cpp =================================================================== --- source/main.cpp +++ source/main.cpp @@ -360,6 +360,17 @@ g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); + bool takeScreenshots; + float screenshotsFPS; + int screenshotsStartTime; + + CFG_GET_VAL("videorendering.enabled", takeScreenshots); + CFG_GET_VAL("videorendering.fps", screenshotsFPS); + CFG_GET_VAL("videorendering.start", screenshotsStartTime); + + bool doScreenshot = takeScreenshots && g_Game && + g_Game->IsGameStarted() && g_Game->SimTime() >= screenshotsStartTime * 1000; + // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); @@ -373,13 +384,17 @@ // .. new method - filtered and more smooth, but errors may accumulate #else - const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); + const float realTimeSinceLastFrame = 1.0f / (doScreenshot ? screenshotsFPS : g_frequencyFilter->SmoothedFrequency()); #endif + ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; + // Don't pause renderer nor GameView Update on focus loss + g_app_has_focus = g_app_has_focus || doScreenshot; + // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. @@ -443,6 +458,9 @@ g_Renderer.RenderFrame(true); + if (doScreenshot) + g_Renderer.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::DEFAULT); + g_Profiler.Frame(); LimitFPS(); Index: source/ps/Game.h =================================================================== --- source/ps/Game.h +++ source/ps/Game.h @@ -191,6 +191,8 @@ inline OsPath GetReplayPath() const { return m_ReplayPath; } + const int SimTime() const; + /** * Replace the current turn manager. * This class will take ownership of the pointer. Index: source/ps/Game.cpp =================================================================== --- source/ps/Game.cpp +++ source/ps/Game.cpp @@ -370,6 +370,11 @@ m_ViewedPlayerID = playerID; } +const int CGame::SimTime() const +{ + return GetTurnManager()->GetCurrentTurn() * GetTurnManager()->GetCurrentTurnLength(); +} + void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { if (m_ReplayLogger) Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -185,7 +185,6 @@ if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); - g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). @@ -547,6 +546,12 @@ // g_ConfigDB, command line args, globals CONFIG_Init(args); + // Mount this after the config was initialized + CStr screenshotpath; + CFG_GET_VAL("videorendering.path", screenshotpath); + if (g_VFS->Mount(L"screenshots/", OsPath(screenshotpath), VFS_MOUNT_MUST_EXIST, VFS_MAX_PRIORITY) == ERR::VFS_DIR_NOT_FOUND) + LOGERROR("Screenshot directory does not exist! Check the options and restart."); + // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; Index: source/ps/Util.h =================================================================== --- source/ps/Util.h +++ source/ps/Util.h @@ -33,7 +33,7 @@ OsPath createDateIndexSubdirectory(const OsPath& parentDir); -Status tex_write(Tex* t, const VfsPath& filename); +Status tex_write(Tex* t, const VfsPath& filename, int quality); std::string Hexify(const std::string& s); std::string Hexify(const u8* s, size_t length); Index: source/ps/Util.cpp =================================================================== --- source/ps/Util.cpp +++ source/ps/Util.cpp @@ -152,10 +152,10 @@ // write the specified texture to disk. // note: cannot be made const because the image may have to be // transformed to write it out in the format determined by 's extension. -Status tex_write(Tex* t, const VfsPath& filename) +Status tex_write(Tex* t, const VfsPath& filename, int quality) { DynArray da; - RETURN_STATUS_IF_ERR(t->encode(filename.Extension(), &da)); + RETURN_STATUS_IF_ERR(t->encode(filename.Extension(), &da, quality)); // write to disk Status ret = INFO::OK; Index: source/renderer/Renderer.cpp =================================================================== --- source/renderer/Renderer.cpp +++ source/renderer/Renderer.cpp @@ -427,7 +427,8 @@ { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. - if (!ShouldRender()) + // We might need to overwrite this for video rendering. + if (!needsPresent && !ShouldRender()) return; if (m_ScreenShotType == ScreenShotType::BIG) @@ -641,10 +642,12 @@ void CRenderer::RenderScreenShot(const bool needsPresent) { m_ScreenShotType = ScreenShotType::NONE; - + std::string screenshotFormat; + CFG_GET_VAL("videorendering.format", screenshotFormat); // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. - const VfsPath filenameFormat(L"screenshots/screenshot%04d.png"); + const VfsPath basenameFormat(L"screenshots/videoscreen%04d"); + const VfsPath filenameFormat = basenameFormat.ChangeExtension(screenshotFormat); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename); @@ -671,16 +674,12 @@ if (needsPresent) g_VideoMode.GetBackendDevice()->Present(); - if (tex_write(&t, filename) == INFO::OK) + int quality; + CFG_GET_VAL("videorendering.jpeg_quality", quality); + if (tex_write(&t, filename, quality) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); - - LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); - - debug_printf( - CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), - realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); @@ -790,16 +789,13 @@ g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera); } - if (tex_write(&t, filename) == INFO::OK) + int quality; + CFG_GET_VAL("videorendering.jpeg_quality", quality); + + if (tex_write(&t, filename, quality) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); - - LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); - - debug_printf( - CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), - realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); Index: source/simulation2/components/CCmpAIManager.cpp =================================================================== --- source/simulation2/components/CCmpAIManager.cpp +++ source/simulation2/components/CCmpAIManager.cpp @@ -392,7 +392,7 @@ for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); - tex_write(&t, filename); + tex_write(&t, filename, 100); } void SetRNGSeed(u32 seed) Index: source/simulation2/system/TurnManager.h =================================================================== --- source/simulation2/system/TurnManager.h +++ source/simulation2/system/TurnManager.h @@ -152,6 +152,11 @@ u32 GetCurrentTurn() const { return m_CurrentTurn; } /** + * @returns the current turn length. + */ + u32 GetCurrentTurnLength() const { return m_TurnLength; } + + /** * @return how many turns are ready to be computed. * (used to detect players/observers that fall behind the live game. */