standalone

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: GPL-3.0 Imports: 36 Imported by: 0

README

eblitui-standalone

Full-featured desktop application UI for eblitui emulator cores. Built on Ebiten and ebitenui, it provides a complete standalone experience with a game library, settings, shader effects, save states, rewind, RetroAchievements, and gamepad navigation.

Build tag: !libretro (excluded from libretro builds).

Usage

package main

import (
    "log"

    emucore "github.com/user-none/eblitui/api"
    "github.com/user-none/eblitui/standalone"
)

func main() {
    var factory emucore.CoreFactory = myFactory()
    if err := standalone.Run(factory); err != nil {
        log.Fatal(err)
    }
}

Run is the single entry point. It initializes storage, configures the Ebiten window, and starts the application loop. Everything is driven from the CoreFactory passed in.

Application States

The app is a state machine that transitions between screens:

State Screen Description
StateLibrary Library Game grid/list with artwork, search, and sorting
StateDetail Detail Game info, artwork, play/resume, achievement progress
StateSettings Settings Tabbed configuration (Library, Appearance, Video, Audio, Rewind, RetroAchievements)
StateScanProgress Scan Progress ROM scanning with discovery and artwork phases
StateError Error Startup error handling (corrupted config recovery)
StatePlaying Gameplay Active emulation with pause menu overlay

Features

Game Library
  • Scan directories for ROMs with CRC32 hashing
  • Metadata matching via RetroArch RDB databases (auto-downloaded)
  • Artwork downloading from libretro thumbnail repositories
  • Grid (icon) and list view modes
  • Sort by title, last played, or play time
  • Favorites filter
  • Search overlay with keyboard filter
Gameplay
  • Dedicated emulation goroutine with audio-driven timing (ADT)
  • Double-buffered framebuffer for thread-safe rendering
  • Aspect-ratio-correct scaling with optional display cropping
  • Keyboard input: WASD for D-pad, JKL/UIO for buttons
  • Gamepad input: standard layout with 2-player support
  • Gamepad rumble/haptic feedback via RetroArch CHT rumble files
  • Pause menu with resume, return to library, and exit options
  • Play time tracking per game
Save States
  • 10 save slots per game
  • Auto-save on exit, auto-resume on launch
  • SRAM battery save persistence
  • Resume state (separate from manual slots)
Rewind
  • Configurable buffer size and frame step
  • Hold-to-rewind with acceleration curve
  • Requires SaveStater interface from the core
Fast Forward
  • 1x / 2x / 3x speed toggle
  • Audio averaging for downsampled playback
  • Optional mute during fast forward
Rumble

Gamepad rumble support using RetroArch CHT rumble files. Per-game rumble definitions are downloaded automatically during library scans from the libretro-database repository.

CHT files define memory addresses to monitor and conditions that trigger haptic feedback (value changes, increases, decreases, comparisons). The rumble engine evaluates these conditions each frame and fires vibration events via CoreHaptics on macOS.

Rumble strength is configurable with multiple levels:

Level Intensity Duration
Off - -
1x 1x (min 40%) 1x (min 250ms)
2x 2x 2x (min 250ms)
3x 3x 3x (min 250ms)
4x 4x 2x (min 250ms)
Max 100% 2x (min 250ms)

Requires the core to implement MemoryInspector.

Note The number of games with rumble support is severely limited. Very few games have rumble files and the number of events that trigger a rumble is also sparse. The intensity and duration of rumble can be inconsistent between games.

Shader Effects

Shader effects can be enabled independently for the UI and gameplay contexts. Multiple shaders can be active simultaneously and are applied in weight-based order.

Preprocessing effects:

  • xBR - Pixel art edge smoothing
  • Phosphor Persistence - CRT phosphor decay ghosting

Post-processing shaders:

  • CRT - Curved screen with RGB separation and vignette
  • Scanlines - Horizontal scanline effect
  • Phosphor Glow - Bright pixel bloom
  • LCD Grid - Visible pixel grid with RGB subpixels
  • Color Bleed - Composite video color bleeding
  • Dot Matrix - Circular CRT phosphor dots
  • NTSC - NTSC composite signal artifacts
  • Rainbow - Rainbow banding interference
  • Gamma - Gamma correction
  • Halation - CRT internal light scattering
  • RF Noise - RF signal noise
  • Rolling Band - CRT rolling band interference
  • VHS - VHS tape distortion
  • Interlace - Interlaced display simulation
  • Horizontal Soften - Horizontal blur
  • Vertical Blur - Vertical blur
  • Monochrome - Grayscale conversion
  • Sepia - Sepia tone

Shaders are written in Ebiten's Kage shading language.

RetroAchievements
  • Login with RetroAchievements account
  • Achievement tracking and unlock notifications with sound
  • Badge and image caching
  • Auto-screenshot on achievement unlock
  • Encore mode (re-trigger unlocked achievements)
  • Spectator mode (track without submitting)
  • In-game achievement overlay
  • Per-game and library-wide progress tracking

Requires the core to implement MemoryInspector.

Themes

Supports a variety of built-in themes. Font size is configurable (10-32pt).

Settings

Organized in tabbed sections:

  • Library - Scan directories, add/remove folders, rescan
  • Appearance - Theme, font size
  • Video - Shader effects for UI and gameplay
  • Audio - Volume, mute, fast-forward mute
  • Input - Button bindings, analog stick toggle, rumble level
  • Rewind - Enable/disable, buffer size, frame step
  • RetroAchievements - Login, notification preferences, modes

