#pragma once #include "common/bitfield.h" #include "common/fifo_queue.h" #include "common/rectangle.h" #include "timers.h" #include "types.h" #include #include #include #include #include #include class StateWrapper; class HostDisplay; class TimingEvent; class Timers; class GPU { public: enum class BlitterState : u8 { Idle, ReadingVRAM, WritingVRAM, DrawingPolyLine }; enum class DMADirection : u32 { Off = 0, FIFO = 1, CPUtoGP0 = 2, GPUREADtoCPU = 3 }; enum class Primitive : u8 { Reserved = 0, Polygon = 1, Line = 2, Rectangle = 3 }; enum class DrawRectangleSize : u8 { Variable = 0, R1x1 = 1, R8x8 = 2, R16x16 = 3 }; enum class TextureMode : u8 { Palette4Bit = 0, Palette8Bit = 1, Direct16Bit = 2, Reserved_Direct16Bit = 3, // Not register values. RawTextureBit = 4, RawPalette4Bit = RawTextureBit | Palette4Bit, RawPalette8Bit = RawTextureBit | Palette8Bit, RawDirect16Bit = RawTextureBit | Direct16Bit, Reserved_RawDirect16Bit = RawTextureBit | Reserved_Direct16Bit, Disabled = 8 // Not a register value }; enum class TransparencyMode : u8 { HalfBackgroundPlusHalfForeground = 0, BackgroundPlusForeground = 1, BackgroundMinusForeground = 2, BackgroundPlusQuarterForeground = 3, Disabled = 4 // Not a register value }; enum : u32 { VRAM_WIDTH = 1024, VRAM_HEIGHT = 512, VRAM_SIZE = VRAM_WIDTH * VRAM_HEIGHT * sizeof(u16), VRAM_WIDTH_MASK = VRAM_WIDTH - 1, VRAM_HEIGHT_MASK = VRAM_HEIGHT - 1, VRAM_COORD_MASK = 0x3FF, MAX_FIFO_SIZE = 4096, TEXTURE_PAGE_WIDTH = 256, TEXTURE_PAGE_HEIGHT = 256, MAX_PRIMITIVE_WIDTH = 1024, MAX_PRIMITIVE_HEIGHT = 512, DOT_TIMER_INDEX = 0, HBLANK_TIMER_INDEX = 1, MAX_RESOLUTION_SCALE = 16, DITHER_MATRIX_SIZE = 4 }; enum : u16 { NTSC_TICKS_PER_LINE = 3413, NTSC_TOTAL_LINES = 263, PAL_TICKS_PER_LINE = 3406, PAL_TOTAL_LINES = 314, }; // 4x4 dither matrix. static constexpr s32 DITHER_MATRIX[DITHER_MATRIX_SIZE][DITHER_MATRIX_SIZE] = {{-4, +0, -3, +1}, // row 0 {+2, -2, +3, -1}, // row 1 {-3, +1, -4, +0}, // row 2 {+4, -1, +2, -2}}; // row 3 // Base class constructor. GPU(); virtual ~GPU(); virtual bool IsHardwareRenderer() const = 0; virtual bool Initialize(HostDisplay* host_display); virtual void Reset(); virtual bool DoState(StateWrapper& sw); // Graphics API state reset/restore - call when drawing the UI etc. virtual void ResetGraphicsAPIState(); virtual void RestoreGraphicsAPIState(); // Render statistics debug window. void DrawDebugStateWindow(); // MMIO access u32 ReadRegister(u32 offset); void WriteRegister(u32 offset, u32 value); // DMA access void DMARead(u32* words, u32 word_count); ALWAYS_INLINE bool BeginDMAWrite() const { return (m_GPUSTAT.dma_direction == DMADirection::CPUtoGP0); } ALWAYS_INLINE void DMAWrite(u32 address, u32 value) { m_fifo.Push((ZeroExtend64(address) << 32) | ZeroExtend64(value)); } void EndDMAWrite(); /// Returns the number of pending GPU ticks. TickCount GetPendingCRTCTicks() const; TickCount GetPendingCommandTicks() const; /// Returns true if enough ticks have passed for the raster to be on the next line. bool IsCRTCScanlinePending() const; /// Returns true if a raster scanline or command execution is pending. bool IsCommandCompletionPending() const; /// Synchronizes the CRTC, updating the hblank timer. void SynchronizeCRTC(); /// Recompile shaders/recreate framebuffers when needed. virtual void UpdateSettings(); /// Updates the resolution scale when it's set to automatic. virtual void UpdateResolutionScale(); // gpu_hw_d3d11.cpp static std::unique_ptr CreateHardwareD3D11Renderer(); // gpu_hw_opengl.cpp static std::unique_ptr CreateHardwareOpenGLRenderer(); // gpu_hw_vulkan.cpp static std::unique_ptr CreateHardwareVulkanRenderer(); // gpu_sw.cpp static std::unique_ptr CreateSoftwareRenderer(); // Converts window coordinates into horizontal ticks and scanlines. Returns false if out of range. Used for lightguns. bool ConvertScreenCoordinatesToBeamTicksAndLines(s32 window_x, s32 window_y, u32* out_tick, u32* out_line) const; // Returns the video clock frequency. TickCount GetCRTCFrequency() const; protected: TickCount CRTCTicksToSystemTicks(TickCount crtc_ticks, TickCount fractional_ticks) const; TickCount SystemTicksToCRTCTicks(TickCount sysclk_ticks, TickCount* fractional_ticks) const; // The GPU internally appears to run at 2x the system clock. ALWAYS_INLINE static constexpr TickCount GPUTicksToSystemTicks(TickCount gpu_ticks) { return std::max(gpu_ticks >> 1, 1); } ALWAYS_INLINE static constexpr TickCount SystemTicksToGPUTicks(TickCount sysclk_ticks) { return sysclk_ticks << 1; } // Helper/format conversion functions. static constexpr u8 Convert5To8(u8 x5) { return (x5 << 3) | (x5 & 7); } static constexpr u8 Convert8To5(u8 x8) { return (x8 >> 3); } static constexpr u32 RGBA5551ToRGBA8888(u16 color) { u8 r = Truncate8(color & 31); u8 g = Truncate8((color >> 5) & 31); u8 b = Truncate8((color >> 10) & 31); u8 a = Truncate8((color >> 15) & 1); // 00012345 -> 1234545 b = (b << 3) | (b & 0b111); g = (g << 3) | (g & 0b111); r = (r << 3) | (r & 0b111); a = a ? 255 : 0; return ZeroExtend32(r) | (ZeroExtend32(g) << 8) | (ZeroExtend32(b) << 16) | (ZeroExtend32(a) << 24); } static constexpr u16 RGBA8888ToRGBA5551(u32 color) { const u16 r = Truncate16((color >> 3) & 0x1Fu); const u16 g = Truncate16((color >> 11) & 0x1Fu); const u16 b = Truncate16((color >> 19) & 0x1Fu); const u16 a = Truncate16((color >> 31) & 0x01u); return r | (g << 5) | (b << 10) | (a << 15); } static constexpr std::tuple UnpackTexcoord(u16 texcoord) { return std::make_tuple(static_cast(texcoord), static_cast(texcoord >> 8)); } static constexpr std::tuple UnpackColorRGB24(u32 rgb24) { return std::make_tuple(static_cast(rgb24), static_cast(rgb24 >> 8), static_cast(rgb24 >> 16)); } static constexpr u32 PackColorRGB24(u8 r, u8 g, u8 b) { return ZeroExtend32(r) | (ZeroExtend32(g) << 8) | (ZeroExtend32(b) << 16); } static bool DumpVRAMToFile(const char* filename, u32 width, u32 height, u32 stride, const void* buffer, bool remove_alpha); union RenderCommand { u32 bits; BitField color_for_first_vertex; BitField raw_texture_enable; // not valid for lines BitField transparency_enable; BitField texture_enable; BitField rectangle_size; // only for rectangles BitField quad_polygon; // only for polygons BitField polyline; // only for lines BitField shading_enable; // 0 - flat, 1 = gouroud BitField primitive; /// Returns true if texturing should be enabled. Depends on the primitive type. bool IsTexturingEnabled() const { return (primitive != Primitive::Line) ? texture_enable : false; } /// Returns true if dithering should be enabled. Depends on the primitive type. bool IsDitheringEnabled() const { switch (primitive) { case Primitive::Polygon: return shading_enable || (texture_enable && !raw_texture_enable); case Primitive::Line: return true; case Primitive::Rectangle: default: return false; } } }; union VertexPosition { u32 bits; BitField x; BitField y; }; // Sprites/rectangles should be clipped to 12 bits before drawing. static constexpr s32 TruncateVertexPosition(s32 x) { return SignExtendN<11, s32>(x); } struct NativeVertex { s16 x; s16 y; u32 color; u16 texcoord; }; union VRAMPixel { u16 bits; BitField r; BitField g; BitField b; BitField c; u8 GetR8() const { return Convert5To8(r); } u8 GetG8() const { return Convert5To8(g); } u8 GetB8() const { return Convert5To8(b); } void Set(u8 r_, u8 g_, u8 b_, bool c_ = false) { bits = (ZeroExtend16(r_)) | (ZeroExtend16(g_) << 5) | (ZeroExtend16(b_) << 10) | (static_cast(c_) << 15); } void ClampAndSet(u8 r_, u8 g_, u8 b_, bool c_ = false) { Set(std::min(r_, 0x1F), std::min(g_, 0x1F), std::min(b_, 0x1F), c_); } void SetRGB24(u32 rgb24, bool c_ = false) { bits = Truncate16(((rgb24 >> 3) & 0x1F) | (((rgb24 >> 11) & 0x1F) << 5) | (((rgb24 >> 19) & 0x1F) << 10)) | (static_cast(c_) << 15); } void SetRGB24(u8 r8, u8 g8, u8 b8, bool c_ = false) { bits = (ZeroExtend16(r8 >> 3)) | (ZeroExtend16(g8 >> 3) << 5) | (ZeroExtend16(b8 >> 3) << 10) | (static_cast(c_) << 15); } void SetRGB24Dithered(u32 x, u32 y, u8 r8, u8 g8, u8 b8, bool c_ = false) { const s32 offset = DITHER_MATRIX[y & 3][x & 3]; r8 = static_cast(std::clamp(static_cast(ZeroExtend32(r8)) + offset, 0, 255)); g8 = static_cast(std::clamp(static_cast(ZeroExtend32(g8)) + offset, 0, 255)); b8 = static_cast(std::clamp(static_cast(ZeroExtend32(b8)) + offset, 0, 255)); SetRGB24(r8, g8, b8, c_); } u32 ToRGB24() const { const u32 r_ = ZeroExtend32(r.GetValue()); const u32 g_ = ZeroExtend32(g.GetValue()); const u32 b_ = ZeroExtend32(b.GetValue()); return ((r_ << 3) | (r_ & 7)) | (((g_ << 3) | (g_ & 7)) << 8) | (((b_ << 3) | (b_ & 7)) << 16); } }; void SoftReset(); // Sets dots per scanline float ComputeHorizontalFrequency() const; float ComputeVerticalFrequency() const; void UpdateCRTCConfig(); void UpdateCRTCDisplayParameters(); // Update ticks for this execution slice void UpdateCRTCTickEvent(); void UpdateCommandTickEvent(); // Updates dynamic bits in GPUSTAT (ready to send VRAM/ready to receive DMA) void UpdateDMARequest(); void UpdateGPUIdle(); // Ticks for hblank/vblank. void CRTCTickEvent(TickCount ticks); void CommandTickEvent(TickCount ticks); /// Returns false if the DAC is loading any data from VRAM. ALWAYS_INLINE bool IsDisplayDisabled() const { return m_GPUSTAT.display_disable || m_crtc_state.display_vram_width == 0 || m_crtc_state.display_vram_height == 0; } /// Returns true if scanout should be interlaced. ALWAYS_INLINE bool IsInterlacedDisplayEnabled() const { return (!m_force_progressive_scan) & m_GPUSTAT.vertical_interlace; } /// Returns true if interlaced rendering is enabled and force progressive scan is disabled. ALWAYS_INLINE bool IsInterlacedRenderingEnabled() const { return (!m_force_progressive_scan) & m_GPUSTAT.SkipDrawingToActiveField(); } /// Returns 0 if the currently-displayed field is on odd lines (1,3,5,...) or 1 if even (2,4,6,...). ALWAYS_INLINE u32 GetInterlacedDisplayField() const { return ZeroExtend32(m_crtc_state.interlaced_field); } /// Returns 0 if the currently-displayed field is on an even line in VRAM, otherwise 1. ALWAYS_INLINE u32 GetActiveLineLSB() const { return ZeroExtend32(m_crtc_state.active_line_lsb); } /// Sets/decodes GP0(E1h) (set draw mode). void SetDrawMode(u16 bits); /// Sets/decodes polygon/rectangle texture palette value. void SetTexturePalette(u16 bits); /// Sets/decodes texture window bits. void SetTextureWindow(u32 value); u32 ReadGPUREAD(); void FinishVRAMWrite(); /// Returns the number of vertices in the buffered poly-line. ALWAYS_INLINE u32 GetPolyLineVertexCount() const { return (static_cast(m_blit_buffer.size()) + BoolToUInt32(m_render_command.shading_enable)) >> BoolToUInt8(m_render_command.shading_enable); } /// Returns true if the drawing area is valid (i.e. left <= right, top <= bottom). ALWAYS_INLINE bool IsDrawingAreaIsValid() const { return m_drawing_area.Valid(); } void AddCommandTicks(TickCount ticks); void WriteGP1(u32 value); void EndCommand(); void ExecuteCommands(); void HandleGetGPUInfoCommand(u32 value); // Rendering in the backend virtual void ReadVRAM(u32 x, u32 y, u32 width, u32 height); virtual void FillVRAM(u32 x, u32 y, u32 width, u32 height, u32 color); virtual void UpdateVRAM(u32 x, u32 y, u32 width, u32 height, const void* data); virtual void CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32 height); virtual void DispatchRenderCommand(); virtual void FlushRender(); virtual void ClearDisplay(); virtual void UpdateDisplay(); virtual void DrawRendererStats(bool is_idle_frame); // These are **very** approximate. ALWAYS_INLINE void AddDrawTriangleTicks(u32 width, u32 height, bool shaded, bool textured, bool semitransparent) { const u32 average_width = ((width + 2) / 3); u32 ticks_per_row = average_width; if (textured) ticks_per_row += average_width; if (semitransparent || m_GPUSTAT.check_mask_before_draw) ticks_per_row += (average_width + 1u) / 2u; if (IsInterlacedRenderingEnabled()) height = std::max(height / 2, 1u); AddCommandTicks(ticks_per_row * height); } ALWAYS_INLINE void AddDrawRectangleTicks(u32 width, u32 height, bool textured, bool semitransparent) { u32 ticks_per_row = width; if (textured) ticks_per_row += width; if (semitransparent || m_GPUSTAT.check_mask_before_draw) ticks_per_row += (width + 1u) / 2u; if (IsInterlacedRenderingEnabled()) height = std::max(height / 2, 1u); AddCommandTicks(ticks_per_row * height); } ALWAYS_INLINE void AddDrawLineTicks(u32 width, u32 height, bool shaded) { if (IsInterlacedRenderingEnabled()) height = std::max(height / 2, 1u); AddCommandTicks(std::max(width, height)); } HostDisplay* m_host_display = nullptr; std::unique_ptr m_crtc_tick_event; std::unique_ptr m_command_tick_event; // Pointer to VRAM, used for reads/writes. In the hardware backends, this is the shadow buffer. u16* m_vram_ptr = nullptr; union GPUSTAT { u32 bits; BitField texture_page_x_base; BitField texture_page_y_base; BitField semi_transparency_mode; BitField texture_color_mode; BitField dither_enable; BitField draw_to_displayed_field; BitField set_mask_while_drawing; BitField check_mask_before_draw; BitField interlaced_field; BitField reverse_flag; BitField texture_disable; BitField horizontal_resolution_2; BitField horizontal_resolution_1; BitField vertical_resolution; BitField pal_mode; BitField display_area_color_depth_24; BitField vertical_interlace; BitField display_disable; BitField interrupt_request; BitField dma_data_request; BitField gpu_idle; BitField ready_to_send_vram; BitField ready_to_recieve_dma; BitField dma_direction; BitField display_line_lsb; bool IsMaskingEnabled() const { static constexpr u32 MASK = ((1 << 11) | (1 << 12)); return ((bits & MASK) != 0); } bool SkipDrawingToActiveField() const { static constexpr u32 MASK = (1 << 19) | (1 << 22) | (1 << 10); static constexpr u32 ACTIVE = (1 << 19) | (1 << 22); return ((bits & MASK) == ACTIVE); } bool InInterleaved480iMode() const { static constexpr u32 ACTIVE = (1 << 19) | (1 << 22); return ((bits & ACTIVE) == ACTIVE); } // During transfer/render operations, if ((dst_pixel & mask_and) == 0) { pixel = src_pixel | mask_or } u16 GetMaskAND() const { // return check_mask_before_draw ? 0x8000 : 0x0000; return Truncate16((bits << 3) & 0x8000); } u16 GetMaskOR() const { // return set_mask_while_drawing ? 0x8000 : 0x0000; return Truncate16((bits << 4) & 0x8000); } } m_GPUSTAT = {}; struct DrawMode { static constexpr u16 PALETTE_MASK = UINT16_C(0b0111111111111111); static constexpr u32 TEXTURE_WINDOW_MASK = UINT32_C(0b11111111111111111111); // bits in GP0(E1h) or texpage part of polygon union Reg { static constexpr u16 MASK = 0b1111111111111; static constexpr u16 TEXTURE_PAGE_MASK = UINT16_C(0b0000000000011111); // Polygon texpage commands only affect bits 0-8, 11 static constexpr u16 POLYGON_TEXPAGE_MASK = 0b0000100111111111; // Bits 0..5 are returned in the GPU status register, latched at E1h/polygon draw time. static constexpr u32 GPUSTAT_MASK = 0b11111111111; u16 bits; BitField texture_page_x_base; BitField texture_page_y_base; BitField transparency_mode; BitField texture_mode; BitField dither_enable; BitField draw_to_displayed_field; BitField texture_disable; BitField texture_x_flip; BitField texture_y_flip; u32 GetTexturePageXBase() const { return ZeroExtend32(texture_page_x_base.GetValue()) * 64; } u32 GetTexturePageYBase() const { return ZeroExtend32(texture_page_y_base.GetValue()) * 256; } }; // original values Reg mode_reg; u16 palette_reg; // from vertex u32 texture_window_value; // decoded values u32 texture_page_x; u32 texture_page_y; u32 texture_palette_x; u32 texture_palette_y; u8 texture_window_mask_x; // in 8 pixel steps u8 texture_window_mask_y; // in 8 pixel steps u8 texture_window_offset_x; // in 8 pixel steps u8 texture_window_offset_y; // in 8 pixel steps bool texture_x_flip; bool texture_y_flip; bool texture_page_changed; bool texture_window_changed; /// Returns the texture/palette rendering mode. TextureMode GetTextureMode() const { return mode_reg.texture_mode; } /// Returns the semi-transparency mode when enabled. TransparencyMode GetTransparencyMode() const { return mode_reg.transparency_mode; } /// Returns true if the texture mode requires a palette. bool IsUsingPalette() const { return (mode_reg.bits & (2 << 7)) == 0; } /// Returns a rectangle comprising the texture page area. Common::Rectangle GetTexturePageRectangle() const { static constexpr std::array texture_page_widths = { {TEXTURE_PAGE_WIDTH / 4, TEXTURE_PAGE_WIDTH / 2, TEXTURE_PAGE_WIDTH, TEXTURE_PAGE_WIDTH}}; return Common::Rectangle::FromExtents(texture_page_x, texture_page_y, texture_page_widths[static_cast(mode_reg.texture_mode.GetValue())], TEXTURE_PAGE_HEIGHT); } /// Returns a rectangle comprising the texture palette area. Common::Rectangle GetTexturePaletteRectangle() const { static constexpr std::array palette_widths = {{16, 256, 0, 0}}; return Common::Rectangle::FromExtents(texture_palette_x, texture_palette_y, palette_widths[static_cast(mode_reg.texture_mode.GetValue())], 1); } bool IsTexturePageChanged() const { return texture_page_changed; } void SetTexturePageChanged() { texture_page_changed = true; } void ClearTexturePageChangedFlag() { texture_page_changed = false; } bool IsTextureWindowChanged() const { return texture_window_changed; } void SetTextureWindowChanged() { texture_window_changed = true; } void ClearTextureWindowChangedFlag() { texture_window_changed = false; } } m_draw_mode = {}; Common::Rectangle m_drawing_area{0, 0, VRAM_WIDTH, VRAM_HEIGHT}; struct DrawingOffset { s32 x; s32 y; } m_drawing_offset = {}; bool m_console_is_pal = false; bool m_set_texture_disable_mask = false; bool m_drawing_area_changed = false; bool m_force_progressive_scan = false; bool m_force_ntsc_timings = false; struct CRTCState { struct Regs { static constexpr u32 DISPLAY_ADDRESS_START_MASK = 0b111'11111111'11111110; static constexpr u32 HORIZONTAL_DISPLAY_RANGE_MASK = 0b11111111'11111111'11111111; static constexpr u32 VERTICAL_DISPLAY_RANGE_MASK = 0b1111'11111111'11111111; union { u32 display_address_start; BitField X; BitField Y; }; union { u32 horizontal_display_range; BitField X1; BitField X2; }; union { u32 vertical_display_range; BitField Y1; BitField Y2; }; } regs; u16 dot_clock_divider; // Size of the simulated screen in pixels. Depending on crop mode, this may include overscan area. u16 display_width; u16 display_height; // Top-left corner where the VRAM is displayed. Depending on the CRTC config, this may indicate padding. u16 display_origin_left; u16 display_origin_top; // Rectangle describing the displayed area of VRAM, in coordinates. u16 display_vram_left; u16 display_vram_top; u16 display_vram_width; u16 display_vram_height; u16 horizontal_total; u16 horizontal_active_start; u16 horizontal_active_end; u16 horizontal_display_start; u16 horizontal_display_end; u16 vertical_total; u16 vertical_active_start; u16 vertical_active_end; u16 vertical_display_start; u16 vertical_display_end; TickCount fractional_ticks; TickCount current_tick_in_scanline; u32 current_scanline; float display_aspect_ratio; bool in_hblank; bool in_vblank; u8 interlaced_field; // 0 = odd, 1 = even u8 interlaced_display_field; u8 active_line_lsb; } m_crtc_state = {}; BlitterState m_blitter_state = BlitterState::Idle; u32 m_command_total_words = 0; TickCount m_pending_command_ticks = 0; /// GPUREAD value for non-VRAM-reads. u32 m_GPUREAD_latch = 0; /// True if currently executing/syncing. bool m_syncing = false; bool m_fifo_pushed = false; struct VRAMTransfer { u16 x; u16 y; u16 width; u16 height; u16 col; u16 row; } m_vram_transfer = {}; HeapFIFOQueue m_fifo; std::vector m_blit_buffer; u32 m_blit_remaining_words; RenderCommand m_render_command{}; ALWAYS_INLINE u32 FifoPop() { return Truncate32(m_fifo.Pop()); } ALWAYS_INLINE u32 FifoPeek() { return Truncate32(m_fifo.Peek()); } ALWAYS_INLINE u32 FifoPeek(u32 i) { return Truncate32(m_fifo.Peek(i)); } TickCount m_max_run_ahead = 128; u32 m_fifo_size = 128; struct Stats { u32 num_vram_reads; u32 num_vram_fills; u32 num_vram_writes; u32 num_vram_copies; u32 num_vertices; u32 num_polygons; }; Stats m_stats = {}; Stats m_last_stats = {}; private: using GP0CommandHandler = bool (GPU::*)(); using GP0CommandHandlerTable = std::array; static GP0CommandHandlerTable GenerateGP0CommandHandlerTable(); // Rendering commands, returns false if not enough data is provided bool HandleUnknownGP0Command(); bool HandleNOPCommand(); bool HandleClearCacheCommand(); bool HandleInterruptRequestCommand(); bool HandleSetDrawModeCommand(); bool HandleSetTextureWindowCommand(); bool HandleSetDrawingAreaTopLeftCommand(); bool HandleSetDrawingAreaBottomRightCommand(); bool HandleSetDrawingOffsetCommand(); bool HandleSetMaskBitCommand(); bool HandleRenderPolygonCommand(); bool HandleRenderRectangleCommand(); bool HandleRenderLineCommand(); bool HandleRenderPolyLineCommand(); bool HandleFillRectangleCommand(); bool HandleCopyRectangleCPUToVRAMCommand(); bool HandleCopyRectangleVRAMToCPUCommand(); bool HandleCopyRectangleVRAMToVRAMCommand(); static const GP0CommandHandlerTable s_GP0_command_handler_table; }; IMPLEMENT_ENUM_CLASS_BITWISE_OPERATORS(GPU::TextureMode); extern std::unique_ptr g_gpu;