/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "DrawingEngineFactory.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace OpenRCT2; using namespace OpenRCT2::Drawing; using namespace OpenRCT2::Ui; class HardwareDisplayDrawingEngine final : public X8DrawingEngine { private: constexpr static uint32_t kDirtyVisualTime = 40; constexpr static uint32_t kDirtyRegionAlpha = 100; IUiContext& _uiContext; SDL_Window* _window = nullptr; SDL_Renderer* _sdlRenderer = nullptr; SDL_Texture* _screenTexture = nullptr; SDL_Texture* _scaledScreenTexture = nullptr; SDL_PixelFormat* _screenTextureFormat = nullptr; uint32_t _paletteHWMapped[256] = { 0 }; uint32_t _lightPaletteHWMapped[256] = { 0 }; bool _useVsync = true; std::vector _dirtyVisualsTime; bool smoothNN = false; public: explicit HardwareDisplayDrawingEngine(IUiContext& uiContext) : X8DrawingEngine(uiContext) , _uiContext(uiContext) { _window = static_cast(_uiContext.GetWindow()); } ~HardwareDisplayDrawingEngine() override { if (_screenTexture != nullptr) { SDL_DestroyTexture(_screenTexture); } if (_scaledScreenTexture != nullptr) { SDL_DestroyTexture(_scaledScreenTexture); } SDL_FreeFormat(_screenTextureFormat); SDL_DestroyRenderer(_sdlRenderer); } void Initialise() override { _sdlRenderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED | (_useVsync ? SDL_RENDERER_PRESENTVSYNC : 0)); } void SetVSync(bool vsync) override { if (_useVsync != vsync) { _useVsync = vsync; #if SDL_VERSION_ATLEAST(2, 0, 18) SDL_RenderSetVSync(_sdlRenderer, vsync ? 1 : 0); #else SDL_DestroyRenderer(_sdlRenderer); _screenTexture = nullptr; _scaledScreenTexture = nullptr; Initialise(); Resize(_uiContext->GetWidth(), _uiContext->GetHeight()); #endif } } void Resize(uint32_t width, uint32_t height) override { if (width == 0 || height == 0) { return; } if (_screenTexture != nullptr) { SDL_DestroyTexture(_screenTexture); } SDL_FreeFormat(_screenTextureFormat); SDL_RendererInfo rendererInfo = {}; int32_t result = SDL_GetRendererInfo(_sdlRenderer, &rendererInfo); if (result < 0) { LOG_WARNING("HWDisplayDrawingEngine::Resize error: %s", SDL_GetError()); return; } uint32_t pixelFormat = SDL_PIXELFORMAT_UNKNOWN; for (uint32_t i = 0; i < rendererInfo.num_texture_formats; i++) { uint32_t format = rendererInfo.texture_formats[i]; if (!SDL_ISPIXELFORMAT_FOURCC(format) && !SDL_ISPIXELFORMAT_INDEXED(format) && (pixelFormat == SDL_PIXELFORMAT_UNKNOWN || SDL_BYTESPERPIXEL(format) < SDL_BYTESPERPIXEL(pixelFormat))) { pixelFormat = format; } } ScaleQuality scaleQuality = GetContext()->GetUiContext().GetScaleQuality(); if (scaleQuality == ScaleQuality::SmoothNearestNeighbour) { scaleQuality = ScaleQuality::Linear; smoothNN = true; } else { smoothNN = false; } if (smoothNN) { if (_scaledScreenTexture != nullptr) { SDL_DestroyTexture(_scaledScreenTexture); } char scaleQualityBuffer[4]; snprintf(scaleQualityBuffer, sizeof(scaleQualityBuffer), "%d", static_cast(scaleQuality)); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); _screenTexture = SDL_CreateTexture(_sdlRenderer, pixelFormat, SDL_TEXTUREACCESS_STREAMING, width, height); Guard::Assert( _screenTexture != nullptr, "Failed to create unscaled screen texture (%ux%u, pixelFormat = %u): %s", width, height, pixelFormat, SDL_GetError()); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, scaleQualityBuffer); uint32_t scale = std::ceil(Config::Get().general.WindowScale); _scaledScreenTexture = SDL_CreateTexture( _sdlRenderer, pixelFormat, SDL_TEXTUREACCESS_TARGET, width * scale, height * scale); Guard::Assert( _scaledScreenTexture != nullptr, "Failed to create scaled screen texture (%ux%u, scale = %u, pixelFormat = %u): %s", width, height, scale, pixelFormat, SDL_GetError()); } else { _screenTexture = SDL_CreateTexture(_sdlRenderer, pixelFormat, SDL_TEXTUREACCESS_STREAMING, width, height); Guard::Assert( _screenTexture != nullptr, "Failed to create screen texture (%ux%u, pixelFormat = %u): %s", width, height, pixelFormat, SDL_GetError()); } uint32_t format; SDL_QueryTexture(_screenTexture, &format, nullptr, nullptr, nullptr); _screenTextureFormat = SDL_AllocFormat(format); X8DrawingEngine::Resize(width, height); } void SetPalette(const GamePalette& palette) override { if (_screenTextureFormat != nullptr) { for (int32_t i = 0; i < 256; i++) { _paletteHWMapped[i] = SDL_MapRGB(_screenTextureFormat, palette[i].Red, palette[i].Green, palette[i].Blue); } if (Config::Get().general.EnableLightFx) { auto& lightPalette = LightFx::GetPalette(); for (int32_t i = 0; i < 256; i++) { const auto& src = lightPalette[i]; _lightPaletteHWMapped[i] = SDL_MapRGBA(_screenTextureFormat, src.Red, src.Green, src.Blue, src.Alpha); } } } } void BeginDraw() override { X8DrawingEngine::BeginDraw(); } void EndDraw() override { X8DrawingEngine::EndDraw(); Display(); } protected: void OnDrawDirtyBlock(int32_t left, int32_t top, int32_t right, int32_t bottom) override { if (gShowDirtyVisuals) { const auto columns = ((right - left) + (_invalidationGrid.getBlockWidth() - 1)) / _invalidationGrid.getBlockWidth(); const auto rows = ((bottom - top) + (_invalidationGrid.getBlockHeight() - 1)) / _invalidationGrid.getBlockHeight(); const auto firstRow = top / _invalidationGrid.getBlockHeight(); const auto firstColumn = left / _invalidationGrid.getBlockWidth(); for (uint32_t y = 0; y < rows; y++) { for (uint32_t x = 0; x < columns; x++) { SetDirtyVisualTime(firstColumn + x, firstRow + y, gCurrentRealTimeTicks + kDirtyVisualTime); } } } } private: void Display() { if (Config::Get().general.EnableLightFx) { void* pixels; int32_t pitch; if (SDL_LockTexture(_screenTexture, nullptr, &pixels, &pitch) == 0) { LightFx::RenderToTexture(pixels, pitch, _bits, _width, _height, _paletteHWMapped, _lightPaletteHWMapped); SDL_UnlockTexture(_screenTexture); } } else { CopyBitsToTexture( _screenTexture, _bits, static_cast(_width), static_cast(_height), _paletteHWMapped); } if (smoothNN) { SDL_SetRenderTarget(_sdlRenderer, _scaledScreenTexture); SDL_RenderCopy(_sdlRenderer, _screenTexture, nullptr, nullptr); SDL_SetRenderTarget(_sdlRenderer, nullptr); SDL_RenderCopy(_sdlRenderer, _scaledScreenTexture, nullptr, nullptr); } else { SDL_RenderCopy(_sdlRenderer, _screenTexture, nullptr, nullptr); } if (gShowDirtyVisuals) { RenderDirtyVisuals(); } SDL_RenderPresent(_sdlRenderer); } void CopyBitsToTexture(SDL_Texture* texture, uint8_t* src, int32_t width, int32_t height, const uint32_t* palette) { void* pixels; int32_t pitch; if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) == 0) { int32_t padding = pitch - (width * 4); if (pitch == width * 4) { uint32_t* dst = static_cast(pixels); for (int32_t i = width * height; i > 0; i--) { *dst++ = palette[*src++]; } } else { if (pitch == (width * 2) + padding) { uint16_t* dst = static_cast(pixels); for (int32_t y = height; y > 0; y--) { for (int32_t x = width; x > 0; x--) { const uint8_t lower = *reinterpret_cast(&palette[*src++]); const uint8_t upper = *reinterpret_cast(&palette[*src++]); *dst++ = (lower << 8) | upper; } dst = reinterpret_cast(reinterpret_cast(dst) + padding); } } else if (pitch == width + padding) { uint8_t* dst = static_cast(pixels); for (int32_t y = height; y > 0; y--) { for (int32_t x = width; x > 0; x--) { *dst++ = *reinterpret_cast(&palette[*src++]); } dst += padding; } } } SDL_UnlockTexture(texture); } } uint32_t GetDirtyVisualTime(uint32_t x, uint32_t y) { uint32_t result = 0; uint32_t i = y * _invalidationGrid.getColumnCount() + x; if (_dirtyVisualsTime.size() > i) { result = _dirtyVisualsTime[i]; } return result; } void SetDirtyVisualTime(uint32_t x, uint32_t y, uint32_t value) { const auto rows = _invalidationGrid.getRowCount(); const auto columns = _invalidationGrid.getColumnCount(); _dirtyVisualsTime.resize(rows * columns); uint32_t i = y * _invalidationGrid.getColumnCount() + x; if (_dirtyVisualsTime.size() > i) { _dirtyVisualsTime[i] = value; } } void RenderDirtyVisuals() { int windowX, windowY, renderX, renderY; SDL_GetWindowSize(_window, &windowX, &windowY); SDL_GetRendererOutputSize(_sdlRenderer, &renderX, &renderY); float scaleX = Config::Get().general.WindowScale * renderX / static_cast(windowX); float scaleY = Config::Get().general.WindowScale * renderY / static_cast(windowY); SDL_SetRenderDrawBlendMode(_sdlRenderer, SDL_BLENDMODE_BLEND); for (uint32_t y = 0; y < _invalidationGrid.getRowCount(); y++) { for (uint32_t x = 0; x < _invalidationGrid.getColumnCount(); x++) { const auto timeEnd = GetDirtyVisualTime(x, y); const auto timeLeft = gCurrentRealTimeTicks < timeEnd ? timeEnd - gCurrentRealTimeTicks : 0; if (timeLeft > 0) { uint8_t alpha = timeLeft * kDirtyRegionAlpha / kDirtyVisualTime; SDL_Rect ddRect; ddRect.x = static_cast(x * _invalidationGrid.getBlockWidth() * scaleX); ddRect.y = static_cast(y * _invalidationGrid.getBlockHeight() * scaleY); ddRect.w = static_cast(_invalidationGrid.getBlockWidth() * scaleX); ddRect.h = static_cast(_invalidationGrid.getBlockHeight() * scaleY); SDL_SetRenderDrawColor(_sdlRenderer, 255, 255, 255, alpha); SDL_RenderFillRect(_sdlRenderer, &ddRect); } } } } }; std::unique_ptr OpenRCT2::Ui::CreateHardwareDisplayDrawingEngine(IUiContext& uiContext) { return std::make_unique(uiContext); }