Data Storage

Application data is stored per-emulator using the core's DataDirName:

Platform Path
macOS ~/Library/Application Support/<DataDirName>/
Linux ~/.local/share/<DataDirName>/ (or $XDG_DATA_HOME)
Windows %APPDATA%/<DataDirName>/

Directory structure:

<DataDirName>/
    config.json      - Application settings
    library.json     - Game library and scan state
    metadata/        - RDB databases and artwork index
    artwork/         - Game artwork images
    rumble/          - CHT rumble definition files
    saves/           - SRAM and save state files
    screenshots/     - Screenshot captures

All JSON writes are atomic (write to temp, rename) to prevent corruption.

Sub-packages

Package Description
screens UI screens (library, detail, settings, scan, error)
storage Config and library persistence with validation
shader Shader registry, compilation, and effect pipeline
style Themes, DPI-aware layout constants, widget builders
types Shared interfaces for cross-package use
achievements RetroAchievements manager and unlock sounds
rdb RetroArch database (RDB) binary parser

Dependencies

Key external dependencies:

  • github.com/hajimehoshi/ebiten/v2 - Game engine and rendering
  • github.com/ebitenui/ebitenui - UI widget framework
  • github.com/ebitengine/oto/v3 - Audio output
  • github.com/sqweek/dialog - Native file dialogs
  • github.com/user-none/eblitui/api - Core interfaces
  • github.com/user-none/eblitui/romloader - ROM loading
  • github.com/user-none/go-rcheevos - RetroAchievements client
  • golang.design/x/clipboard - Clipboard access
  • golang.org/x/image - Font rendering

Testing

go test ./...

Test coverage includes state management, audio buffering, save states, rewind, turbo, pause menu, achievement overlay, search, scanning, storage validation, shader ordering, themes, rumble engine, and RDB parsing.

Documentation

Index

Constants

View Source
const Version = "0.3.0"

Standalone UI version

Variables

This section is empty.

Functions

func FireRumbleEvents added in v0.3.0

func FireRumbleEvents(events []RumbleEvent, level int)

FireRumbleEvents sends rumble events to gamepads via Ebiten. Levels 1-3 scale CHT intensity and duration by that multiplier. Level 4 scales intensity by 4x but caps duration at 2x. Level 5 (Max) uses maximum intensity with 2x duration. Minimum thresholds ensure any non-zero rumble is perceptible.

func GetPlaceholderImage

func GetPlaceholderImage() *ebiten.Image

GetPlaceholderImage returns the placeholder image for missing artwork

func GetRDBPath

func GetRDBPath() (string, error)

GetRDBPath returns the path to the RDB file

func IsReservedKey added in v0.2.0

func IsReservedKey(k ebiten.Key) bool

IsReservedKey returns true if the key is reserved for UI functions.

func KeyToName added in v0.2.0

func KeyToName(k ebiten.Key) (string, bool)

KeyToName converts an ebiten.Key to its name string. Returns the name and true if the key has a name, or "" and false otherwise.

func PadToName added in v0.2.0

func PadToName(b ebiten.StandardGamepadButton) (string, bool)

PadToName converts an ebiten.StandardGamepadButton to its name string. Returns the name and true if the button has a name, or "" and false otherwise.

func ParseKey

func ParseKey(name string) (ebiten.Key, bool)

ParseKey converts a key name string to an ebiten.Key. Returns the key and true if the name is valid, or 0 and false otherwise.

func ParsePad

func ParsePad(name string) (ebiten.StandardGamepadButton, bool)

ParsePad converts a gamepad button name string to an ebiten.StandardGamepadButton. Returns the button and true if the name is valid, or 0 and false otherwise.

func PollButtons

func PollButtons(mapping InputMapping, gamepadID ebiten.GamepadID, hasGamepad, disableAnalog bool) uint32

PollButtons reads P1 input from keyboard and gamepad (including D-pad and analog stick). All buttons including D-pad are in the mapping. When disableAnalog is true, the analog stick is not polled. Returns a button bitmask.

func PollGamepadButtons

func PollGamepadButtons(mapping InputMapping, gamepadID ebiten.GamepadID, disableAnalog bool) uint32

PollGamepadButtons reads P2 input from gamepad only (D-pad + analog stick + mapped buttons). No keyboard since that belongs to P1. When disableAnalog is true, the analog stick is not polled. All D-pad buttons are expected in the mapping.

func RDBExists

func RDBExists() bool

RDBExists checks if the RDB file exists on disk

func ResolveKeyDisplay added in v0.2.0

func ResolveKeyDisplay(buttonName string, defaultKey string, overrides map[string]string) string

ResolveKeyDisplay returns the display string for a button's current keyboard binding, checking overrides first then falling back to the provided default.

func ResolvePadDisplay added in v0.2.0

func ResolvePadDisplay(buttonName string, defaultPad string, overrides map[string]string) string

ResolvePadDisplay returns the display string for a button's current controller binding, checking overrides first then falling back to the provided default.

func Run

func Run(factory emucore.CoreFactory) error

Run is the public entry point for the standalone UI. It initializes storage, configures the window, creates the app, and starts the Ebiten game loop.

func RunDirect

func RunDirect(factory emucore.CoreFactory, romPath, regionStr string, options map[string]string) error

RunDirect loads a ROM and runs it directly without the full UI. The regionStr parameter accepts "auto", "ntsc", or "pal". The options map is applied to the emulator via SetOption.

