/** * @file dx.cpp * * Implementation of functions setting up the graphics pipeline. */ #include "engine/dx.h" #include #ifdef USE_SDL3 #include #include #include #include #include #else #include #endif #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/render/primitive_render.hpp" #include "headless_mode.hpp" #include "init.hpp" #include "options.h" #include "utils/display.h" #include "utils/log.hpp" #include "utils/sdl_wrap.h" #ifndef USE_SDL1 #include "controls/touch/renderers.h" #endif #ifdef __3DS__ #include <3ds.h> #endif #ifdef __DREAMCAST__ #include "platform/dreamcast/dc_video.h" #endif namespace devilution { int refreshDelay; SDL_Renderer *renderer; #ifndef USE_SDL1 SDLTextureUniquePtr texture; #endif /** Currently active palette */ SDLPaletteUniquePtr Palette; /** 24-bit renderer texture surface */ SDLSurfaceUniquePtr RendererTextureSurface; /** 8-bit surface that we render to */ SDL_Surface *PalSurface; namespace { SDLSurfaceUniquePtr PinnedPalSurface; } // namespace /** Whether we render directly to the screen surface, i.e. `PalSurface == GetOutputSurface()` */ bool RenderDirectlyToOutputSurface; namespace { bool CanRenderDirectlyToOutputSurface() { #ifdef USE_SDL1 #ifdef SDL1_FORCE_DIRECT_RENDER return true; #else auto *outputSurface = GetOutputSurface(); return ((outputSurface->flags & SDL_DOUBLEBUF) == SDL_DOUBLEBUF && outputSurface->w == gnScreenWidth && outputSurface->h == gnScreenHeight && outputSurface->format->BitsPerPixel == 8); #endif #else // !USE_SDL1 if (renderer != nullptr) return false; SDL_Surface *outputSurface = GetOutputSurface(); // Assumes double-buffering is available. return outputSurface->w == static_cast(gnScreenWidth) && outputSurface->h == static_cast(gnScreenHeight) && SDLC_SURFACE_BITSPERPIXEL(outputSurface) == 8; #endif } /** * @brief Limit FPS to avoid high CPU load, use when v-sync isn't available */ void LimitFrameRate() { if (*GetOptions().Graphics.frameRateControl != FrameRateControl::CPUSleep) return; static uint32_t frameDeadline; const uint32_t tc = SDL_GetTicks() * 1000; uint32_t v = 0; if (frameDeadline > tc) { v = tc % refreshDelay; SDL_Delay(v / 1000 + 1); // ceil } frameDeadline = tc + v + refreshDelay; } } // namespace void dx_init() { #ifndef USE_SDL1 SDL_RaiseWindow(ghMainWnd); SDL_ShowWindow(ghMainWnd); #endif Palette = SDLWrap::AllocPalette(); palette_init(); CreateBackBuffer(); } Surface GlobalBackBuffer() { return Surface(PalSurface, SDL_Rect { 0, 0, gnScreenWidth, gnScreenHeight }); } void dx_cleanup() { #ifndef USE_SDL1 if (ghMainWnd != nullptr) SDL_HideWindow(ghMainWnd); #endif #ifdef __DREAMCAST__ dc::VideoShutdown(); #endif PalSurface = nullptr; PinnedPalSurface = nullptr; Palette = nullptr; RendererTextureSurface = nullptr; #ifndef USE_SDL1 texture = nullptr; FreeVirtualGamepadTextures(); if (*GetOptions().Graphics.upscale) SDL_DestroyRenderer(renderer); #endif SDL_DestroyWindow(ghMainWnd); } void CreateBackBuffer() { if (CanRenderDirectlyToOutputSurface()) { Log("{}", "Will render directly to the SDL output surface"); PalSurface = GetOutputSurface(); RenderDirectlyToOutputSurface = true; } else { PinnedPalSurface = SDLWrap::CreateRGBSurfaceWithFormat( /*flags=*/0, /*width=*/gnScreenWidth, /*height=*/gnScreenHeight, /*depth=*/8, SDL_PIXELFORMAT_INDEX8); PalSurface = PinnedPalSurface.get(); } #if defined(USE_SDL3) if (!SDL_SetSurfacePalette(PalSurface, Palette.get())) ErrSdl(); #elif !defined(USE_SDL1) // In SDL2, `PalSurface` points to the global `palette`. if (SDL_SetSurfacePalette(PalSurface, Palette.get()) < 0) ErrSdl(); #else // In SDL1, `PalSurface` owns its palette and we must update it every // time the global `palette` is changed. No need to do anything here as // the global `palette` doesn't have any colors set yet. #endif #ifdef __DREAMCAST__ // Initialize Dreamcast 8bpp->16bpp video converter // On DC, output is always 16bpp but we render to 8bpp PalSurface if (!RenderDirectlyToOutputSurface) { dc::VideoInit(gnScreenWidth, gnScreenHeight); } #endif } void BltFast(SDL_Rect *srcRect, SDL_Rect *dstRect) { if (RenderDirectlyToOutputSurface) return; Blit(PalSurface, srcRect, dstRect); } void Blit(SDL_Surface *src, SDL_Rect *srcRect, SDL_Rect *dstRect) { if (HeadlessMode) return; SDL_Surface *dst = GetOutputSurface(); #if defined(USE_SDL3) if (!SDL_BlitSurface(src, srcRect, dst, dstRect)) ErrSdl(); #elif !defined(USE_SDL1) if (SDL_BlitSurface(src, srcRect, dst, dstRect) < 0) ErrSdl(); #else if (!OutputRequiresScaling()) { if (SDL_BlitSurface(src, srcRect, dst, dstRect) < 0) ErrSdl(); return; } SDL_Rect scaledDstRect; if (dstRect != NULL) { scaledDstRect = *dstRect; ScaleOutputRect(&scaledDstRect); dstRect = &scaledDstRect; } // Same pixel format: We can call BlitScaled directly. if (SDLBackport_PixelFormatFormatEq(src->format, dst->format)) { if (SDL_BlitScaled(src, srcRect, dst, dstRect) < 0) ErrSdl(); return; } // If the surface has a color key, we must stretch first and can then call BlitSurface. if (SDL_HasColorKey(src)) { SDLSurfaceUniquePtr stretched = SDLWrap::CreateRGBSurface(SDL_SWSURFACE, dstRect->w, dstRect->h, src->format->BitsPerPixel, src->format->Rmask, src->format->Gmask, src->format->BitsPerPixel, src->format->Amask); SDL_SetColorKey(stretched.get(), SDL_SRCCOLORKEY, src->format->colorkey); if (src->format->palette != NULL) SDL_SetPalette(stretched.get(), SDL_LOGPAL, src->format->palette->colors, 0, src->format->palette->ncolors); SDL_Rect stretched_rect = { 0, 0, dstRect->w, dstRect->h }; if (SDL_SoftStretch(src, srcRect, stretched.get(), &stretched_rect) < 0 || SDL_BlitSurface(stretched.get(), &stretched_rect, dst, dstRect) < 0) { ErrSdl(); } return; } // A surface with a non-output pixel format but without a color key needs scaling. // We can convert the format and then call BlitScaled. SDLSurfaceUniquePtr converted = SDLWrap::ConvertSurface(src, dst->format, 0); if (SDL_BlitScaled(converted.get(), srcRect, dst, dstRect) < 0) ErrSdl(); #endif } void RenderPresent() { if (HeadlessMode) return; SDL_Surface *surface = GetOutputSurface(); if (!gbActive) { LimitFrameRate(); return; } #ifndef USE_SDL1 if (renderer != nullptr) { #ifdef USE_SDL3 if (!SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)) ErrSdl(); if (!SDL_RenderClear(renderer)) ErrSdl(); if (!SDL_UpdateTexture(texture.get(), nullptr, surface->pixels, surface->pitch)) ErrSdl(); if (!SDL_RenderTexture(renderer, texture.get(), nullptr, nullptr)) ErrSdl(); #else if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) <= -1) ErrSdl(); if (SDL_RenderClear(renderer) <= -1) ErrSdl(); if (SDL_UpdateTexture(texture.get(), nullptr, surface->pixels, surface->pitch) <= -1) ErrSdl(); if (SDL_RenderCopy(renderer, texture.get(), nullptr, nullptr) <= -1) ErrSdl(); #endif if (ControlMode == ControlTypes::VirtualGamepad) { RenderVirtualGamepad(renderer); } SDL_RenderPresent(renderer); if (*GetOptions().Graphics.frameRateControl != FrameRateControl::VerticalSync) { LimitFrameRate(); } } else { if (ControlMode == ControlTypes::VirtualGamepad) { RenderVirtualGamepad(surface); } #ifdef USE_SDL3 if (!SDL_UpdateWindowSurface(ghMainWnd)) ErrSdl(); #else if (SDL_UpdateWindowSurface(ghMainWnd) <= -1) ErrSdl(); #endif if (RenderDirectlyToOutputSurface) PalSurface = GetOutputSurface(); LimitFrameRate(); } #else #ifdef __DREAMCAST__ // Dreamcast: Convert 8bpp PalSurface to 16bpp output surface before flip if (!RenderDirectlyToOutputSurface && dc::IsInitialized()) { dc::ConvertAndUpload(PalSurface, surface); } #endif if (SDL_Flip(surface) <= -1) { ErrSdl(); } if (RenderDirectlyToOutputSurface) PalSurface = GetOutputSurface(); LimitFrameRate(); #endif } } // namespace devilution