mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-07-29 01:47:19 +12:00
Merge branch 'master' into specialized-shaderz
This commit is contained in:
commit
58da6ea8a4
97 changed files with 2168 additions and 501 deletions
src
config.cpp
core
emulator.cppio_file.cppjni_driver.cpplua.cppminiaudio.cpppanda_qt
panda_sdl
pandroid/app
build.gradle.ktsproguard-rules.pro
src/main
AndroidManifest.xml
java/com/panda3ds/pandroid
res
drawable
color_surface.xmlmedium_card_background.xmlrounded_selectable_item_background.xmlsimple_card_background.xmlswitch_thumb.xmlswitch_track.xml
layout-land
layout
activity_input_map.xmlactivity_main.xmlactivity_preference.xmldialog_loading.xmldialog_lua_scripts.xmlfragment_game_drawer.xmlfragment_games.xmlfragment_search.xmlholder_game.xmlholder_lua_script.xmlpreference_simple_about.xml
menu
values-pt-rBR
values-v27
values-v29
values
xml
|
@ -59,6 +59,18 @@ void EmulatorConfig::load() {
|
|||
}
|
||||
|
||||
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", shaderJitDefault);
|
||||
vsyncEnabled = toml::find_or<toml::boolean>(gpu, "EnableVSync", true);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.contains("Audio")) {
|
||||
auto audioResult = toml::expect<toml::value>(data.at("Audio"));
|
||||
if (audioResult.is_ok()) {
|
||||
auto audio = audioResult.unwrap();
|
||||
|
||||
auto dspCoreName = toml::find_or<std::string>(audio, "DSPEmulation", "Null");
|
||||
dspType = Audio::DSPCore::typeFromString(dspCoreName);
|
||||
audioEnabled = toml::find_or<toml::boolean>(audio, "EnableAudio", false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +121,9 @@ void EmulatorConfig::save() {
|
|||
data["General"]["UsePortableBuild"] = usePortableBuild;
|
||||
data["GPU"]["EnableShaderJIT"] = shaderJitEnabled;
|
||||
data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType));
|
||||
data["GPU"]["EnableVSync"] = vsyncEnabled;
|
||||
data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType));
|
||||
data["Audio"]["EnableAudio"] = audioEnabled;
|
||||
|
||||
data["Battery"]["ChargerPlugged"] = chargerPlugged;
|
||||
data["Battery"]["BatteryPercentage"] = batteryPercentage;
|
||||
|
|
|
@ -21,7 +21,6 @@ CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), sched
|
|||
void CPU::reset() {
|
||||
setCPSR(CPSR::UserMode);
|
||||
setFPSCR(FPSCR::MainThreadDefault);
|
||||
env.totalTicks = 0;
|
||||
|
||||
cp15->reset();
|
||||
cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage
|
||||
|
|
52
src/core/audio/dsp_core.cpp
Normal file
52
src/core/audio/dsp_core.cpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#include "audio/dsp_core.hpp"
|
||||
|
||||
#include "audio/null_core.hpp"
|
||||
#include "audio/teakra_core.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <unordered_map>
|
||||
|
||||
std::unique_ptr<Audio::DSPCore> Audio::makeDSPCore(DSPCore::Type type, Memory& mem, Scheduler& scheduler, DSPService& dspService) {
|
||||
std::unique_ptr<DSPCore> core;
|
||||
|
||||
switch (type) {
|
||||
case DSPCore::Type::Null: core = std::make_unique<NullDSP>(mem, scheduler, dspService); break;
|
||||
case DSPCore::Type::Teakra: core = std::make_unique<TeakraDSP>(mem, scheduler, dspService); break;
|
||||
|
||||
default:
|
||||
Helpers::warn("Invalid DSP core selected!");
|
||||
core = std::make_unique<NullDSP>(mem, scheduler, dspService);
|
||||
break;
|
||||
}
|
||||
|
||||
mem.setDSPMem(core->getDspMemory());
|
||||
return core;
|
||||
}
|
||||
|
||||
Audio::DSPCore::Type Audio::DSPCore::typeFromString(std::string inString) {
|
||||
// Transform to lower-case to make the setting case-insensitive
|
||||
std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
static const std::unordered_map<std::string, Audio::DSPCore::Type> map = {
|
||||
{"null", Audio::DSPCore::Type::Null},
|
||||
{"none", Audio::DSPCore::Type::Null},
|
||||
{"lle", Audio::DSPCore::Type::Teakra},
|
||||
{"teakra", Audio::DSPCore::Type::Teakra},
|
||||
};
|
||||
|
||||
if (auto search = map.find(inString); search != map.end()) {
|
||||
return search->second;
|
||||
}
|
||||
|
||||
printf("Invalid DSP type. Defaulting to null\n");
|
||||
return Audio::DSPCore::Type::Null;
|
||||
}
|
||||
|
||||
const char* Audio::DSPCore::typeToString(Audio::DSPCore::Type type) {
|
||||
switch (type) {
|
||||
case Audio::DSPCore::Type::Null: return "null";
|
||||
case Audio::DSPCore::Type::Teakra: return "teakra";
|
||||
default: return "invalid";
|
||||
}
|
||||
}
|
143
src/core/audio/miniaudio_device.cpp
Normal file
143
src/core/audio/miniaudio_device.cpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
#include "audio/miniaudio_device.hpp"
|
||||
|
||||
#include "helpers.hpp"
|
||||
|
||||
MiniAudioDevice::MiniAudioDevice() : initialized(false), running(false), samples(nullptr) {}
|
||||
|
||||
void MiniAudioDevice::init(Samples& samples, bool safe) {
|
||||
this->samples = &samples;
|
||||
running = false;
|
||||
|
||||
// Probe for device and available backends and initialize audio
|
||||
ma_backend backends[ma_backend_null + 1];
|
||||
uint count = 0;
|
||||
|
||||
if (safe) {
|
||||
backends[0] = ma_backend_null;
|
||||
count = 1;
|
||||
} else {
|
||||
bool found = false;
|
||||
for (uint i = 0; i <= ma_backend_null; i++) {
|
||||
ma_backend backend = ma_backend(i);
|
||||
if (!ma_is_backend_enabled(backend)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
backends[count++] = backend;
|
||||
|
||||
// TODO: Make backend selectable here
|
||||
found = true;
|
||||
//count = 1;
|
||||
//backends[0] = backend;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
initialized = false;
|
||||
|
||||
Helpers::warn("No valid audio backend found\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ma_context_init(backends, count, nullptr, &context) != MA_SUCCESS) {
|
||||
initialized = false;
|
||||
|
||||
Helpers::warn("Unable to initialize audio context");
|
||||
return;
|
||||
}
|
||||
audioDevices.clear();
|
||||
|
||||
struct UserContext {
|
||||
MiniAudioDevice* miniAudio;
|
||||
ma_device_config& config;
|
||||
bool found = false;
|
||||
};
|
||||
UserContext userContext = {.miniAudio = this, .config = deviceConfig};
|
||||
|
||||
ma_context_enumerate_devices(
|
||||
&context,
|
||||
[](ma_context* pContext, ma_device_type deviceType, const ma_device_info* pInfo, void* pUserData) -> ma_bool32 {
|
||||
if (deviceType != ma_device_type_playback) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UserContext* userContext = reinterpret_cast<UserContext*>(pUserData);
|
||||
userContext->miniAudio->audioDevices.push_back(pInfo->name);
|
||||
|
||||
// TODO: Check if this is the device we want here
|
||||
userContext->config.playback.pDeviceID = &pInfo->id;
|
||||
userContext->found = true;
|
||||
return true;
|
||||
},
|
||||
&userContext
|
||||
);
|
||||
|
||||
if (!userContext.found) {
|
||||
Helpers::warn("MiniAudio: Device not found");
|
||||
}
|
||||
|
||||
deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||
// The 3DS outputs s16 stereo audio @ 32768 Hz
|
||||
deviceConfig.playback.format = ma_format_s16;
|
||||
deviceConfig.playback.channels = channelCount;
|
||||
deviceConfig.sampleRate = sampleRate;
|
||||
//deviceConfig.periodSizeInFrames = 64;
|
||||
//deviceConfig.periods = 16;
|
||||
deviceConfig.pUserData = this;
|
||||
deviceConfig.aaudio.usage = ma_aaudio_usage_game;
|
||||
deviceConfig.wasapi.noAutoConvertSRC = true;
|
||||
|
||||
deviceConfig.dataCallback = [](ma_device* device, void* out, const void* input, ma_uint32 frameCount) {
|
||||
auto self = reinterpret_cast<MiniAudioDevice*>(device->pUserData);
|
||||
s16* output = reinterpret_cast<ma_int16*>(out);
|
||||
|
||||
// Wait until there's enough samples to pop
|
||||
while (self->samples->size() < frameCount * channelCount) {
|
||||
// If audio output is disabled from the emulator thread, make sure that this callback will return and not hang
|
||||
if (!self->running) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self->samples->pop(output, frameCount * channelCount);
|
||||
};
|
||||
|
||||
if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) {
|
||||
Helpers::warn("Unable to initialize audio device");
|
||||
initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
void MiniAudioDevice::start() {
|
||||
if (!initialized) {
|
||||
Helpers::warn("MiniAudio device not initialized, won't start");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the call to start if the device is already running
|
||||
if (!running) {
|
||||
if (ma_device_start(&device) == MA_SUCCESS) {
|
||||
running = true;
|
||||
} else {
|
||||
Helpers::warn("Failed to start audio device");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MiniAudioDevice::stop() {
|
||||
if (!initialized) {
|
||||
Helpers::warn("MiniAudio device not initialized, can't start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (running) {
|
||||
running = false;
|
||||
|
||||
if (ma_device_stop(&device) != MA_SUCCESS) {
|
||||
Helpers::warn("Failed to stop audio device");
|
||||
}
|
||||
}
|
||||
}
|
166
src/core/audio/null_core.cpp
Normal file
166
src/core/audio/null_core.cpp
Normal file
|
@ -0,0 +1,166 @@
|
|||
#include "audio/null_core.hpp"
|
||||
|
||||
#include "services/dsp.hpp"
|
||||
|
||||
namespace Audio {
|
||||
namespace DSPPipeType {
|
||||
enum : u32 {
|
||||
Debug = 0,
|
||||
DMA = 1,
|
||||
Audio = 2,
|
||||
Binary = 3,
|
||||
};
|
||||
}
|
||||
|
||||
void NullDSP::resetAudioPipe() {
|
||||
// Hardcoded responses for now
|
||||
// These are DSP DRAM offsets for various variables
|
||||
// https://www.3dbrew.org/wiki/DSP_Memory_Region
|
||||
static constexpr std::array<u16, 16> responses = {
|
||||
0x000F, // Number of responses
|
||||
0xBFFF, // Frame counter
|
||||
0x9E92, // Source configs
|
||||
0x8680, // Source statuses
|
||||
0xA792, // ADPCM coefficients
|
||||
0x9430, // DSP configs
|
||||
0x8400, // DSP status
|
||||
0x8540, // Final samples
|
||||
0x9492, // Intermediate mix samples
|
||||
0x8710, // Compressor
|
||||
0x8410, // Debug
|
||||
0xA912, // ??
|
||||
0xAA12, // ??
|
||||
0xAAD2, // ??
|
||||
0xAC52, // Surround sound biquad filter 1
|
||||
0xAC5C // Surround sound biquad filter 2
|
||||
};
|
||||
|
||||
std::vector<u8>& audioPipe = pipeData[DSPPipeType::Audio];
|
||||
audioPipe.resize(responses.size() * sizeof(u16));
|
||||
|
||||
// Push back every response to the audio pipe
|
||||
size_t index = 0;
|
||||
for (auto e : responses) {
|
||||
audioPipe[index++] = e & 0xff;
|
||||
audioPipe[index++] = e >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
void NullDSP::reset() {
|
||||
loaded = false;
|
||||
for (auto& e : pipeData) {
|
||||
e.clear();
|
||||
}
|
||||
|
||||
// Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted
|
||||
resetAudioPipe();
|
||||
}
|
||||
|
||||
void NullDSP::loadComponent(std::vector<u8>& data, u32 programMask, u32 dataMask) {
|
||||
if (loaded) {
|
||||
Helpers::warn("Loading DSP component when already loaded");
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame);
|
||||
}
|
||||
|
||||
void NullDSP::unloadComponent() {
|
||||
if (!loaded) {
|
||||
Helpers::warn("Audio: unloadComponent called without a running program");
|
||||
}
|
||||
|
||||
loaded = false;
|
||||
scheduler.removeEvent(Scheduler::EventType::RunDSP);
|
||||
}
|
||||
|
||||
void NullDSP::runAudioFrame() {
|
||||
// Signal audio pipe when an audio frame is done
|
||||
if (dspState == DSPState::On) [[likely]] {
|
||||
dspService.triggerPipeEvent(DSPPipeType::Audio);
|
||||
}
|
||||
|
||||
scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame);
|
||||
}
|
||||
|
||||
u16 NullDSP::recvData(u32 regId) {
|
||||
if (regId != 0) {
|
||||
Helpers::panic("Audio: invalid register in null frontend");
|
||||
}
|
||||
|
||||
return dspState == DSPState::On;
|
||||
}
|
||||
|
||||
void NullDSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) {
|
||||
enum class StateChange : u8 {
|
||||
Initialize = 0,
|
||||
Shutdown = 1,
|
||||
Wakeup = 2,
|
||||
Sleep = 3,
|
||||
};
|
||||
|
||||
switch (channel) {
|
||||
case DSPPipeType::Audio: {
|
||||
if (size != 4) {
|
||||
printf("Invalid size written to DSP Audio Pipe\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get new state
|
||||
const u8 state = mem.read8(buffer);
|
||||
if (state > 3) {
|
||||
log("WriteProcessPipe::Audio: Unknown state change type");
|
||||
} else {
|
||||
switch (static_cast<StateChange>(state)) {
|
||||
case StateChange::Initialize:
|
||||
// TODO: Other initialization stuff here
|
||||
dspState = DSPState::On;
|
||||
resetAudioPipe();
|
||||
|
||||
dspService.triggerPipeEvent(DSPPipeType::Audio);
|
||||
break;
|
||||
|
||||
case StateChange::Shutdown:
|
||||
dspState = DSPState::Off;
|
||||
break;
|
||||
|
||||
default: Helpers::panic("Unimplemented DSP audio pipe state change %d", state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DSPPipeType::Binary:
|
||||
Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size);
|
||||
|
||||
// This pipe and interrupt are normally used for requests like AAC decode
|
||||
dspService.triggerPipeEvent(DSPPipeType::Binary);
|
||||
break;
|
||||
|
||||
default: log("Audio::NullDSP: Wrote to unimplemented pipe %d\n", channel); break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<u8> NullDSP::readPipe(u32 pipe, u32 peer, u32 size, u32 buffer) {
|
||||
if (size & 1) Helpers::panic("Tried to read odd amount of bytes from DSP pipe");
|
||||
if (pipe >= pipeCount || size > 0xffff) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (pipe != DSPPipeType::Audio) {
|
||||
log("Reading from non-audio pipe! This might be broken, might need to check what pipe is being read from and implement writing to it\n");
|
||||
}
|
||||
|
||||
std::vector<u8>& data = pipeData[pipe];
|
||||
size = std::min<u32>(size, data.size()); // Clamp size to the maximum available data size
|
||||
|
||||
if (size == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Return "size" bytes from the audio pipe and erase them
|
||||
std::vector<u8> out(data.begin(), data.begin() + size);
|
||||
data.erase(data.begin(), data.begin() + size);
|
||||
return out;
|
||||
}
|
||||
} // namespace Audio
|
345
src/core/audio/teakra_core.cpp
Normal file
345
src/core/audio/teakra_core.cpp
Normal file
|
@ -0,0 +1,345 @@
|
|||
#include "audio/teakra_core.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include "services/dsp.hpp"
|
||||
|
||||
using namespace Audio;
|
||||
|
||||
struct Dsp1 {
|
||||
// All sizes are in bytes unless otherwise specified
|
||||
u8 signature[0x100];
|
||||
u8 magic[4];
|
||||
u32 size;
|
||||
u8 codeMemLayout;
|
||||
u8 dataMemLayout;
|
||||
u8 pad[3];
|
||||
u8 specialType;
|
||||
u8 segmentCount;
|
||||
u8 flags;
|
||||
u32 specialStart;
|
||||
u32 specialSize;
|
||||
u64 zeroBits;
|
||||
|
||||
struct Segment {
|
||||
u32 offs; // Offset of the segment data
|
||||
u32 dspAddr; // Start of the segment in 16-bit units
|
||||
u32 size;
|
||||
u8 pad[3];
|
||||
u8 type;
|
||||
u8 hash[0x20];
|
||||
};
|
||||
|
||||
Segment segments[10];
|
||||
};
|
||||
|
||||
TeakraDSP::TeakraDSP(Memory& mem, Scheduler& scheduler, DSPService& dspService)
|
||||
: DSPCore(mem, scheduler, dspService), pipeBaseAddr(0), running(false) {
|
||||
// Set up callbacks for Teakra
|
||||
Teakra::AHBMCallback ahbm;
|
||||
|
||||
// The AHBM read handlers read from paddrs rather than vaddrs which mem.read8 and the like use
|
||||
// TODO: When we implement more efficient paddr accesses with a page table or similar, these handlers
|
||||
// Should be made to properly use it, since this method is hacky and will segfault if given an invalid addr
|
||||
ahbm.read8 = [&](u32 addr) -> u8 { return mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; };
|
||||
ahbm.read16 = [&](u32 addr) -> u16 { return *(u16*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; };
|
||||
ahbm.read32 = [&](u32 addr) -> u32 { return *(u32*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; };
|
||||
|
||||
ahbm.write8 = [&](u32 addr, u8 value) { mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; };
|
||||
ahbm.write16 = [&](u32 addr, u16 value) { *(u16*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; };
|
||||
ahbm.write32 = [&](u32 addr, u32 value) { *(u32*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; };
|
||||
|
||||
teakra.SetAHBMCallback(ahbm);
|
||||
teakra.SetAudioCallback([](std::array<s16, 2> sample) { /* Do nothing */ });
|
||||
|
||||
// Set up event handlers. These handlers forward a hardware interrupt to the DSP service, which is responsible
|
||||
// For triggering the appropriate DSP kernel events
|
||||
// Note: It's important not to fire any events if "loaded" is false, ie if we haven't fully loaded a DSP component yet
|
||||
teakra.SetRecvDataHandler(0, [&]() {
|
||||
if (loaded) {
|
||||
dspService.triggerInterrupt0();
|
||||
}
|
||||
});
|
||||
|
||||
teakra.SetRecvDataHandler(1, [&]() {
|
||||
if (loaded) {
|
||||
dspService.triggerInterrupt1();
|
||||
}
|
||||
});
|
||||
|
||||
auto processPipeEvent = [&](bool dataEvent) {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataEvent) {
|
||||
signalledData = true;
|
||||
} else {
|
||||
if ((teakra.GetSemaphore() & 0x8000) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
signalledSemaphore = true;
|
||||
}
|
||||
|
||||
if (signalledSemaphore && signalledData) {
|
||||
signalledSemaphore = signalledData = false;
|
||||
|
||||
u16 slot = teakra.RecvData(2);
|
||||
u16 side = slot & 1;
|
||||
u16 pipe = slot / 2;
|
||||
|
||||
if (side != static_cast<u16>(PipeDirection::DSPtoCPU)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pipe == 0) {
|
||||
Helpers::warn("Pipe event for debug pipe: Should be ignored and the data should be flushed");
|
||||
} else {
|
||||
dspService.triggerPipeEvent(pipe);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
teakra.SetRecvDataHandler(2, [processPipeEvent]() { processPipeEvent(true); });
|
||||
teakra.SetSemaphoreHandler([processPipeEvent]() { processPipeEvent(false); });
|
||||
}
|
||||
|
||||
void TeakraDSP::reset() {
|
||||
teakra.Reset();
|
||||
running = false;
|
||||
loaded = false;
|
||||
signalledData = signalledSemaphore = false;
|
||||
|
||||
audioFrameIndex = 0;
|
||||
}
|
||||
|
||||
void TeakraDSP::setAudioEnabled(bool enable) {
|
||||
if (audioEnabled != enable) {
|
||||
audioEnabled = enable;
|
||||
|
||||
// Set the appropriate audio callback for Teakra
|
||||
if (audioEnabled) {
|
||||
teakra.SetAudioCallback([this](std::array<s16, 2> sample) {
|
||||
audioFrame[audioFrameIndex++] = sample[0];
|
||||
audioFrame[audioFrameIndex++] = sample[1];
|
||||
|
||||
// Push our samples at the end of an audio frame
|
||||
if (audioFrameIndex >= audioFrame.size()) {
|
||||
audioFrameIndex -= audioFrame.size();
|
||||
|
||||
// Wait until we've actually got room to do so
|
||||
while (sampleBuffer.size() + 2 > sampleBuffer.Capacity()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
||||
}
|
||||
|
||||
sampleBuffer.push(audioFrame.data(), audioFrame.size());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
teakra.SetAudioCallback([](std::array<s16, 2> sample) { /* Do nothing */ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/citra-emu/citra/blob/master/src/audio_core/lle/lle.cpp
|
||||
void TeakraDSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) {
|
||||
size &= 0xffff;
|
||||
|
||||
PipeStatus status = getPipeStatus(channel, PipeDirection::CPUtoDSP);
|
||||
bool needUpdate = false; // Do we need to update the pipe status and catch up Teakra?
|
||||
|
||||
std::vector<u8> data;
|
||||
data.reserve(size);
|
||||
|
||||
// Read data to write
|
||||
for (int i = 0; i < size; i++) {
|
||||
const u8 byte = mem.read8(buffer + i);
|
||||
data.push_back(byte);
|
||||
}
|
||||
u8* dataPointer = data.data();
|
||||
|
||||
while (size != 0) {
|
||||
if (status.isFull()) {
|
||||
Helpers::warn("Teakra: Writing to full pipe");
|
||||
}
|
||||
|
||||
// Calculate begin/end/size for write
|
||||
const u16 writeEnd = status.isWrapped() ? (status.readPointer & PipeStatus::pointerMask) : status.byteSize;
|
||||
const u16 writeBegin = status.writePointer & PipeStatus::pointerMask;
|
||||
const u16 writeSize = std::min<u16>(u16(size), writeEnd - writeBegin);
|
||||
|
||||
if (writeEnd <= writeBegin) [[unlikely]] {
|
||||
Helpers::warn("Teakra: Writing to pipe but end <= start");
|
||||
}
|
||||
|
||||
// Write data to pipe, increment write and buffer pointers, decrement size
|
||||
std::memcpy(getDataPointer(status.address * 2 + writeBegin), dataPointer, writeSize);
|
||||
dataPointer += writeSize;
|
||||
status.writePointer += writeSize;
|
||||
size -= writeSize;
|
||||
|
||||
if ((status.writePointer & PipeStatus::pointerMask) > status.byteSize) [[unlikely]] {
|
||||
Helpers::warn("Teakra: Writing to pipe but write > size");
|
||||
}
|
||||
|
||||
if ((status.writePointer & PipeStatus::pointerMask) == status.byteSize) {
|
||||
status.writePointer &= PipeStatus::wrapBit;
|
||||
status.writePointer ^= PipeStatus::wrapBit;
|
||||
}
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
updatePipeStatus(status);
|
||||
while (!teakra.SendDataIsEmpty(2)) {
|
||||
runSlice();
|
||||
}
|
||||
|
||||
teakra.SendData(2, status.slot);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<u8> TeakraDSP::readPipe(u32 channel, u32 peer, u32 size, u32 buffer) {
|
||||
size &= 0xffff;
|
||||
|
||||
PipeStatus status = getPipeStatus(channel, PipeDirection::DSPtoCPU);
|
||||
|
||||
std::vector<u8> pipeData(size);
|
||||
u8* dataPointer = pipeData.data();
|
||||
bool needUpdate = false; // Do we need to update the pipe status and catch up Teakra?
|
||||
|
||||
while (size != 0) {
|
||||
if (status.isEmpty()) [[unlikely]] {
|
||||
Helpers::warn("Teakra: Reading from empty pipe");
|
||||
return pipeData;
|
||||
}
|
||||
|
||||
// Read as many bytes as possible
|
||||
const u16 readEnd = status.isWrapped() ? status.byteSize : (status.writePointer & PipeStatus::pointerMask);
|
||||
const u16 readBegin = status.readPointer & PipeStatus::pointerMask;
|
||||
const u16 readSize = std::min<u16>(u16(size), readEnd - readBegin);
|
||||
|
||||
// Copy bytes to the output vector, increment the read and vector pointers and decrement the size appropriately
|
||||
std::memcpy(dataPointer, getDataPointer(status.address * 2 + readBegin), readSize);
|
||||
dataPointer += readSize;
|
||||
status.readPointer += readSize;
|
||||
size -= readSize;
|
||||
|
||||
if ((status.readPointer & PipeStatus::pointerMask) > status.byteSize) [[unlikely]] {
|
||||
Helpers::warn("Teakra: Reading from pipe but read > size");
|
||||
}
|
||||
|
||||
if ((status.readPointer & PipeStatus::pointerMask) == status.byteSize) {
|
||||
status.readPointer &= PipeStatus::wrapBit;
|
||||
status.readPointer ^= PipeStatus::wrapBit;
|
||||
}
|
||||
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
updatePipeStatus(status);
|
||||
while (!teakra.SendDataIsEmpty(2)) {
|
||||
runSlice();
|
||||
}
|
||||
|
||||
teakra.SendData(2, status.slot);
|
||||
}
|
||||
|
||||
return pipeData;
|
||||
}
|
||||
|
||||
void TeakraDSP::loadComponent(std::vector<u8>& data, u32 programMask, u32 dataMask) {
|
||||
// TODO: maybe move this to the DSP service
|
||||
if (loaded) {
|
||||
Helpers::warn("Loading DSP component when already loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
teakra.Reset();
|
||||
running = true;
|
||||
|
||||
u8* dspCode = teakra.GetDspMemory().data();
|
||||
u8* dspData = dspCode + 0x40000;
|
||||
|
||||
Dsp1 dsp1;
|
||||
std::memcpy(&dsp1, data.data(), sizeof(dsp1));
|
||||
|
||||
// TODO: verify DSP1 signature
|
||||
|
||||
// Load DSP segments to DSP RAM
|
||||
// TODO: verify hashes
|
||||
for (uint i = 0; i < dsp1.segmentCount; i++) {
|
||||
auto& segment = dsp1.segments[i];
|
||||
u32 addr = segment.dspAddr << 1;
|
||||
u8* src = data.data() + segment.offs;
|
||||
u8* dst = nullptr;
|
||||
|
||||
switch (segment.type) {
|
||||
case 0:
|
||||
case 1: dst = dspCode + addr; break;
|
||||
default: dst = dspData + addr; break;
|
||||
}
|
||||
|
||||
std::memcpy(dst, src, segment.size);
|
||||
}
|
||||
|
||||
bool syncWithDsp = dsp1.flags & 0x1;
|
||||
bool loadSpecialSegment = (dsp1.flags >> 1) & 0x1;
|
||||
|
||||
// TODO: how does the special segment work?
|
||||
if (loadSpecialSegment) {
|
||||
log("LoadComponent: special segment not supported");
|
||||
}
|
||||
|
||||
if (syncWithDsp) {
|
||||
// Wait for the DSP to reply with 1s in all RECV registers
|
||||
for (int i = 0; i < 3; i++) {
|
||||
do {
|
||||
while (!teakra.RecvDataIsReady(i)) {
|
||||
runSlice();
|
||||
}
|
||||
} while (teakra.RecvData(i) != 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the pipe base address
|
||||
while (!teakra.RecvDataIsReady(2)) {
|
||||
runSlice();
|
||||
}
|
||||
pipeBaseAddr = teakra.RecvData(2);
|
||||
|
||||
// Schedule next DSP event
|
||||
scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::lleSlice * 2);
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
void TeakraDSP::unloadComponent() {
|
||||
if (!loaded) {
|
||||
Helpers::warn("Audio: unloadComponent called without a running program");
|
||||
return;
|
||||
}
|
||||
loaded = false;
|
||||
// Stop scheduling DSP events
|
||||
scheduler.removeEvent(Scheduler::EventType::RunDSP);
|
||||
|
||||
// Wait for SEND2 to be ready, then send the shutdown command to the DSP
|
||||
while (!teakra.SendDataIsEmpty(2)) {
|
||||
runSlice();
|
||||
}
|
||||
|
||||
teakra.SendData(2, 0x8000);
|
||||
|
||||
// Wait for shutdown to be acknowledged
|
||||
while (!teakra.RecvDataIsReady(2)) {
|
||||
runSlice();
|
||||
}
|
||||
|
||||
// Read the value and discard it, completing shutdown
|
||||
teakra.RecvData(2);
|
||||
running = false;
|
||||
}
|
|
@ -12,9 +12,9 @@ const char* Kernel::resetTypeToString(u32 type) {
|
|||
}
|
||||
}
|
||||
|
||||
Handle Kernel::makeEvent(ResetType resetType) {
|
||||
Handle Kernel::makeEvent(ResetType resetType, Event::CallbackType callback) {
|
||||
Handle ret = makeObject(KernelObjectType::Event);
|
||||
objects[ret].data = new Event(resetType);
|
||||
objects[ret].data = new Event(resetType, callback);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -42,8 +42,13 @@ bool Kernel::signalEvent(Handle handle) {
|
|||
event->fired = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rescheduleThreads();
|
||||
// Run the callback for events that require a special callback
|
||||
if (event->callback != Event::CallbackType::None) [[unlikely]] {
|
||||
runEventCallback(event->callback);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -230,4 +235,12 @@ void Kernel::waitSynchronizationN() {
|
|||
} else {
|
||||
Helpers::panic("WaitSynchronizationN with waitAll");
|
||||
}
|
||||
}
|
||||
|
||||
void Kernel::runEventCallback(Event::CallbackType callback) {
|
||||
switch (callback) {
|
||||
case Event::CallbackType::None: break;
|
||||
case Event::CallbackType::DSPSemaphore: serviceManager.getDSP().onSemaphoreEventSignal(); break;
|
||||
default: Helpers::panic("Unimplemented special callback for kernel event!"); break;
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
|
|||
size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
|
||||
exheaderSize = *(u32*)&header[0x180];
|
||||
|
||||
const u64 programID = *(u64*)&header[0x118];
|
||||
programID = *(u64*)&header[0x118];
|
||||
|
||||
// Read NCCH flags
|
||||
secondaryKeySlot = header[0x188 + 3];
|
||||
|
|
|
@ -15,7 +15,6 @@ using namespace KernelMemoryTypes;
|
|||
|
||||
Memory::Memory(u64& cpuTicks, const EmulatorConfig& config) : cpuTicks(cpuTicks), config(config) {
|
||||
fcram = new uint8_t[FCRAM_SIZE]();
|
||||
dspRam = new uint8_t[DSP_RAM_SIZE]();
|
||||
|
||||
readTable.resize(totalPageCount, 0);
|
||||
writeTable.resize(totalPageCount, 0);
|
||||
|
@ -525,4 +524,14 @@ void Memory::copySharedFont(u8* pointer) {
|
|||
auto fonts = cmrc::ConsoleFonts::get_filesystem();
|
||||
auto font = fonts.open("CitraSharedFontUSRelocated.bin");
|
||||
std::memcpy(pointer, font.begin(), font.size());
|
||||
}
|
||||
|
||||
std::optional<u64> Memory::getProgramID() {
|
||||
auto cxi = getCXI();
|
||||
|
||||
if (cxi) {
|
||||
return cxi->programID;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
|
@ -84,7 +84,7 @@ void APTService::appletUtility(u32 messagePointer) {
|
|||
u32 outputSize = mem.read32(messagePointer + 12);
|
||||
u32 inputPointer = mem.read32(messagePointer + 20);
|
||||
|
||||
log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X) (Stubbed)\n", utility, inputSize, outputSize,
|
||||
log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X)\n", utility, inputSize, outputSize,
|
||||
inputPointer);
|
||||
|
||||
std::vector<u8> out(outputSize);
|
||||
|
@ -218,7 +218,7 @@ void APTService::initialize(u32 messagePointer) {
|
|||
}
|
||||
|
||||
void APTService::inquireNotification(u32 messagePointer) {
|
||||
log("APT::InquireNotification (STUBBED TO RETURN NONE)\n");
|
||||
log("APT::InquireNotification\n");
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0xB, 2, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
|
|
|
@ -31,13 +31,8 @@ namespace Result {
|
|||
}
|
||||
|
||||
void DSPService::reset() {
|
||||
for (auto& e : pipeData)
|
||||
e.clear();
|
||||
|
||||
// Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted
|
||||
resetAudioPipe();
|
||||
totalEventCount = 0;
|
||||
dspState = DSPState::Off;
|
||||
semaphoreMask = 0;
|
||||
|
||||
semaphoreEvent = std::nullopt;
|
||||
interrupt0 = std::nullopt;
|
||||
|
@ -48,40 +43,6 @@ void DSPService::reset() {
|
|||
}
|
||||
}
|
||||
|
||||
void DSPService::resetAudioPipe() {
|
||||
// Hardcoded responses for now
|
||||
// These are DSP DRAM offsets for various variables
|
||||
// https://www.3dbrew.org/wiki/DSP_Memory_Region
|
||||
static constexpr std::array<u16, 16> responses = {
|
||||
0x000F, // Number of responses
|
||||
0xBFFF, // Frame counter
|
||||
0x9E92, // Source configs
|
||||
0x8680, // Source statuses
|
||||
0xA792, // ADPCM coefficients
|
||||
0x9430, // DSP configs
|
||||
0x8400, // DSP status
|
||||
0x8540, // Final samples
|
||||
0x9492, // Intermediate mix samples
|
||||
0x8710, // Compressor
|
||||
0x8410, // Debug
|
||||
0xA912, // ??
|
||||
0xAA12, // ??
|
||||
0xAAD2, // ??
|
||||
0xAC52, // Surround sound biquad filter 1
|
||||
0xAC5C // Surround sound biquad filter 2
|
||||
};
|
||||
|
||||
std::vector<u8>& audioPipe = pipeData[DSPPipeType::Audio];
|
||||
audioPipe.resize(responses.size() * sizeof(u16));
|
||||
|
||||
// Push back every response to the audio pipe
|
||||
size_t index = 0;
|
||||
for (auto e : responses) {
|
||||
audioPipe[index++] = e & 0xff;
|
||||
audioPipe[index++] = e >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
void DSPService::handleSyncRequest(u32 messagePointer) {
|
||||
const u32 command = mem.read32(messagePointer);
|
||||
switch (command) {
|
||||
|
@ -117,8 +78,16 @@ void DSPService::loadComponent(u32 messagePointer) {
|
|||
u32 size = mem.read32(messagePointer + 4);
|
||||
u32 programMask = mem.read32(messagePointer + 8);
|
||||
u32 dataMask = mem.read32(messagePointer + 12);
|
||||
u32 buffer = mem.read32(messagePointer + 20);
|
||||
|
||||
std::vector<u8> data(size);
|
||||
for (u32 i = 0; i < size; i++) {
|
||||
data[i] = mem.read8(buffer + i);
|
||||
}
|
||||
|
||||
log("DSP::LoadComponent (size = %08X, program mask = %X, data mask = %X\n", size, programMask, dataMask);
|
||||
dsp->loadComponent(data, programMask, dataMask);
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x11, 2, 2));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
mem.write32(messagePointer + 8, 1); // Component loaded
|
||||
|
@ -128,32 +97,12 @@ void DSPService::loadComponent(u32 messagePointer) {
|
|||
|
||||
void DSPService::unloadComponent(u32 messagePointer) {
|
||||
log("DSP::UnloadComponent\n");
|
||||
dsp->unloadComponent();
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x12, 1, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
}
|
||||
|
||||
std::vector<u8> DSPService::readPipe(u32 pipe, u32 size) {
|
||||
if (size & 1) Helpers::panic("Tried to read odd amount of bytes from DSP pipe");
|
||||
if (pipe >= pipeCount || size > 0xffff) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (pipe != DSPPipeType::Audio) {
|
||||
log("Reading from non-audio pipe! This might be broken, might need to check what pipe is being read from and implement writing to it\n");
|
||||
}
|
||||
|
||||
std::vector<u8>& data = pipeData[pipe];
|
||||
size = std::min<u32>(size, u32(data.size())); // Clamp size to the maximum available data size
|
||||
|
||||
if (size == 0)
|
||||
return {};
|
||||
|
||||
// Return "size" bytes from the audio pipe and erase them
|
||||
std::vector<u8> out(data.begin(), data.begin() + size);
|
||||
data.erase(data.begin(), data.begin() + size);
|
||||
return out;
|
||||
}
|
||||
|
||||
void DSPService::readPipeIfPossible(u32 messagePointer) {
|
||||
u32 channel = mem.read32(messagePointer + 4);
|
||||
u32 peer = mem.read32(messagePointer + 8);
|
||||
|
@ -162,7 +111,7 @@ void DSPService::readPipeIfPossible(u32 messagePointer) {
|
|||
log("DSP::ReadPipeIfPossible (channel = %d, peer = %d, size = %04X, buffer = %08X)\n", channel, peer, size, buffer);
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x10, 2, 2));
|
||||
|
||||
std::vector<u8> data = readPipe(channel, size);
|
||||
std::vector<u8> data = dsp->readPipe(channel, peer, size, buffer);
|
||||
for (uint i = 0; i < data.size(); i++) {
|
||||
mem.write8(buffer + i, data[i]);
|
||||
}
|
||||
|
@ -176,22 +125,22 @@ void DSPService::recvData(u32 messagePointer) {
|
|||
log("DSP::RecvData (register = %d)\n", registerIndex);
|
||||
if (registerIndex != 0) Helpers::panic("Unknown register in DSP::RecvData");
|
||||
|
||||
// Return 0 if the DSP is running, otherwise 1
|
||||
const u16 ret = dspState == DSPState::On ? 0 : 1;
|
||||
const u16 data = dsp->recvData(registerIndex);
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x01, 2, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
mem.write16(messagePointer + 8, ret);
|
||||
mem.write16(messagePointer + 8, data);
|
||||
}
|
||||
|
||||
void DSPService::recvDataIsReady(u32 messagePointer) {
|
||||
const u32 registerIndex = mem.read32(messagePointer + 4);
|
||||
log("DSP::RecvDataIsReady (register = %d)\n", registerIndex);
|
||||
if (registerIndex != 0) Helpers::panic("Unknown register in DSP::RecvDataIsReady");
|
||||
|
||||
bool isReady = dsp->recvDataIsReady(registerIndex);
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x02, 2, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
mem.write32(messagePointer + 8, 1); // Always return that the register is ready for now
|
||||
mem.write32(messagePointer + 8, isReady ? 1 : 0);
|
||||
}
|
||||
|
||||
DSPService::DSPEvent& DSPService::getEventRef(u32 type, u32 pipe) {
|
||||
|
@ -236,7 +185,6 @@ void DSPService::registerInterruptEvents(u32 messagePointer) {
|
|||
mem.write32(messagePointer + 4, Result::Success);
|
||||
|
||||
totalEventCount++;
|
||||
kernel.signalEvent(eventHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,7 +201,7 @@ void DSPService::getSemaphoreEventHandle(u32 messagePointer) {
|
|||
log("DSP::GetSemaphoreEventHandle\n");
|
||||
|
||||
if (!semaphoreEvent.has_value()) {
|
||||
semaphoreEvent = kernel.makeEvent(ResetType::OneShot);
|
||||
semaphoreEvent = kernel.makeEvent(ResetType::OneShot, Event::CallbackType::DSPSemaphore);
|
||||
}
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x16, 1, 2));
|
||||
|
@ -267,6 +215,7 @@ void DSPService::setSemaphore(u32 messagePointer) {
|
|||
const u16 value = mem.read16(messagePointer + 4);
|
||||
log("DSP::SetSemaphore(value = %04X)\n", value);
|
||||
|
||||
dsp->setSemaphore(value);
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
}
|
||||
|
@ -275,6 +224,9 @@ void DSPService::setSemaphoreMask(u32 messagePointer) {
|
|||
const u16 mask = mem.read16(messagePointer + 4);
|
||||
log("DSP::SetSemaphoreMask(mask = %04X)\n", mask);
|
||||
|
||||
dsp->setSemaphoreMask(mask);
|
||||
semaphoreMask = mask;
|
||||
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x17, 1, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
}
|
||||
|
@ -285,51 +237,7 @@ void DSPService::writeProcessPipe(u32 messagePointer) {
|
|||
const u32 buffer = mem.read32(messagePointer + 16);
|
||||
log("DSP::writeProcessPipe (channel = %d, size = %X, buffer = %08X)\n", channel, size, buffer);
|
||||
|
||||
enum class StateChange : u8 {
|
||||
Initialize = 0,
|
||||
Shutdown = 1,
|
||||
Wakeup = 2,
|
||||
Sleep = 3,
|
||||
};
|
||||
|
||||
switch (channel) {
|
||||
case DSPPipeType::Audio: {
|
||||
if (size != 4) {
|
||||
printf("Invalid size written to DSP Audio Pipe\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get new state
|
||||
const u8 state = mem.read8(buffer);
|
||||
if (state > 3) {
|
||||
log("WriteProcessPipe::Audio: Unknown state change type");
|
||||
} else {
|
||||
switch (static_cast<StateChange>(state)) {
|
||||
case StateChange::Initialize:
|
||||
// TODO: Other initialization stuff here
|
||||
dspState = DSPState::On;
|
||||
resetAudioPipe();
|
||||
break;
|
||||
|
||||
case StateChange::Shutdown:
|
||||
dspState = DSPState::Off;
|
||||
break;
|
||||
|
||||
default: Helpers::panic("Unimplemented DSP audio pipe state change %d", state);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DSPPipeType::Binary:
|
||||
Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size);
|
||||
break;
|
||||
|
||||
default:
|
||||
log("DSP: Wrote to unimplemented pipe %d\n", channel);
|
||||
break;
|
||||
}
|
||||
|
||||
dsp->writeProcessPipe(channel, size, buffer);
|
||||
mem.write32(messagePointer, IPC::responseHeader(0xD, 1, 0));
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
}
|
||||
|
@ -354,12 +262,26 @@ void DSPService::invalidateDCache(u32 messagePointer) {
|
|||
mem.write32(messagePointer + 4, Result::Success);
|
||||
}
|
||||
|
||||
void DSPService::signalEvents() {
|
||||
for (const DSPEvent& e : pipeEvents) {
|
||||
if (e.has_value()) { kernel.signalEvent(e.value()); }
|
||||
void DSPService::triggerPipeEvent(int index) {
|
||||
if (index < pipeCount && pipeEvents[index].has_value()) {
|
||||
kernel.signalEvent(*pipeEvents[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (semaphoreEvent.has_value()) { kernel.signalEvent(semaphoreEvent.value()); }
|
||||
if (interrupt0.has_value()) { kernel.signalEvent(interrupt0.value()); }
|
||||
if (interrupt1.has_value()) { kernel.signalEvent(interrupt1.value()); }
|
||||
void DSPService::triggerSemaphoreEvent() {
|
||||
if (semaphoreEvent.has_value()) {
|
||||
kernel.signalEvent(*semaphoreEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void DSPService::triggerInterrupt0() {
|
||||
if (interrupt0.has_value()) {
|
||||
kernel.signalEvent(*interrupt0);
|
||||
}
|
||||
}
|
||||
|
||||
void DSPService::triggerInterrupt1() {
|
||||
if (interrupt1.has_value()) {
|
||||
kernel.signalEvent(*interrupt1);
|
||||
}
|
||||
}
|
|
@ -123,10 +123,6 @@ void GPUService::registerInterruptRelayQueue(u32 messagePointer) {
|
|||
}
|
||||
|
||||
void GPUService::requestInterrupt(GPUInterrupt type) {
|
||||
// HACK: Signal DSP events on GPU interrupt for now until we have the DSP since games need DSP events
|
||||
// Maybe there's a better alternative?
|
||||
kernel.signalDSPEvents();
|
||||
|
||||
if (sharedMem == nullptr) [[unlikely]] { // Shared memory hasn't been set up yet
|
||||
return;
|
||||
}
|
||||
|
@ -289,7 +285,7 @@ void GPUService::setBufferSwap(u32 messagePointer) {
|
|||
info.displayFb = mem.read32(messagePointer + 28); // Selects either framebuffer A or B
|
||||
|
||||
log("GSP::GPU::SetBufferSwap\n");
|
||||
Helpers::panic("Untested GSP::GPU::SetBufferSwap call");
|
||||
Helpers::warn("Untested GSP::GPU::SetBufferSwap call");
|
||||
|
||||
setBufferSwapImpl(screenId, info);
|
||||
mem.write32(messagePointer, IPC::responseHeader(0x05, 1, 0));
|
||||
|
@ -529,4 +525,4 @@ void GPUService::importDisplayCaptureInfo(u32 messagePointer) {
|
|||
mem.write32(messagePointer + 28, bottomScreenCapture.rightFramebuffer);
|
||||
mem.write32(messagePointer + 32, bottomScreenCapture.format);
|
||||
mem.write32(messagePointer + 36, bottomScreenCapture.stride);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,8 @@ void NwmUdsService::initializeWithVersion(u32 messagePointer) {
|
|||
|
||||
initialized = true;
|
||||
|
||||
mem.write32(messagePointer + 4, Result::Success);
|
||||
// Stubbed to fail temporarily, since some games will break trying to establish networks otherwise
|
||||
mem.write32(messagePointer + 4, Result::FailurePlaceholder);
|
||||
mem.write32(messagePointer + 8, 0);
|
||||
mem.write32(messagePointer + 12, eventHandle.value());
|
||||
}
|
||||
|
|
|
@ -18,12 +18,20 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
|
|||
|
||||
Emulator::Emulator()
|
||||
: config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config),
|
||||
cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false)
|
||||
cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false), programRunning(false)
|
||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
||||
,
|
||||
httpServer(this)
|
||||
#endif
|
||||
{
|
||||
DSPService& dspService = kernel.getServiceManager().getDSP();
|
||||
|
||||
dsp = Audio::makeDSPCore(config.dspType, memory, scheduler, dspService);
|
||||
dspService.setDSPCore(dsp.get());
|
||||
|
||||
audioDevice.init(dsp->getSamples());
|
||||
setAudioEnabled(config.audioEnabled);
|
||||
|
||||
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
|
||||
if (config.discordRpcEnabled) {
|
||||
discordRpc.init();
|
||||
|
@ -46,6 +54,8 @@ void Emulator::reset(ReloadOption reload) {
|
|||
cpu.reset();
|
||||
gpu.reset();
|
||||
memory.reset();
|
||||
dsp->reset();
|
||||
|
||||
// Reset scheduler and add a VBlank event
|
||||
scheduler.reset();
|
||||
|
||||
|
@ -95,8 +105,19 @@ void Emulator::step() {}
|
|||
void Emulator::render() {}
|
||||
|
||||
// Only resume if a ROM is properly loaded
|
||||
void Emulator::resume() { running = (romType != ROMType::None); }
|
||||
void Emulator::pause() { running = false; }
|
||||
void Emulator::resume() {
|
||||
running = (romType != ROMType::None);
|
||||
|
||||
if (running && config.audioEnabled) {
|
||||
audioDevice.start();
|
||||
}
|
||||
}
|
||||
|
||||
void Emulator::pause() {
|
||||
running = false;
|
||||
audioDevice.stop();
|
||||
}
|
||||
|
||||
void Emulator::togglePause() { running ? pause() : resume(); }
|
||||
|
||||
void Emulator::runFrame() {
|
||||
|
@ -136,13 +157,17 @@ void Emulator::pollScheduler() {
|
|||
ServiceManager& srv = kernel.getServiceManager();
|
||||
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
|
||||
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
|
||||
|
||||
|
||||
// Queue next VBlank event
|
||||
scheduler.addEvent(Scheduler::EventType::VBlank, time + CPU::ticksPerSec / 60);
|
||||
break;
|
||||
}
|
||||
|
||||
case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break;
|
||||
case Scheduler::EventType::RunDSP: {
|
||||
dsp->runAudioFrame();
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast<int>(eventType));
|
||||
|
@ -376,4 +401,16 @@ RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) {
|
|||
dumpRomFSNode(*node, (const char*)&romFS[0], path);
|
||||
|
||||
return DumpingResult::Success;
|
||||
}
|
||||
}
|
||||
|
||||
void Emulator::setAudioEnabled(bool enable) {
|
||||
if (!enable) {
|
||||
audioDevice.stop();
|
||||
} else if (enable && romType != ROMType::None && running) {
|
||||
// Don't start the audio device yet if there's no ROM loaded or the emulator is paused
|
||||
// Resume and Pause will handle it
|
||||
audioDevice.start();
|
||||
}
|
||||
|
||||
dsp->setAudioEnabled(enable);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
#include <unistd.h> // For ftruncate
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include "android_utils.hpp"
|
||||
#endif
|
||||
|
||||
IOFile::IOFile(const std::filesystem::path& path, const char* permissions) : handle(nullptr) { open(path, permissions); }
|
||||
|
||||
bool IOFile::open(const std::filesystem::path& path, const char* permissions) {
|
||||
|
@ -34,8 +38,19 @@ bool IOFile::open(const char* filename, const char* permissions) {
|
|||
if (isOpen()) {
|
||||
close();
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
std::string path(filename);
|
||||
|
||||
// Check if this is a URI directory, which will need special handling due to SAF
|
||||
if (path.find("://") != std::string::npos ) {
|
||||
handle = fdopen(AndroidUtils::openDocument(filename, permissions), permissions);
|
||||
} else {
|
||||
handle = std::fopen(filename, permissions);
|
||||
}
|
||||
#else
|
||||
handle = std::fopen(filename, permissions);
|
||||
#endif
|
||||
|
||||
handle = std::fopen(filename, permissions);
|
||||
return isOpen();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "emulator.hpp"
|
||||
#include "renderer_gl/renderer_gl.hpp"
|
||||
#include "services/hid.hpp"
|
||||
#include "android_utils.hpp"
|
||||
|
||||
std::unique_ptr<Emulator> emulator = nullptr;
|
||||
HIDService* hidService = nullptr;
|
||||
|
@ -14,6 +15,9 @@ RendererGL* renderer = nullptr;
|
|||
bool romLoaded = false;
|
||||
JavaVM* jvm = nullptr;
|
||||
|
||||
jclass alberClass;
|
||||
jmethodID alberClassOpenDocument;
|
||||
|
||||
#define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name
|
||||
|
||||
void throwException(JNIEnv* env, const char* message) {
|
||||
|
@ -42,7 +46,13 @@ MAKE_SETTING(setShaderJitEnabled, jboolean, shaderJitEnabled)
|
|||
|
||||
#undef MAKE_SETTING
|
||||
|
||||
AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { env->GetJavaVM(&jvm); }
|
||||
AlberFunction(void, Setup)(JNIEnv* env, jobject obj) {
|
||||
env->GetJavaVM(&jvm);
|
||||
|
||||
alberClass = (jclass)env->NewGlobalRef((jclass)env->FindClass("com/panda3ds/pandroid/AlberDriver"));
|
||||
alberClassOpenDocument = env->GetStaticMethodID(alberClass, "openDocument", "(Ljava/lang/String;Ljava/lang/String;)I");
|
||||
}
|
||||
|
||||
AlberFunction(void, Pause)(JNIEnv* env, jobject obj) { emulator->pause(); }
|
||||
AlberFunction(void, Resume)(JNIEnv* env, jobject obj) { emulator->resume(); }
|
||||
|
||||
|
@ -81,10 +91,12 @@ AlberFunction(void, Finalize)(JNIEnv* env, jobject obj) {
|
|||
|
||||
AlberFunction(jboolean, HasRomLoaded)(JNIEnv* env, jobject obj) { return romLoaded; }
|
||||
|
||||
AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) {
|
||||
AlberFunction(jboolean, LoadRom)(JNIEnv* env, jobject obj, jstring path) {
|
||||
const char* pathStr = env->GetStringUTFChars(path, nullptr);
|
||||
romLoaded = emulator->loadROM(pathStr);
|
||||
env->ReleaseStringUTFChars(path, pathStr);
|
||||
|
||||
return romLoaded;
|
||||
}
|
||||
|
||||
AlberFunction(void, LoadLuaScript)(JNIEnv* env, jobject obj, jstring script) {
|
||||
|
@ -114,3 +126,17 @@ AlberFunction(jbyteArray, GetSmdh)(JNIEnv* env, jobject obj) {
|
|||
}
|
||||
|
||||
#undef AlberFunction
|
||||
|
||||
int AndroidUtils::openDocument(const char* path, const char* perms) {
|
||||
auto env = jniEnv();
|
||||
|
||||
jstring uri = env->NewStringUTF(path);
|
||||
jstring jmode = env->NewStringUTF(perms);
|
||||
|
||||
jint result = env->CallStaticIntMethod(alberClass, alberClassOpenDocument, uri, jmode);
|
||||
|
||||
env->DeleteLocalRef(uri);
|
||||
env->DeleteLocalRef(jmode);
|
||||
|
||||
return (int)result;
|
||||
}
|
161
src/lua.cpp
161
src/lua.cpp
|
@ -1,4 +1,5 @@
|
|||
#ifdef PANDA3DS_ENABLE_LUA
|
||||
#include "emulator.hpp"
|
||||
#include "lua_manager.hpp"
|
||||
|
||||
#ifndef __ANDROID__
|
||||
|
@ -42,7 +43,7 @@ void LuaManager::loadFile(const char* path) {
|
|||
if (!initialized) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
|
||||
// If init failed, don't execute
|
||||
if (!initialized) {
|
||||
printf("Lua initialization failed, file won't run\n");
|
||||
|
@ -88,8 +89,8 @@ void LuaManager::loadString(const std::string& code) {
|
|||
}
|
||||
|
||||
void LuaManager::signalEventInternal(LuaEvent e) {
|
||||
lua_getglobal(L, "eventHandler"); // We want to call the event handler
|
||||
lua_pushnumber(L, static_cast<int>(e)); // Push event type
|
||||
lua_getglobal(L, "eventHandler"); // We want to call the event handler
|
||||
lua_pushnumber(L, static_cast<int>(e)); // Push event type
|
||||
|
||||
// Call the function with 1 argument and 0 outputs, without an error handler
|
||||
lua_pcall(L, 1, 0, 0);
|
||||
|
@ -103,28 +104,105 @@ void LuaManager::reset() {
|
|||
// Initialize C++ thunks for Lua code to call here
|
||||
// All code beyond this point is terrible and full of global state, don't judge
|
||||
|
||||
Memory* LuaManager::g_memory = nullptr;
|
||||
Emulator* LuaManager::g_emulator = nullptr;
|
||||
|
||||
#define MAKE_MEMORY_FUNCTIONS(size) \
|
||||
static int read##size##Thunk(lua_State* L) { \
|
||||
const u32 vaddr = (u32)lua_tonumber(L, 1); \
|
||||
lua_pushnumber(L, LuaManager::g_memory->read##size(vaddr)); \
|
||||
return 1; \
|
||||
} \
|
||||
static int write##size##Thunk(lua_State* L) { \
|
||||
const u32 vaddr = (u32)lua_tonumber(L, 1); \
|
||||
const u##size value = (u##size)lua_tonumber(L, 2); \
|
||||
LuaManager::g_memory->write##size(vaddr, value); \
|
||||
return 0; \
|
||||
#define MAKE_MEMORY_FUNCTIONS(size) \
|
||||
static int read##size##Thunk(lua_State* L) { \
|
||||
const u32 vaddr = (u32)lua_tonumber(L, 1); \
|
||||
lua_pushnumber(L, LuaManager::g_emulator->getMemory().read##size(vaddr)); \
|
||||
return 1; \
|
||||
} \
|
||||
static int write##size##Thunk(lua_State* L) { \
|
||||
const u32 vaddr = (u32)lua_tonumber(L, 1); \
|
||||
const u##size value = (u##size)lua_tonumber(L, 2); \
|
||||
LuaManager::g_emulator->getMemory().write##size(vaddr, value); \
|
||||
return 0; \
|
||||
}
|
||||
|
||||
|
||||
MAKE_MEMORY_FUNCTIONS(8)
|
||||
MAKE_MEMORY_FUNCTIONS(16)
|
||||
MAKE_MEMORY_FUNCTIONS(32)
|
||||
MAKE_MEMORY_FUNCTIONS(64)
|
||||
#undef MAKE_MEMORY_FUNCTIONS
|
||||
|
||||
static int getAppIDThunk(lua_State* L) {
|
||||
std::optional<u64> id = LuaManager::g_emulator->getMemory().getProgramID();
|
||||
|
||||
// If the app has an ID, return true + its ID
|
||||
// Otherwise return false and 0 as the ID
|
||||
if (id.has_value()) {
|
||||
lua_pushboolean(L, 1); // Return true
|
||||
lua_pushnumber(L, u32(*id)); // Return bottom 32 bits
|
||||
lua_pushnumber(L, u32(*id >> 32)); // Return top 32 bits
|
||||
} else {
|
||||
lua_pushboolean(L, 0); // Return false
|
||||
// Return no ID
|
||||
lua_pushnumber(L, 0);
|
||||
lua_pushnumber(L, 0);
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
static int pauseThunk(lua_State* L) {
|
||||
LuaManager::g_emulator->pause();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int resumeThunk(lua_State* L) {
|
||||
LuaManager::g_emulator->resume();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int resetThunk(lua_State* L) {
|
||||
LuaManager::g_emulator->reset(Emulator::ReloadOption::Reload);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int loadROMThunk(lua_State* L) {
|
||||
// Path argument is invalid, report that loading failed and exit
|
||||
if (lua_type(L, -1) != LUA_TSTRING) {
|
||||
lua_pushboolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t pathLength;
|
||||
const char* const str = lua_tolstring(L, -1, &pathLength);
|
||||
|
||||
const auto path = std::filesystem::path(std::string(str, pathLength));
|
||||
// Load ROM and reply if it succeeded or not
|
||||
lua_pushboolean(L, LuaManager::g_emulator->loadROM(path) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int getButtonsThunk(lua_State* L) {
|
||||
auto buttons = LuaManager::g_emulator->getServiceManager().getHID().getOldButtons();
|
||||
lua_pushinteger(L, static_cast<lua_Integer>(buttons));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int getCirclepadThunk(lua_State* L) {
|
||||
auto& hid = LuaManager::g_emulator->getServiceManager().getHID();
|
||||
s16 x = hid.getCirclepadX();
|
||||
s16 y = hid.getCirclepadY();
|
||||
|
||||
lua_pushinteger(L, static_cast<lua_Number>(x));
|
||||
lua_pushinteger(L, static_cast<lua_Number>(y));
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int getButtonThunk(lua_State* L) {
|
||||
auto& hid = LuaManager::g_emulator->getServiceManager().getHID();
|
||||
// This function accepts a mask. You can use it to check if one or more buttons are pressed at a time
|
||||
const u32 mask = (u32)lua_tonumber(L, 1);
|
||||
const bool result = (hid.getOldButtons() & mask) == mask;
|
||||
|
||||
// Return whether the selected buttons are all pressed
|
||||
lua_pushboolean(L, result ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static constexpr luaL_Reg functions[] = {
|
||||
{ "__read8", read8Thunk },
|
||||
|
@ -135,6 +213,14 @@ static constexpr luaL_Reg functions[] = {
|
|||
{ "__write16", write16Thunk },
|
||||
{ "__write32", write32Thunk },
|
||||
{ "__write64", write64Thunk },
|
||||
{ "__getAppID", getAppIDThunk },
|
||||
{ "__pause", pauseThunk},
|
||||
{ "__resume", resumeThunk},
|
||||
{ "__reset", resetThunk},
|
||||
{ "__loadROM", loadROMThunk},
|
||||
{ "__getButtons", getButtonsThunk},
|
||||
{ "__getCirclepad", getCirclepadThunk},
|
||||
{ "__getButton", getButtonThunk},
|
||||
{ nullptr, nullptr },
|
||||
};
|
||||
// clang-format on
|
||||
|
@ -150,7 +236,35 @@ void LuaManager::initializeThunks() {
|
|||
write16 = function(addr, value) GLOBALS.__write16(addr, value) end,
|
||||
write32 = function(addr, value) GLOBALS.__write32(addr, value) end,
|
||||
write64 = function(addr, value) GLOBALS.__write64(addr, value) end,
|
||||
|
||||
getAppID = function()
|
||||
local ffi = require("ffi")
|
||||
|
||||
result, low, high = GLOBALS.__getAppID()
|
||||
id = bit.bor(ffi.cast("uint64_t", low), (bit.lshift(ffi.cast("uint64_t", high), 32)))
|
||||
return result, id
|
||||
end,
|
||||
|
||||
pause = function() GLOBALS.__pause() end,
|
||||
resume = function() GLOBALS.__resume() end,
|
||||
reset = function() GLOBALS.__reset() end,
|
||||
loadROM = function(path) return GLOBALS.__loadROM(path) end,
|
||||
|
||||
getButtons = function() return GLOBALS.__getButtons() end,
|
||||
getButton = function(button) return GLOBALS.__getButton(button) end,
|
||||
getCirclepad = function() return GLOBALS.__getCirclepad() end,
|
||||
|
||||
Frame = __Frame,
|
||||
ButtonA = __ButtonA,
|
||||
ButtonB = __ButtonB,
|
||||
ButtonX = __ButtonX,
|
||||
ButtonY = __ButtonY,
|
||||
ButtonL = __ButtonL,
|
||||
ButtonR = __ButtonR,
|
||||
ButtonUp = __ButtonUp,
|
||||
ButtonDown = __ButtonDown,
|
||||
ButtonLeft = __ButtonLeft,
|
||||
ButtonRight= __ButtonRight,
|
||||
}
|
||||
)";
|
||||
|
||||
|
@ -160,8 +274,21 @@ void LuaManager::initializeThunks() {
|
|||
};
|
||||
|
||||
luaL_register(L, "GLOBALS", functions);
|
||||
// Add values for event enum
|
||||
addIntConstant(LuaEvent::Frame, "__Frame");
|
||||
|
||||
// Add enums for 3DS keys
|
||||
addIntConstant(HID::Keys::A, "__ButtonA");
|
||||
addIntConstant(HID::Keys::B, "__ButtonB");
|
||||
addIntConstant(HID::Keys::X, "__ButtonX");
|
||||
addIntConstant(HID::Keys::Y, "__ButtonY");
|
||||
addIntConstant(HID::Keys::Up, "__ButtonUp");
|
||||
addIntConstant(HID::Keys::Down, "__ButtonDown");
|
||||
addIntConstant(HID::Keys::Left, "__ButtonLeft");
|
||||
addIntConstant(HID::Keys::Right, "__ButtonRight");
|
||||
addIntConstant(HID::Keys::L, "__ButtonL");
|
||||
addIntConstant(HID::Keys::R, "__ButtonR");
|
||||
|
||||
// Call our Lua runtime initialization before any Lua script runs
|
||||
luaL_loadstring(L, runtimeInit);
|
||||
int ret = lua_pcall(L, 0, 0, 0); // tell Lua to run the script
|
||||
|
@ -174,4 +301,4 @@ void LuaManager::initializeThunks() {
|
|||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
7
src/miniaudio.cpp
Normal file
7
src/miniaudio.cpp
Normal file
|
@ -0,0 +1,7 @@
|
|||
// We do not need the ability to be able to encode or decode audio files for the time being
|
||||
// So we disable said functionality to make the executable smaller
|
||||
#define MA_NO_DECODING
|
||||
#define MA_NO_ENCODING
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
|
||||
#include "miniaudio.h"
|
|
@ -87,7 +87,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
|
|||
// Make GL context current for this thread, enable VSync
|
||||
GL::Context* glContext = screen.getGLContext();
|
||||
glContext->MakeCurrent();
|
||||
glContext->SetSwapInterval(1);
|
||||
glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0);
|
||||
|
||||
emu->initGraphicsContext(glContext);
|
||||
} else if (usingVk) {
|
||||
|
|
|
@ -49,6 +49,8 @@ FrontendSDL::FrontendSDL() {
|
|||
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||
Helpers::panic("OpenGL init failed");
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0);
|
||||
}
|
||||
|
||||
#ifdef PANDA3DS_ENABLE_VULKAN
|
||||
|
|
|
@ -22,8 +22,8 @@ android {
|
|||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = false
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
proguardFiles(
|
||||
|
|
25
src/pandroid/app/proguard-rules.pro
vendored
25
src/pandroid/app/proguard-rules.pro
vendored
|
@ -1,16 +1,19 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# Pandroid Proguard Rules
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# Keep all JNI and C++ related classes and methods
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# Keep all native libraries and their methods
|
||||
-keep class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# Keep all classes in the specified package and its subpackages
|
||||
-keep class com.panda3ds.pandroid.** {*;}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
|
@ -18,4 +21,4 @@
|
|||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
|
||||
<uses-feature
|
||||
android:required="true"
|
||||
android:glEsVersion="0x0030001"/>
|
||||
|
@ -24,6 +20,10 @@
|
|||
android:hardwareAccelerated="true"
|
||||
android:theme="@style/Theme.Pandroid"
|
||||
tools:targetApi="31">
|
||||
|
||||
<meta-data android:name="android.game_mode_config"
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<activity
|
||||
android:name=".app.MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
package com.panda3ds.pandroid;
|
||||
|
||||
import android.util.Log;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.panda3ds.pandroid.app.PandroidApplication;
|
||||
import com.panda3ds.pandroid.utils.FileUtils;
|
||||
import com.panda3ds.pandroid.utils.GameUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class AlberDriver {
|
||||
AlberDriver() { super(); }
|
||||
|
@ -9,7 +17,7 @@ public class AlberDriver {
|
|||
public static native void Initialize();
|
||||
public static native void RunFrame(int fbo);
|
||||
public static native boolean HasRomLoaded();
|
||||
public static native void LoadRom(String path);
|
||||
public static native boolean LoadRom(String path);
|
||||
public static native void Finalize();
|
||||
|
||||
public static native void KeyDown(int code);
|
||||
|
@ -24,5 +32,26 @@ public class AlberDriver {
|
|||
|
||||
public static native void setShaderJitEnabled(boolean enable);
|
||||
|
||||
public static int openDocument(String path, String mode) {
|
||||
try {
|
||||
mode = FileUtils.parseNativeMode(mode);
|
||||
Context context = PandroidApplication.getAppContext();
|
||||
Uri uri = FileUtils.obtainUri(path);
|
||||
ParcelFileDescriptor parcel;
|
||||
if (Objects.equals(uri.getScheme(), "game")) {
|
||||
if (mode.contains("w")) {
|
||||
throw new IllegalArgumentException("Cannot open ROM file as writable");
|
||||
}
|
||||
uri = FileUtils.obtainUri(GameUtils.getCurrentGame().getRealPath());
|
||||
}
|
||||
parcel = context.getContentResolver().openFileDescriptor(uri, mode);
|
||||
int fd = parcel.detachFd();
|
||||
parcel.close();
|
||||
|
||||
return fd;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
static { System.loadLibrary("Alber"); }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.panda3ds.pandroid.app;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
@ -72,6 +73,9 @@ public class GameActivity extends BaseActivity {
|
|||
InputHandler.reset();
|
||||
InputHandler.setMotionDeadZone(InputMap.getDeadZone());
|
||||
InputHandler.setEventListener(inputListener);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
getTheme().applyStyle(R.style.GameActivityNavigationBar, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,32 +1,18 @@
|
|||
package com.panda3ds.pandroid.app;
|
||||
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
import static android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.MenuItem;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.navigation.NavigationBarView;
|
||||
import com.panda3ds.pandroid.R;
|
||||
import com.panda3ds.pandroid.app.editor.CodeEditorActivity;
|
||||
import com.panda3ds.pandroid.app.main.GamesFragment;
|
||||
import com.panda3ds.pandroid.app.main.SearchFragment;
|
||||
import com.panda3ds.pandroid.app.main.SettingsFragment;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener {
|
||||
private static final int PICK_ROM = 2;
|
||||
private static final int PERMISSION_REQUEST_CODE = 3;
|
||||
|
||||
private final GamesFragment gamesFragment = new GamesFragment();
|
||||
private final SearchFragment searchFragment = new SearchFragment();
|
||||
private final SettingsFragment settingsFragment = new SettingsFragment();
|
||||
|
@ -34,16 +20,6 @@ public class MainActivity extends BaseActivity implements NavigationBarView.OnIt
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
Intent intent = new Intent(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
startActivity(intent);
|
||||
}
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
|
||||
ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
NavigationBarView bar = findViewById(R.id.navigation);
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.panda3ds.pandroid.app.base;
|
|||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
@ -22,6 +23,9 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
|||
}
|
||||
|
||||
protected void setActivityTitle(@StringRes int titleId) {
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId);
|
||||
ActionBar header = ((AppCompatActivity) requireActivity()).getSupportActionBar();
|
||||
if (header != null) {
|
||||
header.setTitle(titleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.panda3ds.pandroid.app.base;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import com.panda3ds.pandroid.R;
|
||||
|
||||
public class LoadingAlertDialog extends BottomAlertDialog {
|
||||
public LoadingAlertDialog(@NonNull Context context, @StringRes int title) {
|
||||
super(context);
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_loading,null, false);
|
||||
setView(view);
|
||||
setCancelable(false);
|
||||
((AppCompatTextView)view.findViewById(R.id.title))
|
||||
.setText(title);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.panda3ds.pandroid.AlberDriver;
|
||||
|
@ -46,8 +48,7 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe
|
|||
((AppCompatTextView)view.findViewById(R.id.game_title)).setText(game.getTitle());
|
||||
((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher());
|
||||
|
||||
((NavigationView)view.findViewById(R.id.action_navigation)).setNavigationItemSelectedListener(this);
|
||||
((NavigationView)view.findViewById(R.id.others_navigation)).setNavigationItemSelectedListener(this);
|
||||
((NavigationView)view.findViewById(R.id.menu)).setNavigationItemSelectedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -104,8 +105,11 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe
|
|||
close();
|
||||
} else if (id == R.id.exit) {
|
||||
requireActivity().finish();
|
||||
} else if (id == R.id.lua_script){
|
||||
} else if (id == R.id.lua_script) {
|
||||
new LuaDialogFragment().show(getParentFragmentManager(), null);
|
||||
} else if (id == R.id.change_orientation) {
|
||||
boolean isLandscape = getResources().getDisplayMetrics().widthPixels > getResources().getDisplayMetrics().heightPixels;
|
||||
requireActivity().setRequestedOrientation(isLandscape ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.panda3ds.pandroid.app.base.BottomAlertDialog;
|
|||
import com.panda3ds.pandroid.app.base.BottomDialogFragment;
|
||||
import com.panda3ds.pandroid.app.editor.CodeEditorActivity;
|
||||
import com.panda3ds.pandroid.lang.Task;
|
||||
import com.panda3ds.pandroid.utils.Constants;
|
||||
import com.panda3ds.pandroid.utils.FileUtils;
|
||||
import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout;
|
||||
import com.panda3ds.pandroid.view.recycler.SimpleListAdapter;
|
||||
|
@ -94,9 +95,8 @@ public class LuaDialogFragment extends BottomDialogFragment {
|
|||
|
||||
((RecyclerView) view.findViewById(R.id.recycler)).setAdapter(adapter);
|
||||
((RecyclerView) view.findViewById(R.id.recycler)).setLayoutManager(new AutoFitGridLayout(getContext(), 140));
|
||||
FileUtils.createDir(FileUtils.getResourcesPath(), "Lua Scripts");
|
||||
ArrayList<LuaFile> files = new ArrayList<>();
|
||||
String path = FileUtils.getResourcesPath() + "/Lua Scripts/";
|
||||
String path = FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_LUA_SCRIPTS);
|
||||
for (String file : FileUtils.listFiles(path)) {
|
||||
files.add(new LuaFile(file));
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ public class LuaDialogFragment extends BottomDialogFragment {
|
|||
}
|
||||
|
||||
private LuaFile(String name) {
|
||||
this(FileUtils.getResourcesPath() + "/Lua Scripts/", name);
|
||||
this(FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_LUA_SCRIPTS), name);
|
||||
}
|
||||
|
||||
private String absolutePath() {
|
||||
|
@ -182,4 +182,4 @@ public class LuaDialogFragment extends BottomDialogFragment {
|
|||
return FileUtils.getLastModified(absolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,17 @@ import androidx.activity.result.ActivityResultLauncher;
|
|||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.panda3ds.pandroid.R;
|
||||
import com.panda3ds.pandroid.app.base.LoadingAlertDialog;
|
||||
import com.panda3ds.pandroid.data.game.GameMetadata;
|
||||
import com.panda3ds.pandroid.lang.Task;
|
||||
import com.panda3ds.pandroid.utils.Constants;
|
||||
import com.panda3ds.pandroid.utils.FileUtils;
|
||||
import com.panda3ds.pandroid.utils.GameUtils;
|
||||
import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class GamesFragment extends Fragment implements ActivityResultCallback<Uri> {
|
||||
private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument();
|
||||
|
@ -49,18 +53,45 @@ public class GamesFragment extends Fragment implements ActivityResultCallback<Ur
|
|||
if (result != null) {
|
||||
String uri = result.toString();
|
||||
if (GameUtils.findByRomPath(uri) == null) {
|
||||
if (FileUtils.obtainRealPath(uri) == null) {
|
||||
if (!FileUtils.exists(uri)) {
|
||||
Toast.makeText(getContext(), "Invalid file path", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
|
||||
GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0], "Unknown");
|
||||
GameUtils.addGame(game);
|
||||
GameUtils.launch(requireActivity(), game);
|
||||
|
||||
String extension = FileUtils.extension(uri);
|
||||
|
||||
// For ELF and AXF files the emulator core uses the C++ iostreams API to be compatible with elfio unlike other file types
|
||||
// As such, instead of writing more SAF code for operating with iostreams we just copy the ELF/AXF file to our own private directory
|
||||
// And use it without caring about SAF
|
||||
if (extension.equals("elf") || extension.endsWith("axf")) {
|
||||
importELF(uri);
|
||||
} else {
|
||||
FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
|
||||
|
||||
GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0], getString(R.string.unknown));
|
||||
GameUtils.addGame(game);
|
||||
GameUtils.launch(requireActivity(), game);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importELF(String uri) {
|
||||
AlertDialog dialog = new LoadingAlertDialog(requireActivity(), R.string.loading).create();
|
||||
dialog.show();
|
||||
new Task(() -> {
|
||||
String uuid = UUID.randomUUID().toString() + "." + FileUtils.extension(uri);
|
||||
String name = FileUtils.getName(uri);
|
||||
FileUtils.copyFile(uri, FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF), uuid);
|
||||
gameListView.post(() -> {
|
||||
dialog.hide();
|
||||
GameMetadata game = new GameMetadata("elf://" + uuid, name.substring(0, name.length() - 4).trim(), "");
|
||||
GameUtils.addGame(game);
|
||||
GameUtils.launch(requireActivity(), game);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
|
@ -8,7 +8,7 @@ import com.panda3ds.pandroid.R;
|
|||
import com.panda3ds.pandroid.app.PreferenceActivity;
|
||||
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
|
||||
import com.panda3ds.pandroid.app.preferences.AppearancePreferences;
|
||||
import com.panda3ds.pandroid.app.preferences.DeveloperPreferences;
|
||||
import com.panda3ds.pandroid.app.preferences.AdvancedPreferences;
|
||||
import com.panda3ds.pandroid.app.preferences.InputPreferences;
|
||||
|
||||
public class SettingsFragment extends BasePreferenceFragment {
|
||||
|
@ -17,6 +17,6 @@ public class SettingsFragment extends BasePreferenceFragment {
|
|||
setPreferencesFromResource(R.xml.start_preferences, rootKey);
|
||||
setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class));
|
||||
setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class));
|
||||
setItemClick("developer", (item)-> PreferenceActivity.launch(requireContext(), DeveloperPreferences.class));
|
||||
setItemClick("advanced", (item)-> PreferenceActivity.launch(requireContext(), AdvancedPreferences.class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
|
|||
import com.panda3ds.pandroid.app.services.LoggerService;
|
||||
import com.panda3ds.pandroid.data.config.GlobalConfig;
|
||||
|
||||
public class DeveloperPreferences extends BasePreferenceFragment {
|
||||
public class AdvancedPreferences extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
setPreferencesFromResource(R.xml.developer_preferences, rootKey);
|
||||
setActivityTitle(R.string.developer_options);
|
||||
setPreferencesFromResource(R.xml.advanced_preferences, rootKey);
|
||||
setActivityTitle(R.string.advanced_options);
|
||||
|
||||
setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreference) pref).isChecked()));
|
||||
setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreference) pref).isChecked()));
|
|
@ -15,7 +15,7 @@ public class AppearancePreferences extends BasePreferenceFragment {
|
|||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
setPreferencesFromResource(R.xml.appearance_preference, rootKey);
|
||||
|
||||
((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.appearance);
|
||||
setActivityTitle(R.string.appearance);
|
||||
|
||||
SingleSelectionPreferences themePreference = findPreference("theme");
|
||||
themePreference.setSelectedItem(GlobalConfig.get(GlobalConfig.KEY_APP_THEME));
|
||||
|
|
|
@ -37,7 +37,9 @@ public class ControllerMapperPreferences extends Fragment {
|
|||
|
||||
currentProfile = ControllerProfileManager.get(getArguments().getString("profile")).clone();
|
||||
|
||||
((BaseActivity) requireActivity()).getSupportActionBar().hide();
|
||||
if (((BaseActivity)requireActivity()).getSupportActionBar() != null) {
|
||||
((BaseActivity) requireActivity()).getSupportActionBar().hide();
|
||||
}
|
||||
mapper = view.findViewById(R.id.mapper);
|
||||
mapper.initialize(this::onLocationChanged, currentProfile);
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ public class InputMapPreferences extends BasePreferenceFragment implements Activ
|
|||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
setPreferencesFromResource(R.xml.input_map_preferences, rootKey);
|
||||
|
||||
((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.controller_mapping);
|
||||
setActivityTitle(R.string.controller_mapping);
|
||||
|
||||
for (KeyName key : KeyName.values()) {
|
||||
if (key == KeyName.NULL) {
|
||||
|
|
|
@ -43,6 +43,10 @@ public class GameMetadata {
|
|||
return romPath;
|
||||
}
|
||||
|
||||
public String getRealPath() {
|
||||
return GameUtils.resolvePath(romPath);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
|
@ -26,4 +26,6 @@ public class Constants {
|
|||
public static final String PREF_GAME_UTILS = "app.GameUtils";
|
||||
public static final String PREF_INPUT_MAP = "app.InputMap";
|
||||
public static final String PREF_SCREEN_CONTROLLER_PROFILES = "app.input.ScreenControllerManager";
|
||||
public static final String RESOURCE_FOLDER_ELF = "ELF"; // Folder for caching ELF files
|
||||
public static final String RESOURCE_FOLDER_LUA_SCRIPTS = "Lua Scripts";
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public class FileUtils {
|
|||
return parseFile(path).getName();
|
||||
}
|
||||
|
||||
public static String getResourcesPath(){
|
||||
public static String getResourcesPath() {
|
||||
File file = new File(getPrivatePath(), "config/resources");
|
||||
if (!file.exists()) {
|
||||
file.mkdirs();
|
||||
|
@ -48,6 +48,13 @@ public class FileUtils {
|
|||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getResourcePath(String name) {
|
||||
File file = new File(getResourcesPath(), name);
|
||||
file.mkdirs();
|
||||
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getPrivatePath() {
|
||||
File file = getContext().getFilesDir();
|
||||
if (!file.exists()) {
|
||||
|
@ -66,11 +73,33 @@ public class FileUtils {
|
|||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String parseNativeMode(String mode) {
|
||||
mode = mode.toLowerCase();
|
||||
switch (mode) {
|
||||
case "r":
|
||||
case "rb":
|
||||
return "r";
|
||||
case "r+":
|
||||
case "r+b":
|
||||
case "rb+":
|
||||
return "rw";
|
||||
case "w+":
|
||||
return "rwt";
|
||||
case "w":
|
||||
case "wb":
|
||||
return "wt";
|
||||
case "wa":
|
||||
return "wa";
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid file mode: "+mode);
|
||||
}
|
||||
|
||||
public static boolean exists(String path) {
|
||||
return parseFile(path).exists();
|
||||
}
|
||||
|
||||
public static void rename(String path, String newName){
|
||||
public static void rename(String path, String newName) {
|
||||
parseFile(path).renameTo(newName);
|
||||
}
|
||||
|
||||
|
@ -206,7 +235,7 @@ public class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void updateFile(String path){
|
||||
public static void updateFile(String path) {
|
||||
DocumentFile file = parseFile(path);
|
||||
Uri uri = file.getUri();
|
||||
|
||||
|
@ -232,15 +261,53 @@ public class FileUtils {
|
|||
return parseFile(path).lastModified();
|
||||
}
|
||||
|
||||
public static String[] listFiles(String path){
|
||||
public static String[] listFiles(String path) {
|
||||
DocumentFile folder = parseFile(path);
|
||||
DocumentFile[] files = folder.listFiles();
|
||||
|
||||
String[] result = new String[files.length];
|
||||
for (int i = 0; i < result.length; i++){
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = files[i].getName();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Uri obtainUri(String path) {
|
||||
return parseFile(path).getUri();
|
||||
}
|
||||
|
||||
public static String extension(String uri) {
|
||||
String name = getName(uri);
|
||||
if (!name.contains(".")) {
|
||||
return name.toLowerCase();
|
||||
}
|
||||
String[] parts = name.split("\\.");
|
||||
|
||||
return parts[parts.length-1].toLowerCase();
|
||||
}
|
||||
|
||||
public static boolean copyFile(String source, String path, String name) {
|
||||
try {
|
||||
String fullPath = path + "/" + name;
|
||||
if (!FileUtils.exists(fullPath)) {
|
||||
FileUtils.delete(fullPath);
|
||||
}
|
||||
FileUtils.createFile(path, name);
|
||||
InputStream in = getInputStream(source);
|
||||
OutputStream out = getOutputStream(fullPath);
|
||||
byte[] buffer = new byte[1024 * 128]; //128 KB
|
||||
int length;
|
||||
while ((length = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, length);
|
||||
}
|
||||
out.flush();
|
||||
out.close();
|
||||
in.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.LOG_TAG, "ERROR ON COPY FILE", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ public class GameUtils {
|
|||
|
||||
public static GameMetadata findByRomPath(String romPath) {
|
||||
for (GameMetadata game : data.games) {
|
||||
if (Objects.equals(romPath, game.getRomPath())) {
|
||||
if (Objects.equals(romPath, game.getRealPath())) {
|
||||
return game;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,13 @@ public class GameUtils {
|
|||
|
||||
public static void launch(Context context, GameMetadata game) {
|
||||
currentGame = game;
|
||||
String path = FileUtils.obtainRealPath(game.getRomPath());
|
||||
String path = game.getRealPath();
|
||||
if (path.contains("://")) {
|
||||
String[] parts = Uri.decode(game.getRomPath()).split("/");
|
||||
String name = parts[parts.length - 1];
|
||||
path = "game://internal/" + name;
|
||||
}
|
||||
|
||||
context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
|
||||
}
|
||||
|
||||
|
@ -58,6 +64,22 @@ public class GameUtils {
|
|||
writeChanges();
|
||||
}
|
||||
|
||||
public static String resolvePath(String path) {
|
||||
String lower = path.toLowerCase();
|
||||
if (!lower.contains("://")) {
|
||||
return path;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(path);
|
||||
switch (uri.getScheme().toLowerCase()) {
|
||||
case "elf": {
|
||||
return FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF)+"/"+uri.getAuthority();
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static ArrayList<GameMetadata> getGames() {
|
||||
return new ArrayList<>(data.games);
|
||||
}
|
||||
|
|
|
@ -2,20 +2,24 @@ package com.panda3ds.pandroid.view;
|
|||
|
||||
import static android.opengl.GLES32.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import com.panda3ds.pandroid.AlberDriver;
|
||||
import com.panda3ds.pandroid.data.SMDH;
|
||||
import com.panda3ds.pandroid.data.config.GlobalConfig;
|
||||
import com.panda3ds.pandroid.data.game.GameMetadata;
|
||||
import com.panda3ds.pandroid.utils.Constants;
|
||||
import com.panda3ds.pandroid.utils.GameUtils;
|
||||
import com.panda3ds.pandroid.utils.PerformanceMonitor;
|
||||
import com.panda3ds.pandroid.view.renderer.ConsoleRenderer;
|
||||
import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout;
|
||||
import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout;
|
||||
import com.panda3ds.pandroid.data.SMDH;
|
||||
import com.panda3ds.pandroid.data.game.GameMetadata;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
|
@ -25,9 +29,11 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
private int screenWidth, screenHeight;
|
||||
private int screenTexture;
|
||||
public int screenFbo;
|
||||
private final Context context;
|
||||
|
||||
PandaGlRenderer(String romPath) {
|
||||
PandaGlRenderer(Context context, String romPath) {
|
||||
super();
|
||||
this.context = context;
|
||||
this.romPath = romPath;
|
||||
|
||||
screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
|
||||
|
@ -40,8 +46,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
if (screenTexture != 0) {
|
||||
glDeleteTextures(1, new int[] {screenTexture}, 0);
|
||||
}
|
||||
|
||||
if (screenFbo != 0) {
|
||||
|
||||
if (screenFbo != 0) {
|
||||
glDeleteFramebuffers(1, new int[] {screenFbo}, 0);
|
||||
}
|
||||
|
||||
|
@ -84,7 +90,29 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
|
||||
AlberDriver.Initialize();
|
||||
AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT));
|
||||
AlberDriver.LoadRom(romPath);
|
||||
|
||||
// If loading the ROM failed, display an error message and early exit
|
||||
if (!AlberDriver.LoadRom(romPath)) {
|
||||
// Get a handler that can be used to post to the main thread
|
||||
Handler mainHandler = new Handler(context.getMainLooper());
|
||||
|
||||
Runnable runnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle("Failed to load ROM")
|
||||
.setMessage("Make sure it's a valid 3DS ROM and that storage permissions are configured properly.")
|
||||
.setPositiveButton("OK", null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
};
|
||||
mainHandler.post(runnable);
|
||||
|
||||
GameMetadata game = GameUtils.getCurrentGame();
|
||||
GameUtils.removeGame(game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the SMDH
|
||||
byte[] smdhData = AlberDriver.GetSmdh();
|
||||
|
@ -93,12 +121,12 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
} else {
|
||||
SMDH smdh = new SMDH(smdhData);
|
||||
Log.i(Constants.LOG_TAG, "Loaded rom SDMH");
|
||||
Log.i(Constants.LOG_TAG, String.format("Are you playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher()));
|
||||
Log.i(Constants.LOG_TAG, String.format("You are playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher()));
|
||||
GameMetadata game = GameUtils.getCurrentGame();
|
||||
GameUtils.removeGame(game);
|
||||
GameUtils.addGame(GameMetadata.applySMDH(game, smdh));
|
||||
}
|
||||
|
||||
|
||||
PerformanceMonitor.initialize(getBackendName());
|
||||
}
|
||||
|
||||
|
@ -150,4 +178,4 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
public String getBackendName() {
|
||||
return "OpenGL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ public class PandaGlSurfaceView extends GLSurfaceView implements TouchScreenNode
|
|||
if (Debug.isDebuggerConnected()) {
|
||||
setDebugFlags(DEBUG_LOG_GL_CALLS);
|
||||
}
|
||||
renderer = new PandaGlRenderer(romPath);
|
||||
renderer = new PandaGlRenderer(getContext(), romPath);
|
||||
setRenderer(renderer);
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ public class BasicTextEditor extends AppCompatEditText {
|
|||
super.scrollTo(scrollX, scrollY);
|
||||
}
|
||||
|
||||
public void adjustScroll(){
|
||||
public void adjustScroll() {
|
||||
setScroll(getScrollX(), getScrollY());
|
||||
}
|
||||
|
||||
|
|
|
@ -3,16 +3,17 @@ package com.panda3ds.pandroid.view.code.syntax;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
class PatternUtils {
|
||||
public static Pattern buildGenericKeywords(String... keywords){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("\\b(");
|
||||
for (int i = 0; i < keywords.length; i++){
|
||||
builder.append(keywords[i]);
|
||||
if (i+1 != keywords.length){
|
||||
builder.append("|");
|
||||
}
|
||||
}
|
||||
builder.append(")\\b");
|
||||
return Pattern.compile(builder.toString());
|
||||
}
|
||||
public static Pattern buildGenericKeywords(String... keywords) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("\\b(");
|
||||
for (int i = 0; i < keywords.length; i++) {
|
||||
builder.append(keywords[i]);
|
||||
if (i + 1 != keywords.length) {
|
||||
builder.append("|");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(")\\b");
|
||||
return Pattern.compile(builder.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ public class PerformanceView extends AppCompatTextView {
|
|||
setShadowLayer(padding,0,0,Color.BLACK);
|
||||
}
|
||||
|
||||
public void refresh(){
|
||||
public void refresh() {
|
||||
running = isShown();
|
||||
if (!running) {
|
||||
return;
|
||||
|
|
8
src/pandroid/app/src/main/res/drawable/color_surface.xml
Normal file
8
src/pandroid/app/src/main/res/drawable/color_surface.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?colorSurface"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="16dp"/>
|
||||
<solid android:color="#FFF"/>
|
||||
</shape>
|
|
@ -0,0 +1,9 @@
|
|||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorOnSurface">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="9dp" />
|
||||
<solid android:color="#FFFF" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
|
@ -1,9 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<corners android:radius="8dp"/>
|
||||
<solid android:color="#FFF"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="8dp"/>
|
||||
<solid android:color="#FFF"/>
|
||||
</shape>
|
37
src/pandroid/app/src/main/res/drawable/switch_thumb.xml
Normal file
37
src/pandroid/app/src/main/res/drawable/switch_thumb.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape>
|
||||
<padding android:bottom="3dp" android:top="3dp" android:right="3dp" android:left="3dp"/>
|
||||
<solid android:color="#0000"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<size android:width="18dp" android:height="18dp"/>
|
||||
<corners android:radius="999dp"/>
|
||||
<solid android:color="?colorPrimary"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item>
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape>
|
||||
<padding android:bottom="3dp" android:top="3dp" android:right="3dp" android:left="3dp"/>
|
||||
<solid android:color="#0000"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:tintMode="multiply" android:tint="#2FFF">
|
||||
<size android:width="18dp" android:height="18dp"/>
|
||||
<corners android:radius="999dp"/>
|
||||
<solid android:color="?colorOnSurface"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</selector>
|
19
src/pandroid/app/src/main/res/drawable/switch_track.xml
Normal file
19
src/pandroid/app/src/main/res/drawable/switch_track.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true">
|
||||
<shape android:tintMode="multiply" android:tint="#5FFF">
|
||||
<padding android:left="5dp" android:right="5dp" />
|
||||
<solid android:color="?colorPrimary"/>
|
||||
<corners android:radius="24dp"/>
|
||||
<size android:width="32dp" android:height="22dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:tintMode="multiply" android:tint="#2FFF">
|
||||
<padding android:left="5dp" android:right="5dp" />
|
||||
<solid android:color="?colorOnSurface"/>
|
||||
<corners android:radius="24dp"/>
|
||||
<size android:width="32dp" android:height="22dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -5,7 +5,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".app.MainActivity">
|
||||
tools:context=".app.MainActivity"
|
||||
android:background="?colorSurface">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
|
@ -25,6 +26,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:menu="@menu/main_activity_navigation"
|
||||
app:labelVisibilityMode="selected"
|
||||
style="@style/ThemedNavigationBottom"/>
|
||||
style="@style/ThemedNavigationBottom"
|
||||
android:background="@drawable/color_surface"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,7 +4,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:gravity="center"
|
||||
android:background="?colorSurface">
|
||||
|
||||
<View
|
||||
android:layout_width="100dp"
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".app.MainActivity">
|
||||
tools:context=".app.MainActivity"
|
||||
android:background="?colorSurface">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
|
@ -24,6 +25,7 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/fragment_container"
|
||||
app:labelVisibilityMode="selected"
|
||||
app:menu="@menu/main_activity_navigation"
|
||||
style="@style/ThemedNavigationBottom"/>
|
||||
style="@style/ThemedNavigationBottom"
|
||||
android:background="@drawable/color_surface"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:background="?colorSurface">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
|
|
33
src/pandroid/app/src/main/res/layout/dialog_loading.xml
Normal file
33
src/pandroid/app/src/main/res/layout/dialog_loading.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurface"
|
||||
android:padding="30dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="24sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:indeterminate="true"
|
||||
android:layout_gravity="bottom"
|
||||
android:indeterminateTint="?colorPrimary"
|
||||
android:background="#0000"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>
|
||||
|
||||
</FrameLayout>
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
@ -12,7 +12,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -31,35 +30,35 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|center">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/open_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:text="@string/open_file"
|
||||
android:layout_margin="5dp"
|
||||
android:textColor="?colorOnPrimary"
|
||||
android:backgroundTint="?colorPrimary"
|
||||
app:backgroundTint="?colorPrimary"
|
||||
android:background="@drawable/simple_card_background"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/create"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:text="@string/create_new"
|
||||
android:layout_margin="5dp"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:backgroundTint="?colorSurfaceVariant"
|
||||
app:backgroundTint="?colorSurfaceVariant"
|
||||
android:background="@drawable/simple_card_background"
|
||||
android:layout_marginBottom="10dp"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,105 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:background="?colorSurface">
|
||||
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<!-- Main content layout -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:minHeight="190dp"
|
||||
android:background="?colorSurfaceVariant">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
app:cardCornerRadius="10dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<com.panda3ds.pandroid.view.gamesgrid.GameIconView
|
||||
android:id="@+id/game_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurface"/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<!-- Game related content -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="20dp">
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:minHeight="190dp"
|
||||
android:background="?colorSurfaceVariant">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/game_title"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
app:cardCornerRadius="10dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:strokeWidth="0px">
|
||||
|
||||
<com.panda3ds.pandroid.view.gamesgrid.GameIconView
|
||||
android:id="@+id/game_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurface"/>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:paddingBottom="4dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:textSize="19sp"/>
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/game_publisher"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:textSize="14sp"/>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/game_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:paddingBottom="4dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:textSize="19sp"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/game_publisher"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Drawer content layout -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="10dp">
|
||||
android:orientation="vertical"
|
||||
android:gravity="start"
|
||||
android:layout_marginTop="195dp">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/menu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
app:menu="@menu/game_drawer"
|
||||
android:background="?colorSurface"
|
||||
android:theme="@style/Widget.App.NavigationView"
|
||||
app:subheaderTextAppearance="@style/TextAppearanceGameDrawerSubTitle"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/actions"
|
||||
style="@style/TextAppearanceGameDrawerSubTitle"/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/action_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/game_drawer_actions"
|
||||
android:background="?colorSurface"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/others"
|
||||
style="@style/TextAppearanceGameDrawerSubTitle"/>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/others_navigation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/game_drawer_others"
|
||||
android:background="?colorSurface"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -12,14 +13,15 @@
|
|||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/add_rom"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_width="58dp"
|
||||
android:layout_height="58dp"
|
||||
android:layout_margin="22dp"
|
||||
android:background="@drawable/medium_card_background"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:src="@drawable/ic_add"
|
||||
android:gravity="center"
|
||||
android:tint="?colorOnPrimary"
|
||||
android:background="@drawable/simple_card_background"
|
||||
android:backgroundTint="?colorPrimary"/>
|
||||
android:elevation="5dp"
|
||||
app:backgroundTint="?colorPrimary"/>
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
|
|
@ -11,14 +11,14 @@
|
|||
android:layout_height="64dp"
|
||||
android:paddingHorizontal="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
android:hint="@string/search"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingStart="50dp"
|
||||
android:hint="@string/search"
|
||||
android:textSize="16sp"
|
||||
android:background="@drawable/search_bar_background"
|
||||
android:backgroundTint="?colorSurfaceVariant" />
|
||||
|
@ -35,15 +35,15 @@
|
|||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.panda3ds.pandroid.view.gamesgrid.GamesGridView
|
||||
android:id="@+id/games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="15dp"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="@drawable/rounded_selectable_item_background"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="15dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="16dp">
|
||||
app:cardCornerRadius="10dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<com.panda3ds.pandroid.view.gamesgrid.GameIconView
|
||||
android:id="@+id/icon"
|
||||
|
@ -19,23 +22,25 @@
|
|||
android:scaleType="centerCrop"
|
||||
android:background="?colorSurfaceVariant"/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?colorOnSurface"
|
||||
android:text="@string/app_name"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"/>
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="10sp"
|
||||
android:textSize="11sp"
|
||||
android:alpha="0.75"
|
||||
android:gravity="center"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
|
|
@ -1,43 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center"
|
||||
android:padding="5dp">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/simple_card_background"
|
||||
android:backgroundTint="?colorSurfaceVariant"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="10dp">
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="10dp"
|
||||
app:strokeWidth="0px"
|
||||
app:cardCornerRadius="8dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginHorizontal="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:tint="?colorOnSurfaceVariant"
|
||||
android:alpha="0.5"
|
||||
android:padding="14dp"
|
||||
android:src="@drawable/ic_code"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"/>
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="10dp">
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginHorizontal="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:alpha="0.5"
|
||||
android:padding="14dp"
|
||||
android:src="@drawable/ic_code"
|
||||
android:tint="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="15sp"
|
||||
android:gravity="center"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/edit"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
|
@ -46,7 +54,6 @@
|
|||
android:padding="10dp"
|
||||
android:tint="?colorOnSurfaceVariant"
|
||||
android:layout_gravity="end|top"
|
||||
android:background="#0000"
|
||||
android:backgroundTint="#0000"
|
||||
android:layout_margin="10dp"/>
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="320dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
|
@ -26,4 +27,4 @@
|
|||
android:alpha="0.5"
|
||||
android:textColor="?colorOnSurface"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
|
34
src/pandroid/app/src/main/res/menu/game_drawer.xml
Normal file
34
src/pandroid/app/src/main/res/menu/game_drawer.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:divider="@null"
|
||||
android:dividerHeight="0dp">
|
||||
<item
|
||||
android:id="@+id/header_actions"
|
||||
android:title="@string/actions">
|
||||
<menu>
|
||||
|
||||
<item
|
||||
android:id="@+id/resume"
|
||||
android:icon="@drawable/ic_shortcut"
|
||||
android:title="@string/resume" />
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:icon="@drawable/ic_exit"
|
||||
android:title="@string/exit"/>
|
||||
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/header_others"
|
||||
android:title="@string/others">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/change_orientation"
|
||||
android:icon="@drawable/ic_rotate_screen"
|
||||
android:title="@string/rotate" />
|
||||
<item
|
||||
android:id="@+id/lua_script"
|
||||
android:icon="@drawable/ic_code"
|
||||
android:title="@string/lua_script" />
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/resume"
|
||||
android:icon="@drawable/ic_shortcut"
|
||||
android:title="@string/resume"/>
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:icon="@drawable/ic_exit"
|
||||
android:title="@string/exit"/>
|
||||
</menu>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/lua_script"
|
||||
android:icon="@drawable/ic_code"
|
||||
android:title="@string/lua_script"/>
|
||||
</menu>
|
|
@ -45,8 +45,8 @@
|
|||
<string name="open_file">Abrir arquivo</string>
|
||||
<string name="create_new">Criar novo</string>
|
||||
<string name="running_ff">Executando \"%s\" ...</string>
|
||||
<string name="developer_options">Opções de desenvolvedor</string>
|
||||
<string name="pref_developer_summary">Depuração, mostrar fps, etc.</string>
|
||||
<string name="advanced_options">Opções avançada.</string>
|
||||
<string name="pref_advanced_summary">Depuração, mostrar fps, etc.</string>
|
||||
<string name="pref_performance_monitor_title">Monitor de desempenho</string>
|
||||
<string name="pref_performance_monitor_summary">Mostrar um overlay com fps, memoria, etc.</string>
|
||||
<string name="pref_logger_service_title">Depuração</string>
|
||||
|
@ -54,4 +54,6 @@
|
|||
<string name="pref_shader_jit_title">Shader Jit</string>
|
||||
<string name="pref_shader_jit_summary">Usar recompilador de shaders.</string>
|
||||
<string name="graphics">Gráficos</string>
|
||||
<string name="loading">Carregando</string>
|
||||
<string name="rotate">Rotacionar</string>
|
||||
</resources>
|
||||
|
|
16
src/pandroid/app/src/main/res/values-v27/themes.xml
Normal file
16
src/pandroid/app/src/main/res/values-v27/themes.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Base.Theme.Pandroid.V27" parent="Base.Theme.Pandroid">
|
||||
<item name="android:windowLightNavigationBar">?isLightTheme</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
</style>
|
||||
<style name="GameActivityNavigationBar">
|
||||
<item name="android:statusBarColor">#5000</item>
|
||||
<item name="android:navigationBarColor">#5000</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
</style>
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid.V27"/>
|
||||
</resources>
|
7
src/pandroid/app/src/main/res/values-v29/themes.xml
Normal file
7
src/pandroid/app/src/main/res/values-v29/themes.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Base.Theme.Pandroid.V29" parent="Base.Theme.Pandroid.V27">
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
</style>
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid.V29"/>
|
||||
</resources>
|
|
@ -1,16 +1,16 @@
|
|||
<resources>
|
||||
<string name="app_name">pandroid</string>
|
||||
<!-- Common -->
|
||||
<string name="load_rom">Load ROM</string>
|
||||
<string name="games">Games</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
|
||||
<!-- Input Settings -->
|
||||
<string name="left">Left</string>
|
||||
<string name="right">Right</string>
|
||||
<string name="up">Up</string>
|
||||
<string name="down">Down</string>
|
||||
|
||||
<string name="others">Others</string>
|
||||
<string name="press_any_key">Press any key</string>
|
||||
<string name="axis">Axis</string>
|
||||
|
@ -18,16 +18,6 @@
|
|||
<string name="options">Options</string>
|
||||
<string name="pref_input_map_summary">Map physical controller or keyboard</string>
|
||||
<string name="controller_mapping">Controller mapping</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="pref_appearance_summary">Set application theme</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="theme_device">Device</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="black">Black</string>
|
||||
<string name="actions">Actions</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="resume">Resume</string>
|
||||
<string name="saved">Saved</string>
|
||||
<string name="create_profile">Create profile</string>
|
||||
<string name="input">Input</string>
|
||||
|
@ -36,6 +26,18 @@
|
|||
<string name="pref_screen_controllers_title">Screen gamepad layouts</string>
|
||||
<string name="pref_default_controller_title">Default screen gamepad layout</string>
|
||||
<string name="invalid_name">Invalid name</string>
|
||||
<!-- Appearance Settings -->
|
||||
<string name="theme">Theme</string>
|
||||
<string name="pref_appearance_summary">Set application theme</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="theme_device">Device</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="black">Black</string>
|
||||
<!-- Game Menu -->
|
||||
<string name="actions">Actions</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="resume">Resume</string>
|
||||
<string name="hacks">Hacks</string>
|
||||
<string name="lua_script">Lua script</string>
|
||||
<string name="scripts">Scripts</string>
|
||||
|
@ -46,8 +48,10 @@
|
|||
<string name="open_file">Open file</string>
|
||||
<string name="create_new">Create new</string>
|
||||
<string name="running_ff">Running \"%s\" ...</string>
|
||||
<string name="developer_options">Developer options</string>
|
||||
<string name="pref_developer_summary">Logger, FPS Counter, etc.</string>
|
||||
<string name="rotate">Rotate</string>
|
||||
<!-- Advanced Settings -->
|
||||
<string name="advanced_options">Advanced options</string>
|
||||
<string name="pref_advanced_summary">Logger, performance statistics, etc.</string>
|
||||
<string name="pref_performance_monitor_title">Performance monitor</string>
|
||||
<string name="pref_performance_monitor_summary">Show overlay with fps, memory, etc.</string>
|
||||
<string name="pref_logger_service_title">Logger</string>
|
||||
|
@ -55,4 +59,5 @@
|
|||
<string name="pref_shader_jit_title">Shader JIT</string>
|
||||
<string name="pref_shader_jit_summary">Use shader recompiler.</string>
|
||||
<string name="graphics">Graphics</string>
|
||||
<string name="loading">Loading</string>
|
||||
</resources>
|
||||
|
|
|
@ -5,6 +5,22 @@
|
|||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
<item name="alertDialogTheme">@style/AlertDialog</item>
|
||||
<item name="preferenceTheme">@style/PreferenceTheme</item>
|
||||
<item name="android:statusBarColor">?colorSurface</item>
|
||||
<item name="android:windowLightStatusBar">?isLightTheme</item>
|
||||
|
||||
<item name="switchStyle">@style/SwitchStyle</item>
|
||||
<item name="materialSwitchStyle">@style/SwitchStyle</item>
|
||||
<item name="android:switchStyle">@style/SwitchStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="SwitchStyle" parent="Widget.Material3.CompoundButton.MaterialSwitch">
|
||||
<item name="android:thumb">@drawable/switch_thumb</item>
|
||||
<item name="android:track">@drawable/switch_track</item>
|
||||
<item name="thumbRadius">0dp</item>
|
||||
<item name="android:thumbOffset">0dp</item>
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="showText">false</item>
|
||||
<item name="android:showText">false</item>
|
||||
</style>
|
||||
|
||||
<style name="PreferenceTheme" parent="PreferenceThemeOverlay">
|
||||
|
@ -24,17 +40,16 @@
|
|||
<item name="android:textSize">32sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid">
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid"/>
|
||||
|
||||
<style name="Widget.App.NavigationView" parent="Widget.Material3.NavigationView">
|
||||
<item name="android:listDivider">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Custom" parent="Theme.Pandroid">
|
||||
<item name="android:textColor">?colorOnSurface</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColorHint">?colorOnSurfaceVariant</item>
|
||||
<item name="android:statusBarColor">?colorSurfaceVariant</item>
|
||||
<item name="android:windowBackground">?colorSurface</item>
|
||||
<item name="titleTextColor">?colorOnSurface</item>
|
||||
<item name="hintTextColor">?colorOnSurfaceVariant</item>
|
||||
|
@ -57,8 +72,7 @@
|
|||
|
||||
<item name="android:textColorPrimary">@color/text_secondary_light</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_light</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="isLightTheme">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Black" parent="Theme.Pandroid.Custom">
|
||||
|
@ -76,8 +90,7 @@
|
|||
|
||||
<item name="android:textColorPrimary">@color/text_secondary_light</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_light</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="isLightTheme">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Light" parent="Theme.Pandroid.Custom">
|
||||
|
@ -93,10 +106,9 @@
|
|||
<item name="colorSecondary">#B37749</item>
|
||||
<item name="colorOnSecondary">#FFF</item>
|
||||
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:textColorPrimary">@color/text_secondary_dark</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_dark</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="isLightTheme">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
android:key="loggerService"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_logger_service_title"
|
||||
app:defaultValue="true"
|
||||
android:summary="@string/pref_logger_service_summary"/>
|
||||
|
||||
<PreferenceCategory
|
7
src/pandroid/app/src/main/res/xml/game_mode_config.xml
Normal file
7
src/pandroid/app/src/main/res/xml/game_mode_config.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<game-mode-config
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:supportsBatteryGameMode="true"
|
||||
android:supportsPerformanceGameMode="true"
|
||||
android:allowGameDownscaling="false"
|
||||
android:allowGameFpsOverride="false"/>
|
|
@ -24,10 +24,10 @@
|
|||
app:layout="@layout/preference_start_item"/>
|
||||
|
||||
<Preference
|
||||
app:key="developer"
|
||||
app:key="advanced"
|
||||
app:icon="@drawable/ic_code"
|
||||
app:title="@string/developer_options"
|
||||
app:summary="@string/pref_developer_summary"
|
||||
app:title="@string/advanced_options"
|
||||
app:summary="@string/pref_advanced_summary"
|
||||
app:layout="@layout/preference_start_item"/>
|
||||
|
||||
</PreferenceScreen>
|
Loading…
Add table
Add a link
Reference in a new issue