Types

type AchievementOverlay

type AchievementOverlay struct {
	// contains filtered or unexported fields
}

AchievementOverlay shows achievements during gameplay

func NewAchievementOverlay

func NewAchievementOverlay(manager *achievements.Manager) *AchievementOverlay

NewAchievementOverlay creates a new achievement overlay

func (*AchievementOverlay) Draw

func (o *AchievementOverlay) Draw(screen *ebiten.Image)

Draw renders the overlay

func (*AchievementOverlay) Hide

func (o *AchievementOverlay) Hide()

Hide hides the achievement overlay

func (*AchievementOverlay) InitForGame

func (o *AchievementOverlay) InitForGame()

InitForGame prepares the overlay for a new game session. The manager already caches achievements on LoadGame, so this just resets overlay state.

func (*AchievementOverlay) IsVisible

func (o *AchievementOverlay) IsVisible() bool

IsVisible returns whether the overlay is visible

func (*AchievementOverlay) Reset

func (o *AchievementOverlay) Reset()

Reset clears session state when the game ends

func (*AchievementOverlay) Show

func (o *AchievementOverlay) Show()

Show displays the achievement overlay

func (*AchievementOverlay) Update

func (o *AchievementOverlay) Update()

Update handles input for the overlay

type App

type App struct {
	// contains filtered or unexported fields
}

App is the main application struct that implements ebiten.Game

func (*App) Draw

func (a *App) Draw(screen *ebiten.Image)

Draw implements ebiten.Game

func (*App) Exit

func (a *App) Exit()

Exit closes the application

func (*App) GetExtensions

func (a *App) GetExtensions() []string

GetExtensions returns the supported ROM file extensions

func (*App) GetPlaceholderImageData

func (a *App) GetPlaceholderImageData() []byte

GetPlaceholderImageData returns the raw embedded placeholder image data

func (*App) GetRDB

func (a *App) GetRDB() *rdb.RDB

GetRDB returns the RDB for metadata lookups

func (*App) GetWindowConfig

func (a *App) GetWindowConfig() (width, height int, x, y *int, fullscreen bool)

GetWindowConfig returns the saved window dimensions, position, and fullscreen state from config. This should be called before RunGame to set the initial window size.

func (*App) GetWindowWidth

func (a *App) GetWindowWidth() int

GetWindowWidth returns the current window width for responsive layouts

func (*App) LaunchGame

func (a *App) LaunchGame(gameCRC string, resume bool)

LaunchGame starts the emulator with the specified game

func (*App) Layout

func (a *App) Layout(outsideWidth, outsideHeight int) (int, int)

Layout implements ebiten.Game

func (*App) RequestRebuild

func (a *App) RequestRebuild()

RequestRebuild triggers a UI rebuild for the current screen. This is safe to call from goroutines - the rebuild happens on the main thread. Focus restoration is handled in the Update loop after ui.Update()

func (*App) SaveAndClose

func (a *App) SaveAndClose()

SaveAndClose saves config and library before exit

func (*App) SwitchToDetail

func (a *App) SwitchToDetail(gameCRC string)

SwitchToDetail transitions to the detail screen

func (*App) SwitchToLibrary

func (a *App) SwitchToLibrary()

SwitchToLibrary transitions to the library screen

func (*App) SwitchToScanProgress

func (a *App) SwitchToScanProgress(rescanAll bool)

SwitchToScanProgress transitions to the scan progress screen

func (*App) SwitchToSettings

func (a *App) SwitchToSettings()

SwitchToSettings transitions to the settings screen

func (*App) Update

func (a *App) Update() error

Update implements ebiten.Game

type AppState

type AppState int

AppState represents the current state of the application

const (
	// StateLibrary is the main library screen showing all games
	StateLibrary AppState = iota
	// StateDetail shows information about a selected game
	StateDetail
	// StateSettings shows application settings
	StateSettings
	// StateScanProgress shows ROM scanning progress
	StateScanProgress
	// StateError shows a startup error (corrupted config)
	StateError
	// StatePlaying is active gameplay
	StatePlaying
)

func (AppState) String

func (s AppState) String() string

String returns the string representation of the state

type AudioPlayer

type AudioPlayer struct {
	// contains filtered or unexported fields
}

AudioPlayer manages audio playback via oto. It writes int16 stereo samples to a ring buffer which oto's player reads from in a pull model.

func NewAudioPlayer

func NewAudioPlayer(volume float64) (*AudioPlayer, error)

NewAudioPlayer creates and initializes audio playback via oto. The volume parameter sets the initial volume before playback starts, preventing audio pops when muted (matching iOS behavior).

func (*AudioPlayer) ClearQueue

func (a *AudioPlayer) ClearQueue()

ClearQueue flushes all buffered audio from the ring buffer. Used when entering rewind mode to prevent stale audio playback.

func (*AudioPlayer) Close

func (a *AudioPlayer) Close()

Close cleans up audio resources.

func (*AudioPlayer) GetBufferLevel

func (a *AudioPlayer) GetBufferLevel() int

GetBufferLevel returns the total bytes of audio data currently buffered (ring buffer + oto player internal buffer). Used for ADT pacing.

func (*AudioPlayer) QueueSamples

func (a *AudioPlayer) QueueSamples(samples []int16)

QueueSamples converts int16 stereo samples to bytes and writes them to the ring buffer for oto to consume.

