diff --git a/cmake/aurora_core.cmake b/cmake/aurora_core.cmake index 70c8ea9..c19c935 100644 --- a/cmake/aurora_core.cmake +++ b/cmake/aurora_core.cmake @@ -1,5 +1,7 @@ add_library(aurora_core STATIC lib/aurora.cpp + lib/device.cpp + lib/device.hpp lib/input.cpp lib/window.cpp lib/logging.cpp @@ -25,6 +27,13 @@ elseif (APPLE) target_sources(aurora_core PRIVATE lib/system_info_mac.mm) endif () +if (IOS) + find_library(COREHAPTICS_FRAMEWORK CoreHaptics REQUIRED) + target_sources(aurora_core PRIVATE lib/device_ios.mm) + set_source_files_properties(lib/device_ios.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) + target_link_libraries(aurora_core PUBLIC ${COREHAPTICS_FRAMEWORK}) +endif () + if (AURORA_ENABLE_GX) target_sources(aurora_core PRIVATE lib/imgui.cpp) target_link_libraries(aurora_core PUBLIC imgui) @@ -35,6 +44,7 @@ if(AURORA_ENABLE_RMLUI) target_sources(aurora_core PRIVATE lib/rmlui.cpp + lib/rmlui/RuntimeTextureProvider.cpp lib/rmlui/RmlUi_Backend_Aurora.cpp lib/rmlui/WebGPURenderInterface.cpp lib/rmlui/SystemInterface_Aurora.cpp diff --git a/cmake/aurora_gx.cmake b/cmake/aurora_gx.cmake index b973137..7aab6cc 100644 --- a/cmake/aurora_gx.cmake +++ b/cmake/aurora_gx.cmake @@ -40,6 +40,6 @@ add_library(aurora_gx STATIC add_library(aurora::gx ALIAS aurora_gx) set_target_properties(aurora_gx PROPERTIES FOLDER "aurora") -target_link_libraries(aurora_gx PUBLIC aurora::core xxhash) -target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map dawn::webgpu_dawn sqlite3 TracyClient PNG::PNG) +target_link_libraries(aurora_gx PUBLIC aurora::core dawn::webgpu_dawn xxhash) +target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map sqlite3 TracyClient PNG::PNG) target_compile_definitions(aurora_gx PRIVATE WEBGPU_DAWN) diff --git a/include/aurora/aurora.h b/include/aurora/aurora.h index 59254eb..9d22cc6 100644 --- a/include/aurora/aurora.h +++ b/include/aurora/aurora.h @@ -83,6 +83,7 @@ typedef struct { const char* appName; const char* userPath; const char* cachePath; + const char* resourcesPath; AuroraBackend desiredBackend; uint32_t msaa; uint16_t maxTextureAnisotropy; diff --git a/include/aurora/rmlui.hpp b/include/aurora/rmlui.hpp index 3b13abe..7d901b2 100644 --- a/include/aurora/rmlui.hpp +++ b/include/aurora/rmlui.hpp @@ -4,6 +4,14 @@ #include +#include +#include +#include +#include +#include +#include +#include + namespace aurora::rmlui { enum class InputType { @@ -11,12 +19,29 @@ enum class InputType { Number, }; +inline constexpr const char* TouchStartEvent = "touchstart"; +inline constexpr const char* TouchMoveEvent = "touchmove"; +inline constexpr const char* TouchEndEvent = "touchend"; +inline constexpr const char* TouchCancelEvent = "touchcancel"; + Rml::Context* get_context() noexcept; bool is_initialized() noexcept; void set_input_type(InputType type) noexcept; void set_ui_scale(float scale) noexcept; float get_ui_scale() noexcept; +struct RuntimeTexture { + uint32_t width = 0; + uint32_t height = 0; + std::span rgba8; + bool premultipliedAlpha = false; +}; + +using TextureProvider = std::function(std::string_view)>; + +void register_texture_provider(std::string scheme, TextureProvider provider); +void unregister_texture_provider(std::string_view scheme) noexcept; + } // namespace aurora::rmlui #endif diff --git a/include/dolphin/pad.h b/include/dolphin/pad.h index 284e6f5..252fca7 100644 --- a/include/dolphin/pad.h +++ b/include/dolphin/pad.h @@ -222,6 +222,10 @@ PADSignedNativeAxis PADGetNativeAxisPulled(u32 port); void PADRestoreDefaultMapping(u32 port); void PADBlockInput(bool block); +void PADSetVirtualStatus(u32 port, const PADStatus* status); +void PADClearVirtualStatus(u32 port); +void PADClearAllVirtualStatus(); + /** * Set the default controller mapping used. * @@ -270,6 +274,9 @@ BOOL PADHasLED(u32 port); BOOL PADSetRumbleIntensity(u32 port, u16 low, u16 high); BOOL PADGetRumbleIntensity(u32 port, u16* low, u16* high); BOOL PADSupportsRumbleIntensity(u32 port); +BOOL PADCanForceDeviceRumble(u32 port); +BOOL PADGetForceDeviceRumble(u32 port); +BOOL PADSetForceDeviceRumble(u32 port, BOOL force); typedef enum PADBatteryState { PAD_BATTERYSTATE_ERROR = -1, diff --git a/lib/aurora.cpp b/lib/aurora.cpp index 73cbf80..8cef877 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -89,6 +89,11 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce } else { g_config.cachePath = strdup(g_config.cachePath); } + if (g_config.resourcesPath == nullptr) { + g_config.resourcesPath = SDL_GetBasePath(); + } else { + g_config.resourcesPath = strdup(g_config.resourcesPath); + } if (g_config.msaa == 0) { g_config.msaa = 1; } diff --git a/lib/device.cpp b/lib/device.cpp new file mode 100644 index 0000000..41a4924 --- /dev/null +++ b/lib/device.cpp @@ -0,0 +1,217 @@ +#include "device.hpp" + +#include +#include + +#include +#include +#include + +namespace aurora::device { +namespace detail { +void shutdown_rumble() noexcept; +} + +namespace { +SDL_Sensor* g_gyro = nullptr; +#if defined(SDL_PLATFORM_ANDROID) +SDL_Haptic* g_haptic = nullptr; + +constexpr std::string_view kAndroidDeviceHapticName = "VIBRATOR_SERVICE"; +constexpr uint32_t kIndefiniteRumbleDurationMs = 30000; + +float rumble_strength(const uint16_t low, const uint16_t high) noexcept { + const float mixed = static_cast(low) * 0.6f + static_cast(high) * 0.4f; + return std::clamp(mixed / 65535.0f, 0.0f, 1.0f); +} + +bool open_haptic() noexcept { + if (g_haptic != nullptr) { + return true; + } + + int count = 0; + SDL_HapticID* haptics = SDL_GetHaptics(&count); + if (haptics == nullptr) { + return false; + } + + SDL_HapticID selected = 0; + for (int i = 0; i < count; ++i) { + const char* name = SDL_GetHapticNameForID(haptics[i]); + if (name != nullptr && std::string_view{name} == kAndroidDeviceHapticName) { + selected = haptics[i]; + break; + } + } + SDL_free(haptics); + + if (selected == 0) { + return false; + } + + g_haptic = SDL_OpenHaptic(selected); + if (g_haptic == nullptr) { + return false; + } + if (!SDL_HapticRumbleSupported(g_haptic) || !SDL_InitHapticRumble(g_haptic)) { + SDL_CloseHaptic(g_haptic); + g_haptic = nullptr; + return false; + } + + return true; +} +#endif + +SDL_Sensor* open_sensor(const SDL_SensorType type, SDL_Sensor*& sensor) noexcept { + if (sensor != nullptr) { + return sensor; + } + + int count = 0; + SDL_SensorID* sensors = SDL_GetSensors(&count); + if (sensors == nullptr) { + return nullptr; + } + + SDL_SensorID selected = 0; + for (int i = 0; i < count; ++i) { + if (SDL_GetSensorTypeForID(sensors[i]) == type) { + selected = sensors[i]; + break; + } + } + SDL_free(sensors); + + if (selected == 0) { + return nullptr; + } + + sensor = SDL_OpenSensor(selected); + return sensor; +} + +int orientation_quarter_turns(const SDL_DisplayOrientation orientation) noexcept { + switch (orientation) { + case SDL_ORIENTATION_PORTRAIT: + return 0; + case SDL_ORIENTATION_LANDSCAPE: + return 1; + case SDL_ORIENTATION_PORTRAIT_FLIPPED: + return 2; + case SDL_ORIENTATION_LANDSCAPE_FLIPPED: + return 3; + default: + return -1; + } +} + +void rotate_sensor_to_display(float* data, const int n_values) noexcept { + if (n_values < 2) { + return; + } + + const SDL_DisplayID display = SDL_GetPrimaryDisplay(); + if (display == 0) { + return; + } + + const int natural = orientation_quarter_turns(SDL_GetNaturalDisplayOrientation(display)); + const int current = orientation_quarter_turns(SDL_GetCurrentDisplayOrientation(display)); + if (natural < 0 || current < 0) { + return; + } + + const float x = data[0]; + const float y = data[1]; + switch ((current - natural + 4) % 4) { + case 1: + data[0] = -y; + data[1] = x; + break; + case 2: + data[0] = -x; + data[1] = -y; + break; + case 3: + data[0] = y; + data[1] = -x; + break; + default: + break; + } +} +} // namespace + +#if defined(SDL_PLATFORM_ANDROID) +bool rumble_available() noexcept { return open_haptic(); } + +void rumble(const uint16_t low_freq_intensity, const uint16_t high_freq_intensity, + const uint16_t duration_ms) noexcept { + if (!open_haptic()) { + return; + } + + const float strength = rumble_strength(low_freq_intensity, high_freq_intensity); + if (strength <= 0.0f) { + SDL_StopHapticRumble(g_haptic); + return; + } + + SDL_PlayHapticRumble(g_haptic, strength, + duration_ms == 0 ? kIndefiniteRumbleDurationMs : static_cast(duration_ms)); +} + +namespace detail { +void shutdown_rumble() noexcept { + if (g_haptic != nullptr) { + SDL_StopHapticRumble(g_haptic); + SDL_CloseHaptic(g_haptic); + g_haptic = nullptr; + } +} +} // namespace detail +#elif !defined(SDL_PLATFORM_IOS) +bool rumble_available() noexcept { return false; } + +void rumble(const uint16_t lowFreq, const uint16_t highFreq, + const uint16_t durationMs) noexcept { + static_cast(lowFreq); + static_cast(highFreq); + static_cast(durationMs); +} + +namespace detail { +void shutdown_rumble() noexcept {} +} // namespace detail +#endif + +bool gyro_available() noexcept { return open_sensor(SDL_SENSOR_GYRO, g_gyro) != nullptr; } + +bool gyro(float* data, const int n_values) noexcept { + if (data == nullptr || n_values <= 0) { + return false; + } + + SDL_Sensor* sensor = open_sensor(SDL_SENSOR_GYRO, g_gyro); + if (sensor == nullptr) { + return false; + } + + SDL_UpdateSensors(); + if (!SDL_GetSensorData(sensor, data, n_values)) { + return false; + } + rotate_sensor_to_display(data, n_values); + return true; +} + +void shutdown() noexcept { + detail::shutdown_rumble(); + if (g_gyro != nullptr) { + SDL_CloseSensor(g_gyro); + g_gyro = nullptr; + } +} +} // namespace aurora::device diff --git a/lib/device.hpp b/lib/device.hpp new file mode 100644 index 0000000..2694ad9 --- /dev/null +++ b/lib/device.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace aurora::device { + +bool rumble_available() noexcept; +void rumble(uint16_t lowFreq, uint16_t highFreq, uint16_t durationMs) noexcept; + +bool gyro_available() noexcept; +bool gyro(float* data, int n_values) noexcept; + +void shutdown() noexcept; + +} // namespace aurora::device diff --git a/lib/device_ios.mm b/lib/device_ios.mm new file mode 100644 index 0000000..c4564cb --- /dev/null +++ b/lib/device_ios.mm @@ -0,0 +1,182 @@ +#include "device.hpp" + +#import +#import + +#include +#include + +@interface AuroraDeviceHaptics : NSObject +{ + std::atomic _generation; +} +@property(nonatomic, strong) CHHapticEngine* engine; +@property(nonatomic, strong) id player; +@property(nonatomic) BOOL active; +- (BOOL)available; +- (void)rumbleLow:(uint16_t)low high:(uint16_t)high duration:(uint16_t)durationMs; +- (void)shutdown; +@end + +@implementation AuroraDeviceHaptics + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _generation.store(0); + } + return self; +} + +- (BOOL)available { + return CHHapticEngine.capabilitiesForHardware.supportsHaptics; +} + +- (BOOL)ensurePlayer { + if (![self available]) { + return NO; + } + + NSError* error = nil; + if (self.engine == nil) { + self.engine = [[CHHapticEngine alloc] initAndReturnError:&error]; + if (error != nil || self.engine == nil) { + return NO; + } + + __weak AuroraDeviceHaptics* weakSelf = self; + self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason) { + AuroraDeviceHaptics* strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + strongSelf.player = nil; + strongSelf.engine = nil; + strongSelf.active = NO; + }; + self.engine.resetHandler = ^{ + AuroraDeviceHaptics* strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + strongSelf.player = nil; + strongSelf.active = NO; + [strongSelf.engine startAndReturnError:nil]; + }; + + [self.engine startAndReturnError:&error]; + if (error != nil) { + self.engine = nil; + return NO; + } + } + + if (self.player == nil) { + CHHapticEventParameter* intensity = + [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f]; + CHHapticEvent* event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[ intensity ] + relativeTime:0 + duration:1.0]; + CHHapticPattern* pattern = [[CHHapticPattern alloc] initWithEvents:@[ event ] parameters:@[] error:&error]; + if (error != nil || pattern == nil) { + return NO; + } + + self.player = [self.engine createAdvancedPlayerWithPattern:pattern error:&error]; + if (error != nil || self.player == nil) { + return NO; + } + self.player.loopEnabled = YES; + self.player.loopEnd = 1.0; + } + + return YES; +} + +- (void)stop { + ++_generation; + if (self.player != nil && self.active) { + [self.player stopAtTime:CHHapticTimeImmediate error:nil]; + } + self.active = NO; +} + +- (void)rumbleLow:(uint16_t)low high:(uint16_t)high duration:(uint16_t)durationMs { + const float lowValue = static_cast(low) / 65535.0f; + const float highValue = static_cast(high) / 65535.0f; + const float intensity = std::clamp(lowValue * 0.6f + highValue * 0.4f, 0.0f, 1.0f); + if (intensity <= 0.0f) { + [self stop]; + return; + } + + if (![self ensurePlayer]) { + return; + } + + const float sharpness = std::clamp(highValue * 0.75f + lowValue * 0.25f, 0.0f, 1.0f); + CHHapticDynamicParameter* intensityParam = + [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl + value:intensity + relativeTime:0]; + CHHapticDynamicParameter* sharpnessParam = + [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticSharpnessControl + value:sharpness + relativeTime:0]; + if (![self.player sendParameters:@[ intensityParam, sharpnessParam ] atTime:CHHapticTimeImmediate error:nil]) { + return; + } + + if (!self.active) { + if (![self.player startAtTime:CHHapticTimeImmediate error:nil]) { + return; + } + self.active = YES; + } + + const uint64_t generation = ++_generation; + if (durationMs != 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, static_cast(durationMs) * NSEC_PER_MSEC), + dispatch_get_main_queue(), ^{ + if (_generation.load() == generation) { + [self stop]; + } + }); + } +} + +- (void)shutdown { + [self stop]; + if (self.player != nil) { + [self.player cancelAndReturnError:nil]; + self.player = nil; + } + if (self.engine != nil) { + [self.engine stopWithCompletionHandler:nil]; + self.engine = nil; + } +} + +@end + +namespace aurora::device { +namespace { +AuroraDeviceHaptics* haptics() { + static AuroraDeviceHaptics* s_haptics = [[AuroraDeviceHaptics alloc] init]; + return s_haptics; +} +} // namespace + +bool rumble_available() noexcept { return [haptics() available]; } + +void rumble(const uint16_t loqFreq, const uint16_t highFreq, const uint16_t durationMs) noexcept { + [haptics() rumbleLow:loqFreq high:highFreq duration:durationMs]; +} + +namespace detail { +void shutdown_rumble() noexcept { + [haptics() shutdown]; +} +} // namespace detail +} // namespace aurora::device diff --git a/lib/dolphin/pad/pad.cpp b/lib/dolphin/pad/pad.cpp index 13501f0..08f6058 100644 --- a/lib/dolphin/pad/pad.cpp +++ b/lib/dolphin/pad/pad.cpp @@ -1,4 +1,5 @@ #include "../../input.hpp" +#include "../../device.hpp" #include "../../internal.hpp" #include #include @@ -9,7 +10,8 @@ #include namespace { -constexpr int32_t k_mappingsFileVersion = 3; +constexpr int32_t k_mappingsFileVersion = 4; +constexpr int32_t k_minMappingsFileVersion = 3; std::array g_defaultButtonsStandard{{ {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, @@ -26,6 +28,9 @@ std::array g_defaultButtonsStandard{{ {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, }}; +std::array g_virtualPadStatus{}; +std::array g_virtualPadActive{}; + std::array g_defaultButtonsXBox360{{ {SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A}, {SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_B}, @@ -306,6 +311,30 @@ void PADSetSpec(u32 spec [[maybe_unused]]) {} static void load_keyboard_bindings(); static void save_keyboard_bindings(); +static bool device_rumble_available_for_port(const u32 port) { + return port == PAD_CHAN0 && aurora::device::rumble_available(); +} + +static bool should_use_device_rumble(const u32 port, const aurora::input::GameController* controller) { + return device_rumble_available_for_port(port) && + (controller == nullptr || + (!controller->m_isGameCube && (!controller->m_hasRumble || controller->m_forceDeviceRumble))); +} + +static bool device_gyro_available_for_port(const u32 port) { + return port == PAD_CHAN0 && aurora::device::gyro_available(); +} + +static bool controller_has_sensor(const aurora::input::GameController* controller, const PADSensorType sensor) { + return controller != nullptr && SDL_GamepadHasSensor(controller->m_controller, static_cast(sensor)); +} + +static bool should_use_device_sensor(const u32 port, const aurora::input::GameController* controller, + const PADSensorType sensor) { + return sensor == PAD_SENSOR_GYRO && !controller_has_sensor(controller, sensor) && + device_gyro_available_for_port(port); +} + // ReSharper disable once CppDFAConstantFunctionResult BOOL PADInit() { if (g_initialized) { @@ -472,9 +501,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re uint32_t version = 0; SDL_ReadU32LE(file, &version); - if (version != k_mappingsFileVersion) { - aurora::input::Log.warn("Invalid controller mapping version! (Expected {0}, found {1})", k_mappingsFileVersion, - version); + if (version < k_minMappingsFileVersion || version > k_mappingsFileVersion) { + aurora::input::Log.warn("Invalid controller mapping version! (Expected {0}..{1}, found {2})", + k_minMappingsFileVersion, k_mappingsFileVersion, version); return; } @@ -499,6 +528,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re if (!isGameCube) { SDL_ReadIO(file, &controller->m_rumbleIntensityLow, sizeof(u16)); SDL_ReadIO(file, &controller->m_rumbleIntensityHigh, sizeof(u16)); + if (version >= k_mappingsFileVersion) { + SDL_ReadIO(file, &controller->m_forceDeviceRumble, sizeof(bool)); + } } SDL_CloseIO(file); @@ -597,6 +629,23 @@ static void apply_unblock_suppression(PADStatus& status, const u32 port, const b } } +static int dominant_axis_value(const int physical, const int virtualValue, const int min, const int max) { + return std::clamp(std::abs(virtualValue) > std::abs(physical) ? virtualValue : physical, min, max); +} + +static void merge_virtual_status(PADStatus& status, const PADStatus& virtualStatus) { + status.button |= virtualStatus.button; + status.extButton |= virtualStatus.extButton; + status.stickX = static_cast(dominant_axis_value(status.stickX, virtualStatus.stickX, -127, 127)); + status.stickY = static_cast(dominant_axis_value(status.stickY, virtualStatus.stickY, -127, 127)); + status.substickX = static_cast(dominant_axis_value(status.substickX, virtualStatus.substickX, -127, 127)); + status.substickY = static_cast(dominant_axis_value(status.substickY, virtualStatus.substickY, -127, 127)); + status.triggerLeft = std::max(status.triggerLeft, virtualStatus.triggerLeft); + status.triggerRight = std::max(status.triggerRight, virtualStatus.triggerRight); + status.analogA = std::max(status.analogA, virtualStatus.analogA); + status.analogB = std::max(status.analogB, virtualStatus.analogB); +} + u32 PADRead(PADStatus* status) { if (!g_keyboardBindingsLoaded) { g_keyboardBindingsLoaded = true; @@ -611,8 +660,12 @@ u32 PADRead(PADStatus* status) { uint32_t rumbleSupport = 0; for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { memset(&status[i], 0, sizeof(PADStatus)); + // Support device rumble on port 0 regardless of whether a controller is connected. + if (device_rumble_available_for_port(i)) { + rumbleSupport |= PAD_CHAN0_BIT; + } auto controller = aurora::input::get_controller_for_player(i); - if (controller == nullptr && !g_keyboardBindings[i].m_mappingsSet) { + if (controller == nullptr && !g_keyboardBindings[i].m_mappingsSet && !g_virtualPadActive[i]) { status[i].err = PAD_ERR_NO_CONTROLLER; g_suppressedButtons[i] = 0; g_suppressLeftTrigger[i] = false; @@ -825,18 +878,63 @@ u32 PADRead(PADStatus* status) { neutralize_status(status[i]); } else { apply_unblock_suppression(status[i], i, captureHeldInput); + if (g_virtualPadActive[i]) { + merge_virtual_status(status[i], g_virtualPadStatus[i]); + } } } return rumbleSupport; } +void PADSetVirtualStatus(const u32 port, const PADStatus* virtualStatus) { + if (port >= PAD_CHANMAX || virtualStatus == nullptr) { + return; + } + + g_virtualPadStatus[port] = *virtualStatus; + g_virtualPadStatus[port].err = PAD_ERR_NONE; + g_virtualPadActive[port] = true; +} + +void PADClearVirtualStatus(const u32 port) { + if (port >= PAD_CHANMAX) { + return; + } + + g_virtualPadStatus[port] = {}; + g_virtualPadActive[port] = false; +} + +void PADClearAllVirtualStatus() { + g_virtualPadStatus.fill({}); + g_virtualPadActive.fill(false); +} + void PADControlMotor(const u32 chan, const u32 cmd) { const auto controller = aurora::input::get_controller_for_player(chan); - const auto instance = aurora::input::get_instance_for_player(chan); + if (should_use_device_rumble(chan, controller)) { + u16 low = 0; + u16 high = 0; + if (controller != nullptr) { + EnsureMappingLoaded(controller); + low = controller->m_rumbleIntensityLow; + high = controller->m_rumbleIntensityHigh; + } else { + aurora::input::get_device_rumble_intensity(&low, &high); + } + if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) { + aurora::device::rumble(0, 0, 0); + } else if (cmd == PAD_MOTOR_RUMBLE) { + aurora::device::rumble(low, high, 0); + } + return; + } + if (controller == nullptr) { return; } + const auto instance = aurora::input::get_instance_for_player(chan); if (controller->m_isGameCube) { if (cmd == PAD_MOTOR_STOP) { aurora::input::controller_rumble(instance, 0, 1, 0); @@ -1302,6 +1400,7 @@ void PADSerializeMappings() { if (!controller.m_isGameCube) { SDL_WriteIO(file, &controller.m_rumbleIntensityLow, sizeof(u16)); SDL_WriteIO(file, &controller.m_rumbleIntensityHigh, sizeof(u16)); + SDL_WriteIO(file, &controller.m_forceDeviceRumble, sizeof(bool)); } SDL_CloseIO(file); } @@ -1533,31 +1632,35 @@ BOOL PADGetColor(const u32 port, u8* red, u8* green, u8* blue) { BOOL PADSetSensorEnabled(const u32 port, const PADSensorType sensor, const BOOL enabled) { const auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr) { - return FALSE; + if (controller_has_sensor(ctrl, sensor)) { + return SDL_SetGamepadSensorEnabled(ctrl->m_controller, static_cast(sensor), enabled ? true : false) + ? TRUE + : FALSE; } - return SDL_SetGamepadSensorEnabled(ctrl->m_controller, static_cast(sensor), enabled ? true : false) - ? TRUE - : FALSE; + return should_use_device_sensor(port, ctrl, sensor) ? TRUE : FALSE; } BOOL PADHasSensor(const u32 port, const PADSensorType sensor) { const auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr) { - return FALSE; + if (controller_has_sensor(ctrl, sensor)) { + return TRUE; } - return SDL_GamepadHasSensor(ctrl->m_controller, static_cast(sensor)) ? TRUE : FALSE; + return should_use_device_sensor(port, ctrl, sensor) ? TRUE : FALSE; } BOOL PADGetSensorData(const u32 port, const PADSensorType sensor, f32* data, const int nValues) { const auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr) { - return FALSE; + if (controller_has_sensor(ctrl, sensor)) { + return SDL_GetGamepadSensorData(ctrl->m_controller, static_cast(sensor), data, nValues); } - return SDL_GetGamepadSensorData(ctrl->m_controller, static_cast(sensor), data, nValues); + if (should_use_device_sensor(port, ctrl, sensor)) { + return aurora::device::gyro(data, nValues) ? TRUE : FALSE; + } + + return FALSE; } BOOL PADHasLED(const u32 port) { @@ -1572,35 +1675,82 @@ BOOL PADHasLED(const u32 port) { BOOL PADSetRumbleIntensity(const u32 port, const u16 low, const u16 high) { auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr || ctrl->m_isGameCube || !ctrl->m_hasRumble) { + if (ctrl != nullptr) { + if (ctrl->m_isGameCube || (!ctrl->m_hasRumble && !should_use_device_rumble(port, ctrl))) { + return FALSE; + } + EnsureMappingLoaded(ctrl); + ctrl->m_rumbleIntensityLow = low; + ctrl->m_rumbleIntensityHigh = high; + return TRUE; + } + + if (!should_use_device_rumble(port, nullptr)) { return FALSE; } - ctrl->m_rumbleIntensityLow = low; - ctrl->m_rumbleIntensityHigh = high; - + aurora::input::set_device_rumble_intensity(low, high); return TRUE; } BOOL PADGetRumbleIntensity(const u32 port, u16* low, u16* high) { - const auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr || ctrl->m_isGameCube || !ctrl->m_hasRumble) { + auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl != nullptr) { + if (ctrl->m_isGameCube || (!ctrl->m_hasRumble && !should_use_device_rumble(port, ctrl))) { + *low = 0; + *high = 0; + return FALSE; + } + EnsureMappingLoaded(ctrl); + *low = ctrl->m_rumbleIntensityLow; + *high = ctrl->m_rumbleIntensityHigh; + return TRUE; + } + + if (!should_use_device_rumble(port, nullptr)) { *low = 0; *high = 0; return FALSE; } - *low = ctrl->m_rumbleIntensityLow; - *high = ctrl->m_rumbleIntensityHigh; + aurora::input::get_device_rumble_intensity(low, high); return TRUE; } BOOL PADSupportsRumbleIntensity(const u32 port) { + if (const auto* ctrl = aurora::input::get_controller_for_player(port)) { + if (!ctrl->m_isGameCube && (ctrl->m_hasRumble || should_use_device_rumble(port, ctrl))) { + return TRUE; + } + return FALSE; + } + return should_use_device_rumble(port, nullptr) ? TRUE : FALSE; +} + +BOOL PADCanForceDeviceRumble(const u32 port) { const auto* ctrl = aurora::input::get_controller_for_player(port); - if (ctrl == nullptr) { + return ctrl != nullptr && !ctrl->m_isGameCube && ctrl->m_hasRumble && device_rumble_available_for_port(port) ? TRUE + : FALSE; +} + +BOOL PADGetForceDeviceRumble(const u32 port) { + auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr || !PADCanForceDeviceRumble(port)) { return FALSE; } - return !ctrl->m_isGameCube && ctrl->m_hasRumble; + EnsureMappingLoaded(ctrl); + return ctrl->m_forceDeviceRumble ? TRUE : FALSE; +} + +BOOL PADSetForceDeviceRumble(const u32 port, const BOOL force) { + auto* ctrl = aurora::input::get_controller_for_player(port); + if (ctrl == nullptr || !PADCanForceDeviceRumble(port)) { + return FALSE; + } + + EnsureMappingLoaded(ctrl); + ctrl->m_forceDeviceRumble = force != FALSE; + return TRUE; } BOOL PADIsGCAdapter(const u32 port) { diff --git a/lib/gfx/texture_convert.cpp b/lib/gfx/texture_convert.cpp index e6fd2d0..072debf 100644 --- a/lib/gfx/texture_convert.cpp +++ b/lib/gfx/texture_convert.cpp @@ -571,8 +571,10 @@ ConvertedTexture convert_texture_palette(u32 textureFormat, uint32_t width, uint const auto* indexData = reinterpret_cast(indices.data.data()); size_t offset = 0; + uint32_t mipWidth = width; + uint32_t mipHeight = height; for (u32 mip = 0; mip < mips; ++mip) { - const size_t pixelCount = static_cast(width) * height; + const size_t pixelCount = static_cast(mipWidth) * mipHeight; for (size_t i = 0; i < pixelCount; ++i) { const u32 index = indexData[offset + i]; if (index >= tlutEntries) { @@ -591,8 +593,8 @@ ConvertedTexture convert_texture_palette(u32 textureFormat, uint32_t width, uint } } offset += pixelCount; - width = std::max(width >> 1, 1u); - height = std::max(height >> 1, 1u); + mipWidth = std::max(mipWidth >> 1, 1u); + mipHeight = std::max(mipHeight >> 1, 1u); } bool hasArbitraryMips = arb_mip_check(width, height, mips, pixels); diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index fa5e2c4..dd505b8 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -1018,18 +1018,28 @@ static u16 wgpu_aniso(GXAnisotropy aniso) { wgpu::SamplerDescriptor aurora::gfx::TextureBind::get_descriptor() const noexcept { auto [minFilter, mipFilter] = wgpu_filter_mode(texObj.min_filter()); - const auto [magFilter, _] = wgpu_filter_mode(texObj.mag_filter()); + auto [magFilter, _] = wgpu_filter_mode(texObj.mag_filter()); + const bool mipsEnabled = mipFilter != wgpu::MipmapFilterMode::Undefined; float minLod = texObj.min_lod(); float maxLod = texObj.max_lod(); + u16 maxAnisotropy = wgpu_aniso(texObj.max_aniso()); if (ref && ref->isReplacement) { - minFilter = wgpu::FilterMode::Linear; - mipFilter = wgpu::MipmapFilterMode::Linear; minLod = 0.f; - maxLod = static_cast(std::max(ref->mipCount, 1u) - 1u); + maxLod = 1000.f; + if (!mipsEnabled) { + mipFilter = wgpu::MipmapFilterMode::Nearest; + } } else if (mipFilter == wgpu::MipmapFilterMode::Undefined) { minLod = 0.f; maxLod = 0.f; } + if ((ref && ref->hasArbitraryMips) || !mipsEnabled) { + maxAnisotropy = 1; + } else if (maxAnisotropy > 1) { + magFilter = wgpu::FilterMode::Linear; + minFilter = wgpu::FilterMode::Linear; + mipFilter = wgpu::MipmapFilterMode::Linear; + } return { .label = "Generated Filtering Sampler", .addressModeU = wgpu_address_mode(texObj.wrap_s()), @@ -1040,6 +1050,6 @@ wgpu::SamplerDescriptor aurora::gfx::TextureBind::get_descriptor() const noexcep .mipmapFilter = mipFilter, .lodMinClamp = minLod, .lodMaxClamp = maxLod, - .maxAnisotropy = wgpu_aniso(texObj.max_aniso()), + .maxAnisotropy = maxAnisotropy, }; } // namespace aurora::gx diff --git a/lib/gx/shader_info.cpp b/lib/gx/shader_info.cpp index 31ba6ed..df5fff2 100644 --- a/lib/gx/shader_info.cpp +++ b/lib/gx/shader_info.cpp @@ -16,11 +16,14 @@ bool is_alpha_bump_channel(GXChannelID id) { return id == GX_ALPHA_BUMP || id == Vec4 texture_size_bias(const gfx::TextureBind& tex) { auto width = static_cast(tex.texObj.width()); auto height = static_cast(tex.texObj.height()); - const auto vpBias = - enableLodBias && tex.ref && tex.ref->hasArbitraryMips - ? log2(std::min(g_gxState.renderViewport.width / std::max(g_gxState.logicalViewport.width, 1.f), - g_gxState.renderViewport.height / std::max(g_gxState.logicalViewport.height, 1.f))) - : 0.f; + float vpBias = 0.f; + if (enableLodBias && tex.ref && tex.ref->hasArbitraryMips) { + const float viewportScale = + std::min(g_gxState.renderViewport.width / std::max(g_gxState.logicalViewport.width, 1.f), + g_gxState.renderViewport.height / std::max(g_gxState.logicalViewport.height, 1.f)); + const float replacementScale = static_cast(tex.ref->size.width) / std::max(width, 1.f); + vpBias = std::log2(viewportScale / std::max(replacementScale, 0.001f)); + } return {width, height, tex.texObj.lod_bias() + vpBias, 0.0f}; } diff --git a/lib/input.cpp b/lib/input.cpp index 1d5406e..26508f6 100644 --- a/lib/input.cpp +++ b/lib/input.cpp @@ -1,4 +1,5 @@ #include "input.hpp" +#include "device.hpp" #include "internal.hpp" #include "magic_enum.hpp" @@ -22,8 +23,10 @@ absl::flat_hash_map g_GameControllers; namespace { constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT'); -constexpr uint32_t kPortPreferencesVersion = 2; +constexpr uint32_t kPortPreferencesVersion = 3; constexpr uint32_t kMaxPersistedStringLength = 256; +constexpr uint16_t kDefaultDeviceRumbleIntensityLow = 0x8000; +constexpr uint16_t kDefaultDeviceRumbleIntensityHigh = 0x8000; enum class PortPreferenceState : uint8_t { Unset = 0, @@ -41,7 +44,13 @@ struct PortPreference { ControllerIdentity identity; }; +struct DevicePreference { + uint16_t rumbleIntensityLow = kDefaultDeviceRumbleIntensityLow; + uint16_t rumbleIntensityHigh = kDefaultDeviceRumbleIntensityHigh; +}; + std::array g_portPreferences; +DevicePreference g_devicePreference; bool g_portPreferencesLoaded = false; std::string port_preferences_path() { @@ -158,7 +167,7 @@ bool read_port_preferences_file(std::array& uint32_t magic = 0; uint32_t version = 0; bool ok = read_value(file, magic) && read_value(file, version) && magic == kPortPreferencesMagic && - version == kPortPreferencesVersion; + (version == 2 || version == kPortPreferencesVersion); for (auto& preference : preferences) { uint8_t state = 0; @@ -170,6 +179,10 @@ bool read_port_preferences_file(std::array& preference.identity = std::move(identity); } } + if (ok && version >= 3) { + ok = read_value(file, g_devicePreference.rumbleIntensityLow) && + read_value(file, g_devicePreference.rumbleIntensityHigh); + } SDL_CloseIO(file); if (!ok) { @@ -212,6 +225,8 @@ void save_port_preferences() { const auto state = static_cast(preference.state); ok = ok && write_value(file, state) && write_identity(file, preference.identity); } + ok = ok && write_value(file, g_devicePreference.rumbleIntensityLow) && + write_value(file, g_devicePreference.rumbleIntensityHigh); if (!SDL_FlushIO(file)) { ok = false; @@ -437,6 +452,19 @@ void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t } } +void get_device_rumble_intensity(uint16_t* low_freq_intensity, uint16_t* high_freq_intensity) noexcept { + ensure_port_preferences_loaded(); + *low_freq_intensity = g_devicePreference.rumbleIntensityLow; + *high_freq_intensity = g_devicePreference.rumbleIntensityHigh; +} + +void set_device_rumble_intensity(const uint16_t low_freq_intensity, const uint16_t high_freq_intensity) noexcept { + ensure_port_preferences_loaded(); + g_devicePreference.rumbleIntensityLow = low_freq_intensity; + g_devicePreference.rumbleIntensityHigh = high_freq_intensity; + save_port_preferences(); +} + uint32_t controller_count() noexcept { return g_GameControllers.size(); } void persist_controller_for_player(uint32_t player, const GameController* controller) noexcept { @@ -458,8 +486,8 @@ void persist_controller_for_player(uint32_t player, const GameController* contro void initialize() noexcept { /* Make sure we initialize everything input related now, this will automatically add all of the connected controllers * as expected */ - ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD), "Failed to initialize SDL subsystems: {}", - SDL_GetError()); + ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD | SDL_INIT_SENSOR), + "Failed to initialize SDL subsystems: {}", SDL_GetError()); } struct MouseScrollStatus { @@ -488,5 +516,6 @@ void shutdown() noexcept { } controller_rumble(controller.first, 0, 0, 0); } + device::shutdown(); } } // namespace aurora::input diff --git a/lib/input.hpp b/lib/input.hpp index 35f7d17..572ddee 100644 --- a/lib/input.hpp +++ b/lib/input.hpp @@ -32,6 +32,7 @@ struct GameController { std::array m_axisMapping{}; uint16_t m_rumbleIntensityLow = 32767; uint16_t m_rumbleIntensityHigh = 32767; + bool m_forceDeviceRumble = false; bool m_mappingLoaded = false; constexpr bool operator==(const GameController& other) const { return m_controller == other.m_controller && m_index == other.m_index; @@ -54,6 +55,8 @@ bool is_gamecube(Uint32 instance) noexcept; bool controller_has_rumble(Uint32 instance) noexcept; void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity, uint16_t duration_ms) noexcept; +void get_device_rumble_intensity(uint16_t* low_freq_intensity, uint16_t* high_freq_intensity) noexcept; +void set_device_rumble_intensity(uint16_t low_freq_intensity, uint16_t high_freq_intensity) noexcept; uint32_t controller_count() noexcept; void initialize() noexcept; void persist_controller_for_player(uint32_t player, const GameController* controller) noexcept; diff --git a/lib/rmlui.cpp b/lib/rmlui.cpp index 876e425..2183f1e 100644 --- a/lib/rmlui.cpp +++ b/lib/rmlui.cpp @@ -26,6 +26,9 @@ constexpr size_t MaxTrackedTouches = 16; struct TrackedTouch { SDL_FingerID id = 0; Rml::Vector2f position; + Rml::Vector2f rmlPosition; + Rml::Vector2f startPosition; + Rml::Element* target = nullptr; bool active = false; }; @@ -80,6 +83,7 @@ void ensure_render_target(Rml::Vector2i dimensions) noexcept { struct MappedPoint { Rml::Vector2f position; + bool valid = false; bool inside = false; }; @@ -99,8 +103,7 @@ MappedPoint map_native_point_to_content(float nativeX, float nativeY) noexcept { static_cast(contentSize.x), static_cast(contentSize.y)); const float right = viewport.left + viewport.width; const float bottom = viewport.top + viewport.height; - if (viewport.width <= 0.f || viewport.height <= 0.f || nativeX < viewport.left || nativeY < viewport.top || - nativeX >= right || nativeY >= bottom) { + if (viewport.width <= 0.f || viewport.height <= 0.f) { return {}; } @@ -110,7 +113,8 @@ MappedPoint map_native_point_to_content(float nativeX, float nativeY) noexcept { (nativeX - viewport.left) * static_cast(contentSize.x) / viewport.width, (nativeY - viewport.top) * static_cast(contentSize.y) / viewport.height, }, - .inside = true, + .valid = true, + .inside = nativeX >= viewport.left && nativeY >= viewport.top && nativeX < right && nativeY < bottom, }; } @@ -170,6 +174,24 @@ Rml::TouchList touch_list(SDL_FingerID id, Rml::Vector2f position) { return {Rml::Touch{static_cast(id), position}}; } +void dispatch_touch_event( + TrackedTouch& touch, const char* type, Rml::Vector2f position, bool inside, Rml::Vector2f delta = {}) noexcept { + if (touch.target == nullptr) { + return; + } + + Rml::Dictionary parameters; + parameters["finger_id"] = touch.id; + parameters["x"] = position.x; + parameters["y"] = position.y; + parameters["dx"] = delta.x; + parameters["dy"] = delta.y; + parameters["start_x"] = touch.startPosition.x; + parameters["start_y"] = touch.startPosition.y; + parameters["inside"] = inside; + touch.target->DispatchEvent(type, parameters, true, true); +} + void handle_mouse_motion(const SDL_MouseMotionEvent& motion) noexcept { const MappedPoint mapped = map_window_point_to_content(motion.x, motion.y); if (!mapped.inside) { @@ -234,11 +256,19 @@ void handle_touch_down(const SDL_TouchFingerEvent& finger) noexcept { if (tracked == nullptr) { return; } + auto* target = g_context->GetElementAtPoint(mapped.position); + if (target == nullptr) { + target = g_context->GetRootElement(); + } *tracked = { .id = finger.fingerID, .position = mapped.position, + .rmlPosition = mapped.position, + .startPosition = mapped.position, + .target = target, .active = true, }; + dispatch_touch_event(*tracked, TouchStartEvent, mapped.position, true); g_context->ProcessTouchStart(touch_list(finger.fingerID, mapped.position), RmlSDL::GetKeyModifierState()); } @@ -248,11 +278,16 @@ void handle_touch_motion(const SDL_TouchFingerEvent& finger) noexcept { return; } const auto mapped = map_touch_point_to_content(finger); - if (!mapped.inside) { + if (!mapped.valid) { return; } + const Rml::Vector2f delta = mapped.position - tracked->position; tracked->position = mapped.position; - g_context->ProcessTouchMove(touch_list(finger.fingerID, mapped.position), RmlSDL::GetKeyModifierState()); + dispatch_touch_event(*tracked, TouchMoveEvent, mapped.position, mapped.inside, delta); + if (mapped.inside) { + tracked->rmlPosition = mapped.position; + g_context->ProcessTouchMove(touch_list(finger.fingerID, mapped.position), RmlSDL::GetKeyModifierState()); + } } void handle_touch_up(const SDL_TouchFingerEvent& finger) noexcept { @@ -260,9 +295,24 @@ void handle_touch_up(const SDL_TouchFingerEvent& finger) noexcept { if (tracked == nullptr) { return; } - const auto position = tracked->position; + const auto mapped = map_touch_point_to_content(finger); + const Rml::Vector2f position = mapped.valid ? mapped.position : tracked->position; + const Rml::Vector2f delta = position - tracked->position; + dispatch_touch_event(*tracked, TouchEndEvent, position, mapped.valid && mapped.inside, delta); + const auto rmlPosition = tracked->rmlPosition; *tracked = {}; - g_context->ProcessTouchEnd(touch_list(finger.fingerID, position), RmlSDL::GetKeyModifierState()); + g_context->ProcessTouchEnd(touch_list(finger.fingerID, rmlPosition), RmlSDL::GetKeyModifierState()); +} + +void handle_touch_cancel(const SDL_TouchFingerEvent& finger) noexcept { + auto* tracked = find_tracked_touch(finger.fingerID); + if (tracked == nullptr) { + return; + } + dispatch_touch_event(*tracked, TouchCancelEvent, tracked->position, false); + const auto rmlPosition = tracked->rmlPosition; + *tracked = {}; + g_context->ProcessTouchCancel(touch_list(finger.fingerID, rmlPosition)); } } // namespace @@ -338,9 +388,11 @@ void handle_event(SDL_Event& event) noexcept { handle_touch_motion(event.tfinger); return; case SDL_EVENT_FINGER_UP: - case SDL_EVENT_FINGER_CANCELED: handle_touch_up(event.tfinger); return; + case SDL_EVENT_FINGER_CANCELED: + handle_touch_cancel(event.tfinger); + return; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: sync_context_metrics(presentation_dimensions_from_window_size(window::get_window_size())); return; diff --git a/lib/rmlui/FileInterface_SDL.cpp b/lib/rmlui/FileInterface_SDL.cpp index 49cc255..c1c065a 100644 --- a/lib/rmlui/FileInterface_SDL.cpp +++ b/lib/rmlui/FileInterface_SDL.cpp @@ -2,6 +2,8 @@ #include "../logging.hpp" +#include + #include #include @@ -33,7 +35,7 @@ std::string resolve_path(const Rml::String& source) { if (path.empty() || is_absolute_path(path)) { return path; } - const char* basePath = SDL_GetBasePath(); + const char* basePath = g_config.resourcesPath; if (basePath == nullptr || basePath[0] == '\0') { return path; } diff --git a/lib/rmlui/RuntimeTextureProvider.cpp b/lib/rmlui/RuntimeTextureProvider.cpp new file mode 100644 index 0000000..63605ef --- /dev/null +++ b/lib/rmlui/RuntimeTextureProvider.cpp @@ -0,0 +1,78 @@ +#include "RuntimeTextureProvider.hpp" + +#include +#include +#include +#include + +namespace aurora::rmlui { +namespace { + +struct ProviderRegistry { + std::mutex mutex; + std::unordered_map providers; +}; + +ProviderRegistry& registry() { + static auto* instance = new ProviderRegistry(); + return *instance; +} + +std::string_view source_scheme(std::string_view source) noexcept { + const std::string_view delimiter = "://"; + const auto delimiterPos = source.find(delimiter); + if (delimiterPos == std::string_view::npos || delimiterPos == 0) { + return {}; + } + return source.substr(0, delimiterPos); +} + +} // namespace + +void register_texture_provider(std::string scheme, TextureProvider provider) { + if (scheme.empty() || !provider) { + return; + } + if (const auto delimiterPos = scheme.find("://"); delimiterPos != std::string::npos) { + scheme.erase(delimiterPos); + } + + auto& providers = registry(); + const std::lock_guard lock(providers.mutex); + providers.providers[std::move(scheme)] = std::move(provider); +} + +void unregister_texture_provider(std::string_view scheme) noexcept { + if (scheme.empty()) { + return; + } + if (const auto delimiterPos = scheme.find("://"); delimiterPos != std::string_view::npos) { + scheme = scheme.substr(0, delimiterPos); + } + + auto& providers = registry(); + const std::lock_guard lock(providers.mutex); + providers.providers.erase(std::string(scheme)); +} + +std::optional load_runtime_texture(std::string_view source) { + const auto scheme = source_scheme(source); + if (scheme.empty()) { + return std::nullopt; + } + + TextureProvider provider; + { + auto& providers = registry(); + const std::lock_guard lock(providers.mutex); + const auto it = providers.providers.find(std::string(scheme)); + if (it == providers.providers.end()) { + return std::nullopt; + } + provider = it->second; + } + + return provider(source); +} + +} // namespace aurora::rmlui diff --git a/lib/rmlui/RuntimeTextureProvider.hpp b/lib/rmlui/RuntimeTextureProvider.hpp new file mode 100644 index 0000000..f6d95ed --- /dev/null +++ b/lib/rmlui/RuntimeTextureProvider.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include +#include + +namespace aurora::rmlui { + +std::optional load_runtime_texture(std::string_view source); + +} // namespace aurora::rmlui diff --git a/lib/rmlui/WebGPURenderInterface.cpp b/lib/rmlui/WebGPURenderInterface.cpp index 422442e..c34c93a 100644 --- a/lib/rmlui/WebGPURenderInterface.cpp +++ b/lib/rmlui/WebGPURenderInterface.cpp @@ -1,6 +1,7 @@ #include "WebGPURenderInterface.hpp" #include "FileInterface_SDL.h" +#include "RuntimeTextureProvider.hpp" #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include "../logging.hpp" #include "../webgpu/gpu.hpp" @@ -625,6 +627,32 @@ void WebGPURenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle geometry } Rml::TextureHandle WebGPURenderInterface::LoadTexture(Rml::Vector2i& dimensions, const Rml::String& source) { + if (const auto runtimeTexture = load_runtime_texture(source)) { + const size_t size = static_cast(runtimeTexture->width) * static_cast(runtimeTexture->height) * 4; + if (runtimeTexture->width == 0 || runtimeTexture->height == 0 || runtimeTexture->rgba8.size() < size) { + Log.error("Runtime texture provider returned invalid texture! Path: {}", source); + return 0; + } + + const auto* texels = reinterpret_cast(runtimeTexture->rgba8.data()); + std::vector premultiplied; + if (!runtimeTexture->premultipliedAlpha) { + premultiplied.assign(texels, texels + size); + for (size_t offset = 0; offset < premultiplied.size(); offset += 4) { + const uint8_t alpha = premultiplied[offset + 3]; + for (size_t channel = 0; channel < 3; ++channel) { + premultiplied[offset + channel] = static_cast( + (static_cast(premultiplied[offset + channel]) * static_cast(alpha)) / 255); + } + } + texels = premultiplied.data(); + } + + dimensions.x = static_cast(runtimeTexture->width); + dimensions.y = static_cast(runtimeTexture->height); + return GenerateTexture({texels, size}, dimensions); + } + // load texels from image source const auto image = get_image(source); if (image.size == 0) {