OpenRCT2/src/openrct2-ui/drawing/engines/HardwareDisplayDrawingEngin...

415 lines
14 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2024 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 <SDL.h>
#include <cmath>
#include <memory>
#include <openrct2/Game.h>
#include <openrct2/common.h>
#include <openrct2/config/Config.h>
#include <openrct2/drawing/IDrawingEngine.h>
#include <openrct2/drawing/LightFX.h>
#include <openrct2/drawing/X8DrawingEngine.h>
#include <openrct2/paint/Paint.h>
#include <openrct2/ui/UiContext.h>
#include <vector>
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Ui;
class HardwareDisplayDrawingEngine final : public X8DrawingEngine
{
private:
constexpr static uint32_t DIRTY_VISUAL_TIME = 32;
std::shared_ptr<IUiContext> const _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 };
// Steam overlay checking
uint32_t _pixelBeforeOverlay = 0;
uint32_t _pixelAfterOverlay = 0;
bool _overlayActive = false;
bool _pausedBeforeOverlay = false;
bool _useVsync = true;
std::vector<uint32_t> _dirtyVisualsTime;
bool smoothNN = false;
public:
explicit HardwareDisplayDrawingEngine(const std::shared_ptr<IUiContext>& uiContext)
: X8DrawingEngine(uiContext)
, _uiContext(uiContext)
{
_window = static_cast<SDL_Window*>(_uiContext->GetWindow());
}
~HardwareDisplayDrawingEngine() override
{
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;
SDL_DestroyRenderer(_sdlRenderer);
_screenTexture = nullptr;
_scaledScreenTexture = nullptr;
Initialise();
Resize(_uiContext->GetWidth(), _uiContext->GetHeight());
}
}
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<int32_t>(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 screen texture: %s", SDL_GetError());
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, scaleQualityBuffer);
uint32_t scale = std::ceil(gConfigGeneral.WindowScale);
_scaledScreenTexture = SDL_CreateTexture(
_sdlRenderer, pixelFormat, SDL_TEXTUREACCESS_TARGET, width * scale, height * scale);
Guard::Assert(_scaledScreenTexture != nullptr, "Failed to create scaled screen texture: %s", SDL_GetError());
}
else
{
_screenTexture = SDL_CreateTexture(_sdlRenderer, pixelFormat, SDL_TEXTUREACCESS_STREAMING, width, height);
Guard::Assert(_screenTexture != nullptr, "Failed to create screen texture: %s", SDL_GetError());
}
uint32_t format;
SDL_QueryTexture(_screenTexture, &format, nullptr, nullptr, nullptr);
_screenTextureFormat = SDL_AllocFormat(format);
ConfigureBits(width, height, width);
}
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 (gConfigGeneral.EnableLightFx)
{
auto& lightPalette = LightFXGetPalette();
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 EndDraw() override
{
Display();
if (gShowDirtyVisuals)
{
UpdateDirtyVisuals();
}
}
protected:
void OnDrawDirtyBlock(uint32_t left, uint32_t top, uint32_t columns, uint32_t rows) override
{
if (gShowDirtyVisuals)
{
uint32_t right = left + columns;
uint32_t bottom = top + rows;
for (uint32_t x = left; x < right; x++)
{
for (uint32_t y = top; y < bottom; y++)
{
SetDirtyVisualTime(x, y, DIRTY_VISUAL_TIME);
}
}
}
}
private:
void Display()
{
if (gConfigGeneral.EnableLightFx)
{
void* pixels;
int32_t pitch;
if (SDL_LockTexture(_screenTexture, nullptr, &pixels, &pitch) == 0)
{
LightFXRenderToTexture(pixels, pitch, _bits, _width, _height, _paletteHWMapped, _lightPaletteHWMapped);
SDL_UnlockTexture(_screenTexture);
}
}
else
{
CopyBitsToTexture(
_screenTexture, _bits, static_cast<int32_t>(_width), static_cast<int32_t>(_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();
}
bool isSteamOverlayActive = GetContext()->GetUiContext()->IsSteamOverlayActive();
if (isSteamOverlayActive && gConfigGeneral.SteamOverlayPause)
{
OverlayPreRenderCheck();
}
SDL_RenderPresent(_sdlRenderer);
if (isSteamOverlayActive && gConfigGeneral.SteamOverlayPause)
{
OverlayPostRenderCheck();
}
}
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<uint32_t*>(pixels);
for (int32_t i = width * height; i > 0; i--)
{
*dst++ = palette[*src++];
}
}
else
{
if (pitch == (width * 2) + padding)
{
uint16_t* dst = static_cast<uint16_t*>(pixels);
for (int32_t y = height; y > 0; y--)
{
for (int32_t x = width; x > 0; x--)
{
const uint8_t lower = *reinterpret_cast<const uint8_t*>(&palette[*src++]);
const uint8_t upper = *reinterpret_cast<const uint8_t*>(&palette[*src++]);
*dst++ = (lower << 8) | upper;
}
dst = reinterpret_cast<uint16_t*>(reinterpret_cast<uint8_t*>(dst) + padding);
}
}
else if (pitch == width + padding)
{
uint8_t* dst = static_cast<uint8_t*>(pixels);
for (int32_t y = height; y > 0; y--)
{
for (int32_t x = width; x > 0; x--)
{
*dst++ = *reinterpret_cast<const uint8_t*>(&palette[*src++]);
}
dst += padding;
}
}
}
SDL_UnlockTexture(texture);
}
}
uint32_t GetDirtyVisualTime(uint32_t x, uint32_t y)
{
uint32_t result = 0;
uint32_t i = y * _dirtyGrid.BlockColumns + x;
if (_dirtyVisualsTime.size() > i)
{
result = _dirtyVisualsTime[i];
}
return result;
}
void SetDirtyVisualTime(uint32_t x, uint32_t y, uint32_t value)
{
uint32_t i = y * _dirtyGrid.BlockColumns + x;
if (_dirtyVisualsTime.size() > i)
{
_dirtyVisualsTime[i] = value;
}
}
void UpdateDirtyVisuals()
{
_dirtyVisualsTime.resize(_dirtyGrid.BlockRows * _dirtyGrid.BlockColumns);
for (uint32_t y = 0; y < _dirtyGrid.BlockRows; y++)
{
for (uint32_t x = 0; x < _dirtyGrid.BlockColumns; x++)
{
auto timeLeft = GetDirtyVisualTime(x, y);
if (timeLeft > 0)
{
SetDirtyVisualTime(x, y, timeLeft - 1);
}
}
}
}
void RenderDirtyVisuals()
{
float scaleX = gConfigGeneral.WindowScale;
float scaleY = gConfigGeneral.WindowScale;
SDL_SetRenderDrawBlendMode(_sdlRenderer, SDL_BLENDMODE_BLEND);
for (uint32_t y = 0; y < _dirtyGrid.BlockRows; y++)
{
for (uint32_t x = 0; x < _dirtyGrid.BlockColumns; x++)
{
auto timeLeft = GetDirtyVisualTime(x, y);
if (timeLeft > 0)
{
uint8_t alpha = static_cast<uint8_t>(timeLeft * 5 / 2);
SDL_Rect ddRect;
ddRect.x = static_cast<int32_t>(x * _dirtyGrid.BlockWidth * scaleX);
ddRect.y = static_cast<int32_t>(y * _dirtyGrid.BlockHeight * scaleY);
ddRect.w = static_cast<int32_t>(_dirtyGrid.BlockWidth * scaleX);
ddRect.h = static_cast<int32_t>(_dirtyGrid.BlockHeight * scaleY);
SDL_SetRenderDrawColor(_sdlRenderer, 255, 255, 255, alpha);
SDL_RenderFillRect(_sdlRenderer, &ddRect);
}
}
}
}
void ReadCentrePixel(uint32_t* pixel)
{
SDL_Rect centrePixelRegion = { static_cast<int32_t>(_width / 2), static_cast<int32_t>(_height / 2), 1, 1 };
SDL_RenderReadPixels(_sdlRenderer, &centrePixelRegion, SDL_PIXELFORMAT_RGBA8888, pixel, sizeof(uint32_t));
}
// Should be called before SDL_RenderPresent to capture frame buffer before Steam overlay is drawn.
void OverlayPreRenderCheck()
{
ReadCentrePixel(&_pixelBeforeOverlay);
}
// Should be called after SDL_RenderPresent, when Steam overlay has had the chance to be drawn.
void OverlayPostRenderCheck()
{
ReadCentrePixel(&_pixelAfterOverlay);
// Detect an active Steam overlay by checking if the centre pixel is changed by the grey fade.
// Will not be triggered by applications rendering to corners, like FRAPS, MSI Afterburner and Friends popups.
bool newOverlayActive = _pixelBeforeOverlay != _pixelAfterOverlay;
// Toggle game pause state consistently with base pause state
if (!_overlayActive && newOverlayActive)
{
_pausedBeforeOverlay = gGamePaused & GAME_PAUSED_NORMAL;
if (!_pausedBeforeOverlay)
{
PauseToggle();
}
}
else if (_overlayActive && !newOverlayActive && !_pausedBeforeOverlay)
{
PauseToggle();
}
_overlayActive = newOverlayActive;
}
};
std::unique_ptr<IDrawingEngine> OpenRCT2::Ui::CreateHardwareDisplayDrawingEngine(const std::shared_ptr<IUiContext>& uiContext)
{
return std::make_unique<HardwareDisplayDrawingEngine>(uiContext);
}