func (*AudioPlayer) SetVolume

func (a *AudioPlayer) SetVolume(vol float64)

SetVolume sets the playback volume (0.0 = silent, 1.0 = normal, 2.0 = max). Values are clamped to [0.0, 2.0].

type AudioRingBuffer

type AudioRingBuffer struct {
	// contains filtered or unexported fields
}

AudioRingBuffer is a thread-safe ring buffer implementing io.Reader. The emulation goroutine writes samples via Write(), and oto's player reads them via Read(). Read blocks when empty; Write drops oldest samples on overflow to prevent stalling the producer.

func NewAudioRingBuffer

func NewAudioRingBuffer(capacity int) *AudioRingBuffer

NewAudioRingBuffer creates a ring buffer with the given capacity in bytes.

func (*AudioRingBuffer) Buffered

func (rb *AudioRingBuffer) Buffered() int

Buffered returns the number of bytes currently in the buffer.

func (*AudioRingBuffer) Clear

func (rb *AudioRingBuffer) Clear()

Clear resets the buffer, discarding all data.

func (*AudioRingBuffer) Close

func (rb *AudioRingBuffer) Close()

Close signals shutdown. Subsequent Reads return io.EOF when the buffer is empty. Unblocks any goroutines waiting in Read.

func (*AudioRingBuffer) Read

func (rb *AudioRingBuffer) Read(p []byte) (int, error)

Read implements io.Reader. Blocks until data is available or the buffer is closed. Returns io.EOF when closed and empty.

func (*AudioRingBuffer) Write

func (rb *AudioRingBuffer) Write(p []byte)

Write adds data to the buffer. Non-blocking; if the buffer overflows, oldest samples are dropped to make room for new data.

type EmuControl

type EmuControl struct {
	// contains filtered or unexported fields
}

EmuControl manages pause/resume/stop coordination between the Ebiten thread and the emulation goroutine.

func NewEmuControl

func NewEmuControl() *EmuControl

NewEmuControl creates a new emulation control.

func (*EmuControl) CheckPause

func (ec *EmuControl) CheckPause() bool

CheckPause is called by the emulation goroutine between frames. If a pause has been requested, it sends an acknowledgment and spins until resumed or stopped. Returns false if the goroutine should exit.

func (*EmuControl) IsPaused

func (ec *EmuControl) IsPaused() bool

IsPaused returns true if the emulation goroutine is currently paused.

func (*EmuControl) RequestPause

func (ec *EmuControl) RequestPause()

RequestPause asks the emulation goroutine to pause and blocks until it acknowledges the pause.

func (*EmuControl) RequestResume

func (ec *EmuControl) RequestResume()

RequestResume tells the emulation goroutine to resume.

func (*EmuControl) ShouldRun

func (ec *EmuControl) ShouldRun() bool

ShouldRun returns true if the goroutine should continue running.

func (*EmuControl) Stop

func (ec *EmuControl) Stop()

Stop signals the emulation goroutine to exit.

type FramebufferRenderer

type FramebufferRenderer struct {
	// contains filtered or unexported fields
}

FramebufferRenderer owns the ebiten offscreen buffer and handles pixel rendering with scaling. Replaces the emulator-specific DrawCachedFramebuffer/GetCachedFramebufferImage methods that were previously on the bridge emulator.

func NewFramebufferRenderer

func NewFramebufferRenderer(screenWidth int, par float64) *FramebufferRenderer

NewFramebufferRenderer creates a renderer for the given native screen width and pixel aspect ratio.

func (*FramebufferRenderer) DrawFramebuffer

func (r *FramebufferRenderer) DrawFramebuffer(screen *ebiten.Image, pixels []byte, stride, activeHeight int)

DrawFramebuffer renders pixel data to the screen with PAR-corrected aspect ratio scaling.

func (*FramebufferRenderer) GetFramebufferImage

func (r *FramebufferRenderer) GetFramebufferImage(pixels []byte, stride, activeHeight int) *ebiten.Image

GetFramebufferImage returns pixel data as an ebiten.Image at native resolution. Used for shader processing.

type GameplayManager

type GameplayManager struct {
	// contains filtered or unexported fields
}

GameplayManager handles all gameplay-related state and logic. This includes emulator control, input handling, save states, play time tracking, and the pause menu.

The emulator runs on a dedicated goroutine with audio-driven timing (ADT). The Ebiten thread handles UI, input polling, and reads the shared framebuffer.

func NewGameplayManager

func NewGameplayManager(
	factory emucore.CoreFactory,
	systemInfo emucore.SystemInfo,
	saveStateManager *SaveStateManager,
	screenshotManager *ScreenshotManager,
	notification *Notification,
	library *storage.Library,
	config *storage.Config,
	achievementManager *achievements.Manager,
	gameRDB *rdb.RDB,
	onExitToLibrary func(),
	onExitApp func(),
) *GameplayManager

NewGameplayManager creates a new gameplay manager

func (*GameplayManager) CurrentGameCRC

func (gm *GameplayManager) CurrentGameCRC() string

CurrentGameCRC returns the CRC of the currently loaded game, or empty string if none

func (*GameplayManager) Draw

func (gm *GameplayManager) Draw(screen *ebiten.Image)

Draw renders the gameplay screen from the shared framebuffer.

func (*GameplayManager) DrawAchievementOverlay

func (gm *GameplayManager) DrawAchievementOverlay(screen *ebiten.Image)

