diff --git a/Code/ww3d2/CMakeLists.txt b/Code/ww3d2/CMakeLists.txt index 9fec44107..4217870b8 100644 --- a/Code/ww3d2/CMakeLists.txt +++ b/Code/ww3d2/CMakeLists.txt @@ -289,4 +289,15 @@ if(BUILD_TESTING AND WIN32) add_test(NAME ww3d2_emitter_serialization_tests COMMAND ww3d2_emitter_serialization_tests) + + add_executable(ww3d2_framegrab_tests + tests/FrameGrabTests.cpp + framgrab.cpp + ) + target_link_libraries(ww3d2_framegrab_tests PRIVATE + wwcommon + vfw32 + winmm + ) + add_test(NAME ww3d2_framegrab_tests COMMAND ww3d2_framegrab_tests) endif() diff --git a/Code/ww3d2/framgrab.cpp b/Code/ww3d2/framgrab.cpp index d457e246a..a096a4608 100644 --- a/Code/ww3d2/framgrab.cpp +++ b/Code/ww3d2/framgrab.cpp @@ -21,125 +21,135 @@ ////////////////////////////////////////////////////////////////////// #include "framgrab.h" -#include +#include +#include +#include #include -//#include +#include +#include ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// -FrameGrabClass::FrameGrabClass(const char *filename, MODE mode, int width, int height, int bitcount, float framerate) +FrameGrabClass::FrameGrabClass(const char *filename, MODE mode, int width, int height, int bitcount, float framerate) : + Filename(filename), + FrameRate(framerate), + Mode(RAW), + Counter(0), + AVIFile(nullptr), + Bitmap(nullptr), + Stream(nullptr), + AVIStreamInfo(), + BitmapInfoHeader() { - HRESULT hr; - - Mode = mode; - Filename = filename; - FrameRate = framerate; - Counter = 0; - - Stream = 0; - AVIFile = 0; - - if(Mode != AVI) return; + if (mode != AVI) { + return; + } - AVIFileInit(); // opens AVIFile library + const unsigned int row_stride = CalculateRowStride(width, bitcount); + const unsigned long long image_size = + static_cast(row_stride) * static_cast(height); + if (filename == nullptr || filename[0] == '\0' || width <= 0 || height <= 0 || + bitcount != 24 || !std::isfinite(framerate) || framerate <= 0.0f || + static_cast(framerate) > std::numeric_limits::max() || row_stride == 0 || + image_size > static_cast(std::numeric_limits::max())) { + return; + } - // find the first free file with this prefix - int counter = 0; - int result; - char file[256]; + // Find the first unused AVI filename with this prefix. + unsigned int counter = 0; + std::string file; do { - sprintf(file, "%s%d.AVI", filename, counter++); - result = _access(file, 0); - } while(result != -1); + file = std::string(filename) + std::to_string(counter++) + ".AVI"; + } while (_access(file.c_str(), 0) != -1 && counter != 0); + if (counter == 0) { + return; + } + + // Do not initialize Video for Windows until all throwing filename work is + // complete; once initialized, CleanupAVI owns the matching AVIFileExit. + Mode = AVI; + AVIFileInit(); // Create new AVI file using AVIFileOpenA. - hr = AVIFileOpenA(&AVIFile, file, OF_WRITE | OF_CREATE, nullptr); - if (hr != 0) { - char buf[256]; - sprintf(buf, "Unable to open %s\n", Filename); - OutputDebugStringA(buf); + HRESULT hr = AVIFileOpenA(&AVIFile, file.c_str(), OF_WRITE | OF_CREATE, nullptr); + if (FAILED(hr)) { + OutputDebugStringA("Unable to open AVI movie capture file.\n"); CleanupAVI(); return; } + BitmapInfoHeader.biWidth = width; + BitmapInfoHeader.biHeight = height; + BitmapInfoHeader.biBitCount = static_cast(bitcount); + BitmapInfoHeader.biSizeImage = static_cast(image_size); + BitmapInfoHeader.biSize = sizeof(BITMAPINFOHEADER); + BitmapInfoHeader.biPlanes = 1; + BitmapInfoHeader.biCompression = BI_RGB; + BitmapInfoHeader.biXPelsPerMeter = 1; + BitmapInfoHeader.biYPelsPerMeter = 1; - // Create a stream using AVIFileCreateStreamA. + // Create an uncompressed, bottom-up BGR24 video stream. AVIStreamInfo.fccType = streamtypeVIDEO; AVIStreamInfo.fccHandler = mmioFOURCC('M','S','V','C'); - AVIStreamInfo.dwFlags = 0; - AVIStreamInfo.dwCaps = 0; - AVIStreamInfo.wPriority = 0; - AVIStreamInfo.wLanguage = 0; AVIStreamInfo.dwScale = 1; - AVIStreamInfo.dwRate = (int)FrameRate; - AVIStreamInfo.dwStart = 0; - AVIStreamInfo.dwLength = 0; - AVIStreamInfo.dwInitialFrames = 0; - AVIStreamInfo.dwSuggestedBufferSize = 0; - AVIStreamInfo.dwQuality = 0; - AVIStreamInfo.dwSampleSize = 0; + AVIStreamInfo.dwRate = static_cast(FrameRate); + if (AVIStreamInfo.dwRate == 0) { + AVIStreamInfo.dwRate = 1; + } + AVIStreamInfo.dwSuggestedBufferSize = BitmapInfoHeader.biSizeImage; SetRect(&AVIStreamInfo.rcFrame, 0, 0, width, height); - AVIStreamInfo.dwEditCount = 0; - AVIStreamInfo.dwFormatChangeCount = 0; - sprintf(AVIStreamInfo.szName,"G"); + AVIStreamInfo.szName[0] = 'G'; - hr = AVIFileCreateStreamA(AVIFile, &Stream, &AVIStreamInfo); - if (hr != 0) { + hr = AVIFileCreateStreamA(AVIFile, &Stream, &AVIStreamInfo); + if (FAILED(hr)) { CleanupAVI(); return; } - // Set format of new stream - BitmapInfoHeader.biWidth = width; - BitmapInfoHeader.biHeight = height; - BitmapInfoHeader.biBitCount = (unsigned short)bitcount; - BitmapInfoHeader.biSizeImage = ((((UINT)BitmapInfoHeader.biBitCount * BitmapInfoHeader.biWidth + 31) & ~31) / 8) * BitmapInfoHeader.biHeight; - BitmapInfoHeader.biSize = sizeof(BITMAPINFOHEADER); // size of structure - BitmapInfoHeader.biPlanes = 1; // must be set to 1 - BitmapInfoHeader.biCompression = BI_RGB; // uncompressed - BitmapInfoHeader.biXPelsPerMeter = 1; // not used - BitmapInfoHeader.biYPelsPerMeter = 1; // not used - BitmapInfoHeader.biClrUsed = 0; // all colors are used - BitmapInfoHeader.biClrImportant = 0; // all colors are important - - hr = AVIStreamSetFormat(Stream, 0, &BitmapInfoHeader, sizeof(BitmapInfoHeader)); - if (hr != 0) { + hr = AVIStreamSetFormat(Stream, 0, &BitmapInfoHeader, sizeof(BitmapInfoHeader)); + if (FAILED(hr)) { CleanupAVI(); return; } - Bitmap = (int *) GlobalAllocPtr(GMEM_MOVEABLE, BitmapInfoHeader.biSizeImage); + Bitmap = static_cast(GlobalAllocPtr( + GMEM_MOVEABLE | GMEM_ZEROINIT, BitmapInfoHeader.biSizeImage)); + if (Bitmap == nullptr) { + CleanupAVI(); + } } FrameGrabClass::~FrameGrabClass() { - if(Mode == AVI) { - CleanupAVI(); - } + CleanupAVI(); } -void FrameGrabClass::CleanupAVI() { - if(Bitmap != 0) { GlobalFreePtr(Bitmap); Bitmap = 0; } - if(Stream != 0) { AVIStreamRelease(Stream); Stream = 0; } - if(AVIFile != 0) { AVIFileRelease(AVIFile); AVIFile = 0; } - - AVIFileExit(); +void FrameGrabClass::CleanupAVI() +{ + const bool avi_library_initialized = (Mode == AVI); + if (Bitmap != nullptr) { + GlobalFreePtr(Bitmap); + Bitmap = nullptr; + } + if (Stream != nullptr) { + AVIStreamRelease(Stream); + Stream = nullptr; + } + if (AVIFile != nullptr) { + AVIFileRelease(AVIFile); + AVIFile = nullptr; + } + if (avi_library_initialized) { + AVIFileExit(); + } Mode = RAW; } void FrameGrabClass::GrabAVI(void *BitmapPointer) { - // CompressDIB(&bi, lpOld, &biNew, lpNew); - - // Save the compressed data using AVIStreamWrite. - HRESULT hr = AVIStreamWrite(Stream, Counter++, 1, BitmapPointer, BitmapInfoHeader.biSizeImage, AVIIF_KEYFRAME, nullptr, nullptr); - if(hr != 0) { - char buf[256]; - sprintf(buf, "avi write error %lx/%ld\n", hr, hr); - OutputDebugStringA(buf); - } + TryGrab(BitmapPointer); } void FrameGrabClass::GrabRawFrame(void * /*BitmapPointer*/) @@ -150,42 +160,147 @@ void FrameGrabClass::GrabRawFrame(void * /*BitmapPointer*/) void FrameGrabClass::ConvertGrab(void *BitmapPointer) { - ConvertFrame(BitmapPointer); - Grab( Bitmap ); + if (!IsReady() || BitmapPointer == nullptr || Bitmap == nullptr) { + return; + } + + const unsigned long long source_size = + static_cast(GetWidth()) * 4ULL; + if (source_size > std::numeric_limits::max() || + !ConvertBGRA32ToBGR24(BitmapPointer, static_cast(source_size), Bitmap, + GetRowStride(), GetWidth(), GetHeight())) { + return; + } + TryGrab(Bitmap); } void FrameGrabClass::Grab(void *BitmapPointer) { - if(Mode == AVI) - GrabAVI(BitmapPointer); - else + if (Mode == AVI) { + TryGrab(BitmapPointer); + } else { GrabRawFrame(BitmapPointer); + } } +bool FrameGrabClass::TryGrab(void *BitmapPointer) +{ + if (!IsReady() || BitmapPointer == nullptr || Counter == std::numeric_limits::max()) { + return false; + } -void FrameGrabClass::ConvertFrame(void *BitmapPointer) + const HRESULT hr = AVIStreamWrite(Stream, Counter, 1, BitmapPointer, + BitmapInfoHeader.biSizeImage, AVIIF_KEYFRAME, nullptr, nullptr); + if (FAILED(hr)) { + char buffer[128]; + std::snprintf(buffer, sizeof(buffer), "avi write error %lx/%ld\n", + static_cast(hr), static_cast(hr)); + OutputDebugStringA(buffer); + CleanupAVI(); + return false; + } + + ++Counter; + return true; +} + +bool FrameGrabClass::IsReady() const +{ + return Mode == AVI && AVIFile != nullptr && Stream != nullptr && Bitmap != nullptr && + BitmapInfoHeader.biSizeImage != 0; +} + +int FrameGrabClass::GetWidth() const { + return BitmapInfoHeader.biWidth; +} + +int FrameGrabClass::GetHeight() const +{ + return BitmapInfoHeader.biHeight; +} + +unsigned int FrameGrabClass::GetRowStride() const +{ + return CalculateRowStride(BitmapInfoHeader.biWidth, BitmapInfoHeader.biBitCount); +} + +unsigned int FrameGrabClass::GetBufferSize() const +{ + return BitmapInfoHeader.biSizeImage; +} + +unsigned int FrameGrabClass::CalculateRowStride(int width, int bitdepth) +{ + if (width <= 0 || bitdepth <= 0) { + return 0; + } - int width = BitmapInfoHeader.biWidth; - int height = BitmapInfoHeader.biHeight; - int *image = (int *) BitmapPointer; - - // copy the data, doing a vertical flip & byte re-ordering of the pixel longwords - int y = height; - while(y--) { - int x = width; - int yoffset = y * width; - int yoffset2 = (height - y) * width; - while(x--) { - int *source = &image[yoffset + x]; - int *dest = &Bitmap[yoffset2 + x]; - *dest = *source; - unsigned char *c = (unsigned char *) dest; - c[3] = c[0]; - c[0] = c[2]; - c[2] = c[3]; - c[3] = 0; + const unsigned long long bit_count = + static_cast(width) * static_cast(bitdepth); + const unsigned long long stride = ((bit_count + 31ULL) & ~31ULL) / 8ULL; + if (stride > std::numeric_limits::max()) { + return 0; + } + return static_cast(stride); +} + +bool FrameGrabClass::ConvertBGRA32ToBGR24(const void *source, int source_pitch, + void *destination, unsigned int destination_stride, int width, int height) +{ + if (source == nullptr || destination == nullptr || source_pitch <= 0 || + width <= 0 || height <= 0) { + return false; + } + + const unsigned long long source_pixel_bytes = static_cast(width) * 4ULL; + const unsigned long long destination_pixel_bytes = static_cast(width) * 3ULL; + const unsigned int minimum_destination_stride = CalculateRowStride(width, 24); + if (source_pixel_bytes > static_cast(std::numeric_limits::max()) || + destination_pixel_bytes > std::numeric_limits::max() || + static_cast(source_pitch) < source_pixel_bytes || + minimum_destination_stride == 0 || destination_stride < minimum_destination_stride) { + return false; + } + + const size_t row_count_minus_one = static_cast(height - 1); + const size_t maximum_offset = std::numeric_limits::max(); + const size_t source_row_bytes = static_cast(source_pixel_bytes); + if (row_count_minus_one > (maximum_offset - source_row_bytes) / + static_cast(source_pitch) || + row_count_minus_one > (maximum_offset - static_cast(destination_stride)) / + destination_stride) { + return false; + } + + const auto *source_bytes = static_cast(source); + auto *destination_bytes = static_cast(destination); + const size_t copied_bytes = static_cast(destination_pixel_bytes); + for (int source_y = 0; source_y < height; ++source_y) { + const auto *source_row = source_bytes + static_cast(source_y) * source_pitch; + auto *destination_row = destination_bytes + + static_cast(height - source_y - 1) * destination_stride; + for (int x = 0; x < width; ++x) { + destination_row[3 * x] = source_row[4 * x]; + destination_row[3 * x + 1] = source_row[4 * x + 1]; + destination_row[3 * x + 2] = source_row[4 * x + 2]; } + std::memset(destination_row + copied_bytes, 0, destination_stride - copied_bytes); + } + return true; +} + + +void FrameGrabClass::ConvertFrame(void *BitmapPointer) +{ + if (BitmapPointer == nullptr || Bitmap == nullptr) { + return; + } + const unsigned long long source_stride = + static_cast(GetWidth()) * 4ULL; + if (source_stride <= std::numeric_limits::max()) { + ConvertBGRA32ToBGR24(BitmapPointer, static_cast(source_stride), Bitmap, + GetRowStride(), GetWidth(), GetHeight()); } } diff --git a/Code/ww3d2/framgrab.h b/Code/ww3d2/framgrab.h index cdaf307f7..944f446ed 100644 --- a/Code/ww3d2/framgrab.h +++ b/Code/ww3d2/framgrab.h @@ -77,9 +77,19 @@ class FrameGrabClass void ConvertGrab(void *BitmapPointer); void Grab(void *BitmapPointer); + bool TryGrab(void *BitmapPointer); int * GetBuffer() { return Bitmap; } float GetFrameRate() { return FrameRate; } + bool IsReady() const; + int GetWidth() const; + int GetHeight() const; + unsigned int GetRowStride() const; + unsigned int GetBufferSize() const; + + static unsigned int CalculateRowStride(int width, int bitdepth); + static bool ConvertBGRA32ToBGR24(const void *source, int source_pitch, + void *destination, unsigned int destination_stride, int width, int height); protected: const char *Filename; diff --git a/Code/ww3d2/tests/FrameGrabTests.cpp b/Code/ww3d2/tests/FrameGrabTests.cpp new file mode 100644 index 000000000..14d277431 --- /dev/null +++ b/Code/ww3d2/tests/FrameGrabTests.cpp @@ -0,0 +1,164 @@ +/* +** Command & Conquer Renegade(tm) +** Copyright 2026 OpenW3D Contributors. +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include "framgrab.h" +#include "ww3d.h" + +#include +#include +#include +#include +#include + +namespace +{ +using LegacyGrabSignature = void (FrameGrabClass::*)(void *); +using TryGrabSignature = bool (FrameGrabClass::*)(void *); +using ConverterSignature = bool (*)(const void *, int, void *, unsigned int, int, int); +using TryStartBackBufferSignature = bool (*)(const char *, float); +using TryUpdateBackBufferSignature = bool (*)(); + +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +bool Expect(bool condition, const char *message) +{ + if (!condition) { + std::cerr << message << '\n'; + } + return condition; +} + +bool TestRowStride() +{ + bool ok = true; + ok &= Expect(FrameGrabClass::CalculateRowStride(3, 24) == 12, "3x24 stride must include padding"); + ok &= Expect(FrameGrabClass::CalculateRowStride(4, 24) == 12, "4x24 stride must be packed"); + ok &= Expect(FrameGrabClass::CalculateRowStride(0, 24) == 0, "zero width must be rejected"); + ok &= Expect(FrameGrabClass::CalculateRowStride(3, 0) == 0, "zero bit depth must be rejected"); + ok &= Expect(FrameGrabClass::CalculateRowStride(INT_MAX, 24) == 0, "overflowing stride must be rejected"); + return ok; +} + +bool TestConversion() +{ + constexpr int width = 3; + constexpr int height = 2; + constexpr int source_stride = 16; + constexpr unsigned int destination_stride = 12; + constexpr unsigned char canary = 0xcd; + + std::array source{}; + source.fill(canary); + unsigned char *source_pixels = source.data() + 4; + const std::array source_image = { + 0x01, 0x02, 0x03, 0xa0, 0x11, 0x12, 0x13, 0xa1, 0x21, 0x22, 0x23, 0xa2, + 0xf1, 0xf2, 0xf3, 0xf4, + 0x31, 0x32, 0x33, 0xb0, 0x41, 0x42, 0x43, 0xb1, 0x51, 0x52, 0x53, 0xb2, + 0xe1, 0xe2, 0xe3, 0xe4 + }; + std::copy(source_image.begin(), source_image.end(), source_pixels); + const auto original_source = source; + + std::array destination{}; + destination.fill(canary); + unsigned char *destination_pixels = destination.data() + 4; + const bool converted = FrameGrabClass::ConvertBGRA32ToBGR24(source_pixels, source_stride, + destination_pixels, destination_stride, width, height); + + const std::array expected = { + 0x31, 0x32, 0x33, 0x41, 0x42, 0x43, 0x51, 0x52, 0x53, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x11, 0x12, 0x13, 0x21, 0x22, 0x23, 0x00, 0x00, 0x00 + }; + + bool ok = true; + ok &= Expect(converted, "valid conversion failed"); + ok &= Expect(std::equal(expected.begin(), expected.end(), destination_pixels), + "converted pixels, vertical order, or padding differ"); + ok &= Expect(source == original_source, "conversion modified source bytes or source canaries"); + ok &= Expect(std::all_of(destination.begin(), destination.begin() + 4, + [](unsigned char byte) { return byte == 0xcd; }), "leading destination canary changed"); + ok &= Expect(std::all_of(destination.end() - 4, destination.end(), + [](unsigned char byte) { return byte == 0xcd; }), "trailing destination canary changed"); + return ok; +} + +bool TestInvalidConversionInputs() +{ + std::array source{}; + std::array destination{}; + bool ok = true; + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(nullptr, 16, destination.data(), 12, 3, 2), + "null source must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 16, nullptr, 12, 3, 2), + "null destination must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 0, destination.data(), 12, 3, 2), + "zero source pitch must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 11, destination.data(), 12, 3, 2), + "undersized source pitch must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 16, destination.data(), 11, 3, 2), + "undersized destination stride must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 16, destination.data(), 12, 0, 2), + "zero width must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), 16, destination.data(), 12, 3, 0), + "zero height must fail"); + ok &= Expect(!FrameGrabClass::ConvertBGRA32ToBGR24(source.data(), INT_MAX, + destination.data(), UINT_MAX, INT_MAX, 2), "overflowing row sizes must fail"); + return ok; +} + +bool TestInvalidConstruction() +{ + FrameGrabClass null_name(nullptr, FrameGrabClass::AVI, 3, 2, 24, 30.0f); + FrameGrabClass empty_name("", FrameGrabClass::AVI, 3, 2, 24, 30.0f); + FrameGrabClass bad_width("Movie", FrameGrabClass::AVI, 0, 2, 24, 30.0f); + FrameGrabClass bad_depth("Movie", FrameGrabClass::AVI, 3, 2, 32, 30.0f); + FrameGrabClass oversized("Movie", FrameGrabClass::AVI, 100000, 10000, 24, 30.0f); + FrameGrabClass raw("Movie", FrameGrabClass::RAW, 3, 2, 24, 30.0f); + bool ok = true; + ok &= Expect(!null_name.IsReady() && null_name.GetBuffer() == nullptr, + "null filename construction must remain inert"); + ok &= Expect(!empty_name.IsReady() && empty_name.GetBuffer() == nullptr, + "empty filename construction must remain inert"); + ok &= Expect(!bad_width.IsReady() && bad_width.GetBuffer() == nullptr && + bad_width.GetWidth() == 0 && bad_width.GetBufferSize() == 0, + "invalid width construction must remain inert"); + ok &= Expect(!bad_depth.IsReady() && bad_depth.GetBuffer() == nullptr, + "non-BGR24 construction must remain inert"); + ok &= Expect(!oversized.IsReady() && oversized.GetBuffer() == nullptr, + "AVIStreamWrite-sized image overflow must remain inert"); + ok &= Expect(!raw.IsReady() && !raw.TryGrab(nullptr), "RAW construction must not report readiness"); + return ok; +} +} + +int main() +{ + bool passed = true; + passed &= TestRowStride(); + passed &= TestConversion(); + passed &= TestInvalidConversionInputs(); + passed &= TestInvalidConstruction(); + return passed ? 0 : 1; +} diff --git a/Code/ww3d2/ww3d.cpp b/Code/ww3d2/ww3d.cpp index b86a8ef4f..557239301 100644 --- a/Code/ww3d2/ww3d.cpp +++ b/Code/ww3d2/ww3d.cpp @@ -106,10 +106,12 @@ #include "bound.h" #include "rddesc.h" #include "vector3i.h" +#include #include #ifdef _WIN32 #include #endif +#include #include "dx8wrapper.h" #include "TARGA.H" #include "sortingrenderer.h" @@ -125,6 +127,19 @@ #endif +namespace +{ +enum class MovieCaptureSource +{ + None, + Front, + Back +}; + +MovieCaptureSource MovieCaptureSourceState = MovieCaptureSource::None; +} + + const char* DAZZLE_INI_FILENAME="DAZZLE.INI"; #define DEFAULT_DEBUG_SHADER_BITS ( SHADE_CNST(\ @@ -803,7 +818,8 @@ WW3DErrorType WW3D::Begin_Render(bool clear,bool clearz,const Vector3 & color, v #endif //WW3D_DX8 Debug_Statistics::Begin_Statistics(); - if (IsCapturing && (!PauseRecord || RecordNextFrame)) { + if (IsCapturing && MovieCaptureSourceState == MovieCaptureSource::Front && + (!PauseRecord || RecordNextFrame)) { Update_Movie_Capture(); RecordNextFrame = false; } @@ -1423,14 +1439,15 @@ int WW3D::Make_Back_Buffer_Screen_Shot( const char * filename_base ) void WW3D::Start_Movie_Capture( const char * filename_base, float frame_rate ) { #ifdef _WIN32 - if (IsCapturing) { + if (IsCapturing || Movie != nullptr || MovieCaptureSourceState != MovieCaptureSource::None) { Stop_Movie_Capture(); } WWASSERT( !IsCapturing); - IsCapturing = true; - RECT bounds; - GetWindowRect(_Hwnd,&bounds); + RECT bounds = {}; + if (_Hwnd == nullptr || !GetWindowRect(_Hwnd, &bounds)) { + return; + } int height=bounds.bottom-bounds.top; int width=bounds.right-bounds.left; int depth=24; @@ -1444,13 +1461,91 @@ void WW3D::Start_Movie_Capture( const char * filename_base, float frame_rate ) PauseRecord = false; } - Movie = new FrameGrabClass( filename_base, FrameGrabClass::AVI, width, height, depth, frame_rate); + FrameGrabClass *movie = new FrameGrabClass( + filename_base, FrameGrabClass::AVI, width, height, depth, frame_rate); + if (movie == nullptr || !movie->IsReady()) { + delete movie; + MovieCaptureSourceState = MovieCaptureSource::None; + IsCapturing = false; + return; + } + + Movie = movie; + MovieCaptureSourceState = MovieCaptureSource::Front; + IsCapturing = true; WWDEBUG_SAY(( "Starting Movie %s\n", filename_base )); #endif } +/*********************************************************************************************** + * WW3D::Try_Start_Movie_Capture_From_Back_Buffer -- starts back-buffer movie capture * + *=============================================================================================*/ +bool WW3D::Try_Start_Movie_Capture_From_Back_Buffer( const char * filename_base, float frame_rate ) +{ +#ifdef _WIN32 + if (IsCapturing || Movie != nullptr || MovieCaptureSourceState != MovieCaptureSource::None) { + Stop_Movie_Capture(); + } + + if (filename_base == nullptr || filename_base[0] == '\0' || + !(frame_rate > 0.0f) || !std::isfinite(frame_rate)) { + return false; + } + + IDirect3DDevice9 *device = DX8Wrapper::_Get_D3D_Device8(); + if (device == nullptr) { + return false; + } + + IDirect3DSurface9 *back_buffer = nullptr; + HRESULT result = device->GetBackBuffer( + 0, 0, D3DBACKBUFFER_TYPE_MONO, &back_buffer); + if (FAILED(result) || back_buffer == nullptr) { + return false; + } + + D3DSURFACE_DESC desc = {}; + result = back_buffer->GetDesc(&desc); + back_buffer->Release(); + if (FAILED(result) || desc.Width == 0 || desc.Height == 0 || + desc.Width > static_cast(std::numeric_limits::max()) || + desc.Height > static_cast(std::numeric_limits::max()) || + desc.MultiSampleType != D3DMULTISAMPLE_NONE || + (desc.Format != D3DFMT_A8R8G8B8 && desc.Format != D3DFMT_X8R8G8B8)) { + return false; + } + + FrameGrabClass *movie = new FrameGrabClass( + filename_base, + FrameGrabClass::AVI, + static_cast(desc.Width), + static_cast(desc.Height), + 24, + frame_rate); + if (movie == nullptr || !movie->IsReady()) { + delete movie; + return false; + } + + Movie = movie; + // Back-buffer capture is advanced explicitly by the caller rather than + // automatically from Begin_Render. + PauseRecord = true; + RecordNextFrame = false; + MovieCaptureSourceState = MovieCaptureSource::Back; + IsCapturing = true; + WWDEBUG_SAY(( "Starting back-buffer movie %s\n", filename_base )); + return true; +#else + (void)filename_base; + (void)frame_rate; + return false; +#endif +} + + /*********************************************************************************************** * WW3D::Stop_Movie_Capture -- ends dumping frames to a movie * * * @@ -1467,10 +1562,14 @@ void WW3D::Stop_Movie_Capture( void ) { #ifdef _WIN32 if (IsCapturing) { - IsCapturing = false; WWDEBUG_SAY(( "Stoping Movie\n" )); + } - WWASSERT( Movie != nullptr); + IsCapturing = false; + PauseRecord = false; + RecordNextFrame = false; + MovieCaptureSourceState = MovieCaptureSource::None; + if (Movie != nullptr) { delete Movie; Movie = nullptr; } @@ -1628,45 +1727,148 @@ void WW3D::Update_Movie_Capture( void ) WWPROFILE("WW3D::Update_Movie_Capture"); WWDEBUG_SAY(( "Updating\n")); - // Lock front buffer and copy + if (!IsCapturing || MovieCaptureSourceState != MovieCaptureSource::Front || + Movie == nullptr || !Movie->IsReady()) { + Stop_Movie_Capture(); + return; + } - IDirect3DSurface9 *fb; - fb=DX8Wrapper::_Get_DX8_Front_Buffer(); - D3DSURFACE_DESC desc; - fb->GetDesc(&desc); + // Lock the front buffer and convert its top-down BGRA rows into the + // bottom-up, padded BGR24 layout required by the AVI stream. + IDirect3DSurface9 *fb = DX8Wrapper::_Get_DX8_Front_Buffer(); + if (fb == nullptr) { + Stop_Movie_Capture(); + return; + } - RECT bounds; - GetWindowRect(_Hwnd,&bounds); + RECT bounds = {}; + if (_Hwnd == nullptr || !GetWindowRect(_Hwnd, &bounds)) { + fb->Release(); + Stop_Movie_Capture(); + return; + } - D3DLOCKED_RECT lrect; + const int width = bounds.right - bounds.left; + const int height = bounds.bottom - bounds.top; + if (width != Movie->GetWidth() || height != Movie->GetHeight()) { + fb->Release(); + Stop_Movie_Capture(); + return; + } - DX8_ErrorCode(fb->LockRect(&lrect,&bounds,D3DLOCK_READONLY)); + D3DLOCKED_RECT lrect = {}; + const HRESULT lock_result = fb->LockRect(&lrect, &bounds, D3DLOCK_READONLY); + if (FAILED(lock_result)) { + fb->Release(); + Stop_Movie_Capture(); + return; + } - unsigned int x,y,index,index2,width,height; + void *movie_buffer = Movie->GetBuffer(); + const bool converted = FrameGrabClass::ConvertBGRA32ToBGR24( + lrect.pBits, + lrect.Pitch, + movie_buffer, + Movie->GetRowStride(), + width, + height); + const HRESULT unlock_result = fb->UnlockRect(); + fb->Release(); + if (!converted || FAILED(unlock_result) || !Movie->TryGrab(movie_buffer)) { + Stop_Movie_Capture(); + } +#endif +} - width=bounds.right-bounds.left; - height=bounds.bottom-bounds.top; - char *image=(char *)Movie->GetBuffer(); +/*********************************************************************************************** + * WW3D::Try_Update_Movie_Capture_From_Back_Buffer -- captures the current back buffer * + *=============================================================================================*/ +bool WW3D::Try_Update_Movie_Capture_From_Back_Buffer( void ) +{ +#ifdef _WIN32 + if (MovieCaptureSourceState != MovieCaptureSource::Back) { + return false; + } + if (!IsCapturing || Movie == nullptr || !Movie->IsReady()) { + Stop_Movie_Capture(); + return false; + } - for (y=0; yGetBackBuffer( + 0, 0, D3DBACKBUFFER_TYPE_MONO, &back_buffer); + if (FAILED(result) || back_buffer == nullptr) { + Stop_Movie_Capture(); + return false; + } + + D3DSURFACE_DESC desc = {}; + result = back_buffer->GetDesc(&desc); + if (FAILED(result) || + desc.Width != static_cast(Movie->GetWidth()) || + desc.Height != static_cast(Movie->GetHeight()) || + desc.MultiSampleType != D3DMULTISAMPLE_NONE || + (desc.Format != D3DFMT_A8R8G8B8 && desc.Format != D3DFMT_X8R8G8B8)) { + back_buffer->Release(); + Stop_Movie_Capture(); + return false; + } + + IDirect3DSurface9 *staging_buffer = nullptr; + result = device->CreateOffscreenPlainSurface( + desc.Width, + desc.Height, + desc.Format, + D3DPOOL_SYSTEMMEM, + &staging_buffer, + nullptr); + if (SUCCEEDED(result)) { + result = device->GetRenderTargetData(back_buffer, staging_buffer); + } + back_buffer->Release(); + if (FAILED(result) || staging_buffer == nullptr) { + if (staging_buffer != nullptr) { + staging_buffer->Release(); } + Stop_Movie_Capture(); + return false; } - fb->Release(); + D3DLOCKED_RECT locked = {}; + result = staging_buffer->LockRect(&locked, nullptr, D3DLOCK_READONLY); + if (FAILED(result)) { + staging_buffer->Release(); + Stop_Movie_Capture(); + return false; + } - Movie->Grab(image); + void *movie_buffer = Movie->GetBuffer(); + const bool converted = FrameGrabClass::ConvertBGRA32ToBGR24( + locked.pBits, + locked.Pitch, + movie_buffer, + Movie->GetRowStride(), + Movie->GetWidth(), + Movie->GetHeight()); + const HRESULT unlock_result = staging_buffer->UnlockRect(); + staging_buffer->Release(); + if (!converted || FAILED(unlock_result) || !Movie->TryGrab(movie_buffer)) { + Stop_Movie_Capture(); + return false; + } + + return true; +#else + return false; #endif } diff --git a/Code/ww3d2/ww3d.h b/Code/ww3d2/ww3d.h index 2fa5cea8e..7b1b98037 100644 --- a/Code/ww3d2/ww3d.h +++ b/Code/ww3d2/ww3d.h @@ -175,11 +175,13 @@ class WW3D static void Make_Screen_Shot( const char * filename = "ScreenShot"); static int Make_Back_Buffer_Screen_Shot( const char * filename = "ScreenShot"); static void Start_Movie_Capture( const char * filename_base = "Movie", float frame_rate = 15); + static bool Try_Start_Movie_Capture_From_Back_Buffer( const char * filename_base, float frame_rate); static void Stop_Movie_Capture( void); static void Toggle_Movie_Capture( const char * filename_base = "Movie", float frame_rate = 15); static void Start_Single_Frame_Movie_Capture(const char *filename_base = "Frames"); static void Capture_Next_Movie_Frame(); static void Update_Movie_Capture( void); + static bool Try_Update_Movie_Capture_From_Back_Buffer( void); static float Get_Movie_Capture_Frame_Rate( void); static void Pause_Movie(bool mode); static bool Is_Movie_Paused();