From 7290649bb863834244ab78dfef3732939040a206 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 9 Jun 2026 00:58:17 -0600 Subject: [PATCH 1/5] Prune old logs & dawn_cache.db entries --- extern/aurora | 2 +- src/dusk/logging.cpp | 123 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 6fa2cbb961..9bc79d649c 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 6fa2cbb961626c1ef1b13eb12a5b3b43d3bde5f0 +Subproject commit 9bc79d649cd54939961f349d36acf8dd6b143be2 diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index b24f54878a..1303515e5a 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -1,12 +1,16 @@ #include "dusk/logging.h" +#include #include #include #include +#include +#include #include #include #include #include #include +#include #include "dusk/io.hpp" #include "tracy/Tracy.hpp" @@ -44,6 +48,9 @@ namespace { // We use this to check if the LogState is destroyed before attempting to acquire it. std::atomic g_logStateAlive(true); std::atomic g_logFd(-1); +constexpr size_t MaxRetainedLogCount = 10; +constexpr size_t MaxRetainedOldLogCount = MaxRetainedLogCount - 1; +constexpr uintmax_t MaxRetainedOldLogBytes = 100ull * 1024ull * 1024ull; struct LogState { std::mutex mutex; @@ -91,6 +98,121 @@ FILE* LogStreamForLevel(AuroraLogLevel level) { return level >= LOG_ERROR ? stderr : stdout; } +struct LogFileCandidate { + std::filesystem::path path; + std::string filename; + uintmax_t size; +}; + +void warn_log_cleanup_failure( + const char* action, const std::filesystem::path& path, const std::error_code& ec) { + std::fprintf(stderr, "[WARNING | dusk] Failed to %s '%s': %s\n", action, + dusk::io::fs_path_to_string(path).c_str(), ec.message().c_str()); +} + +bool is_digit_at(const std::string_view value, size_t index) { + return std::isdigit(static_cast(value[index])) != 0; +} + +bool is_generated_log_file_name(const std::filesystem::path& path) { + const std::string filename = path.filename().string(); + constexpr std::string_view currentPrefix = "dusklight-"sv; + constexpr std::string_view legacyPrefix = "dusk-"sv; + constexpr std::string_view suffix = ".log"sv; + size_t timestampOffset = 0; + + if (filename.starts_with(currentPrefix)) { + timestampOffset = currentPrefix.size(); + } else if (filename.starts_with(legacyPrefix)) { + timestampOffset = legacyPrefix.size(); + } else { + return false; + } + + if (filename.size() != timestampOffset + 19 || !filename.ends_with(suffix) || + filename[timestampOffset + 8] != '-') { + return false; + } + + for (size_t i = timestampOffset; i < timestampOffset + 8; ++i) { + if (!is_digit_at(filename, i)) { + return false; + } + } + for (size_t i = timestampOffset + 9; i < timestampOffset + 15; ++i) { + if (!is_digit_at(filename, i)) { + return false; + } + } + + return true; +} + +void delete_log_file(const std::filesystem::path& path) { + std::error_code ec; + std::filesystem::remove(path, ec); + if (ec) { + warn_log_cleanup_failure("remove old log file", path, ec); + } +} + +void prune_old_log_files(const std::filesystem::path& logsDir) { + std::error_code ec; + std::filesystem::directory_iterator entries{logsDir, ec}; + if (ec) { + warn_log_cleanup_failure("inspect log directory", logsDir, ec); + return; + } + + std::vector candidates; + for (const auto& entry : entries) { + const std::filesystem::path path = entry.path(); + if (!is_generated_log_file_name(path)) { + continue; + } + + ec.clear(); + const auto status = entry.symlink_status(ec); + if (ec) { + warn_log_cleanup_failure("inspect log file", path, ec); + continue; + } + if (!std::filesystem::is_regular_file(status)) { + continue; + } + + ec.clear(); + const uintmax_t size = entry.file_size(ec); + if (ec) { + warn_log_cleanup_failure("inspect size of log file", path, ec); + continue; + } + + candidates.push_back({path, path.filename().string(), size}); + } + + std::sort(candidates.begin(), candidates.end(), + [](const LogFileCandidate& a, const LogFileCandidate& b) { + return a.filename > b.filename; + }); + + const size_t retainedCount = std::min(candidates.size(), MaxRetainedOldLogCount); + uintmax_t retainedBytes = 0; + for (size_t i = 0; i < retainedCount; ++i) { + retainedBytes += candidates[i].size; + } + + size_t retainedAfterSizeLimit = retainedCount; + while (retainedAfterSizeLimit > 0 && retainedBytes > MaxRetainedOldLogBytes) { + --retainedAfterSizeLimit; + retainedBytes -= candidates[retainedAfterSizeLimit].size; + } + + for (size_t i = retainedAfterSizeLimit; i < candidates.size(); ++i) { + delete_log_file(candidates[i].path); + } +} + std::string MakeTimestampedLogName() { const auto now = std::chrono::system_clock::now(); const std::time_t nowTime = std::chrono::system_clock::to_time_t(now); @@ -230,6 +352,7 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL io::fs_path_to_string(logsDir).c_str(), ec.message().c_str()); return; } + prune_old_log_files(logsDir); const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); g_logState.file = io::FileStream::Create(logPath).ToInner(); From a58f9c7b43e133e3a2ae312971542122e1d2b1d8 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 9 Jun 2026 22:51:23 -0600 Subject: [PATCH 2/5] Update Android shell to SDL 3.4.10 --- .../app/HIDDeviceBLESteamController.java | 244 +++++++++++++++--- .../java/org/libsdl/app/HIDDeviceManager.java | 13 +- .../java/org/libsdl/app/HIDDeviceUSB.java | 49 +++- .../main/java/org/libsdl/app/SDLActivity.java | 23 +- .../org/libsdl/app/SDLControllerManager.java | 85 +++++- .../java/org/libsdl/app/SDLSensorManager.java | 32 +++ .../main/java/org/libsdl/app/SDLSurface.java | 21 +- 7 files changed, 411 insertions(+), 56 deletions(-) create mode 100644 platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java index bf1ca2149d..bcd8806c41 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -19,9 +19,13 @@ import android.os.*; import java.lang.Runnable; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.UUID; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { private static final String TAG = "hidapi"; @@ -33,10 +37,19 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private boolean mIsConnected = false; private boolean mIsChromebook = false; private boolean mIsReconnecting = false; + private boolean mHasEnabledNotifications = false; + private boolean mHasSeenInputUpdate = false; private boolean mFrozen = false; private LinkedList mOperations; GattOperation mCurrentOperation = null; private Handler mHandler; + private int mProductId = -1; + private int mReportId = 0; + private UUID mInputCharacteristic; + + private static final int D0G_BLE2_PID = 0x1106; + private static final int TRITON_BLE_PID = 0x1303; + private static final int TRANSPORT_AUTO = 0; private static final int TRANSPORT_BREDR = 1; @@ -45,10 +58,14 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3"); static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + private HashMap mOutputReportChars = new HashMap(); + static class GattOperation { private enum Operation { CHR_READ, @@ -61,6 +78,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe byte[] mValue; BluetoothGatt mGatt; boolean mResult = true; + int mDelayMs = 0; private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { mGatt = gatt; @@ -68,6 +86,13 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mUuid = uuid; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mDelayMs = delayMs; + } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { mGatt = gatt; mOp = operation; @@ -75,6 +100,14 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mValue = value; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + mDelayMs = delayMs; + } + public void run() { // This is executed in main thread BluetoothGattCharacteristic chr; @@ -136,6 +169,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe return mResult; } + public int getDelayMs() { return mDelayMs; } + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { BluetoothGattService valveService = mGatt.getService(steamControllerService); if (valveService == null) @@ -154,6 +189,10 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs); + } } HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { @@ -166,6 +205,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mHandler = new Handler(Looper.getMainLooper()); mGatt = connectGatt(); + mHasEnabledNotifications = false; + mHasSeenInputUpdate = false; // final HIDDeviceBLESteamController finalThis = this; // mHandler.postDelayed(new Runnable() { // @Override @@ -314,8 +355,45 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); + if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) { + Log.v(TAG, "Found Triton input characteristic 0x45"); + mProductId = TRITON_BLE_PID; + mReportId = 0x45; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) { + Log.v(TAG, "Found Triton input characteristic 0x47"); + mProductId = TRITON_BLE_PID; + mReportId = 0x47; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicD0G)) { + Log.v(TAG, "Found D0G input characteristic"); + mProductId = D0G_BLE2_PID; + mReportId = 0x03; + mInputCharacteristic = chr.getUuid(); + } else { + Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE); + Matcher matcher = reportPattern.matcher(chr.getUuid().toString()); + + if (matcher.find()) { + try { + int reportId = Integer.parseInt(matcher.group(1), 16); + + reportId -= 0x35; + if (reportId >= 0x80) { + // This is a Triton output report characteristic that we need to care about. + Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16)); + mOutputReportChars.put(reportId, chr); + } + } + catch (NumberFormatException nfe) { + Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString()); + } + } + } + } + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(mInputCharacteristic)) { // Start notifications BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); if (cccd != null) { @@ -372,21 +450,30 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mCurrentOperation = mOperations.removeFirst(); } - // Run in main thread - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (mOperations) { - if (mCurrentOperation == null) { - Log.e(TAG, "Current operation null in executor?"); - return; - } + Runnable gattOperationRunnable = new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } - mCurrentOperation.run(); - // now wait for the GATT callback and when it comes, finish this operation + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } } - } - }); + }; + + if (mCurrentOperation.getDelayMs() == 0) { + // Run in main thread + mHandler.post(gattOperationRunnable); + } + else { + // If we have a delay on this operation, wait before we post it. + mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs()); + } + } private void queueGattOperation(GattOperation op) { @@ -397,8 +484,39 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe } private void enableNotification(UUID chrUuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + // Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting + // it will return success and then silently drop the operation on the floor. + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500); queueGattOperation(op); + + // Amazon Fire devices can also silently timeout on writeDescriptor, so + // set up a little delayed check that will attempt to write a second time. + // + // While this only seems to be needed on Amazon Fire TV devices at present, it + // doesn't hurt to have a retry on other devices as well. + // + final HIDDeviceBLESteamController finalThis = this; + final UUID finalUuid = chrUuid; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!finalThis.mHasEnabledNotifications) { + + if (finalThis.mHasSeenInputUpdate) { + // Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen + // input reports, though, somewhat by definition notifications are enabled. + Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization."); + finalThis.mHasEnabledNotifications = true; + finalThis.enableValveMode(); + return; + } + + // Give one more try. + GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500); + finalThis.queueGattOperation(retry); + } + } + }, 1000); } void writeCharacteristic(UUID uuid, byte[] value) { @@ -448,8 +566,16 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mIsConnected = false; gatt.disconnect(); mGatt = connectGatt(false); - } - else { + } else { + if (getProductId() == TRITON_BLE_PID) { + // Android will not properly play well with Data Length Extensions without manually requesting a large MTU, + // and Triton controllers require DLE support. + // + // 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change + // this value. It is functionally "please enable data length extensions" on some Android builds. + mGatt.requestMtu(517); + } + probeService(this); } } @@ -474,7 +600,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // Only register controller with the native side once it has been fully configured if (!isRegistered()) { Log.v(TAG, "Registering Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); setRegistered(); } } @@ -487,7 +613,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // Enable this for verbose logging of controller input reports //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) { + mHasSeenInputUpdate = true; mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); } } @@ -497,19 +624,36 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe //Log.v(TAG, "onDescriptorRead status=" + status); } + private void enableValveMode() + { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return; + + BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic); + if (reportChr != null) { + if (getProductId() == TRITON_BLE_PID) { + // For Triton we just mark things registered. + Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); + setRegistered(); + } else { + // For the original controller, we need to manually enter Valve mode. + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + mGatt.writeCharacteristic(reportChr); + } + } + } + @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); - if (chr.getUuid().equals(inputCharacteristic)) { - boolean hasWrittenInputDescriptor = true; - BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); - if (reportChr != null) { - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); - } + if (chr.getUuid().equals(mInputCharacteristic)) { + mHasEnabledNotifications = true; + enableValveMode(); } finishCurrentGattOperation(); @@ -548,9 +692,20 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe @Override public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; + if (mProductId > 0) { + // We've already set a product ID. + return mProductId; + } + + if (mDevice.getName().startsWith("Steam Ctrl")) { + // We're a newer Triton device + mProductId = TRITON_BLE_PID; + } else { + // We're an OG Steam Controller + mProductId = D0G_BLE2_PID; + } + + return mProductId; } @Override @@ -601,10 +756,29 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe writeCharacteristic(reportCharacteristic, actual_report); return report.length; } else { - //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; + // If we're an original-recipe Steam Controller we just write to the characteristic directly. + if (getProductId() == D0G_BLE2_PID) { + //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + // If we're a Triton, we need to find the correct report characteristic. + if (report.length > 0) { + int reportId = report[0] & 0xFF; + BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId); + if (targetedReportCharacteristic != null) { + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report)); + writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report); + return report.length; + } else { + Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16)); + } + } } + + return -1; } @Override diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index 1fb2bfb4a7..691416c1c9 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -256,6 +256,7 @@ public class HIDDeviceManager { 0x24c6, // PowerA 0x2c22, // Qanba 0x2dc8, // 8BitDo + 0x3537, // GameSir 0x37d7, // Flydigi 0x9886, // ASTRO Gaming }; @@ -360,7 +361,7 @@ public class HIDDeviceManager { HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); int id = device.getId(); mDevicesById.put(id, device); - HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0); } } } @@ -529,7 +530,13 @@ public class HIDDeviceManager { return false; } - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + // Steam Controllers will always support Bluetooth Low Energy + if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) { + return false; + } + + // Match on the name either the original Steam Controller or the new second-generation one advertise with. + return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl"); } private void close() { @@ -681,7 +688,7 @@ public class HIDDeviceManager { private native void HIDDeviceRegisterCallback(); private native void HIDDeviceReleaseCallback(); - native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth); + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID); native void HIDDeviceOpenPending(int deviceID); native void HIDDeviceOpenResult(int deviceID, boolean opened); native void HIDDeviceDisconnected(int deviceID); diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java index f9e9389802..8954639733 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -21,6 +21,7 @@ class HIDDeviceUSB implements HIDDevice { protected InputThread mInputThread; protected boolean mRunning; protected boolean mFrozen; + protected boolean mClaimed; public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { mManager = manager; @@ -29,6 +30,7 @@ class HIDDeviceUSB implements HIDDevice { mInterface = mDevice.getInterface(mInterfaceIndex).getId(); mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); mRunning = false; + mClaimed = false; } String getIdentifier() { @@ -114,6 +116,7 @@ class HIDDeviceUSB implements HIDDevice { close(); return false; } + mClaimed = true; // Find the endpoints for (int j = 0; j < iface.getEndpointCount(); j++) { @@ -132,9 +135,12 @@ class HIDDeviceUSB implements HIDDevice { } } - // Make sure the required endpoints were present - if (mInputEndpoint == null || mOutputEndpoint == null) { + // Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT + // actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall + // back to the Android system gamepad functionality (and lose our paddles et al). + if (mInputEndpoint == null) { Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + mConnection.releaseInterface(iface); close(); return false; } @@ -154,6 +160,11 @@ class HIDDeviceUSB implements HIDDevice { return -1; } + if (!mClaimed) { + Log.w(TAG, "writeReport() called but some other process currently owns the USB device"); + return -1; + } + if (feature) { int res = -1; int offset = 0; @@ -185,6 +196,11 @@ class HIDDeviceUSB implements HIDDevice { } return length; } else { + if (mOutputEndpoint == null) + { + Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!"); + return -1; + } int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); if (res != report.length) { Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); @@ -205,6 +221,12 @@ class HIDDeviceUSB implements HIDDevice { Log.w(TAG, "readReport() called with no device connection"); return false; } + if (!mClaimed) { + if (feature) { + return false; + } + return true; + } if (report_number == 0x0) { /* Offset the return buffer by 1, so that the report ID @@ -258,10 +280,13 @@ class HIDDeviceUSB implements HIDDevice { mInputThread = null; } if (mConnection != null) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); + if (mClaimed) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + mConnection.releaseInterface(iface); + } mConnection.close(); mConnection = null; + mClaimed = false; } } @@ -274,6 +299,22 @@ class HIDDeviceUSB implements HIDDevice { @Override public void setFrozen(boolean frozen) { mFrozen = frozen; + + /* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */ + if (mConnection != null && mClaimed == mFrozen) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + if (frozen) { + mClaimed = !mConnection.releaseInterface(iface); + if (mClaimed) { + Log.e(TAG, "Tried to release claim on USB device, but failed!"); + } + } else { + mClaimed = mConnection.claimInterface(iface, true); + if (!mClaimed) { + Log.e(TAG, "Tried to regain claim on USB device, but failed!"); + } + } + } } protected class InputThread extends Thread { diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 5c54cde863..dcc49852ac 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MINOR_VERSION = 4; - private static final int SDL_MICRO_VERSION = 8; + private static final int SDL_MICRO_VERSION = 10; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -530,7 +530,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(true); - } + } + if (!mHasMultiWindow) { pauseNativeThread(); } @@ -543,7 +544,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(false); - } + } + if (!mHasMultiWindow) { resumeNativeThread(); } @@ -616,6 +618,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh super.onWindowFocusChanged(hasFocus); Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + // If we are gaining focus, we can always try to restore our USB devices. If we are losing focus, + // only try to relinquish them if we don't have background events allowed (for multi-window Android setups). + if (hasFocus || !SDLActivity.nativeGetHintBoolean("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", false)) { + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(!hasFocus); + } + } + if (SDLActivity.mBrokenLibraries) { return; } @@ -1481,11 +1491,11 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode, event.getScanCode())) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode, event.getScanCode())) { return true; } } @@ -1963,7 +1973,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); - int flags = Intent.FLAG_ACTIVITY_NO_HISTORY + int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT; i.addFlags(flags); @@ -2227,3 +2237,4 @@ class SDLClipboardHandler implements SDLActivity.onNativeClipboardChanged(); } } + diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java index 7655ecfd6f..8681d050b7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -10,6 +10,10 @@ import android.hardware.lights.Light; import android.hardware.lights.LightsRequest; import android.hardware.lights.LightsManager; import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; @@ -30,16 +34,18 @@ public class SDLControllerManager static native void nativeAddJoystick(int device_id, String name, String desc, int vendor_id, int product_id, int button_mask, - int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led); + int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led, + boolean has_accelerometer, boolean has_gyroscope); static native void nativeRemoveJoystick(int device_id); static native void nativeAddHaptic(int device_id, String name); static native void nativeRemoveHaptic(int device_id); - static public native boolean onNativePadDown(int device_id, int keycode); - static public native boolean onNativePadUp(int device_id, int keycode); + static public native boolean onNativePadDown(int device_id, int keycode, int scancode); + static public native boolean onNativePadUp(int device_id, int keycode, int scancode); static native void onNativeJoy(int device_id, int axis, float value); static native void onNativeHat(int device_id, int hat_id, int x, int y); + static native void onNativeJoySensor(int device_id, int sensor_type, long sensor_timestamp, float x, float y, float z); protected static SDLJoystickHandler mJoystickHandler; protected static SDLHapticHandler mHapticHandler; @@ -81,6 +87,13 @@ public class SDLControllerManager mJoystickHandler.setLED(device_id, red, green, blue); } + /** + * This method is called by SDL using JNI. + */ + static void joystickSetSensorsEnabled(int device_id, boolean enabled) { + mJoystickHandler.setSensorsEnabled(device_id, enabled); + } + /** * This method is called by SDL using JNI. */ @@ -153,6 +166,10 @@ class SDLJoystickHandler { ArrayList hats; ArrayList lights; LightsManager.LightsSession lightsSession; + SensorManager sensorManager; + SDLJoySensorListener sensorListener; + Sensor accelerometerSensor; + Sensor gyroscopeSensor; } static class RangeComparator implements Comparator { @Override @@ -225,12 +242,13 @@ class SDLJoystickHandler { joystick.desc = getJoystickDescriptor(joystickDevice); joystick.axes = new ArrayList(); joystick.hats = new ArrayList(); + java.util.Set axisStrsSet = new java.util.HashSet(); joystick.lights = new ArrayList(); List ranges = joystickDevice.getMotionRanges(); Collections.sort(ranges, new RangeComparator()); for (InputDevice.MotionRange range : ranges) { - if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + if (((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) && axisStrsSet.add(range.getAxis())) { if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { joystick.hats.add(range); } else { @@ -241,6 +259,8 @@ class SDLJoystickHandler { boolean can_rumble = false; boolean has_rgb_led = false; + boolean has_accelerometer = false; + boolean has_gyroscope = false; if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { VibratorManager vibratorManager = joystickDevice.getVibratorManager(); int[] vibrators = vibratorManager.getVibratorIds(); @@ -258,12 +278,26 @@ class SDLJoystickHandler { joystick.lightsSession = lightsManager.openSession(); has_rgb_led = true; } + SensorManager sensorManager = joystickDevice.getSensorManager(); + if (sensorManager != null) { + joystick.sensorManager = sensorManager; + joystick.sensorListener = new SDLJoySensorListener(joystick.device_id); + joystick.accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (joystick.accelerometerSensor != null) { + has_accelerometer = true; + } + joystick.gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (joystick.gyroscopeSensor != null) { + has_gyroscope = true; + } + } } mJoysticks.add(joystick); SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, getVendorId(joystickDevice), getProductId(joystickDevice), - getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led); + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led, + has_accelerometer, has_gyroscope); } } } @@ -508,6 +542,31 @@ class SDLJoystickHandler { } joystick.lightsSession.requestLights(lightsRequest.build()); } + + void setSensorsEnabled(int device_id, boolean enabled) { + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + return; + } + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null || joystick.sensorManager == null) { + return; + } + if (enabled) { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor, SensorManager.SENSOR_DELAY_GAME); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor, SensorManager.SENSOR_DELAY_GAME); + } + } else { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor); + } + } + } } class SDLHapticHandler_API31 extends SDLHapticHandler { @@ -933,3 +992,19 @@ class SDLGenericMotionListener_API29 extends SDLGenericMotionListener_API26 { return penDevice.isExternal() ? SDL_PEN_DEVICE_TYPE_INDIRECT : SDL_PEN_DEVICE_TYPE_DIRECT; } } + +class SDLJoySensorListener implements SensorEventListener { + int device_id; + + public SDLJoySensorListener(int device_id) { + this.device_id = device_id; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + @Override + public void onSensorChanged(SensorEvent event) { + SDLControllerManager.onNativeJoySensor(device_id, event.sensor.getType(), event.timestamp, event.values[0], event.values[1], event.values[2]); + } +} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java new file mode 100644 index 0000000000..586e3fab6e --- /dev/null +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java @@ -0,0 +1,32 @@ +package org.libsdl.app; + +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +// This class coordinates synchronized access to sensor manager registration +// +// This prevents a java.util.ConcurrentModificationException exception on +// Android 16, specifically on the Samsung Tab S9 Ultra. + +class SDLSensorManager +{ + static private SDLSensorManager mManager = new SDLSensorManager(); + + public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs); + } + + public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + mManager.UnregisterListener(manager, listener, sensor); + } + + private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + manager.registerListener(listener, sensor, samplingPeriodUs, null); + } + + private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + manager.unregisterListener(listener, sensor); + } +} + diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java index 5ed335ac39..8d56658958 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -47,6 +47,9 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, // Is SurfaceView ready for rendering protected boolean mIsSurfaceReady; + // Is on-screen keyboard visible + protected boolean mKeyboardVisible; + // Pinch events private final ScaleGestureDetector scaleGestureDetector; @@ -213,6 +216,18 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, WindowInsets.Type.displayCutout()); SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); + + if (insets.isVisible(WindowInsets.Type.ime())) { + if (!mKeyboardVisible) { + mKeyboardVisible = true; + SDLActivity.onNativeScreenKeyboardShown(); + } + } else { + if (mKeyboardVisible) { + mKeyboardVisible = false; + SDLActivity.onNativeScreenKeyboardHidden(); + } + } } // Pass these to any child views in case they need them @@ -318,11 +333,11 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, protected void enableSensor(int sensortype, boolean enabled) { // TODO: This uses getDefaultSensor - what if we have >1 accels? if (enabled) { - mSensorManager.registerListener(this, + SDLSensorManager.registerListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype), - SensorManager.SENSOR_DELAY_GAME, null); + SensorManager.SENSOR_DELAY_GAME); } else { - mSensorManager.unregisterListener(this, + SDLSensorManager.unregisterListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype)); } } From 131a09f317b479cef9375a84c7aafd9ceced860f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 9 Jun 2026 23:47:45 -0600 Subject: [PATCH 3/5] Implement JPA particle batching --- extern/aurora | 2 +- .../include/JSystem/JParticle/JPABaseShape.h | 65 ++-- .../include/JSystem/JParticle/JPAResource.h | 28 +- libs/JSystem/src/JParticle/JPABaseShape.cpp | 299 ++++++++++++++++-- libs/JSystem/src/JParticle/JPAResource.cpp | 281 +++++++++++++++- 5 files changed, 616 insertions(+), 59 deletions(-) diff --git a/extern/aurora b/extern/aurora index 9bc79d649c..5143394381 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 9bc79d649cd54939961f349d36acf8dd6b143be2 +Subproject commit 514339438178ef2bed1b14e5149d90ece0c6e0cc diff --git a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h index 795a5a2628..ec15c0430a 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h +++ b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h @@ -3,6 +3,20 @@ #include +#if TARGET_PC +#include + +struct ParticleDrawCtx { + bool batch; // off = immediate mode + bool useTexMtx; // UVs transformed by texMtx + bool useClr0; // prm color in GX_VA_CLR0 + bool useClr1; // env color in GX_VA_CLR1 + Mtx texMtx; + GXColor clr0; + GXColor clr1; +}; +#endif + struct JPAEmitterWorkData; class JPABaseParticle; class JKRHeap; @@ -75,6 +89,9 @@ public: const GXTevColorArg* getTevColorArg() const { return st_ca[(pBsd->mFlags >> 0x0F) & 0x07]; } const GXTevAlphaArg* getTevAlphaArg() const { return st_aa[(pBsd->mFlags >> 0x12) & 0x01]; } +#if TARGET_PC + u32 getTevColorArgSel() const { return (pBsd->mFlags >> 0x0F) & 0x07; } +#endif u32 getType() const { return (pBsd->mFlags >> 0) & 0x0F; } u32 getDirType() const { return (pBsd->mFlags >> 4) & 0x07; } @@ -186,26 +203,34 @@ void JPARegistPrm(JPAEmitterWorkData*); void JPARegistEnv(JPAEmitterWorkData*); void JPARegistPrmEnv(JPAEmitterWorkData*); -void JPADrawPoint(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawLine(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotDirection(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawDirection(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotation(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawDBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotYBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawYBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawParticleCallBack(JPAEmitterWorkData*, JPABaseParticle*); -void JPALoadTexAnm(JPAEmitterWorkData*, JPABaseParticle*); -void JPASetPointSize(JPAEmitterWorkData*, JPABaseParticle*); -void JPASetLineWidth(JPAEmitterWorkData*, JPABaseParticle*); -void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistAlpha(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistEnv(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistPrmAlpha(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistPrmAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); +#if TARGET_PC +#define JPA_DRAW_PARTICLE_ARGS JPAEmitterWorkData*, JPABaseParticle*, ParticleDrawCtx* +#else +#define JPA_DRAW_PARTICLE_ARGS JPAEmitterWorkData*, JPABaseParticle* +#endif + +void JPADrawPoint(JPA_DRAW_PARTICLE_ARGS); +void JPADrawLine(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotDirection(JPA_DRAW_PARTICLE_ARGS); +void JPADrawDirection(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotation(JPA_DRAW_PARTICLE_ARGS); +void JPADrawDBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotYBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawYBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawParticleCallBack(JPA_DRAW_PARTICLE_ARGS); +void JPALoadTexAnm(JPA_DRAW_PARTICLE_ARGS); +void JPASetPointSize(JPA_DRAW_PARTICLE_ARGS); +void JPASetLineWidth(JPA_DRAW_PARTICLE_ARGS); +void JPALoadCalcTexCrdMtxAnm(JPA_DRAW_PARTICLE_ARGS); +void JPARegistAlpha(JPA_DRAW_PARTICLE_ARGS); +void JPARegistEnv(JPA_DRAW_PARTICLE_ARGS); +void JPARegistAlphaEnv(JPA_DRAW_PARTICLE_ARGS); +void JPARegistPrmAlpha(JPA_DRAW_PARTICLE_ARGS); +void JPARegistPrmAlphaEnv(JPA_DRAW_PARTICLE_ARGS); + +#undef JPA_DRAW_PARTICLE_ARGS #if TARGET_PC void JPAInterpBillboard(JPAEmitterWorkData*, JPABaseParticle*); diff --git a/libs/JSystem/include/JSystem/JParticle/JPAResource.h b/libs/JSystem/include/JSystem/JParticle/JPAResource.h index ebf2127033..07563fa73d 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPAResource.h +++ b/libs/JSystem/include/JSystem/JParticle/JPAResource.h @@ -17,6 +17,10 @@ class JPADynamicsBlock; class JPAFieldBlock; class JPAKeyBlock; +#if TARGET_PC +struct ParticleDrawCtx; +#endif + /** * @ingroup jsystem-jparticle * @@ -50,13 +54,19 @@ public: public: typedef void (*EmitterFunc)(JPAEmitterWorkData*); typedef void (*ParticleFunc)(JPAEmitterWorkData*, JPABaseParticle*); +#if TARGET_PC + typedef void (*DrawParticleFunc)(JPAEmitterWorkData*, JPABaseParticle*, + ParticleDrawCtx*); +#else + typedef ParticleFunc DrawParticleFunc; +#endif /* 0x00 */ EmitterFunc* mpCalcEmitterFuncList; /* 0x04 */ EmitterFunc* mpDrawEmitterFuncList; /* 0x08 */ EmitterFunc* mpDrawEmitterChildFuncList; /* 0x0C */ ParticleFunc* mpCalcParticleFuncList; - /* 0x10 */ ParticleFunc* mpDrawParticleFuncList; + /* 0x10 */ DrawParticleFunc* mpDrawParticleFuncList; /* 0x14 */ ParticleFunc* mpCalcParticleChildFuncList; - /* 0x18 */ ParticleFunc* mpDrawParticleChildFuncList; + /* 0x18 */ DrawParticleFunc* mpDrawParticleChildFuncList; /* 0x1C */ JPABaseShape* pBsp; /* 0x20 */ JPAExtraShape* pEsp; @@ -77,6 +87,20 @@ public: /* 0x45 */ u8 mpDrawParticleFuncListNum; /* 0x46 */ u8 mpCalcParticleChildFuncListNum; /* 0x47 */ u8 mpDrawParticleChildFuncListNum; + +#if TARGET_PC + struct BatchInfo { + f32 vtxPos[8][3]; + f32 vtxUv[8][2]; + u8 vtxCount; // 4 (quad) or 8 (cross) + bool supported; // draw func list contains only batchable funcs + bool hasPtclColor; // per-particle JPARegist* func is present + bool hasPtclTexMtx; // JPALoadCalcTexCrdMtxAnm is present + }; + BatchInfo mBatchInfo; + + void initBatchInfo(); +#endif }; #endif /* JPARESOURCE_H */ diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index add61135fe..a569d96842 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -14,6 +14,33 @@ #endif #include "tracy/Tracy.hpp" +#if TARGET_PC +#define JPA_DRAW_CTX_PARAM , ParticleDrawCtx* ctx + +namespace { +GXColor emitter_prm_color(JPAEmitterWorkData* work) { + JPABaseEmitter* emtr = work->mpEmtr; + GXColor prm = emtr->mPrmClr; + prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); + prm.g = COLOR_MULTI(prm.g, emtr->mGlobalPrmClr.g); + prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); + prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); + return prm; +} + +GXColor emitter_env_color(JPAEmitterWorkData* work) { + JPABaseEmitter* emtr = work->mpEmtr; + GXColor env = emtr->mEnvClr; + env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); + env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); + env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); + return env; +} +} // namespace +#else +#define JPA_DRAW_CTX_PARAM +#endif + void JPASetPointSize(JPAEmitterWorkData* work) { GXSetPointSize((u8)(25.0f * work->mGlobalPtclScl.x), GX_TO_ONE); } @@ -22,15 +49,16 @@ void JPASetLineWidth(JPAEmitterWorkData* work) { GXSetLineWidth((u8)(25.0f * work->mGlobalPtclScl.x), GX_TO_ONE); } -void JPASetPointSize(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPASetPointSize(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { GXSetPointSize((u8)(ptcl->mParticleScaleX * (25.0f * work->mGlobalPtclScl.x)), GX_TO_ONE); } -void JPASetLineWidth(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPASetLineWidth(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { GXSetLineWidth((u8)(ptcl->mParticleScaleX * (25.0f * work->mGlobalPtclScl.x)), GX_TO_ONE); } void JPARegistPrm(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); @@ -41,6 +69,7 @@ void JPARegistPrm(JPAEmitterWorkData* work) { } void JPARegistEnv(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor env = emtr->mEnvClr; env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); @@ -50,6 +79,7 @@ void JPARegistEnv(JPAEmitterWorkData* work) { } void JPARegistPrmEnv(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; GXColor env = emtr->mEnvClr; @@ -64,7 +94,8 @@ void JPARegistPrmEnv(JPAEmitterWorkData* work) { GXSetTevColor(GX_TEVREG1, env); } -void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); @@ -72,10 +103,19 @@ void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); prm.a = COLOR_MULTI(prm.a, ptcl->mPrmColorAlphaAnm); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + if (ctx->useClr1) { + ctx->clr1 = emitter_env_color(work); + } + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); } -void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; @@ -84,10 +124,19 @@ void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); prm.a = COLOR_MULTI(prm.a, ptcl->mPrmColorAlphaAnm); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + if (ctx->useClr1) { + ctx->clr1 = emitter_env_color(work); + } + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); } -void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; @@ -100,11 +149,19 @@ void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); GXSetTevColor(GX_TEVREG1, env); } -void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; GXColor env = ptcl->mEnvClr; @@ -116,16 +173,31 @@ void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); GXSetTevColor(GX_TEVREG1, env); } -void JPARegistEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor env = ptcl->mEnvClr; env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = emitter_prm_color(work); + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG1, env); } @@ -258,7 +330,7 @@ void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0); } -void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); f32 dVar16 = param_1->mAge; @@ -286,6 +358,12 @@ void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) local_108[2][1] = 0.0f; local_108[2][2] = 1.0f; local_108[2][3] = 0.0f; +#if TARGET_PC + if (ctx->batch) { + MTXCopy(local_108, ctx->texMtx); + return; + } +#endif GXLoadTexMtxImm(local_108, 0x1e, GX_MTX2x4); } @@ -299,7 +377,7 @@ void JPALoadTexAnm(JPAEmitterWorkData* work) { work->mpResMgr->load(work->mpRes->getTexIdx(work->mpEmtr->mTexAnmIdx), GX_TEXMAP0); } -void JPALoadTexAnm(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPALoadTexAnm(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; work->mpResMgr->load(work->mpRes->getTexIdx(ptcl->mTexAnmIdx), GX_TEXMAP0); } @@ -429,6 +507,47 @@ static projectionFunc p_prj[3] = { }; #if TARGET_PC +static void emit_batch_quad(JPAEmitterWorkData* work, const ParticleDrawCtx* ctx, + const Mtx posMtx) { + const JPAResource::BatchInfo& info = work->mpRes->mBatchInfo; + + for (int i = 0; i < info.vtxCount; i++) { + Vec localPos = {info.vtxPos[i][0], info.vtxPos[i][1], info.vtxPos[i][2]}; + Vec drawPos; + MTXMultVec(posMtx, &localPos, &drawPos); + + f32 texS = info.vtxUv[i][0]; + f32 texT = info.vtxUv[i][1]; + if (ctx->useTexMtx) { + f32 srcS = texS; + f32 srcT = texT; + texS = ctx->texMtx[0][0] * srcS + ctx->texMtx[0][1] * srcT + ctx->texMtx[0][3]; + texT = ctx->texMtx[1][0] * srcS + ctx->texMtx[1][1] * srcT + ctx->texMtx[1][3]; + } + + GXPosition3f32(drawPos.x, drawPos.y, drawPos.z); + if (ctx->useClr0) { + GXColor4u8(ctx->clr0.r, ctx->clr0.g, ctx->clr0.b, ctx->clr0.a); + } + if (ctx->useClr1) { + GXColor4u8(ctx->clr1.r, ctx->clr1.g, ctx->clr1.b, ctx->clr1.a); + } + GXTexCoord2f32(texS, texT); + } +} + +static void submit_particle_quad( + JPAEmitterWorkData* work, ParticleDrawCtx* ctx, const Mtx posMtx, const u8* dl, u32 dlSize) { + if (ctx->batch) { + emit_batch_quad(work, ctx, posMtx); + return; + } + + GXLoadPosMtxImm(posMtx, GX_PNMTX0); + p_prj[work->mPrjType](work, posMtx); + GXCallDisplayList(dl, dlSize); +} + void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx ptclPosMtx; MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); @@ -448,7 +567,7 @@ void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } #endif -void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -473,12 +592,16 @@ void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][2] = 1.0f; posMtx[2][3] = pos.z; posMtx[0][1] = posMtx[0][2] = posMtx[1][0] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -517,12 +640,16 @@ void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][2] = 1.0f; posMtx[2][3] = pos.z; posMtx[0][2] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -542,12 +669,16 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][2] = work->mYBBCamMtx[2][2]; local_38[2][3] = local_48.z; local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[2][0] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, local_38, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -576,9 +707,13 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][1] = local_94 * fVar1; local_38[2][2] = local_90; local_38[2][3] = local_48.z; +#if TARGET_PC + submit_particle_quad(work, ctx, local_38, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } void dirTypeVel(JPAEmitterWorkData const* work, JPABaseParticle const* param_1, @@ -741,6 +876,88 @@ static u8* p_dl[2] = { }; #if TARGET_PC +static bool make_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); + p_direction[work->mDirType](work, ptcl, &axisY); + if (axisY.isZero()) { + return false; + } + + axisY.normalize(); + axisZ.cross(baseAxis, axisY); + if (axisZ.isZero()) { + return false; + } + + axisZ.normalize(); + baseAxis.cross(axisY, axisZ); + baseAxis.normalize(); + ptcl->mBaseAxis.set(baseAxis); + + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + posMtx[0][0] = baseAxis.x; + posMtx[0][1] = axisY.x; + posMtx[0][2] = axisZ.x; + posMtx[0][3] = ptcl->mPosition.x; + posMtx[1][0] = baseAxis.y; + posMtx[1][1] = axisY.y; + posMtx[1][2] = axisZ.y; + posMtx[1][3] = ptcl->mPosition.y; + posMtx[2][0] = baseAxis.z; + posMtx[2][1] = axisY.z; + posMtx[2][2] = axisZ.z; + posMtx[2][3] = ptcl->mPosition.z; + p_plane[work->mPlaneType](posMtx, scaleX, scaleY); + return true; +} + +static bool make_rot_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { + f32 sinRot = JMASSin(ptcl->mRotateAngle); + f32 cosRot = JMASCos(ptcl->mRotateAngle); + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); + p_direction[work->mDirType](work, ptcl, &axisY); + if (axisY.isZero()) { + return false; + } + + axisY.normalize(); + axisZ.cross(baseAxis, axisY); + if (axisZ.isZero()) { + return false; + } + + axisZ.normalize(); + baseAxis.cross(axisY, axisZ); + baseAxis.normalize(); + ptcl->mBaseAxis.set(baseAxis); + + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + Mtx rotMtx; + Mtx dirMtx; + p_rot[work->mRotType](sinRot, cosRot, rotMtx); + p_plane[work->mPlaneType](rotMtx, scaleX, scaleY); + dirMtx[0][0] = baseAxis.x; + dirMtx[0][1] = axisY.x; + dirMtx[0][2] = axisZ.x; + dirMtx[0][3] = ptcl->mPosition.x; + dirMtx[1][0] = baseAxis.y; + dirMtx[1][1] = axisY.y; + dirMtx[1][2] = axisZ.y; + dirMtx[1][3] = ptcl->mPosition.y; + dirMtx[2][0] = baseAxis.z; + dirMtx[2][1] = axisY.z; + dirMtx[2][2] = axisZ.z; + dirMtx[2][3] = ptcl->mPosition.z; + MTXConcat(dirMtx, rotMtx, posMtx); + return true; +} + void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { JGeometry::TVec3 axisY; JGeometry::TVec3 axisZ; @@ -823,7 +1040,7 @@ void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } #endif -void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -832,8 +1049,12 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx posMtx; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx)) -#endif + if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx) && + !make_direction_mtx(work, ptcl, posMtx)) + { + return; + } +#else { JGeometry::TVec3 axisY; JGeometry::TVec3 axisZ; @@ -869,14 +1090,19 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][3] = ptcl->mPosition.z; p_plane[work->mPlaneType](posMtx, scaleX, scaleY); } +#endif MTXConcat(work->mPosCamMtx, posMtx, posMtx); +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, p_dl[work->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -886,8 +1112,12 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx mtx1; Mtx mtx2; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1)) -#endif + if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1) && + !make_rot_direction_mtx(work, ptcl, mtx1)) + { + return; + } +#else { f32 sinRot = JMASSin(ptcl->mRotateAngle); f32 cosRot = JMASCos(ptcl->mRotateAngle); @@ -927,13 +1157,18 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { mtx2[2][3] = ptcl->mPosition.z; MTXConcat(mtx2, mtx1, mtx1); } +#endif MTXConcat(work->mPosCamMtx, mtx1, mtx2); +#if TARGET_PC + submit_particle_quad(work, ctx, mtx2, p_dl[work->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(mtx2, GX_PNMTX0); p_prj[work->mPrjType](work, mtx2); GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -970,7 +1205,7 @@ void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } -void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -988,12 +1223,16 @@ void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { auStack_88[1][3] = param_1->mPosition.y; auStack_88[2][3] = param_1->mPosition.z; MTXConcat(param_0->mPosCamMtx, auStack_88, auStack_88); +#if TARGET_PC + submit_particle_quad(param_0, ctx, auStack_88, p_dl[param_0->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(auStack_88, 0); p_prj[param_0->mPrjType](param_0, auStack_88); GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -1010,7 +1249,7 @@ void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); } -void JPADrawLine(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawLine(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -1086,7 +1325,7 @@ void JPADrawStripe(JPAEmitterWorkData* param_0) { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1111,7 +1350,7 @@ void JPADrawStripe(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_f8, local_104); particle->mBaseAxis.normalize(); - + local_c8[0][0] = local_104.x; local_c8[0][1] = local_f8.x; local_c8[0][2] = particle->mBaseAxis.x; @@ -1177,7 +1416,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1202,7 +1441,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_c0, local_cc); particle->mBaseAxis.normalize(); - + local_90[0][0] = local_cc.x; local_90[0][1] = local_c0.x; local_90[0][2] = particle->mBaseAxis.x; @@ -1227,7 +1466,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { coord = start_coord; GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1252,7 +1491,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_c0, local_cc); particle->mBaseAxis.normalize(); - + local_90[0][0] = local_cc.x; local_90[0][1] = local_c0.x; local_90[0][2] = particle->mBaseAxis.x; @@ -1289,7 +1528,7 @@ void JPADrawEmitterCallBackB(JPAEmitterWorkData* work) { emtr->mpEmtrCallBack->draw(emtr); } -void JPADrawParticleCallBack(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawParticleCallBack(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { JPABaseEmitter* emtr = work->mpEmtr; if (emtr->mpPtclCallBack == NULL) { return; diff --git a/libs/JSystem/src/JParticle/JPAResource.cpp b/libs/JSystem/src/JParticle/JPAResource.cpp index 59e679d449..2d5d872bac 100644 --- a/libs/JSystem/src/JParticle/JPAResource.cpp +++ b/libs/JSystem/src/JParticle/JPAResource.cpp @@ -18,9 +18,21 @@ #include "global.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#define JPA_DRAW_CTX_ARG , &ctx +#else +#define JPA_DRAW_CTX_ARG +#endif + JPAResource::JPAResource() { mpCalcEmitterFuncList = mpDrawEmitterFuncList = mpDrawEmitterChildFuncList = NULL; +#if TARGET_PC + mpCalcParticleFuncList = mpCalcParticleChildFuncList = NULL; + mpDrawParticleFuncList = mpDrawParticleChildFuncList = NULL; + mBatchInfo = {}; +#else mpCalcParticleFuncList = mpDrawParticleFuncList = mpCalcParticleChildFuncList = mpDrawParticleChildFuncList = NULL; +#endif pBsp = NULL; pEsp = NULL; pCsp = NULL; @@ -61,6 +73,60 @@ static u8 jpa_crd[32] ATTRIBUTE_ALIGN(32) = { 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x02, }; +#if TARGET_PC +void JPAResource::initBatchInfo() { + mBatchInfo = {}; + + bool hasDrawFunc = false; + for (int i = 0; i < mpDrawParticleFuncListNum; i++) { + DrawParticleFunc func = mpDrawParticleFuncList[i]; + if (func == JPADrawBillboard || func == JPADrawRotBillboard || + func == JPADrawYBillboard || func == JPADrawRotYBillboard || + func == JPADrawDirection || func == JPADrawRotDirection || func == JPADrawRotation) + { + hasDrawFunc = true; + } else if (func == JPADrawParticleCallBack) { + // Batchable only for emitters without a particle callback; checked per draw + } else if (func == JPALoadCalcTexCrdMtxAnm) { + mBatchInfo.hasPtclTexMtx = true; + } else if (func == JPARegistAlpha || func == JPARegistPrmAlpha || + func == JPARegistPrmAlphaEnv || func == JPARegistAlphaEnv || + func == static_cast(JPARegistEnv)) // overloaded + { + mBatchInfo.hasPtclColor = true; + } else { + // JPADrawPoint, JPADrawLine, JPADrawDBillboard, JPALoadTexAnm, + // JPASetPointSize, JPASetLineWidth + return; + } + } + if (!hasDrawFunc) { + return; + } + + // Template array offsets, same math as setPTev + int base_plane_type = (pBsp->getType() == 3 || pBsp->getType() == 7) ? + pBsp->getBasePlaneType() : 0; + int center_offset = pEsp != nullptr ? (pEsp->getScaleCenterX() + 3 * pEsp->getScaleCenterY()) * 0xC : 0x30; + const s8* pos = reinterpret_cast(jpa_pos) + center_offset + base_plane_type * 0x6C; + const s8* crd = reinterpret_cast(jpa_crd) + (pBsp->getTilingS() + 2 * pBsp->getTilingT()) * 8; + + bool cross = pBsp->getType() == 4 || pBsp->getType() == 8; + mBatchInfo.vtxCount = cross ? 8 : 4; + for (int i = 0; i < mBatchInfo.vtxCount; i++) { + int posIdx = i < 4 ? i : 72 + (i - 4); + int crdIdx = i & 3; + mBatchInfo.vtxPos[i][0] = pos[posIdx * 3 + 0]; + mBatchInfo.vtxPos[i][1] = pos[posIdx * 3 + 1]; + mBatchInfo.vtxPos[i][2] = pos[posIdx * 3 + 2]; + mBatchInfo.vtxUv[i][0] = crd[crdIdx * 2 + 0]; + mBatchInfo.vtxUv[i][1] = crd[crdIdx * 2 + 1]; + } + + mBatchInfo.supported = true; +} +#endif + void JPAResource::init(JKRHeap* heap) { BOOL is_glbl_clr_anm = pBsp->isGlblClrAnm(); BOOL is_glbl_tex_anm = pBsp->isGlblTexAnm(); @@ -525,7 +591,10 @@ void JPAResource::init(JKRHeap* heap) { if (mpDrawParticleFuncListNum != 0) { mpDrawParticleFuncList = - (ParticleFunc*)JKRAllocFromHeap(heap, mpDrawParticleFuncListNum * sizeof(ParticleFunc), alignof(ParticleFunc)); + (DrawParticleFunc*)JKRAllocFromHeap( + heap, + mpDrawParticleFuncListNum * sizeof(DrawParticleFunc), + alignof(DrawParticleFunc)); } func_no = 0; @@ -635,7 +704,10 @@ void JPAResource::init(JKRHeap* heap) { if (mpDrawParticleChildFuncListNum != 0) { mpDrawParticleChildFuncList = - (ParticleFunc*)JKRAllocFromHeap(heap, mpDrawParticleChildFuncListNum * sizeof(ParticleFunc), sizeof(EmitterFunc)); + (DrawParticleFunc*)JKRAllocFromHeap( + heap, + mpDrawParticleChildFuncListNum * sizeof(DrawParticleFunc), + alignof(DrawParticleFunc)); } func_no = 0; @@ -699,6 +771,10 @@ void JPAResource::init(JKRHeap* heap) { mpDrawParticleChildFuncList[func_no] = &JPARegistPrmAlphaEnv; func_no++; } + +#if TARGET_PC + initBatchInfo(); +#endif } bool JPAResource::calc(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { @@ -808,6 +884,183 @@ void JPAResource::draw(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { } } +#if TARGET_PC +static GXTevAlphaArg to_vtx_alpha_arg(GXTevAlphaArg arg) { + return arg == GX_CA_A0 ? GX_CA_RASA : arg; +} + +static void batch_set_tev_op(GXTevStageID stage) { + GXSetTevColorOp(stage, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaOp(stage, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +} + +static void batch_setup_tev(JPAEmitterWorkData* work, bool useClr1) { + JPABaseShape* shape = work->mpRes->getBsp(); + JPAExTexShape* ets = work->mpRes->getEts(); + bool useIndirect = ets != nullptr && ets->isUseIndirect(); + + // JPAEmitterManager::draw configures both channels to pass vertex color through + GXSetNumChans(useClr1 ? 2 : 1); + + const GXTevAlphaArg* alphaArg = shape->getTevAlphaArg(); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevAlphaIn(GX_TEVSTAGE0, to_vtx_alpha_arg(alphaArg[0]), to_vtx_alpha_arg(alphaArg[1]), + to_vtx_alpha_arg(alphaArg[2]), to_vtx_alpha_arg(alphaArg[3])); + batch_set_tev_op(GX_TEVSTAGE0); + if (!useIndirect) { + GXSetTevDirect(GX_TEVSTAGE0); + } + GXTevStageID nextStage = GX_TEVSTAGE1; + + switch (shape->getTevColorArgSel()) { + case 0: // TEXC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXC, GX_CC_ONE, GX_CC_ZERO); + break; + case 1: // C0 * TEXC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + break; + case 2: // lerp(C0, 1, TEXC) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_RASC, GX_CC_ONE, GX_CC_TEXC, GX_CC_ZERO); + break; + case 3: // lerp(C1, C0, TEXC) = C0 * TEXC (stage 0) + C1 * (1 - TEXC) (stage 1) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevOrder(nextStage, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR1A1); + GXSetTevColorIn(nextStage, GX_CC_RASC, GX_CC_ZERO, GX_CC_TEXC, GX_CC_CPREV); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_APREV); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + break; + case 4: // TEXC * C0 + C1: C0 * TEXC (stage 0), + C1 (stage 1) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevOrder(nextStage, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR1A1); + GXSetTevColorIn(nextStage, GX_CC_CPREV, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_APREV); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + break; + case 5: // C0 + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC); + break; + } + + if (ets != nullptr && ets->isUseSecTex()) { + // Mirrors setPTev's secondary texture stage, at the next free stage + GXTexCoordID texCoord = useIndirect ? GX_TEXCOORD2 : GX_TEXCOORD1; + GXSetTevOrder(nextStage, texCoord, GX_TEXMAP3, GX_COLOR_NULL); + GXSetTevColorIn(nextStage, GX_CC_ZERO, GX_CC_TEXC, GX_CC_CPREV, GX_CC_ZERO); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_TEXA, GX_CA_APREV, GX_CA_ZERO); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + } + + GXSetNumTevStages(nextStage); +} + +static void batch_setup_vtx_desc(bool useClr0, bool useClr1) { + static Mtx identityMtx = { + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + + GXLoadPosMtxImm(identityMtx, GX_PNMTX0); + GXSetCurrentMtx(GX_PNMTX0); + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + if (useClr0) { + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + } + if (useClr1) { + GXSetVtxDesc(GX_VA_CLR1, GX_DIRECT); + } + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + if (useClr0) { + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + } + if (useClr1) { + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR1, GX_CLR_RGBA, GX_RGBA8, 0); + } + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); +} + +static void batch_restore_gx(JPAEmitterWorkData* work, bool changedTev, bool changedTexMtx) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_INDEX8); + GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S8, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_S8, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSetCurrentMtx(GX_PNMTX0); + + if (changedTexMtx) { + GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0); + } + + if (changedTev) { + GXSetNumChans(0); + work->mpRes->getBsp()->setGX(work); + work->mpRes->setPTev(); + } +} + +static bool draw_particle_batch(JPAEmitterWorkData* work) { + ZoneScoped; + + JPAResource* res = work->mpRes; + const JPAResource::BatchInfo& info = res->mBatchInfo; + if (!info.supported || work->mPrjType != 0 || work->mpEmtr->mpPtclCallBack != nullptr) { + return false; + } + + bool useClr0 = false; + bool useClr1 = false; + if (info.hasPtclColor) { + u32 colorSel = res->getBsp()->getTevColorArgSel(); + if (colorSel >= 6) { + return false; + } + useClr0 = true; + useClr1 = colorSel == 3 || colorSel == 4; + batch_setup_tev(work, useClr1); + } + + if (info.hasPtclTexMtx) { + // UVs are CPU-transformed; drop the texgen + GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY); + } + + batch_setup_vtx_desc(useClr0, useClr1); + + ParticleDrawCtx ctx{}; + ctx.batch = true; + ctx.useTexMtx = info.hasPtclTexMtx; + ctx.useClr0 = useClr0; + ctx.useClr1 = useClr1; + + bool fwdAhead = res->getBsp()->isDrawFwdAhead(); + JPANode* node = fwdAhead ? work->mpEmtr->mAlivePtclBase.getLast() : + work->mpEmtr->mAlivePtclBase.getFirst(); + + GXBegin(GX_QUADS, GX_VTXFMT1, GX_AUTO); + while (node != work->mpEmtr->mAlivePtclBase.getEnd()) { + work->mpCurNode = node; + for (int i = res->mpDrawParticleFuncListNum - 1; i >= 0; i--) { + (*res->mpDrawParticleFuncList[i])(work, node->getObject(), &ctx); + } + node = fwdAhead ? node->getPrev() : node->getNext(); + } + GXEnd(); + + batch_restore_gx(work, useClr0, info.hasPtclTexMtx); + return true; +} +#endif + void JPAResource::drawP(JPAEmitterWorkData* work) { ZoneScoped; work->mpEmtr->clearStatus(0x80); @@ -842,13 +1095,25 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { (*mpDrawEmitterFuncList[i])(work); } +#if TARGET_PC + if (draw_particle_batch(work)) { + GXSetMisc(GX_MT_XF_FLUSH, 0); + if (work->mpEmtr->mpEmtrCallBack != nullptr) { + work->mpEmtr->mpEmtrCallBack->drawAfter(work->mpEmtr); + } + return; + } + + ParticleDrawCtx ctx{}; // immediate mode +#endif + if (pBsp->isDrawFwdAhead()) { JPANode* node = work->mpEmtr->mAlivePtclBase.getLast(); for (; node != work->mpEmtr->mAlivePtclBase.getEnd(); node = node->getPrev()) { work->mpCurNode = node; if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject()); + (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -858,7 +1123,7 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { work->mpCurNode = node; if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject()); + (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -905,13 +1170,17 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { (*mpDrawEmitterChildFuncList[i])(work); } +#if TARGET_PC + ParticleDrawCtx ctx{}; // immediate mode +#endif + if (pBsp->isDrawFwdAhead()) { JPANode* node = work->mpEmtr->mAlivePtclChld.getLast(); for (; node != work->mpEmtr->mAlivePtclChld.getEnd(); node = node->getPrev()) { work->mpCurNode = node; if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject()); + (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -921,7 +1190,7 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { work->mpCurNode = node; if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject()); + (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } From 7c5ed6a0e1bdbac4a64d5f8b1e15eb3f7fff62d9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 10 Jun 2026 00:18:14 -0600 Subject: [PATCH 4/5] Fix OSWaitCond implementation --- src/dusk/OSMutex.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/dusk/OSMutex.cpp b/src/dusk/OSMutex.cpp index ef1a528f1b..b3127eb6fc 100644 --- a/src/dusk/OSMutex.cpp +++ b/src/dusk/OSMutex.cpp @@ -181,20 +181,22 @@ void OSWaitCond(OSCond* cond, OSMutex* mutex) { mutex->count = 0; mutex->thread = nullptr; - // Unlock the recursive mutex the same number of times it was locked - for (s32 i = 0; i < savedCount; i++) { - mutexData.nativeMutex.unlock(); - } - - // Wait on the condition variable - { - std::unique_lock lock(mutexData.nativeMutex); + // Keep one recursion level held so cv.wait() is what releases the mutex; + // fully unlocking before the wait opens a window where a signal is lost. + if (savedCount >= 1) { + for (s32 i = 1; i < savedCount; i++) { + mutexData.nativeMutex.unlock(); + } + std::unique_lock lock(mutexData.nativeMutex, std::adopt_lock); + condData.cv.wait(lock); + lock.release(); + for (s32 i = 1; i < savedCount; i++) { + mutexData.nativeMutex.lock(); + } + } else { + // Mutex wasn't held on entry (contract violation); wait anyway. + std::unique_lock lock(mutexData.nativeMutex); condData.cv.wait(lock); - } - - // Re-lock the recursive mutex the same number of times - for (s32 i = 0; i < savedCount; i++) { - mutexData.nativeMutex.lock(); } // Restore GC mutex state From b2ac2d6600f68894f397833330353b6e7366262a Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 10 Jun 2026 06:22:03 -0700 Subject: [PATCH 5/5] overhaul seed selection to be on file creation --- include/d/d_file_select.h | 15 +- include/dusk/settings.h | 5 + .../include/JSystem/J2DGraph/J2DTextBox.h | 12 + src/d/d_com_inf_game.cpp | 1 + src/d/d_file_sel_info.cpp | 18 ++ src/d/d_file_select.cpp | 139 +++++++++- src/d/d_menu_save.cpp | 17 +- .../randomizer/game/randomizer_context.cpp | 7 +- .../randomizer/game/randomizer_context.hpp | 1 + src/dusk/settings.cpp | 10 + src/dusk/ui/rando_config.cpp | 253 ++++++++++-------- src/dusk/ui/rando_config.hpp | 14 +- src/f_pc/f_pc_manager.cpp | 2 - 13 files changed, 374 insertions(+), 120 deletions(-) diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index d478126b38..4ecc0b7c24 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -203,6 +203,9 @@ public: DATASELPROC_DATA_SELECT_MOVE_ANIME, DATASELPROC_SELECT_DATA_OPEN_MOVE, DATASELPROC_SELECT_DATA_NAME_MOVE, +#if TARGET_PC + DATASELPROC_SELECT_DATA_PLAY_MOVE, // Select between vanilla or randomizer play +#endif DATASELPROC_SELECT_DATA_OPENERASE_MOVE, DATASELPROC_MENU_SELECT, DATASELPROC_MENU_SELECT_MOVE_ANM, @@ -326,6 +329,9 @@ public: void makeRecInfo(u8); void selectDataOpenMove(); void selectDataNameMove(); +#if TARGET_PC + void selectDataPlayTypeMove(); +#endif void selectDataOpenEraseMove(); void menuSelect(); void menuSelectStart(); @@ -719,7 +725,12 @@ public: /* 0x2378 */ J2DPicture* mpFadePict; #endif #ifdef TARGET_PC - dDlst_FileSelFade_c mFadeDlst; + struct mDusk { + dDlst_FileSelFade_c mFadeDlst; + bool mStartNameAnm; + bool mBackToFileSelect; + int mPendingRmlCloseFrames{0}; + } mDusk; #endif #if PLATFORM_WII || PLATFORM_SHIELD @@ -730,7 +741,7 @@ public: }; #ifdef TARGET_PC -STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C + sizeof(dDlst_FileSelFade_c)); +STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C + sizeof(dFile_select_c::mDusk)); #else STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C); #endif diff --git a/include/dusk/settings.h b/include/dusk/settings.h index e676e425d3..36269e87fd 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -289,6 +289,11 @@ struct UserSettings { std::array openDusklightMenu; std::array turboSpeedButton; } actionBindings; + + // Randomizer seed hashes, 1 per file + struct { + std::array, 3> seedHashes; + } randomizer; }; UserSettings& getSettings(); diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h b/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h index 0d875ad652..46495fac1b 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h @@ -154,6 +154,18 @@ public: return (J2DTextBoxHBinding)((mFlags >> 2) & 3); } +#if TARGET_PC + void setVBinding(J2DTextBoxVBinding vBinding) { + mFlags &= 0b1100; + mFlags |= (vBinding & 3); + } + + void setHBinding(J2DTextBoxHBinding hBinding) { + mFlags &= 0b0011; + mFlags |= ((hBinding & 3) << 2); + } +#endif + JUtility::TColor getCharColor() { return mCharColor; } JUtility::TColor getGradColor() { return mGradientColor; } u16 getStringAllocByte() const { return mStringLength; } diff --git a/src/d/d_com_inf_game.cpp b/src/d/d_com_inf_game.cpp index 71449ff7a9..db6d243f9a 100644 --- a/src/d/d_com_inf_game.cpp +++ b/src/d/d_com_inf_game.cpp @@ -2984,6 +2984,7 @@ void dComIfGs_setupRandomizerSave() { execItemGet(itemId); } + g_randomizerState = RandomizerState(); DuskLog.debug("Created Rando Save"); randoData.mCreatingSave = false; } diff --git a/src/d/d_file_sel_info.cpp b/src/d/d_file_sel_info.cpp index d54bc4424d..f99ff7ef4c 100644 --- a/src/d/d_file_sel_info.cpp +++ b/src/d/d_file_sel_info.cpp @@ -116,6 +116,24 @@ int dFile_info_c::setSaveData(dSv_save_c* i_savedata, BOOL i_validChksum, u8 i_d SAFE_STRCPY(mPlayerName, player_name); setSaveDate(i_savedata); setPlayTime(i_savedata); +#if TARGET_PC + // If this is a randomizer file + auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(i_dataNo).getValue(); + if (!curFileSeedHash.empty()) { + // Overwrite "Save time" text with "Randomizer" + auto saveTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_s_t_02')); + dusk::SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer"); + saveTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT); + + // Overwrite the "Total play time" text with the seed hash + auto playTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_p_t_02')); + dusk::SafeStringCopy(playTimeText->getStringPtr(), curFileSeedHash.c_str()); + + // Give the text double the space on the menu incase the seed hash is long + playTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT); + playTimeText->resize(playTimeText->getWidth() * 2, playTimeText->getHeight()); + } +#endif result = 0; } } else { diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index b660f79fcb..e3d4d56569 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -23,7 +23,13 @@ #include "m_Do/m_Do_graphic.h" #include +#if TARGET_PC +#include "dusk/config.hpp" #include "dusk/string.hpp" +#include "dusk/ui/modal.hpp" +#include "dusk/ui/rando_config.hpp" +#include "dusk/ui/ui.hpp" +#endif static s32 SelStartFrameTbl[3] = { 59, @@ -279,6 +285,9 @@ static DataSelProcFunc DataSelProc[] = { &dFile_select_c::dataSelectMoveAnime, &dFile_select_c::selectDataOpenMove, &dFile_select_c::selectDataNameMove, +#if TARGET_PC + &dFile_select_c::selectDataPlayTypeMove, +#endif &dFile_select_c::selectDataOpenEraseMove, &dFile_select_c::menuSelect, &dFile_select_c::menuSelectMoveAnm, @@ -843,7 +852,12 @@ void dFile_select_c::dataSelectStart() { mpName->initial(); modoruTxtChange(1); +#if TARGET_PC + mDataSelProc = DATASELPROC_SELECT_DATA_PLAY_MOVE; + mDusk.mStartNameAnm = false; +#else mDataSelProc = DATASELPROC_SELECT_DATA_NAME_MOVE; +#endif } else { #if PLATFORM_GCN dComIfGs_setNewFile(0); @@ -1138,6 +1152,93 @@ void dFile_select_c::selectDataNameMove() { } } +#if TARGET_PC +// Custom Proc to allow the player to select the play type and a randomizer seed +// Initially copied and expanded upon from dFile_select_c::selectDataNameMove +void dFile_select_c::selectDataPlayTypeMove() { + bool isHeaderTxtChange = headerTxtChangeAnm(); + bool isFileRecScale = fileRecScaleAnm2(); + bool isModoruTxtDisp = modoruTxtDispAnm(); + + // If we want to start bringing in the name input + if (mDusk.mStartNameAnm) { + // Only do so when no documents are visible + if (!dusk::ui::any_document_visible()) { + if (mDusk.mBackToFileSelect) { + // Code below copied from dFile_select_c::nameInput to initiate going back + // to the file selection + headerTxtSet(0x43, 1, 0); + fileRecScaleAnmInitSet2(0.0f, 1.0f); + nameMoveAnmInitSet(0xd29, 0xd1f); + modoruTxtDispAnmInit(0); + mDataSelProc = DATASELPROC_NAME_TO_DATA_SELECT_MOVE; + } else { + // Show the name pane if we went forward + field_0x0128 = true; + mNameBasePane->show(); + if (mDusk.mPendingRmlCloseFrames > 0) { + mDusk.mPendingRmlCloseFrames -= 1; + } + if (mDusk.mPendingRmlCloseFrames == 0 && nameMoveAnm()) { + mDataSelProc = DATASELPROC_NAME_INPUT_WAIT; + mDusk.mStartNameAnm = false; + } + } + } + } + // If the file select elements have disappeared, setup the play type modal + else if (isHeaderTxtChange == true && isFileRecScale == true && + isModoruTxtDisp == true) + { + // Push our modal for selecting the play type + auto& playTypeModal = dusk::ui::push_document(std::make_unique(dusk::ui::Modal::Props{ + .title = "Play Type", + .bodyRml = "What mode would you like to play?", + .actions = { + // If vanilla is selected, proceed to name entry and reset randomizer context + dusk::ui::ModalAction{ + .label = "Vanilla", + .onPressed = [this](dusk::ui::Modal& modal) { + mDusk.mBackToFileSelect = false; + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + modal.hide(true); + randomizer_GetContext() = RandomizerContext(); + }}, + // If randomizer is selected, proceed to the randomizer menu + dusk::ui::ModalAction{ + .label = "Randomizer", + .onPressed = [this](dusk::ui::Modal& modal) { + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + modal.hide(true); + dusk::ui::push_document(std::make_unique(this)); + }}, + }, + // If we dismiss this modal, go back to file selection + .onDismiss = [this](dusk::ui::Modal& modal) { + mDoAud_seStartMenu(Z2SE_SY_MENU_BACK); + modal.hide(true); + }, + .icon = "question-mark", + })); + + // The next time we call this function, begin waiting to show the name input + mDusk.mStartNameAnm = true; + + // By default, go back to the file selection after closing the various menus. + mDusk.mBackToFileSelect = true; + + // Wait 6 frames for the rmlui window transition after it closes + mDusk.mPendingRmlCloseFrames = 6; + + // Hide the name pane incase we go back + field_0x0128 = false; + mNameBasePane->hide(); + + playTypeModal.focus(); + } +} +#endif + void dFile_select_c::selectDataOpenEraseMove() { bool isHeaderTxtChange = headerTxtChangeAnm(); bool isSelDataMove = selectDataMoveAnm(); @@ -1204,6 +1305,21 @@ void dFile_select_c::menuSelectStart() { mIsSelectEnd = true; mDataSelProc = DATASELPROC_NEXT_MODE_WAIT; dComIfGs_setDataNum(mSelectNum); + #if TARGET_PC + // Load the randomizer seed if one is tied to this file + auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(mSelectNum).getValue(); + // If this is a vanilla file, clear rando data structures + if (curFileSeedHash.empty()) { + g_randomizerState = RandomizerState(); + randomizer_GetContext() = RandomizerContext(); + } + // Reset randomizer state if we're switching to a different file + else if (curFileSeedHash != randomizer_GetContext().mHash || g_randomizerState.mFileNum != mSelectNum) { + g_randomizerState = RandomizerState(); + randomizer_GetContext() = RandomizerContext(); + randomizer_GetContext().LoadFromHash(curFileSeedHash); + } + #endif } else if (mSelectMenuNum == 0) { mSelIcon->setAlphaRate(0.0f); yesnoMenuMoveAnmInitSet(0x473, 0x47d); @@ -1447,6 +1563,14 @@ void dFile_select_c::nameInput() { } void dFile_select_c::nameToDataSelectMove() { +#if TARGET_PC + // Delay this animation to make for a smoother transition from the Rmlui menus + if (mDusk.mPendingRmlCloseFrames > 0) { + mDusk.mPendingRmlCloseFrames -= 1; + return; + } +#endif + bool isHeaderTxtChange = headerTxtChangeAnm(); bool isFileRecScale = fileRecScaleAnm2(); bool isNameMove = nameMoveAnm(); @@ -2368,6 +2492,12 @@ void dFile_select_c::CommandExec() { mDoMemCd_setCopyToPos(mCpDataToNum); dataSave(); mDataSelProc = DATASELPROC_DATA_COPY_WAIT; +#if TARGET_PC + // Copy over the seed hash as well + auto& seedHashes = dusk::getSettings().randomizer.seedHashes; + seedHashes[mCpDataToNum].setValue(seedHashes[mCpDataNum]); + dusk::config::Save(); +#endif break; } @@ -2426,6 +2556,11 @@ void dFile_select_c::ErasePaneMoveOk() { if (iVar1 == 1 && iVar2 == 1) { field_0x0208 = 0; setSaveData(); +#if TARGET_PC + // Clear the seed hash for this file when it gets erased + dusk::getSettings().randomizer.seedHashes.at(mSelectNum).setValue(""); + dusk::config::Save(); +#endif makeRecInfo(mSelectNum); headerTxtSet(0x4b, 0, 0); mpFileWarning->closeInit(); @@ -3197,7 +3332,7 @@ void dFile_select_c::screenSet() { mpFadePict->setBlackWhite(black, white); mpFadePict->setAlpha(0); #ifdef TARGET_PC - mFadeDlst.mpPict = mpFadePict; + mDusk.mFadeDlst.mpPict = mpFadePict; #endif #endif } @@ -3999,7 +4134,7 @@ void dFile_select_c::_draw() { #if PLATFORM_GCN #if TARGET_PC - dComIfGd_set2DOpaTop(&mFadeDlst); + dComIfGd_set2DOpaTop(&mDusk.mFadeDlst); #else mpFadePict->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), false, false, diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index d73e4ff5e8..0c891dccf1 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -18,11 +18,15 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "d/d_msg_scrn_explain.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" #include "f_op/f_op_msg_mng.h" +#if TARGET_PC +#include "dusk/config.hpp" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" +#endif + static int SelStartFrameTbl[3] = { 59, 99, @@ -1341,6 +1345,15 @@ void dMenu_save_c::dataWrite() { dComIfGs_setMemoryToCard(mSaveBuffer, mSelectedFile); mDoMemCdRWm_SetCheckSumGameData(mSaveBuffer, mSelectedFile); +#if TARGET_PC + // Save randomizer hash + dusk::getSettings().randomizer.seedHashes[mSelectedFile].setValue(randomizer_GetContext().mHash); + dusk::config::Save(); + if (randomizer_IsActive()) { + g_randomizerState.mFileNum = mSelectedFile; + } +#endif + u8* save = mSaveBuffer; for (int i = 0; i < 3; i++) { mDoMemCdRWm_TestCheckSumGameData(save); diff --git a/src/dusk/randomizer/game/randomizer_context.cpp b/src/dusk/randomizer/game/randomizer_context.cpp index a845c54f80..64e159a50b 100644 --- a/src/dusk/randomizer/game/randomizer_context.cpp +++ b/src/dusk/randomizer/game/randomizer_context.cpp @@ -278,8 +278,11 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha } } - DuskLog.debug("Loaded Randomizer Seed {}", this->mHash); - + dusk::ui::push_toast(dusk::ui::Toast{ + .title = "Randomizer", + .content = fmt::format("Loaded Randomizer Seed {}", this->mHash), + .duration = std::chrono::seconds(3), + }); return std::nullopt; } diff --git a/src/dusk/randomizer/game/randomizer_context.hpp b/src/dusk/randomizer/game/randomizer_context.hpp index ee74d1d8c7..7f5fcf8bf0 100644 --- a/src/dusk/randomizer/game/randomizer_context.hpp +++ b/src/dusk/randomizer/game/randomizer_context.hpp @@ -171,6 +171,7 @@ public: void setRoomReloadingState(bool newState) { mRoomReloadingState = newState; } bool mInitialized{false}; + int mFileNum{-1}; u8 mEventItemStatus{}; bool mHasPendingToDChange{false}; u8 mTimeChange{}; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 0a58dfd1ec..0422c6fcba 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -185,6 +185,12 @@ UserSettings g_userSettings = { ActionBindConfigVar{"actionBindings.turboButton_port2", PAD_NATIVE_BUTTON_INVALID}, ActionBindConfigVar{"actionBindings.turboButton_port3", PAD_NATIVE_BUTTON_INVALID}, }, + }, + + .randomizer = { + ConfigVar{"randomizer.file1SeedHash", ""}, + ConfigVar{"randomizer.file2SeedHash", ""}, + ConfigVar{"randomizer.file3SeedHash", ""}, } }; @@ -339,6 +345,10 @@ void registerSettings() { Register(g_userSettings.actionBindings.turboSpeedButton[1]); Register(g_userSettings.actionBindings.turboSpeedButton[2]); Register(g_userSettings.actionBindings.turboSpeedButton[3]); + + Register(g_userSettings.randomizer.seedHashes[0]); + Register(g_userSettings.randomizer.seedHashes[1]); + Register(g_userSettings.randomizer.seedHashes[2]); } // Transient settings diff --git a/src/dusk/ui/rando_config.cpp b/src/dusk/ui/rando_config.cpp index 3a9187865a..842969f00a 100644 --- a/src/dusk/ui/rando_config.cpp +++ b/src/dusk/ui/rando_config.cpp @@ -16,6 +16,8 @@ #include "rando_seed_generation.hpp" #include "string_button.hpp" +#include "d/d_file_select.h" + namespace dusk::ui { struct ConfigBoolProps { Rml::String key; @@ -682,111 +684,131 @@ bool focus_closest_child_on_next_pane(Pane& currentPane, Pane& nextPane) { return false; } -RandomizerWindow::RandomizerWindow() { +void delete_seed_callback(Pane& pane) { + pane.clear(); + std::filesystem::path seedDirectory = GetRandomizerSeedsPath(); + + if (std::filesystem::is_empty(seedDirectory)) { + pane.add_rml( + "No seeds generated."); + return; + } + + pane.add_rml( + "Delete any seed not currently being used."); + + for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { + if (entry.is_directory()) { + std::string hash = entry.path().filename().string(); + + // If the hash is attached to any files, add the file number(s) afterward + auto& seedHashes = getSettings().randomizer.seedHashes; + auto hashCopy = hash; + for (int i = 0; i < seedHashes.size(); ++i) { + if (seedHashes[i].getValue() == hashCopy) { + hash += " (File " + std::to_string(i + 1) + ")"; + } + } + + // TODO: our ui lib doesnt have an easy way to either refresh or remove values from the right pane + pane.add_button( + { + .text = hash, + .isDisabled = [hash] { + return !playerIsOnTitleScreen() || hash.ends_with(')'); + } + }) + .on_pressed([entry, &pane] { + std::filesystem::remove_all(entry); + delete_seed_callback(pane); + }); + } + } +}; + +RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : mFileSelectMenu(fileSelect) { // Create rando directories if they don't exist if (!std::filesystem::exists(GetRandomizerSeedsPath())) { std::filesystem::create_directories(GetRandomizerSeedsPath()); } + // If we're bringing this menu up during file selection + if (mFileSelectMenu) { + // Don't allow going back to the main dusklight menu while this menu + // is active + mTabBar->listen(Rml::EventId::Keydown, [](Rml::Event& event) { + auto cmd = map_nav_event(event); + if (cmd == NavCommand::Menu || cmd == NavCommand::Cancel) { + event.StopPropagation(); + } + }); + + add_tab("Play", [this](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Controlled); + + leftPane.register_control( + leftPane.add_select_button({ + .key = "Selected Seed", + .getValue = + [] { + return randomizer_GetContext().mHash.empty() ? + "None" : + randomizer_GetContext().mHash; + }, + }), + rightPane, [](Pane& pane) { + std::filesystem::path seedDirectory = GetRandomizerSeedsPath(); + + if (std::filesystem::is_empty(seedDirectory)) { + pane.add_rml( + "No seeds generated! You can generate a seed from the Seed Management Tab."); + return; + } + + pane.add_rml( + "Choose which seed you want to play."); + + for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { + if (entry.is_directory()) { + std::string hash = entry.path().filename().string(); + + pane.add_button( + { + .text = hash, + .isSelected = [hash] { + return randomizer_GetContext().mHash == hash; + }, + }) + .on_pressed([hash] { + randomizer_GetContext() = RandomizerContext(); + randomizer_GetContext().LoadFromHash(hash); + }); + } + } + }); + + leftPane.add_button({ + .text = "Start Randomizer", + .isDisabled = []{return randomizer_GetContext().mHash.empty();}, + }) + .on_pressed([this]{ + if (mFileSelectMenu) { + mFileSelectMenu->mDusk.mBackToFileSelect = false; + } + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + this->hide(true); + }); + }); + } + add_tab("Seed Management", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Controlled); - leftPane.register_control( - leftPane.add_select_button({ - .key = "Selected Seed", - .getValue = - [] { - return randomizer_GetContext().mHash.empty() ? - "None" : - randomizer_GetContext().mHash; - }, - }), - rightPane, [](Pane& pane) { - std::filesystem::path seedDirectory = GetRandomizerSeedsPath(); - - if (std::filesystem::is_empty(seedDirectory)) { - pane.add_rml( - "No seeds generated! Check out the options tab and generate a seed using the button below."); - return; - } - - pane.add_rml( - "Apply a seed generated using the randomizer generator. (Note: must be done before selecting a save file)."); - - for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { - if (entry.is_directory()) { - std::string hash = entry.path().filename().string(); - - pane.add_button( - { - .text = hash, - .isSelected = [hash] { - return randomizer_GetContext().mHash == hash; - }, - }) - .on_pressed([hash] { - randomizer_GetContext() = RandomizerContext(); - randomizer_GetContext().LoadFromHash(hash); - }); - } - } - }); - - leftPane.register_control( - leftPane.add_button("Delete Seeds"), - rightPane, [](Pane& pane) { - std::filesystem::path seedDirectory = GetRandomizerSeedsPath(); - - if (std::filesystem::is_empty(seedDirectory)) { - pane.add_rml( - "No seeds generated."); - return; - } - - pane.add_rml( - "Delete any seed currently not active in the randomizer."); - - for (const auto& entry : std::filesystem::directory_iterator(seedDirectory)) { - if (entry.is_directory()) { - std::string hash = entry.path().filename().string(); - - // TODO: our ui lib doesnt have an easy way to either refresh or remove values from the right pane - pane.add_button( - { - .text = hash, - .isDisabled = [hash] { - return !playerIsOnTitleScreen() || randomizer_GetContext().mHash == hash; - } - }) - .on_pressed([hash, entry] { - if (randomizer_GetContext().mHash != hash) { - std::filesystem::remove_all(entry); - } else if (!randomizer_IsActive()){ - // If the user selected the currently seed, but it's not active, we'll allow the delete - std::filesystem::remove_all(entry); - randomizer_GetContext() = RandomizerContext(); - } - }); - } - } - }); - - leftPane.register_control(leftPane.add_button(ControlledButton::Props{ - .text = "Deactivate Seed", - .isDisabled = [] { return randomizer_GetContext().mHash.empty() || !playerIsOnTitleScreen(); } - }).on_pressed([] { - if (!randomizer_IsActive()) { - randomizer_GetContext() = RandomizerContext(); - } - }), rightPane, [](Pane& pane) { - pane.clear(); - pane.add_rml("Disables currently chosen seed."); - }); - leftPane.register_control(leftPane.add_button("Generate Seed").on_pressed( - [this, &rightPane] { + [] { if (TryCreateRandomSeed()) { DuskLog.info("Created new Seed for generator."); } @@ -800,21 +822,26 @@ RandomizerWindow::RandomizerWindow() { }); leftPane.register_control(leftPane.add_child(StringButton::Props{ - .key = "Seed String", - .getValue = [] { - return GetRandomizerConfig().GetSeed(); - }, - .setValue = [](Rml::String value) { - GetRandomizerConfig().SetSeed(value); - SaveRandomizerConfig(); - }, - .maxLength = 32, - }), - rightPane, [](Pane& pane) { - pane.clear(); - pane.add_rml( - "Current value of the seed used by the randomizer for generation. Leave blank for a random value."); - }); + .key = "Seed String", + .getValue = [] { + return GetRandomizerConfig().GetSeed(); + }, + .setValue = [](Rml::String value) { + GetRandomizerConfig().SetSeed(value); + SaveRandomizerConfig(); + }, + .maxLength = 32, + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_rml( + "Current value of the seed used by the randomizer for generation. Leave blank for a random value."); + }); + + leftPane.register_control( + leftPane.add_button("Delete Seeds"), + rightPane, delete_seed_callback + ); }); add_tab("Seed Options", [this](Rml::Element* content) { @@ -1141,6 +1168,14 @@ RandomizerWindow::RandomizerWindow() { } } +FileSelectRandomizerWindow::FileSelectRandomizerWindow(dFile_select_c* fileSelectMenu) : + RandomizerWindow(fileSelectMenu) { } + +bool FileSelectRandomizerWindow::consume_close_request() { + hide(true); + return true; +} + std::filesystem::path GetRandomizerPath() { return data::configured_data_path() / "randomizer"; } diff --git a/src/dusk/ui/rando_config.hpp b/src/dusk/ui/rando_config.hpp index 46518a35be..2cf59082fb 100644 --- a/src/dusk/ui/rando_config.hpp +++ b/src/dusk/ui/rando_config.hpp @@ -5,6 +5,7 @@ namespace randomizer::seedgen::config { class Config; } +class dFile_select_c; namespace dusk::ui { class Pane; @@ -19,10 +20,21 @@ class Pane; class RandomizerWindow : public Window { public: - RandomizerWindow(); + explicit RandomizerWindow(dFile_select_c* fileSelectMenu = nullptr); void rando_excluded_locations_update_left_pane(Pane& innerLeftPane, Pane& rightPane, bool forceUpdate = false); auto& get_locations_for_left_pane(); + + protected: + dFile_select_c* mFileSelectMenu{nullptr}; private: std::string m_excludedLocationsFilter{}; }; + + class FileSelectRandomizerWindow : public RandomizerWindow { + public: + FileSelectRandomizerWindow(dFile_select_c* fileSelectMenu = nullptr); + + protected: + bool consume_close_request() override; + }; } diff --git a/src/f_pc/f_pc_manager.cpp b/src/f_pc/f_pc_manager.cpp index 3cc923a492..f095597b57 100644 --- a/src/f_pc/f_pc_manager.cpp +++ b/src/f_pc/f_pc_manager.cpp @@ -100,8 +100,6 @@ void fpcM_Management(fpcM_ManagementFunc i_preExecuteFn, fpcM_ManagementFunc i_p g_randomizerState._create(); } g_randomizerState.execute(); - } else if (g_randomizerState.mInitialized) { - g_randomizerState = RandomizerState{}; } #endif if (!fapGm_HIO_c::isCaptureScreen()) {