DrawAchievementOverlay draws the achievement overlay

func (*GameplayManager) DrawFramebuffer

func (gm *GameplayManager) DrawFramebuffer() *ebiten.Image

DrawFramebuffer returns the native-resolution framebuffer for xBR processing. Reads from the shared framebuffer rather than directly from the emulator.

func (*GameplayManager) DrawPauseMenu

func (gm *GameplayManager) DrawPauseMenu(screen *ebiten.Image)

DrawPauseMenu draws the pause menu overlay

func (*GameplayManager) Exit

func (gm *GameplayManager) Exit(saveResume bool)

Exit cleans up when exiting gameplay

func (*GameplayManager) IsPaused

func (gm *GameplayManager) IsPaused() bool

IsPaused returns whether the pause menu is visible

func (*GameplayManager) IsPlaying

func (gm *GameplayManager) IsPlaying() bool

IsPlaying returns true if a game is currently being played

func (*GameplayManager) Launch

func (gm *GameplayManager) Launch(gameCRC string, resume bool) bool

Launch starts the emulator with the specified game

func (*GameplayManager) Resume

func (gm *GameplayManager) Resume()

Resume resumes gameplay after pause menu

func (*GameplayManager) SetConfig

func (gm *GameplayManager) SetConfig(config *storage.Config)

SetConfig updates the config reference and rebuilds the input mapping

func (*GameplayManager) SetLibrary

func (gm *GameplayManager) SetLibrary(library *storage.Library)

SetLibrary updates the library reference

func (*GameplayManager) Update

func (gm *GameplayManager) Update() (pauseMenuOpened bool, err error)

Update handles the gameplay update loop. Returns true if pause menu was opened. This runs on the Ebiten thread — it polls input and manages UI state. The emulator itself runs on a separate goroutine.

type InputManager

type InputManager struct {
	// contains filtered or unexported fields
}

InputManager handles all input for UI navigation. It tracks gamepad state, handles repeat navigation, and provides a clean interface for UI code to query input state.

func NewInputManager

func NewInputManager() *InputManager

NewInputManager creates a new input manager

func (*InputManager) GetUINavigation

func (im *InputManager) GetUINavigation() UINavigation

GetUINavigation returns the current UI navigation state. This handles keyboard arrow keys and gamepad D-pad/analog stick with repeat navigation, and A/B/Start button presses.

func (*InputManager) Update

func (im *InputManager) Update() (screenshotRequested, fullscreenToggle bool)

Update polls input state. Should be called once per frame. Returns global key states: F12 screenshot and F11 fullscreen toggle.

type InputMapping

type InputMapping struct {
	Keys    map[int]ebiten.Key                   // bit ID -> keyboard key
	Gamepad map[int]ebiten.StandardGamepadButton // bit ID -> gamepad button
}

InputMapping maps button bit IDs to ebiten input types. Keyed by the Button.ID (bit position in the uint32 bitmask).

func BuildDefaultMapping

func BuildDefaultMapping(buttons []emucore.Button) InputMapping

BuildDefaultMapping creates an InputMapping from the given button definitions. It includes D-pad defaults (WASD keyboard, D-pad controller) plus adaptor buttons. Keys that conflict with reserved standalone UI keys are skipped.

func BuildMappingFromConfig added in v0.2.0

func BuildMappingFromConfig(buttons []emucore.Button, kbOverrides, padOverrides map[string]string) InputMapping

BuildMappingFromConfig creates an InputMapping using config overrides with adaptor defaults as fallback. D-pad defaults are WASD (keyboard) and DpadUp/Down/Left/Right (controller). For each button, the override map is checked first; if absent or invalid, the adaptor default is used.

type MetadataManager

type MetadataManager struct {
	// contains filtered or unexported fields
}

MetadataManager handles RDB and artwork downloads

func NewMetadataManager

func NewMetadataManager(rdbName, thumbnailRepo string) *MetadataManager

NewMetadataManager creates a new metadata manager

func (*MetadataManager) DownloadArtwork

func (m *MetadataManager) DownloadArtwork(gameCRC string, gameName string)

DownloadArtwork downloads artwork for a game using the fallback chain Returns silently on any error (per spec)

func (*MetadataManager) DownloadRDB

func (m *MetadataManager) DownloadRDB() error

DownloadRDB downloads the RDB file from libretro-database Downloads to a temp file first, then renames on success

func (*MetadataManager) DownloadRumble added in v0.3.0

func (m *MetadataManager) DownloadRumble(gameCRC, gameName string)

DownloadRumble downloads the rumble CHT file for a game. Returns silently on any error (same as artwork).

func (*MetadataManager) GetRDB

func (m *MetadataManager) GetRDB() *rdb.RDB

GetRDB returns the loaded RDB, or nil if not loaded

func (*MetadataManager) IsRDBLoaded

func (m *MetadataManager) IsRDBLoaded() bool

IsRDBLoaded returns true if the RDB is loaded

func (*MetadataManager) LoadRDB

func (m *MetadataManager) LoadRDB() error

LoadRDB loads the RDB file into memory Returns nil without error if file doesn't exist

func (*MetadataManager) LookupByCRC32

func (m *MetadataManager) LookupByCRC32(crc32 uint32) *rdb.Game

LookupByCRC32 looks up a game by CRC32 Returns nil if not found or RDB not loaded

type Notification

type Notification struct {
	// contains filtered or unexported fields
}

Notification displays temporary messages on screen

