text: /**
   * @file dx.cpp
   *
   * Implementation of functions setting up the graphics pipeline.
   */
  #include "dx.h"
  
  #include <SDL.h>
  
  #include "controls/touch/renderers.h"
  #include "engine.h"
  #include "options.h"
  #include "utils/display.h"
  #include "utils/log.hpp"
  #include "utils/sdl_mutex.h"
  #include "utils/sdl_wrap.h"
  
  #ifdef __3DS__
  #include <3ds.h>
  #endif
  
  namespace devilution {
  
  int refreshDelay;
  SDL_Renderer *renderer;
  #ifndef USE_SDL1
  SDLTextureUniquePtr texture;
  #endif
  
  /** Currently active palette */
  SDLPaletteUniquePtr Palette;
  unsigned int pal_surface_palette_version = 0;
  
  /** 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 {
  
  int sgdwLockCount;
  #ifdef _DEBUG
  int locktbl[256];
  #endif
  SdlMutex MemCrit;
  
  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
  	return false;
  #endif
  }
  
  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();
  	}
  
  #ifndef 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
  
  	pal_surface_palette_version = 1;
  }
  
  void CreatePrimarySurface()
  {
  #ifndef USE_SDL1
  	if (renderer != nullptr) {
  		int width = 0;
  		int height = 0;
  		SDL_RenderGetLogicalSize(renderer, &width, &height);
  		Uint32 format;
  		if (SDL_QueryTexture(texture.get(), &format, nullptr, nullptr, nullptr) < 0)
  			ErrSdl();
  		RendererTextureSurface = SDLWrap::CreateRGBSurfaceWithFormat(0, width, height, SDL_BITSPERPIXEL(format), format);
  	}
  #endif
  }
  
  void LockBufPriv()
  {
  	MemCrit.lock();
  	if (sgdwLockCount != 0) {
  		sgdwLockCount++;
  		return;
  	}
  
  	sgdwLockCount++;
  }
  
  void UnlockBufPriv()
  {
  	if (sgdwLockCount == 0)
  		app_fatal("draw main unlock error");
  
  	sgdwLockCount--;
  	MemCrit.unlock();
  }
  
  /**
   * @brief Limit FPS to avoid high CPU load, use when v-sync isn't available
   */
  void LimitFrameRate()
  {
  	if (!sgOptions.Graphics.bFPSLimit)
  		return;
  	static uint32_t frameDeadline;
  	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
  
  	CreatePrimarySurface();
  	palette_init();
  	CreateBackBuffer();
  }
  
  void lock_buf(int idx) // NOLINT(misc-unused-parameters)
  {
  #ifdef _DEBUG
  	++locktbl[idx];
  #endif
  	LockBufPriv();
  }
  
  void unlock_buf(int idx) // NOLINT(misc-unused-parameters)
  {
  #ifdef _DEBUG
  	if (locktbl[idx] == 0)
  		app_fatal("Draw lock underflow: 0x%x", idx);
  	--locktbl[idx];
  #endif
  	UnlockBufPriv();
  }
  
  Surface GlobalBackBuffer()
  {
  	if (sgdwLockCount == 0) {
  		Log("WARNING: Trying to obtain GlobalBackBuffer() without holding a lock");
  		return Surface();
  	}
  
  	return Surface(PalSurface, SDL_Rect { 0, 0, gnScreenWidth, gnScreenHeight });
  }
  
  void dx_cleanup()
  {
  #ifndef USE_SDL1
  	if (ghMainWnd != nullptr)
  		SDL_HideWindow(ghMainWnd);
  #endif
  	MemCrit.lock();
  	sgdwLockCount = 0;
  	MemCrit.unlock();
  
  	PalSurface = nullptr;
  	PinnedPalSurface = nullptr;
  	Palette = nullptr;
  	RendererTextureSurface = nullptr;
  #ifndef USE_SDL1
  	texture = nullptr;
  	if (sgOptions.Graphics.bUpscale)
  		SDL_DestroyRenderer(renderer);
  #endif
  	SDL_DestroyWindow(ghMainWnd);
  }
  
  void dx_reinit()
  {
  #ifdef USE_SDL1
  	Uint32 flags = ghMainWnd->flags ^ SDL_FULLSCREEN;
  	if (!IsFullScreen()) {
  		flags |= SDL_FULLSCREEN;
  	}
  	ghMainWnd = SDL_SetVideoMode(0, 0, 0, flags);
  	if (ghMainWnd == NULL) {
  		ErrSdl();
  	}
  #else
  	Uint32 flags = 0;
  	if (!IsFullScreen()) {
  		flags = renderer != nullptr ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN;
  	}
  	if (SDL_SetWindowFullscreen(ghMainWnd, flags) != 0) {
  		ErrSdl();
  	}
  #endif
  	force_redraw = 255;
  }
  
  void InitPalette()
  {
  	Palette = SDLWrap::AllocPalette();
  }
  
  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)
  {
  	SDL_Surface *dst = GetOutputSurface();
  #ifndef 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()
  {
  	SDL_Surface *surface = GetOutputSurface();
  
  	if (!gbActive) {
  		LimitFrameRate();
  		return;
  	}
  
  #ifndef USE_SDL1
  	if (renderer != nullptr) {
  		if (SDL_UpdateTexture(texture.get(), nullptr, surface->pixels, surface->pitch) <= -1) { //pitch is 2560
  			ErrSdl();
  		}
  
  		// Clear buffer to avoid artifacts in case the window was resized
  		if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) <= -1) { // TODO only do this if window was resized
  			ErrSdl();
  		}
  
  		if (SDL_RenderClear(renderer) <= -1) {
  			ErrSdl();
  		}
  		if (SDL_RenderCopy(renderer, texture.get(), nullptr, nullptr) <= -1) {
  			ErrSdl();
  		}
  #ifdef VIRTUAL_GAMEPAD
  		RenderVirtualGamepad(renderer);
  #endif
  		SDL_RenderPresent(renderer);
  
  		if (!sgOptions.Graphics.bVSync) {
  			LimitFrameRate();
  		}
  	} else {
  #ifdef VIRTUAL_GAMEPAD
  		RenderVirtualGamepad(surface);
  #endif
  		if (SDL_UpdateWindowSurface(ghMainWnd) <= -1) {
  			ErrSdl();
  		}
  		LimitFrameRate();
  	}
  #else
  	if (SDL_Flip(surface) <= -1) {
  		ErrSdl();
  	}
  	if (RenderDirectlyToOutputSurface)
  		PalSurface = GetOutputSurface();
  	LimitFrameRate();
  #endif
  }
  
  void PaletteGetEntries(int dwNumEntries, SDL_Color *lpEntries)
  {
  	for (int i = 0; i < dwNumEntries; i++) {
  		lpEntries[i] = system_palette[i];
  	}
  }
  } // namespace devilution
  