func NewNotification

func NewNotification() *Notification

NewNotification creates a new notification system

func (*Notification) Clear

func (n *Notification) Clear()

Clear removes the current notification

func (*Notification) Close

func (n *Notification) Close()

Close cleans up audio resources

func (*Notification) Draw

func (n *Notification) Draw(screen *ebiten.Image)

Draw renders the notification

func (*Notification) IsVisible

func (n *Notification) IsVisible() bool

IsVisible returns whether the notification is currently visible

func (*Notification) PlaySound

func (n *Notification) PlaySound(soundData []byte)

PlaySound plays sound data through a one-shot oto player. Sound data should be 48kHz stereo S16LE format.

func (*Notification) SetBadge

func (n *Notification) SetBadge(badge *ebiten.Image)

SetBadge updates the badge image for the current notification (thread-safe)

func (*Notification) Show

func (n *Notification) Show(message string, duration time.Duration)

Show displays a notification message

func (*Notification) ShowAchievementWithBadge

func (n *Notification) ShowAchievementWithBadge(title, description string, badge *ebiten.Image)

ShowAchievementWithBadge displays a prominent achievement notification with a badge image

func (*Notification) ShowDefault

func (n *Notification) ShowDefault(message string)

ShowDefault displays a notification with default 3 second duration

func (*Notification) ShowShort

func (n *Notification) ShowShort(message string)

ShowShort displays a notification with 1 second duration (for gameplay)

type NotificationType

type NotificationType int

NotificationType determines the visual style of the notification

const (
	NotificationTypeDefault     NotificationType = iota // Small, bottom-right
	NotificationTypeAchievement                         // Large, top-center, prominent
)

type PauseMenu

type PauseMenu struct {
	// contains filtered or unexported fields
}

PauseMenu handles the in-game pause menu

func NewPauseMenu

func NewPauseMenu(onResume, onLibrary, onExit func()) *PauseMenu

NewPauseMenu creates a new pause menu

func (*PauseMenu) Draw

func (m *PauseMenu) Draw(screen *ebiten.Image)

Draw renders the pause menu

func (*PauseMenu) Hide

func (m *PauseMenu) Hide()

Hide hides the pause menu

func (*PauseMenu) IsVisible

func (m *PauseMenu) IsVisible() bool

IsVisible returns whether the menu is visible

func (*PauseMenu) Show

func (m *PauseMenu) Show()

Show displays the pause menu

func (*PauseMenu) Update

func (m *PauseMenu) Update()

Update handles input for the pause menu

type PauseMenuOption

type PauseMenuOption int

PauseMenuOption represents a menu option

const (
	PauseMenuResume PauseMenuOption = iota
	PauseMenuLibrary
	PauseMenuExit
	PauseMenuOptionCount
)

type PlayTimeTracker

type PlayTimeTracker struct {
	// contains filtered or unexported fields
}

PlayTimeTracker tracks play time during gameplay

type RewindBuffer

type RewindBuffer struct {
	// contains filtered or unexported fields
}

RewindBuffer stores serialized emulator states in a ring buffer for rewinding gameplay. States are captured every frameStep frames and can be popped in reverse order (LIFO) to step backwards.

func NewRewindBuffer

func NewRewindBuffer(bufferSizeMB, frameStep, stateSize int) *RewindBuffer

NewRewindBuffer allocates a ring buffer sized to fit bufferSizeMB worth of serialized states, each stateSize bytes.

func (*RewindBuffer) Capacity

func (rb *RewindBuffer) Capacity() int

Capacity returns the maximum number of entries the buffer can hold.

func (*RewindBuffer) Capture

func (rb *RewindBuffer) Capture(saveStater emucore.SaveStater) error

Capture serializes the emulator state and stores it in the ring buffer. Only captures every frameStep frames. Should be called after RunFrame.

func (*RewindBuffer) Count

func (rb *RewindBuffer) Count() int

Count returns the number of valid entries in the buffer.

func (*RewindBuffer) IsRewinding

func (rb *RewindBuffer) IsRewinding() bool

IsRewinding returns whether the buffer is currently in rewind mode.

func (*RewindBuffer) Reset

func (rb *RewindBuffer) Reset()

Reset clears the buffer. Call on game launch or save state load.

func (*RewindBuffer) Rewind

func (rb *RewindBuffer) Rewind(emu emucore.Emulator, saveStater emucore.SaveStater, count int) bool

Rewind pops count states from the buffer and deserializes the last one. After deserializing, RunFrame is called to regenerate the framebuffer since the serialized state doesn't include rendered pixels. Returns false if the buffer is empty.

func (*RewindBuffer) SetRewinding

func (rb *RewindBuffer) SetRewinding(v bool)

SetRewinding sets the rewind mode flag.

type RumbleEngine added in v0.3.0

type RumbleEngine struct {
	// contains filtered or unexported fields
}

RumbleEngine evaluates rumble entries each frame and produces rumble events.

func NewRumbleEngine added in v0.3.0

func NewRumbleEngine(entries []RumbleEntry, systemBigEndian bool) *RumbleEngine

NewRumbleEngine creates a new rumble engine from parsed entries. systemBigEndian should match SystemInfo.BigEndianMemory for the core. Byte swapping is determined per-entry by comparing the CHT entry's big_endian field against the system endianness.

func (*RumbleEngine) Evaluate added in v0.3.0

func (re *RumbleEngine) Evaluate(mi emucore.MemoryInspector) []RumbleEvent

Evaluate reads memory for each entry, checks conditions, and returns rumble events.

func (*RumbleEngine) Reset added in v0.3.0

func (re *RumbleEngine) Reset()

Reset clears engine state (for save state loads or rewind).

type RumbleEntry added in v0.3.0

type RumbleEntry struct {
	Address           uint32
	MemorySearchSize  int    // 0=1bit, 1=2bit, 2=4bit, 3=8bit, 4=16bit, 5=32bit
	RumbleType        int    // 0-10 (0 treated as 1/changes)
	RumbleValue       uint32 // comparison value for types 5-10
	RumblePort        int    // 0-15 specific, else all
	BigEndian         bool   // CHT entry's big_endian field
	PrimaryStrength   uint16 // 0-65535
	PrimaryDuration   int    // milliseconds
	SecondaryStrength uint16 // 0-65535
	SecondaryDuration int    // milliseconds
}

RumbleEntry represents a single rumble definition from a CHT file.

func ParseRumbleFile added in v0.3.0

func ParseRumbleFile(path string) ([]RumbleEntry, error)

ParseRumbleFile reads a CHT rumble file and returns the parsed entries.

type RumbleEvent added in v0.3.0

type RumbleEvent struct {
	Port             int
	StrongMagnitude  float64
	WeakMagnitude    float64
	StrongDurationMs int
	WeakDurationMs   int
}

RumbleEvent represents a rumble command to send to a gamepad.

type SaveStateManager

type SaveStateManager struct {
	// contains filtered or unexported fields
}

SaveStateManager handles save state operations

func NewSaveStateManager

func NewSaveStateManager(notification *Notification) *SaveStateManager

NewSaveStateManager creates a new save state manager

func (*SaveStateManager) GetCurrentSlot

func (m *SaveStateManager) GetCurrentSlot() int

GetCurrentSlot returns the current save slot

func (*SaveStateManager) HasResumeState

func (m *SaveStateManager) HasResumeState() bool

HasResumeState checks if a resume state exists

func (*SaveStateManager) Load

func (m *SaveStateManager) Load(saveStater emucore.SaveStater) error

Load loads the state from the current slot

func (*SaveStateManager) LoadResume

func (m *SaveStateManager) LoadResume(saveStater emucore.SaveStater) error

LoadResume loads the resume state

func (*SaveStateManager) LoadSRAM

func (m *SaveStateManager) LoadSRAM(batterySaver emucore.BatterySaver) error

LoadSRAM loads the cartridge SRAM

func (*SaveStateManager) NextSlot

func (m *SaveStateManager) NextSlot()

NextSlot cycles to the next save slot

func (*SaveStateManager) PreviousSlot

func (m *SaveStateManager) PreviousSlot()

PreviousSlot cycles to the previous save slot

func (*SaveStateManager) Save

func (m *SaveStateManager) Save(saveStater emucore.SaveStater) error

Save saves the current state to the current slot

func (*SaveStateManager) SaveResume

func (m *SaveStateManager) SaveResume(saveStater emucore.SaveStater) error

SaveResume saves the resume state

func (*SaveStateManager) SaveResumeData

func (m *SaveStateManager) SaveResumeData(state []byte) error

SaveResumeData saves pre-serialized state data as the resume state. Used by the auto-save system where the emu goroutine caches serialized state.

func (*SaveStateManager) SaveSRAM

func (m *SaveStateManager) SaveSRAM(batterySaver emucore.BatterySaver) error

SaveSRAM saves the cartridge SRAM

func (*SaveStateManager) SetGame

func (m *SaveStateManager) SetGame(gameCRC string)

SetGame sets the current game for save states Restores the last-used slot from the game's settings

func (*SaveStateManager) SetLibrary

func (m *SaveStateManager) SetLibrary(library *storage.Library)

SetLibrary sets the library reference for slot persistence

type ScanManager

type ScanManager struct {
	// contains filtered or unexported fields
}

ScanManager handles ROM scanning orchestration. This includes creating and running scanners, tracking progress, and merging results into the library.

func NewScanManager

func NewScanManager(
	library *storage.Library,
	scanScreen *screens.ScanProgressScreen,
	extensions []string,
	rdbName string,
	thumbnailRepo string,
	onProgress func(),
	onComplete func(msg string),
) *ScanManager

NewScanManager creates a new scan manager

func (*ScanManager) Cancel

func (sm *ScanManager) Cancel()

Cancel stops the current scan

func (*ScanManager) IsScanning

func (sm *ScanManager) IsScanning() bool

IsScanning returns true if a scan is in progress

func (*ScanManager) SetLibrary

func (sm *ScanManager) SetLibrary(library *storage.Library)

SetLibrary updates the library reference

func (*ScanManager) SetScanScreen

func (sm *ScanManager) SetScanScreen(screen *screens.ScanProgressScreen)

SetScanScreen updates the scan screen reference

func (*ScanManager) Start

func (sm *ScanManager) Start(rescanAll bool)

Start begins a new scan operation

func (*ScanManager) Update

func (sm *ScanManager) Update()

Update polls for scan progress and completion. Should be called each frame while scanning.

type ScanPhase

type ScanPhase int

ScanPhase represents the current scanning phase

const (
	ScanPhaseInit ScanPhase = iota
	ScanPhaseDiscovery
	ScanPhaseArtwork
	ScanPhaseComplete
)

type ScanProgress

type ScanProgress struct {
	Phase           ScanPhase
	Progress        float64 // 0.0 to 1.0
	GamesFound      int
	ArtworkTotal    int
	ArtworkComplete int
	StatusText      string
}

ScanProgress represents progress updates from the scanner

type ScanResult

type ScanResult struct {
	NewGames  int
	Errors    []error
	Cancelled bool
}

ScanResult represents the final scan result

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner handles ROM scanning in the background

func NewScanner

func NewScanner(dirs []storage.ScanDirectory, excluded []string, existing map[string]*storage.GameEntry, rescanAll bool, extensions []string, rdbName, thumbnailRepo string) *Scanner

NewScanner creates a new scanner instance

func (*Scanner) Cancel

func (s *Scanner) Cancel()

Cancel signals the scanner to stop

func (*Scanner) Done

func (s *Scanner) Done() <-chan ScanResult

Done returns the done channel

func (*Scanner) Games

func (s *Scanner) Games() map[string]*storage.GameEntry

Games returns the discovered games

func (*Scanner) Progress

func (s *Scanner) Progress() <-chan ScanProgress

Progress returns the progress channel

func (*Scanner) Run

func (s *Scanner) Run()

Run starts the scanning process

type ScreenshotManager

type ScreenshotManager struct {
	// contains filtered or unexported fields
}

ScreenshotManager handles taking and saving screenshots

func NewScreenshotManager

func NewScreenshotManager(notification *Notification) *ScreenshotManager

NewScreenshotManager creates a new screenshot manager

func (*ScreenshotManager) TakeScreenshot

func (m *ScreenshotManager) TakeScreenshot(screen *ebiten.Image, gameCRC string) error

TakeScreenshot captures and saves a screenshot Per design: silent capture with no notification

type SearchOverlay

type SearchOverlay struct {
	// contains filtered or unexported fields
}

SearchOverlay displays a search filter at the bottom-left of the screen

func NewSearchOverlay

func NewSearchOverlay(onChanged func(text string)) *SearchOverlay

NewSearchOverlay creates a new search overlay with the given change callback

func (*SearchOverlay) Activate

func (s *SearchOverlay) Activate()

Activate starts capturing keyboard input

func (*SearchOverlay) Clear

func (s *SearchOverlay) Clear()

Clear removes all search text and deactivates

func (*SearchOverlay) Draw

func (s *SearchOverlay) Draw(screen *ebiten.Image)

Draw renders the search overlay at bottom-left

func (*SearchOverlay) HandleInput

func (s *SearchOverlay) HandleInput() bool

HandleInput processes keyboard input when active. Returns true if input was handled (should not propagate to navigation).

func (*SearchOverlay) IsActive

func (s *SearchOverlay) IsActive() bool

IsActive returns true if the overlay is capturing keyboard input

func (*SearchOverlay) IsVisible

func (s *SearchOverlay) IsVisible() bool

IsVisible returns true if the search has text (overlay should be shown)

type SharedFramebuffer

type SharedFramebuffer struct {
	// contains filtered or unexported fields
}

SharedFramebuffer holds pixel data written by the emulation goroutine and read by Ebiten's Draw() method. Uses separate write and read buffers so the emu goroutine can write new data while Draw uses the read copy.

func NewSharedFramebuffer

func NewSharedFramebuffer(width, height int) *SharedFramebuffer

NewSharedFramebuffer creates a pre-allocated framebuffer sized for the given screen dimensions (width and height in pixels, 4 bytes per pixel).

func (*SharedFramebuffer) Read

func (sf *SharedFramebuffer) Read() (pixels []byte, stride, activeHeight int)

Read returns a snapshot of the current framebuffer state. Copies the write buffer into the read buffer under the lock, then returns the read buffer which is safe to use without holding the lock.

func (*SharedFramebuffer) Update

func (sf *SharedFramebuffer) Update(pixels []byte, stride, activeHeight int)

Update copies framebuffer data from the emulation goroutine.

type SharedInput

type SharedInput struct {
	// contains filtered or unexported fields
}

SharedInput holds controller state as button bitmasks written by the Ebiten thread and read by the emulation goroutine.

func (*SharedInput) Read

func (si *SharedInput) Read() [maxPlayers]uint32

Read returns the current button bitmasks for all players.

func (*SharedInput) Set

func (si *SharedInput) Set(player int, buttons uint32)

Set updates button bitmask for a player from the Ebiten thread.

type TurboState

type TurboState struct {
	// contains filtered or unexported fields
}

TurboState holds fast-forward state shared between the Ebiten thread (which sets it from key input) and the emulation goroutine (which reads it).

func (*TurboState) CycleMultiplier

func (ts *TurboState) CycleMultiplier() int

CycleMultiplier advances through Off(1) → 2x → 3x → Off(1), returning the new value. Called from the Ebiten thread.

func (*TurboState) Read

func (ts *TurboState) Read() int

Read returns the current multiplier. Called from the emulation goroutine.

type UINavigation

type UINavigation struct {
	Direction    int  // 0=none, 1=up, 2=down, 3=left, 4=right
	Activate     bool // A/Cross button just pressed
	Back         bool // B/Circle button just pressed
	OpenSettings bool // Start button just pressed
	FocusChanged bool // True if navigation caused focus change this frame
}

UINavigation represents the result of UI input polling

Directories

Path Synopsis
Package types provides shared interfaces used across UI packages.
Package types provides shared interfaces used across UI packages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL