save screenshot. add screenshot to savestates
Retrieve last frame rgb data (gl, vk, dx9, dx11). Specific save screenshot code for android, iOS and UWP. Add Save Screenshot emu key (F12 by default) vk: defer deletion of in-flight textures when texture cache is cleared. vk: fix issue when updating imgui textures after a render pass has begun (achievements) vk: palette texture not updated after a state has been loaded. gl: Move opengl-specific stuff into opengl imgui driver. savestate: Add non compressed header, following by screenshot png data, before actual savestate. Issue #842
This commit is contained in:
parent
83493cc14b
commit
6f0581032b
|
@ -1294,7 +1294,6 @@ if(NOT LIBRETRO)
|
|||
target_sources(${PROJECT_NAME} PRIVATE
|
||||
core/ui/game_scanner.cpp
|
||||
core/ui/game_scanner.h
|
||||
core/ui/imgui_driver.cpp
|
||||
core/ui/imgui_driver.h
|
||||
core/ui/gui.cpp
|
||||
core/ui/gui.h
|
||||
|
|
|
@ -23,14 +23,11 @@
|
|||
|
||||
const u8 RZipHeader[8] = { '#', 'R', 'Z', 'I', 'P', 'v', 1, '#' };
|
||||
|
||||
bool RZipFile::Open(const std::string& path, bool write)
|
||||
bool RZipFile::Open(FILE *file, bool write)
|
||||
{
|
||||
verify(file == nullptr);
|
||||
this->write = write;
|
||||
|
||||
file = nowide::fopen(path.c_str(), write ? "wb" : "rb");
|
||||
if (file == nullptr)
|
||||
return false;
|
||||
verify(this->file == nullptr);
|
||||
verify(file != nullptr);
|
||||
startOffset = std::ftell(file);
|
||||
if (!write)
|
||||
{
|
||||
u8 header[sizeof(RZipHeader)];
|
||||
|
@ -39,7 +36,7 @@ bool RZipFile::Open(const std::string& path, bool write)
|
|||
|| std::fread(&maxChunkSize, sizeof(maxChunkSize), 1, file) != 1
|
||||
|| std::fread(&size, sizeof(size), 1, file) != 1)
|
||||
{
|
||||
Close();
|
||||
std::fseek(file, startOffset, SEEK_SET);
|
||||
return false;
|
||||
}
|
||||
// savestates created on 32-bit platforms used to have a 32-bit size
|
||||
|
@ -59,11 +56,24 @@ bool RZipFile::Open(const std::string& path, bool write)
|
|||
|| std::fwrite(&maxChunkSize, sizeof(maxChunkSize), 1, file) != 1
|
||||
|| std::fwrite(&size, sizeof(size), 1, file) != 1)
|
||||
{
|
||||
Close();
|
||||
std::fseek(file, startOffset, SEEK_SET);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->write = write;
|
||||
this->file = file;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RZipFile::Open(const std::string& path, bool write)
|
||||
{
|
||||
FILE *f = nowide::fopen(path.c_str(), write ? "wb" : "rb");
|
||||
if (f == nullptr)
|
||||
return false;
|
||||
if (!Open(f, write)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -73,7 +83,7 @@ void RZipFile::Close()
|
|||
{
|
||||
if (write)
|
||||
{
|
||||
std::fseek(file, sizeof(RZipHeader) + sizeof(maxChunkSize), SEEK_SET);
|
||||
std::fseek(file, startOffset + sizeof(RZipHeader) + sizeof(maxChunkSize), SEEK_SET);
|
||||
std::fwrite(&size, sizeof(size), 1, file);
|
||||
}
|
||||
std::fclose(file);
|
||||
|
|
|
@ -28,6 +28,7 @@ public:
|
|||
~RZipFile() { Close(); }
|
||||
|
||||
bool Open(const std::string& path, bool write);
|
||||
bool Open(FILE *file, bool write);
|
||||
void Close();
|
||||
size_t Size() const { return size; }
|
||||
size_t Read(void *data, size_t length);
|
||||
|
@ -42,4 +43,5 @@ private:
|
|||
u32 chunkSize = 0;
|
||||
u32 chunkIndex = 0;
|
||||
bool write = false;
|
||||
long startOffset = 0;
|
||||
};
|
||||
|
|
|
@ -550,10 +550,12 @@ void Emulator::loadGame(const char *path, LoadProgress *progress)
|
|||
settings.input.fastForwardMode = false;
|
||||
if (!settings.content.path.empty())
|
||||
{
|
||||
#ifndef LIBRETRO
|
||||
if (config::GGPOEnable)
|
||||
dc_loadstate(-1);
|
||||
else if (config::AutoLoadState && !NaomiNetworkSupported() && !settings.naomi.multiboard)
|
||||
dc_loadstate(config::SavestateSlot);
|
||||
#endif
|
||||
}
|
||||
EventManager::event(Event::Start);
|
||||
|
||||
|
@ -612,9 +614,11 @@ void Emulator::unloadGame()
|
|||
} catch (...) { }
|
||||
if (state == Loaded || state == Error)
|
||||
{
|
||||
#ifndef LIBRETRO
|
||||
if (state == Loaded && config::AutoSaveState && !settings.content.path.empty()
|
||||
&& !settings.naomi.multiboard && !config::GGPOEnable && !NaomiNetworkSupported())
|
||||
dc_savestate(config::SavestateSlot);
|
||||
gui_saveState(false);
|
||||
#endif
|
||||
try {
|
||||
dc_reset(true);
|
||||
} catch (const FlycastException& e) {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <time.h>
|
||||
|
||||
void loadGameSpecificSettings();
|
||||
void SaveSettings();
|
||||
|
@ -35,10 +36,11 @@ int flycast_init(int argc, char* argv[]);
|
|||
void dc_reset(bool hard); // for tests only
|
||||
void flycast_term();
|
||||
void dc_exit();
|
||||
void dc_savestate(int index = 0);
|
||||
void dc_savestate(int index = 0, const u8 *pngData = nullptr, u32 pngSize = 0);
|
||||
void dc_loadstate(int index = 0);
|
||||
void dc_loadstate(Deserializer& deser);
|
||||
std::string dc_getStateUpdateDate(int index);
|
||||
time_t dc_getStateCreationDate(int index);
|
||||
void dc_getStateScreenshot(int index, std::vector<u8>& pngData);
|
||||
|
||||
enum class Event {
|
||||
Start,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
#include "ta_ctx.h"
|
||||
#include <vector>
|
||||
|
||||
extern u32 FrameCount;
|
||||
|
||||
|
@ -62,6 +63,9 @@ struct Renderer
|
|||
virtual bool Render() = 0;
|
||||
virtual void RenderFramebuffer(const FramebufferInfo& info) = 0;
|
||||
virtual bool RenderLastFrame() { return false; }
|
||||
// Get the last rendered frame pixel data in RGB format
|
||||
// The returned image is rotated and scaled (upward orientation and square pixels)
|
||||
virtual bool GetLastFrame(std::vector<u8>& data, int& width, int& height) { return false; }
|
||||
|
||||
virtual bool Present() { return true; }
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ enum DreamcastKey
|
|||
EMU_BTN_LOADSTATE,
|
||||
EMU_BTN_SAVESTATE,
|
||||
EMU_BTN_BYPASS_KB,
|
||||
EMU_BTN_SCREENSHOT,
|
||||
|
||||
// Real axes
|
||||
DC_AXIS_TRIGGERS = 0x1000000,
|
||||
|
|
|
@ -99,6 +99,10 @@ bool GamepadDevice::handleButtonInput(int port, DreamcastKey key, bool pressed)
|
|||
if (pressed)
|
||||
gui_saveState();
|
||||
break;
|
||||
case EMU_BTN_SCREENSHOT:
|
||||
if (pressed)
|
||||
gui_takeScreenshot();
|
||||
break;
|
||||
case DC_AXIS_LT:
|
||||
if (port >= 0)
|
||||
lt[port] = pressed ? 0xffff : 0;
|
||||
|
|
|
@ -61,6 +61,7 @@ public:
|
|||
set_button(DC_AXIS_LEFT, 13); // J
|
||||
set_button(DC_AXIS_RIGHT, 15); // L
|
||||
set_button(DC_BTN_D, 4); // Q (Coin)
|
||||
set_button(EMU_BTN_SCREENSHOT, 69); // F12
|
||||
|
||||
dirty = false;
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ button_list[] =
|
|||
{ EMU_BTN_LOADSTATE, "emulator", "btn_jump_state" },
|
||||
{ EMU_BTN_SAVESTATE, "emulator", "btn_quick_save" },
|
||||
{ EMU_BTN_BYPASS_KB, "emulator", "btn_bypass_kb" },
|
||||
{ EMU_BTN_SCREENSHOT, "emulator", "btn_screenshot" },
|
||||
};
|
||||
|
||||
static struct
|
||||
|
|
188
core/nullDC.cpp
188
core/nullDC.cpp
|
@ -17,6 +17,29 @@
|
|||
#include "serialize.h"
|
||||
#include <time.h>
|
||||
|
||||
struct SavestateHeader
|
||||
{
|
||||
void init()
|
||||
{
|
||||
memcpy(magic, MAGIC, sizeof(magic));
|
||||
creationDate = time(nullptr);
|
||||
version = Deserializer::Current;
|
||||
pngSize = 0;
|
||||
}
|
||||
|
||||
bool isValid() const {
|
||||
return !memcmp(magic, MAGIC, sizeof(magic));
|
||||
}
|
||||
|
||||
char magic[8];
|
||||
u64 creationDate;
|
||||
u32 version;
|
||||
u32 pngSize;
|
||||
// png data
|
||||
|
||||
static constexpr const char *MAGIC = "FLYSAVE1";
|
||||
};
|
||||
|
||||
int flycast_init(int argc, char* argv[])
|
||||
{
|
||||
#if defined(TEST_AUTOMATION)
|
||||
|
@ -67,7 +90,7 @@ int flycast_init(int argc, char* argv[])
|
|||
void dc_exit()
|
||||
{
|
||||
try {
|
||||
emu.stop();
|
||||
emu.unloadGame();
|
||||
} catch (...) { }
|
||||
mainui_stop();
|
||||
}
|
||||
|
@ -85,15 +108,15 @@ void SaveSettings()
|
|||
|
||||
void flycast_term()
|
||||
{
|
||||
os_DestroyWindow();
|
||||
gui_cancel_load();
|
||||
lua::term();
|
||||
emu.term();
|
||||
os_DestroyWindow();
|
||||
gui_term();
|
||||
os_TermInput();
|
||||
}
|
||||
|
||||
void dc_savestate(int index)
|
||||
void dc_savestate(int index, const u8 *pngData, u32 pngSize)
|
||||
{
|
||||
if (settings.network.online)
|
||||
return;
|
||||
|
@ -113,10 +136,8 @@ void dc_savestate(int index)
|
|||
dc_serialize(ser);
|
||||
|
||||
std::string filename = hostfs::getSavestatePath(index, true);
|
||||
#if 0
|
||||
FILE *f = nowide::fopen(filename.c_str(), "wb");
|
||||
|
||||
if ( f == NULL )
|
||||
if (f == nullptr)
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to save state - could not open %s for writing", filename.c_str());
|
||||
gui_display_notification("Cannot open save file", 5000);
|
||||
|
@ -124,31 +145,41 @@ void dc_savestate(int index)
|
|||
return;
|
||||
}
|
||||
|
||||
RZipFile zipFile;
|
||||
SavestateHeader header;
|
||||
header.init();
|
||||
header.pngSize = pngSize;
|
||||
if (std::fwrite(&header, sizeof(header), 1, f) != 1)
|
||||
goto fail;
|
||||
if (pngSize > 0 && std::fwrite(pngData, 1, pngSize, f) != pngSize)
|
||||
goto fail;
|
||||
|
||||
#if 0
|
||||
// Uncompressed savestate
|
||||
std::fwrite(data, 1, ser.size(), f);
|
||||
std::fclose(f);
|
||||
#else
|
||||
RZipFile zipFile;
|
||||
if (!zipFile.Open(filename, true))
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to save state - could not open %s for writing", filename.c_str());
|
||||
gui_display_notification("Cannot open save file", 5000);
|
||||
free(data);
|
||||
return;
|
||||
}
|
||||
if (!zipFile.Open(f, true))
|
||||
goto fail;
|
||||
if (zipFile.Write(data, ser.size()) != ser.size())
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str());
|
||||
gui_display_notification("Error saving state", 5000);
|
||||
zipFile.Close();
|
||||
free(data);
|
||||
return;
|
||||
}
|
||||
goto fail;
|
||||
zipFile.Close();
|
||||
#endif
|
||||
|
||||
free(data);
|
||||
NOTICE_LOG(SAVESTATE, "Saved state to %s size %d", filename.c_str(), (int)ser.size());
|
||||
gui_display_notification("State saved", 2000);
|
||||
return;
|
||||
|
||||
fail:
|
||||
WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str());
|
||||
gui_display_notification("Error saving state", 5000);
|
||||
if (zipFile.rawFile() != nullptr)
|
||||
zipFile.Close();
|
||||
else
|
||||
std::fclose(f);
|
||||
free(data);
|
||||
// delete failed savestate?
|
||||
}
|
||||
|
||||
void dc_loadstate(int index)
|
||||
|
@ -156,46 +187,54 @@ void dc_loadstate(int index)
|
|||
if (settings.raHardcoreMode)
|
||||
return;
|
||||
u32 total_size = 0;
|
||||
FILE *f = nullptr;
|
||||
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
RZipFile zipFile;
|
||||
if (zipFile.Open(filename, false))
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
if (f == nullptr)
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str());
|
||||
gui_display_notification("Save state not found", 2000);
|
||||
return;
|
||||
}
|
||||
SavestateHeader header;
|
||||
if (std::fread(&header, sizeof(header), 1, f) == 1)
|
||||
{
|
||||
if (!header.isValid())
|
||||
// seek to beginning of file if this isn't a valid header (legacy savestate)
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
else
|
||||
// skip png data
|
||||
std::fseek(f, header.pngSize, SEEK_CUR);
|
||||
}
|
||||
else {
|
||||
// probably not a valid savestate but we'll fail later
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
if (index == -1 && config::GGPOEnable)
|
||||
{
|
||||
long pos = std::ftell(f);
|
||||
MD5Sum().add(f)
|
||||
.getDigest(settings.network.md5.savestate);
|
||||
std::fseek(f, pos, SEEK_SET);
|
||||
}
|
||||
RZipFile zipFile;
|
||||
if (zipFile.Open(f, false)) {
|
||||
total_size = (u32)zipFile.Size();
|
||||
if (index == -1 && config::GGPOEnable)
|
||||
{
|
||||
f = zipFile.rawFile();
|
||||
long pos = std::ftell(f);
|
||||
MD5Sum().add(f)
|
||||
.getDigest(settings.network.md5.savestate);
|
||||
std::fseek(f, pos, SEEK_SET);
|
||||
f = nullptr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f = nowide::fopen(filename.c_str(), "rb");
|
||||
|
||||
if ( f == NULL )
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str());
|
||||
gui_display_notification("Save state not found", 2000);
|
||||
return;
|
||||
}
|
||||
if (index == -1 && config::GGPOEnable)
|
||||
MD5Sum().add(f)
|
||||
.getDigest(settings.network.md5.savestate);
|
||||
long pos = std::ftell(f);
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
total_size = (u32)std::ftell(f);
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
total_size = (u32)std::ftell(f) - pos;
|
||||
std::fseek(f, pos, SEEK_SET);
|
||||
}
|
||||
void *data = malloc(total_size);
|
||||
if (data == nullptr)
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to load state - could not malloc %d bytes", total_size);
|
||||
gui_display_notification("Failed to load state - memory full", 5000);
|
||||
if (f != nullptr)
|
||||
if (zipFile.rawFile() == nullptr)
|
||||
std::fclose(f);
|
||||
else
|
||||
zipFile.Close();
|
||||
|
@ -203,14 +242,14 @@ void dc_loadstate(int index)
|
|||
}
|
||||
|
||||
size_t read_size;
|
||||
if (f == nullptr)
|
||||
if (zipFile.rawFile() != nullptr)
|
||||
{
|
||||
read_size = zipFile.Read(data, total_size);
|
||||
zipFile.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
read_size = fread(data, 1, total_size, f);
|
||||
read_size = std::fread(data, 1, total_size, f);
|
||||
std::fclose(f);
|
||||
}
|
||||
if (read_size != total_size)
|
||||
|
@ -226,6 +265,7 @@ void dc_loadstate(int index)
|
|||
dc_loadstate(deser);
|
||||
NOTICE_LOG(SAVESTATE, "Loaded state ver %d from %s size %d", deser.version(), filename.c_str(), total_size);
|
||||
if (deser.size() != total_size)
|
||||
// Note: this isn't true for RA savestates
|
||||
WARN_LOG(SAVESTATE, "Savestate size %d but only %d bytes used", total_size, (int)deser.size());
|
||||
} catch (const Deserializer::Exception& e) {
|
||||
ERROR_LOG(SAVESTATE, "%s", e.what());
|
||||
|
@ -235,25 +275,41 @@ void dc_loadstate(int index)
|
|||
EventManager::event(Event::LoadState);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static struct tm *localtime_r(const time_t *_clock, struct tm *_result)
|
||||
{
|
||||
return localtime_s(_result, _clock) ? nullptr : _result;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string dc_getStateUpdateDate(int index)
|
||||
time_t dc_getStateCreationDate(int index)
|
||||
{
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
struct stat st;
|
||||
if (flycast::stat(filename.c_str(), &st) != 0)
|
||||
return {};
|
||||
tm t;
|
||||
if (localtime_r(&st.st_mtime, &t) == nullptr)
|
||||
return {};
|
||||
std::string s(32, '\0');
|
||||
s.resize(snprintf(s.data(), 32, "%04d/%02d/%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec));
|
||||
return s;
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
if (f == nullptr)
|
||||
return 0;
|
||||
SavestateHeader header;
|
||||
if (std::fread(&header, sizeof(header), 1, f) != 1 || !header.isValid())
|
||||
{
|
||||
std::fclose(f);
|
||||
struct stat st;
|
||||
if (flycast::stat(filename.c_str(), &st) == 0)
|
||||
return st.st_mtime;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
std::fclose(f);
|
||||
return (time_t)header.creationDate;
|
||||
}
|
||||
|
||||
void dc_getStateScreenshot(int index, std::vector<u8>& pngData)
|
||||
{
|
||||
pngData.clear();
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
if (f == nullptr)
|
||||
return;
|
||||
SavestateHeader header;
|
||||
if (std::fread(&header, sizeof(header), 1, f) == 1 && header.isValid() && header.pngSize != 0)
|
||||
{
|
||||
pngData.resize(header.pngSize);
|
||||
if (std::fread(pngData.data(), 1, pngData.size(), f) != pngData.size())
|
||||
pngData.clear();
|
||||
}
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#endif
|
||||
#if defined(_WIN32) && !defined(TARGET_UWP)
|
||||
#include "windows/rawinput.h"
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
#include "profiler/fc_profiler.h"
|
||||
|
||||
|
@ -151,8 +152,123 @@ std::string getTextureDumpPath()
|
|||
return get_writable_data_path("texdump/");
|
||||
}
|
||||
|
||||
#if defined(__unix__) && !defined(__ANDROID__)
|
||||
|
||||
static std::string runCommand(const std::string& cmd)
|
||||
{
|
||||
char buf[1024] {};
|
||||
FILE *fp = popen(cmd.c_str(), "r");
|
||||
if (fp == nullptr) {
|
||||
INFO_LOG(COMMON, "popen failed: %d", errno);
|
||||
return "";
|
||||
}
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), fp) != nullptr)
|
||||
result += trim_trailing_ws(buf, "\n");
|
||||
|
||||
int rc;
|
||||
if ((rc = pclose(fp)) != 0) {
|
||||
INFO_LOG(COMMON, "Command error: %d", rc);
|
||||
return "";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string getScreenshotsPath()
|
||||
{
|
||||
std::string picturesPath = runCommand("xdg-user-dir PICTURES");
|
||||
if (!picturesPath.empty())
|
||||
return picturesPath;
|
||||
const char *home = nowide::getenv("HOME");
|
||||
if (home != nullptr)
|
||||
return home;
|
||||
else
|
||||
return ".";
|
||||
}
|
||||
|
||||
#elif defined(TARGET_UWP)
|
||||
//TODO move to shell/uwp?
|
||||
using namespace Platform;
|
||||
using namespace Windows::Foundation;
|
||||
using namespace Windows::Storage;
|
||||
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data)
|
||||
{
|
||||
try {
|
||||
StorageFolder^ folder = KnownFolders::PicturesLibrary; // or SavedPictures?
|
||||
if (folder == nullptr) {
|
||||
INFO_LOG(COMMON, "KnownFolders::PicturesLibrary is null");
|
||||
throw FlycastException();
|
||||
}
|
||||
nowide::wstackstring wstr;
|
||||
wchar_t *wname = wstr.convert(name.c_str());
|
||||
String^ msname = ref new String(wname);
|
||||
ArrayReference<u8> arrayRef(const_cast<u8*>(&data[0]), data.size());
|
||||
|
||||
IAsyncOperation<StorageFile^>^ op = folder->CreateFileAsync(msname, CreationCollisionOption::FailIfExists);
|
||||
cResetEvent asyncEvent;
|
||||
op->Completed = ref new AsyncOperationCompletedHandler<StorageFile^>(
|
||||
[&asyncEvent, &arrayRef](IAsyncOperation<StorageFile^>^ op, AsyncStatus) {
|
||||
IAsyncAction^ action = FileIO::WriteBytesAsync(op->GetResults(), arrayRef);
|
||||
action->Completed = ref new AsyncActionCompletedHandler(
|
||||
[&asyncEvent](IAsyncAction^, AsyncStatus){
|
||||
asyncEvent.Set();
|
||||
});
|
||||
});
|
||||
asyncEvent.Wait();
|
||||
}
|
||||
catch (COMException^ e) {
|
||||
WARN_LOG(COMMON, "Save screenshot failed: %S", e->Message->Data());
|
||||
throw FlycastException();
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined(_WIN32) && !defined(TARGET_UWP)
|
||||
|
||||
static std::string getScreenshotsPath()
|
||||
{
|
||||
wchar_t *screenshotPath;
|
||||
if (FAILED(SHGetKnownFolderPath(FOLDERID_Screenshots, KF_FLAG_DEFAULT, NULL, &screenshotPath)))
|
||||
return get_writable_config_path("");
|
||||
nowide::stackstring path;
|
||||
std::string ret;
|
||||
if (path.convert(screenshotPath) == nullptr)
|
||||
ret = get_writable_config_path("");
|
||||
else
|
||||
ret = path.get();
|
||||
CoTaskMemFree(screenshotPath);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
std::string getScreenshotsPath();
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(__ANDROID__) && !defined(TARGET_UWP) && !defined(TARGET_IPHONE) && !defined(__SWITCH__)
|
||||
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data)
|
||||
{
|
||||
std::string path = getScreenshotsPath();
|
||||
path += "/" + name;
|
||||
FILE *f = nowide::fopen(path.c_str(), "wb");
|
||||
if (f == nullptr)
|
||||
throw FlycastException(path);
|
||||
if (std::fwrite(&data[0], data.size(), 1, f) != 1) {
|
||||
std::fclose(f);
|
||||
unlink(path.c_str());
|
||||
throw FlycastException(path);
|
||||
}
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace hostfs
|
||||
|
||||
void os_CreateWindow()
|
||||
{
|
||||
#if defined(USE_SDL)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
#include <vector>
|
||||
#if defined(__SWITCH__)
|
||||
#include <malloc.h>
|
||||
#endif
|
||||
|
@ -59,6 +60,7 @@ namespace hostfs
|
|||
std::string getTextureDumpPath();
|
||||
|
||||
std::string getShaderCachePath(const std::string& filename);
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data);
|
||||
}
|
||||
|
||||
static inline void *allocAligned(size_t alignment, size_t size)
|
||||
|
|
|
@ -92,6 +92,10 @@ public:
|
|||
return (ImTextureID)&texture.imTexture;
|
||||
}
|
||||
|
||||
void deleteTexture(const std::string& name) override {
|
||||
textures.erase(name);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Texture
|
||||
{
|
||||
|
|
|
@ -1345,6 +1345,86 @@ void DX11Renderer::writeFramebufferToVRAM()
|
|||
WriteFramebuffer<2, 1, 0, 3>(width, height, (u8 *)tmp_buf.data(), texAddress, pvrrc.fb_W_CTRL, linestride, xClip, yClip);
|
||||
}
|
||||
|
||||
bool DX11Renderer::GetLastFrame(std::vector<u8>& data, int& width, int& height)
|
||||
{
|
||||
if (!frameRenderedOnce)
|
||||
return false;
|
||||
|
||||
width = this->width;
|
||||
height = this->height;
|
||||
if (config::Rotate90)
|
||||
std::swap(width, height);
|
||||
// We need square pixels for PNG
|
||||
int w = aspectRatio * height;
|
||||
if (width > w)
|
||||
height = width / aspectRatio;
|
||||
else
|
||||
width = w;
|
||||
|
||||
ComPtr<ID3D11Texture2D> dstTex;
|
||||
ComPtr<ID3D11RenderTargetView> dstRenderTarget;
|
||||
createTexAndRenderTarget(dstTex, dstRenderTarget, width, height);
|
||||
|
||||
ID3D11ShaderResourceView *nullResView = nullptr;
|
||||
deviceContext->PSSetShaderResources(0, 1, &nullResView);
|
||||
deviceContext->OMSetRenderTargets(1, &dstRenderTarget.get(), nullptr);
|
||||
D3D11_VIEWPORT vp{};
|
||||
vp.Width = (FLOAT)width;
|
||||
vp.Height = (FLOAT)height;
|
||||
vp.MinDepth = 0.f;
|
||||
vp.MaxDepth = 1.f;
|
||||
deviceContext->RSSetViewports(1, &vp);
|
||||
const D3D11_RECT r = { 0, 0, (LONG)width, (LONG)height };
|
||||
deviceContext->RSSetScissorRects(1, &r);
|
||||
deviceContext->OMSetBlendState(blendStates.getState(false), nullptr, 0xffffffff);
|
||||
deviceContext->GSSetShader(nullptr, nullptr, 0);
|
||||
deviceContext->HSSetShader(nullptr, nullptr, 0);
|
||||
deviceContext->DSSetShader(nullptr, nullptr, 0);
|
||||
deviceContext->CSSetShader(nullptr, nullptr, 0);
|
||||
|
||||
quad->draw(fbTextureView, samplers->getSampler(true), nullptr, -1.f, -1.f, 2.f, 2.f, config::Rotate90);
|
||||
|
||||
deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr);
|
||||
|
||||
D3D11_TEXTURE2D_DESC desc;
|
||||
dstTex->GetDesc(&desc);
|
||||
desc.Usage = D3D11_USAGE_STAGING;
|
||||
desc.BindFlags = 0;
|
||||
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
|
||||
ComPtr<ID3D11Texture2D> stagingTex;
|
||||
HRESULT hr = device->CreateTexture2D(&desc, nullptr, &stagingTex.get());
|
||||
if (FAILED(hr))
|
||||
{
|
||||
WARN_LOG(RENDERER, "Staging screenshot texture creation failed");
|
||||
return false;
|
||||
}
|
||||
deviceContext->CopyResource(stagingTex, dstTex);
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE mappedSubres;
|
||||
hr = deviceContext->Map(stagingTex, 0, D3D11_MAP_READ, 0, &mappedSubres);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
WARN_LOG(RENDERER, "Failed to map staging screenshot texture");
|
||||
return false;
|
||||
}
|
||||
const u8* const src = (const u8 *)mappedSubres.pData;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
const u8 *p = src + y * mappedSubres.RowPitch;
|
||||
for (int x = 0; x < width; x++, p += 4)
|
||||
{
|
||||
data.push_back(p[2]);
|
||||
data.push_back(p[1]);
|
||||
data.push_back(p[0]);
|
||||
}
|
||||
}
|
||||
deviceContext->Unmap(stagingTex, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DX11Renderer::renderVideoRouting()
|
||||
{
|
||||
#ifdef VIDEO_ROUTING
|
||||
|
|
|
@ -53,6 +53,7 @@ struct DX11Renderer : public Renderer
|
|||
bool RenderLastFrame() override;
|
||||
void DrawOSD(bool clear_screen) override;
|
||||
BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override;
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height) override;
|
||||
|
||||
protected:
|
||||
struct VertexConstants
|
||||
|
|
|
@ -1422,6 +1422,96 @@ void D3DRenderer::writeFramebufferToVRAM()
|
|||
WriteFramebuffer<2, 1, 0, 3>(width, height, (u8 *)tmp_buf.data(), texAddress, pvrrc.fb_W_CTRL, linestride, xClip, yClip);
|
||||
}
|
||||
|
||||
bool D3DRenderer::GetLastFrame(std::vector<u8>& data, int& width, int& height)
|
||||
{
|
||||
if (!frameRenderedOnce || !theDXContext.isReady())
|
||||
return false;
|
||||
|
||||
width = this->width;
|
||||
height = this->height;
|
||||
if (config::Rotate90)
|
||||
std::swap(width, height);
|
||||
// We need square pixels for PNG
|
||||
int w = aspectRatio * height;
|
||||
if (width > w)
|
||||
height = width / aspectRatio;
|
||||
else
|
||||
width = w;
|
||||
|
||||
backbuffer.reset();
|
||||
device->GetRenderTarget(0, &backbuffer.get());
|
||||
|
||||
// Target texture and surface
|
||||
ComPtr<IDirect3DTexture9> target;
|
||||
device->CreateTexture(width, height, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &target.get(), NULL);
|
||||
ComPtr<IDirect3DSurface9> surface;
|
||||
target->GetSurfaceLevel(0, &surface.get());
|
||||
device->SetRenderTarget(0, surface);
|
||||
// Draw
|
||||
devCache.SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE);
|
||||
device->SetPixelShader(NULL);
|
||||
device->SetVertexShader(NULL);
|
||||
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
device->SetRenderState(D3DRS_ZENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
|
||||
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||
|
||||
glm::mat4 identity = glm::identity<glm::mat4>();
|
||||
glm::mat4 projection = glm::translate(glm::vec3(-1.f / width, 1.f / height, 0));
|
||||
if (config::Rotate90)
|
||||
projection *= glm::rotate((float)M_PI_2, glm::vec3(0, 0, 1));
|
||||
|
||||
device->SetTransform(D3DTS_WORLD, (const D3DMATRIX *)&identity[0][0]);
|
||||
device->SetTransform(D3DTS_VIEW, (const D3DMATRIX *)&identity[0][0]);
|
||||
device->SetTransform(D3DTS_PROJECTION, (const D3DMATRIX *)&projection[0][0]);
|
||||
|
||||
device->SetFVF(D3DFVF_XYZ | D3DFVF_TEX1);
|
||||
D3DVIEWPORT9 viewport{};
|
||||
viewport.Width = width;
|
||||
viewport.Height = height;
|
||||
viewport.MaxZ = 1;
|
||||
bool rc = SUCCEEDED(device->SetViewport(&viewport));
|
||||
verify(rc);
|
||||
float coords[] {
|
||||
-1, 1, 0.5f, 0, 0,
|
||||
-1, -1, 0.5f, 0, 1,
|
||||
1, 1, 0.5f, 1, 0,
|
||||
1, -1, 0.5f, 1, 1,
|
||||
};
|
||||
device->SetTexture(0, framebufferTexture);
|
||||
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, coords, sizeof(float) * 5);
|
||||
|
||||
// Copy back
|
||||
ComPtr<IDirect3DSurface9> offscreenSurface;
|
||||
rc = SUCCEEDED(device->CreateOffscreenPlainSurface(width, height, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &offscreenSurface.get(), nullptr));
|
||||
verify(rc);
|
||||
rc = SUCCEEDED(device->GetRenderTargetData(surface, offscreenSurface));
|
||||
verify(rc);
|
||||
|
||||
D3DLOCKED_RECT rect;
|
||||
RECT lockRect { 0, 0, (long)width, (long)height };
|
||||
rc = SUCCEEDED(offscreenSurface->LockRect(&rect, &lockRect, D3DLOCK_READONLY));
|
||||
verify(rc);
|
||||
data.clear();
|
||||
data.reserve(width * height * 3);
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
const u8 *src = (const u8 *)rect.pBits + y * rect.Pitch;
|
||||
for (int x = 0; x < width; x++, src += 4)
|
||||
{
|
||||
data.push_back(src[2]);
|
||||
data.push_back(src[1]);
|
||||
data.push_back(src[0]);
|
||||
}
|
||||
}
|
||||
rc = SUCCEEDED(offscreenSurface->UnlockRect());
|
||||
device->SetRenderTarget(0, backbuffer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Renderer* rend_DirectX9()
|
||||
{
|
||||
return new D3DRenderer();
|
||||
|
|
|
@ -116,6 +116,7 @@ struct D3DRenderer : public Renderer
|
|||
void preReset();
|
||||
void postReset();
|
||||
void RenderFramebuffer(const FramebufferInfo& info) override;
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height) override;
|
||||
|
||||
private:
|
||||
enum ModifierVolumeMode { Xor, Or, Inclusion, Exclusion, ModeCount };
|
||||
|
|
|
@ -97,7 +97,11 @@ public:
|
|||
texture.imTexture.d3dTexture = texture.tex.get();
|
||||
texture.imTexture.pointSampling = nearestSampling;
|
||||
|
||||
return (ImTextureID)&texture;
|
||||
return (ImTextureID)&texture.imTexture;
|
||||
}
|
||||
|
||||
void deleteTexture(const std::string& name) override {
|
||||
textures.erase(name);
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
|
@ -790,7 +790,7 @@ bool OpenGLRenderer::renderLastFrame()
|
|||
-1.f, -1.f, 1.f, 0.f, 1.f,
|
||||
-1.f, 1.f, 1.f, 0.f, 0.f,
|
||||
1.f, -1.f, 1.f, 1.f, 1.f,
|
||||
1.f, -1.f, 1.f, 1.f, 0.f,
|
||||
1.f, 1.f, 1.f, 1.f, 0.f,
|
||||
};
|
||||
sverts[0] = sverts[5] = -1.f + gl.ofbo.shiftX * 2.f / framebuffer->getWidth();
|
||||
sverts[10] = sverts[15] = sverts[0] + 2;
|
||||
|
@ -817,6 +817,68 @@ bool OpenGLRenderer::renderLastFrame()
|
|||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLRenderer::GetLastFrame(std::vector<u8>& data, int& width, int& height)
|
||||
{
|
||||
GlFramebuffer *framebuffer = gl.ofbo2.ready ? gl.ofbo2.framebuffer.get() : gl.ofbo.framebuffer.get();
|
||||
if (framebuffer == nullptr)
|
||||
return false;
|
||||
width = framebuffer->getWidth();
|
||||
height = framebuffer->getHeight();
|
||||
if (config::Rotate90)
|
||||
std::swap(width, height);
|
||||
// We need square pixels for PNG
|
||||
int w = gl.ofbo.aspectRatio * height;
|
||||
if (width > w)
|
||||
height = width / gl.ofbo.aspectRatio;
|
||||
else
|
||||
width = w;
|
||||
|
||||
GlFramebuffer dstFramebuffer(width, height, false, false);
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
glcache.Disable(GL_BLEND);
|
||||
verify(framebuffer->getTexture() != 0);
|
||||
const float *vertices = nullptr;
|
||||
if (config::Rotate90)
|
||||
{
|
||||
static float rvertices[4][5] = {
|
||||
{ -1.f, 1.f, 1.f, 1.f, 0.f },
|
||||
{ -1.f, -1.f, 1.f, 1.f, 1.f },
|
||||
{ 1.f, 1.f, 1.f, 0.f, 0.f },
|
||||
{ 1.f, -1.f, 1.f, 0.f, 1.f },
|
||||
};
|
||||
vertices = &rvertices[0][0];
|
||||
}
|
||||
drawQuad(framebuffer->getTexture(), config::Rotate90, false, vertices);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
data.resize(width * height * 3);
|
||||
dstFramebuffer.bind(GL_READ_FRAMEBUFFER);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
if (gl.is_gles)
|
||||
{
|
||||
// GL_RGB not supported
|
||||
std::vector<u8> tmp(width * height * 4);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, tmp.data());
|
||||
u8 *dst = data.data();
|
||||
const u8 *src = tmp.data();
|
||||
while (src <= &tmp.back())
|
||||
{
|
||||
*dst++ = *src++;
|
||||
*dst++ = *src++;
|
||||
*dst++ = *src++;
|
||||
src++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data.data());
|
||||
}
|
||||
restoreCurrentFramebuffer();
|
||||
glCheck();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef LIBRETRO
|
||||
#include "vmu_xhair.h"
|
||||
|
||||
|
|
|
@ -989,9 +989,11 @@ static void gl_create_resources()
|
|||
|
||||
findGLVersion();
|
||||
|
||||
#ifndef LIBRETRO
|
||||
if (gl.gl_major >= 3)
|
||||
// will be used later. Better fail fast
|
||||
verify(glGenVertexArrays != nullptr);
|
||||
#endif
|
||||
|
||||
//create vbos
|
||||
gl.vbo.geometry = std::make_unique<GlBuffer>(GL_ARRAY_BUFFER);
|
||||
|
@ -1510,8 +1512,8 @@ bool OpenGLRenderer::Render()
|
|||
|
||||
if (!config::EmulateFramebuffer)
|
||||
{
|
||||
DrawOSD(false);
|
||||
frameRendered = true;
|
||||
DrawOSD(false);
|
||||
renderVideoRouting();
|
||||
}
|
||||
|
||||
|
|
|
@ -519,6 +519,7 @@ struct OpenGLRenderer : Renderer
|
|||
|
||||
return ret;
|
||||
}
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height) override;
|
||||
|
||||
void DrawOSD(bool clear_screen) override;
|
||||
|
||||
|
@ -570,7 +571,7 @@ protected:
|
|||
|
||||
void initQuad();
|
||||
void termQuad();
|
||||
void drawQuad(GLuint texId, bool rotate = false, bool swapY = false, float *coords = nullptr);
|
||||
void drawQuad(GLuint texId, bool rotate = false, bool swapY = false, const float *coords = nullptr);
|
||||
|
||||
extern const char* ShaderCompatSource;
|
||||
extern const char *VertexCompatShader;
|
||||
|
|
|
@ -315,28 +315,22 @@ void glReadFramebuffer(const FramebufferInfo& info)
|
|||
GLuint init_output_framebuffer(int width, int height)
|
||||
{
|
||||
if (gl.ofbo.framebuffer != nullptr
|
||||
&& (width != gl.ofbo.framebuffer->getWidth() || height != gl.ofbo.framebuffer->getHeight()
|
||||
// if the rotate90 setting has changed
|
||||
|| (gl.gl_major >= 3 && (gl.ofbo.framebuffer->getTexture() == 0) == config::Rotate90)))
|
||||
&& (width != gl.ofbo.framebuffer->getWidth() || height != gl.ofbo.framebuffer->getHeight()))
|
||||
{
|
||||
gl.ofbo.framebuffer.reset();
|
||||
}
|
||||
|
||||
if (gl.ofbo.framebuffer == nullptr)
|
||||
{
|
||||
GLuint texture = 0;
|
||||
if (config::Rotate90)
|
||||
{
|
||||
// Create a texture for rendering to
|
||||
texture = glcache.GenTexture();
|
||||
glcache.BindTexture(GL_TEXTURE_2D, texture);
|
||||
// Create a texture for rendering to
|
||||
GLuint texture = glcache.GenTexture();
|
||||
glcache.BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glcache.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
gl.ofbo.framebuffer = std::make_unique<GlFramebuffer>(width, height, true, texture);
|
||||
|
||||
glcache.Disable(GL_SCISSOR_TEST);
|
||||
|
|
|
@ -69,6 +69,34 @@ OpenGLDriver::~OpenGLDriver()
|
|||
ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
|
||||
void OpenGLDriver::reset()
|
||||
{
|
||||
ImGuiDriver::reset();
|
||||
for (auto& tex : vmu_lcd_tex_ids)
|
||||
tex = ImTextureID{};
|
||||
vmuLastChanged.fill({});
|
||||
}
|
||||
|
||||
void OpenGLDriver::updateVmuTextures()
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (!vmu_lcd_status[i])
|
||||
continue;
|
||||
|
||||
if (this->vmuLastChanged[i] != ::vmuLastChanged[i] || vmu_lcd_tex_ids[i] == ImTextureID())
|
||||
{
|
||||
try {
|
||||
vmu_lcd_tex_ids[i] = updateTexture(":vmugl:" + std::to_string(i), (const u8 *)vmu_lcd_data[i], 48, 32, true);
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
if (vmu_lcd_tex_ids[i] != ImTextureID())
|
||||
this->vmuLastChanged[i] = ::vmuLastChanged[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLDriver::displayVmus()
|
||||
{
|
||||
if (!gameStarted)
|
||||
|
@ -190,3 +218,12 @@ ImTextureID OpenGLDriver::updateTexture(const std::string& name, const u8 *data,
|
|||
|
||||
return textures[name] = (ImTextureID)(u64)texId;
|
||||
}
|
||||
|
||||
void OpenGLDriver::deleteTexture(const std::string& name)
|
||||
{
|
||||
auto it = textures.find(name);
|
||||
if (it != textures.end()) {
|
||||
glcache.DeleteTextures(1, (GLuint *)&it->second);
|
||||
textures.erase(it);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public:
|
|||
void newFrame() override;
|
||||
void renderDrawData(ImDrawData* drawData, bool gui_open) override;
|
||||
void present() override;
|
||||
void reset() override;
|
||||
|
||||
void setFrameRendered() override {
|
||||
frameRendered = true;
|
||||
|
@ -47,6 +48,7 @@ public:
|
|||
}
|
||||
|
||||
ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) override;
|
||||
void deleteTexture(const std::string& name) override;
|
||||
|
||||
private:
|
||||
void emuEvent(Event event)
|
||||
|
@ -66,8 +68,11 @@ private:
|
|||
static void emuEventCallback(Event event, void *p) {
|
||||
((OpenGLDriver *)p)->emuEvent(event);
|
||||
}
|
||||
void updateVmuTextures();
|
||||
|
||||
ImTextureID crosshairTexId = ImTextureID();
|
||||
ImTextureID vmu_lcd_tex_ids[8] {};
|
||||
std::array<u64, 8> vmuLastChanged {};
|
||||
bool gameStarted = false;
|
||||
bool frameRendered = false;
|
||||
std::unordered_map<std::string, ImTextureID> textures;
|
||||
|
|
|
@ -145,7 +145,7 @@ void termQuad()
|
|||
}
|
||||
|
||||
// coords is an optional array of 20 floats (4 vertices with x,y,z,u,v each)
|
||||
void drawQuad(GLuint texId, bool rotate, bool swapY, float *coords)
|
||||
void drawQuad(GLuint texId, bool rotate, bool swapY, const float *coords)
|
||||
{
|
||||
glcache.Disable(GL_SCISSOR_TEST);
|
||||
glcache.Disable(GL_DEPTH_TEST);
|
||||
|
|
|
@ -67,11 +67,11 @@ struct BufferData
|
|||
allocation.UnmapMemory();
|
||||
}
|
||||
|
||||
void *MapMemory()
|
||||
void *MapMemory() const
|
||||
{
|
||||
return allocation.MapMemory();
|
||||
}
|
||||
void UnmapMemory()
|
||||
void UnmapMemory() const
|
||||
{
|
||||
allocation.UnmapMemory();
|
||||
}
|
||||
|
|
|
@ -212,9 +212,14 @@ public:
|
|||
|
||||
void Clear()
|
||||
{
|
||||
BaseTextureCache::Clear();
|
||||
VulkanContext *context = VulkanContext::Instance();
|
||||
for (auto& set : inFlightTextures)
|
||||
{
|
||||
for (Texture *tex : set)
|
||||
tex->deferDeleteResource(context);
|
||||
set.clear();
|
||||
}
|
||||
BaseTextureCache::Clear();
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "wsi/context.h"
|
||||
#include "commandpool.h"
|
||||
#include "overlay.h"
|
||||
#include <vector>
|
||||
|
||||
static vk::Format findDepthFormat(vk::PhysicalDevice physicalDevice);
|
||||
|
||||
|
@ -43,6 +44,7 @@ public:
|
|||
|
||||
u32 GetGraphicsQueueFamilyIndex() const { return retro_render_if->queue_index; }
|
||||
void PresentFrame(vk::Image image, vk::ImageView imageView, const vk::Extent2D& extent, float aspectRatio);
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height) { return false; }
|
||||
|
||||
vk::PhysicalDevice GetPhysicalDevice() const { return physicalDevice; }
|
||||
vk::Device GetDevice() const { return device; }
|
||||
|
|
|
@ -966,6 +966,7 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const
|
|||
try {
|
||||
NewFrame();
|
||||
auto overlayCmdBuffer = PrepareOverlay(config::FloatVMUs, true);
|
||||
gui_draw_osd();
|
||||
|
||||
BeginRenderPass();
|
||||
|
||||
|
@ -973,6 +974,7 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const
|
|||
DrawFrame(imageView, extent, aspectRatio);
|
||||
|
||||
DrawOverlay(settings.display.uiScale, config::FloatVMUs, true);
|
||||
imguiDriver->renderDrawData(ImGui::GetDrawData(), false);
|
||||
renderer->DrawOSD(false);
|
||||
EndFrame(overlayCmdBuffer);
|
||||
static_cast<BaseVulkanRenderer*>(renderer)->RenderVideoRouting();
|
||||
|
@ -1230,3 +1232,107 @@ VulkanContext::~VulkanContext()
|
|||
verify(contextInstance == this);
|
||||
contextInstance = nullptr;
|
||||
}
|
||||
|
||||
bool VulkanContext::GetLastFrame(std::vector<u8>& data, int& width, int& height)
|
||||
{
|
||||
if (!lastFrameView)
|
||||
return false;
|
||||
|
||||
width = lastFrameExtent.width;
|
||||
height = lastFrameExtent.height;
|
||||
if (config::Rotate90)
|
||||
std::swap(width, height);
|
||||
// We need square pixels for PNG
|
||||
int w = lastFrameAR * height;
|
||||
if (width > w)
|
||||
height = width / lastFrameAR;
|
||||
else
|
||||
width = w;
|
||||
// color attachment
|
||||
FramebufferAttachment attachment(physicalDevice, *device);
|
||||
attachment.Init(width, height, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, "screenshot");
|
||||
// command buffer
|
||||
vk::UniqueCommandBuffer commandBuffer = std::move(device->allocateCommandBuffersUnique(
|
||||
vk::CommandBufferAllocateInfo(*commandPools.back(), vk::CommandBufferLevel::ePrimary, 1)).front());
|
||||
commandBuffer->begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
// render pass
|
||||
vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1,
|
||||
vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare,
|
||||
vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal);
|
||||
vk::AttachmentReference colorReference(0, vk::ImageLayout::eColorAttachmentOptimal);
|
||||
vk::SubpassDescription subpass(vk::SubpassDescriptionFlags(), vk::PipelineBindPoint::eGraphics, nullptr, colorReference,
|
||||
nullptr, nullptr);
|
||||
vk::UniqueRenderPass renderPass = device->createRenderPassUnique(vk::RenderPassCreateInfo(vk::RenderPassCreateFlags(),
|
||||
attachmentDescription, subpass));
|
||||
// framebuffer
|
||||
vk::ImageView imageView = attachment.GetImageView();
|
||||
vk::UniqueFramebuffer framebuffer = device->createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(),
|
||||
*renderPass, imageView, width, height, 1));
|
||||
vk::ClearValue clearValue;
|
||||
commandBuffer->beginRenderPass(vk::RenderPassBeginInfo(*renderPass, *framebuffer, vk::Rect2D({0, 0}, {(u32)width, (u32)height}), clearValue),
|
||||
vk::SubpassContents::eInline);
|
||||
|
||||
// Pipeline
|
||||
QuadPipeline pipeline(true, config::Rotate90);
|
||||
pipeline.Init(shaderManager.get(), *renderPass, 0);
|
||||
pipeline.BindPipeline(*commandBuffer);
|
||||
|
||||
// Draw
|
||||
QuadVertex vtx[] {
|
||||
{ -1, -1, 0, 0, 0 },
|
||||
{ 1, -1, 0, 1, 0 },
|
||||
{ -1, 1, 0, 0, 1 },
|
||||
{ 1, 1, 0, 1, 1 },
|
||||
};
|
||||
|
||||
vk::Viewport viewport(0, 0, width, height);
|
||||
commandBuffer->setViewport(0, viewport);
|
||||
commandBuffer->setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), vk::Extent2D(width, height)));
|
||||
QuadDrawer drawer;
|
||||
drawer.Init(&pipeline);
|
||||
drawer.Draw(*commandBuffer, lastFrameView, vtx, false);
|
||||
commandBuffer->endRenderPass();
|
||||
|
||||
// Copy back
|
||||
vk::BufferImageCopy copyRegion(0, width, height, vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), vk::Offset3D(0, 0, 0),
|
||||
vk::Extent3D(width, height, 1));
|
||||
commandBuffer->copyImageToBuffer(attachment.GetImage(), vk::ImageLayout::eTransferSrcOptimal,
|
||||
*attachment.GetBufferData()->buffer, copyRegion);
|
||||
|
||||
vk::BufferMemoryBarrier bufferMemoryBarrier(
|
||||
vk::AccessFlagBits::eTransferWrite,
|
||||
vk::AccessFlagBits::eHostRead,
|
||||
VK_QUEUE_FAMILY_IGNORED,
|
||||
VK_QUEUE_FAMILY_IGNORED,
|
||||
*attachment.GetBufferData()->buffer,
|
||||
0,
|
||||
VK_WHOLE_SIZE);
|
||||
commandBuffer->pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
|
||||
vk::PipelineStageFlagBits::eHost, {}, nullptr, bufferMemoryBarrier, nullptr);
|
||||
commandBuffer->end();
|
||||
|
||||
vk::UniqueFence fence = device->createFenceUnique(vk::FenceCreateInfo());
|
||||
vk::SubmitInfo submitInfo(nullptr, nullptr, commandBuffer.get(), nullptr);
|
||||
graphicsQueue.submit(submitInfo, *fence);
|
||||
|
||||
vk::Result res = device->waitForFences(fence.get(), true, UINT64_MAX);
|
||||
if (res != vk::Result::eSuccess)
|
||||
WARN_LOG(RENDERER, "VulkanContext::GetLastFrame: waitForFences failed %d", (int)res);
|
||||
|
||||
const u8 *img = (const u8 *)attachment.GetBufferData()->MapMemory();
|
||||
data.clear();
|
||||
data.reserve(width * height * 3);
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
data.push_back(*img++);
|
||||
data.push_back(*img++);
|
||||
data.push_back(*img++);
|
||||
img++;
|
||||
}
|
||||
}
|
||||
attachment.GetBufferData()->UnmapMemory();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
#include "rend/TexCache.h"
|
||||
#include "overlay.h"
|
||||
#include "wsi/context.h"
|
||||
#include <vector>
|
||||
|
||||
struct ImDrawData;
|
||||
|
||||
|
@ -60,6 +61,7 @@ public:
|
|||
void Present() noexcept;
|
||||
void PresentFrame(vk::Image image, vk::ImageView imageView, const vk::Extent2D& extent, float aspectRatio) noexcept;
|
||||
void PresentLastFrame();
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height);
|
||||
|
||||
vk::PhysicalDevice GetPhysicalDevice() const { return physicalDevice; }
|
||||
vk::Device GetDevice() const { return *device; }
|
||||
|
|
|
@ -133,6 +133,27 @@ public:
|
|||
return texId;
|
||||
}
|
||||
|
||||
void deleteTexture(const std::string& name) override
|
||||
{
|
||||
auto it = textures.find(name);
|
||||
if (it != textures.end())
|
||||
{
|
||||
class DescSetDeleter : public Deletable
|
||||
{
|
||||
public:
|
||||
DescSetDeleter(VkDescriptorSet descSet) : descSet(descSet) {}
|
||||
~DescSetDeleter() {
|
||||
ImGui_ImplVulkan_RemoveTexture(descSet);
|
||||
}
|
||||
VkDescriptorSet descSet;
|
||||
};
|
||||
getContext()->addToFlight(new DescSetDeleter((VkDescriptorSet)it->second.textureId));
|
||||
if (it->second.texture != nullptr)
|
||||
it->second.texture->deferDeleteResource(getContext());
|
||||
textures.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct VkTexture {
|
||||
VkTexture() = default;
|
||||
|
|
|
@ -21,6 +21,281 @@
|
|||
#include "vulkan.h"
|
||||
#include "vulkan_renderer.h"
|
||||
#include "drawer.h"
|
||||
#include "hw/pvr/ta.h"
|
||||
#include "rend/osd.h"
|
||||
#include "rend/transform_matrix.h"
|
||||
|
||||
bool BaseVulkanRenderer::BaseInit(vk::RenderPass renderPass, int subpass)
|
||||
{
|
||||
texCommandPool.Init();
|
||||
fbCommandPool.Init();
|
||||
|
||||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
if (!vjoyTexture)
|
||||
{
|
||||
int w, h;
|
||||
u8 *image_data = loadOSDButtons(w, h);
|
||||
texCommandPool.BeginFrame();
|
||||
vjoyTexture = std::make_unique<Texture>();
|
||||
vjoyTexture->tex_type = TextureType::_8888;
|
||||
vk::CommandBuffer cmdBuffer = texCommandPool.Allocate();
|
||||
cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
vjoyTexture->SetCommandBuffer(cmdBuffer);
|
||||
vjoyTexture->UploadToGPU(w, h, image_data, false);
|
||||
vjoyTexture->SetCommandBuffer(nullptr);
|
||||
cmdBuffer.end();
|
||||
texCommandPool.EndFrame();
|
||||
delete [] image_data;
|
||||
osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass());
|
||||
}
|
||||
if (!osdBuffer)
|
||||
{
|
||||
osdBuffer = std::make_unique<BufferData>(sizeof(OSDVertex) * VJOY_VISIBLE * 4,
|
||||
vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
}
|
||||
#endif
|
||||
quadPipeline = std::make_unique<QuadPipeline>(false, false);
|
||||
quadPipeline->Init(&shaderManager, renderPass, subpass);
|
||||
framebufferDrawer = std::make_unique<QuadDrawer>();
|
||||
framebufferDrawer->Init(quadPipeline.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::Term()
|
||||
{
|
||||
GetContext()->WaitIdle();
|
||||
GetContext()->PresentFrame(nullptr, nullptr, vk::Extent2D(), 0);
|
||||
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
|
||||
os_VideoRoutingTermVk();
|
||||
#endif
|
||||
framebufferDrawer.reset();
|
||||
quadPipeline.reset();
|
||||
osdBuffer.reset();
|
||||
osdPipeline.Term();
|
||||
vjoyTexture.reset();
|
||||
textureCache.Clear();
|
||||
fogTexture = nullptr;
|
||||
paletteTexture = nullptr;
|
||||
texCommandPool.Term();
|
||||
fbCommandPool.Term();
|
||||
framebufferTextures.clear();
|
||||
framebufferTexIndex = 0;
|
||||
shaderManager.term();
|
||||
}
|
||||
|
||||
BaseTextureCacheData *BaseVulkanRenderer::GetTexture(TSP tsp, TCW tcw)
|
||||
{
|
||||
Texture* tf = textureCache.getTextureCacheData(tsp, tcw);
|
||||
|
||||
//update if needed
|
||||
if (tf->NeedsUpdate())
|
||||
{
|
||||
// This kills performance when a frame is skipped and lots of texture updated each frame
|
||||
//if (textureCache.IsInFlight(tf, true))
|
||||
// textureCache.DestroyLater(tf);
|
||||
tf->SetCommandBuffer(texCommandBuffer);
|
||||
if (!tf->Update())
|
||||
{
|
||||
tf->SetCommandBuffer(nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
else if (tf->IsCustomTextureAvailable())
|
||||
{
|
||||
tf->deferDeleteResource(&texCommandPool);
|
||||
tf->SetCommandBuffer(texCommandBuffer);
|
||||
tf->CheckCustomTexture();
|
||||
}
|
||||
tf->SetCommandBuffer(nullptr);
|
||||
textureCache.SetInFlight(tf);
|
||||
|
||||
return tf;
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::Process(TA_context* ctx)
|
||||
{
|
||||
if (KillTex)
|
||||
textureCache.Clear();
|
||||
|
||||
texCommandPool.BeginFrame();
|
||||
textureCache.SetCurrentIndex(texCommandPool.GetIndex());
|
||||
textureCache.Cleanup();
|
||||
|
||||
texCommandBuffer = texCommandPool.Allocate();
|
||||
texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
|
||||
ta_parse(ctx, true);
|
||||
|
||||
CheckFogTexture();
|
||||
CheckPaletteTexture();
|
||||
texCommandBuffer.end();
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::ReInitOSD()
|
||||
{
|
||||
texCommandPool.Init();
|
||||
fbCommandPool.Init();
|
||||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass());
|
||||
#endif
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::DrawOSD(bool clear_screen)
|
||||
{
|
||||
#ifndef LIBRETRO
|
||||
if (!vjoyTexture)
|
||||
return;
|
||||
try {
|
||||
if (clear_screen)
|
||||
{
|
||||
GetContext()->NewFrame();
|
||||
GetContext()->BeginRenderPass();
|
||||
GetContext()->PresentLastFrame();
|
||||
}
|
||||
const float dc2s_scale_h = settings.display.height / 480.0f;
|
||||
const float sidebarWidth = (settings.display.width - dc2s_scale_h * 640.0f) / 2;
|
||||
|
||||
std::vector<OSDVertex> osdVertices = GetOSDVertices();
|
||||
const float x1 = 2.0f / (settings.display.width / dc2s_scale_h);
|
||||
const float y1 = 2.0f / 480;
|
||||
const float x2 = 1 - 2 * sidebarWidth / settings.display.width;
|
||||
const float y2 = 1;
|
||||
for (OSDVertex& vtx : osdVertices)
|
||||
{
|
||||
vtx.x = vtx.x * x1 - x2;
|
||||
vtx.y = vtx.y * y1 - y2;
|
||||
}
|
||||
|
||||
const vk::CommandBuffer cmdBuffer = GetContext()->GetCurrentCommandBuffer();
|
||||
cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, osdPipeline.GetPipeline());
|
||||
|
||||
osdPipeline.BindDescriptorSets(cmdBuffer);
|
||||
const vk::Viewport viewport(0, 0, (float)settings.display.width, (float)settings.display.height, 0, 1.f);
|
||||
cmdBuffer.setViewport(0, viewport);
|
||||
const vk::Rect2D scissor({ 0, 0 }, { (u32)settings.display.width, (u32)settings.display.height });
|
||||
cmdBuffer.setScissor(0, scissor);
|
||||
osdBuffer->upload((u32)(osdVertices.size() * sizeof(OSDVertex)), osdVertices.data());
|
||||
cmdBuffer.bindVertexBuffers(0, osdBuffer->buffer.get(), {0});
|
||||
for (u32 i = 0; i < (u32)osdVertices.size(); i += 4)
|
||||
cmdBuffer.draw(4, 1, i, 0);
|
||||
if (clear_screen)
|
||||
GetContext()->EndFrame();
|
||||
} catch (const InvalidVulkanContext&) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::RenderFramebuffer(const FramebufferInfo& info)
|
||||
{
|
||||
framebufferTexIndex = (framebufferTexIndex + 1) % GetContext()->GetSwapChainSize();
|
||||
|
||||
if (framebufferTextures.size() != GetContext()->GetSwapChainSize())
|
||||
framebufferTextures.resize(GetContext()->GetSwapChainSize());
|
||||
std::unique_ptr<Texture>& curTexture = framebufferTextures[framebufferTexIndex];
|
||||
if (!curTexture)
|
||||
{
|
||||
curTexture = std::make_unique<Texture>();
|
||||
curTexture->tex_type = TextureType::_8888;
|
||||
}
|
||||
|
||||
fbCommandPool.BeginFrame();
|
||||
vk::CommandBuffer commandBuffer = fbCommandPool.Allocate();
|
||||
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
curTexture->SetCommandBuffer(commandBuffer);
|
||||
|
||||
if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1)
|
||||
{
|
||||
// Video output disabled
|
||||
u8 rgba[] { (u8)info.vo_border_col._red, (u8)info.vo_border_col._green, (u8)info.vo_border_col._blue, 255 };
|
||||
curTexture->UploadToGPU(1, 1, rgba, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
PixelBuffer<u32> pb;
|
||||
int width;
|
||||
int height;
|
||||
ReadFramebuffer(info, pb, width, height);
|
||||
|
||||
curTexture->UploadToGPU(width, height, (u8*)pb.data(), false);
|
||||
}
|
||||
|
||||
curTexture->SetCommandBuffer(nullptr);
|
||||
commandBuffer.end();
|
||||
fbCommandPool.EndFrame();
|
||||
framebufferRendered = true;
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::RenderVideoRouting()
|
||||
{
|
||||
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
|
||||
if (config::VideoRouting)
|
||||
{
|
||||
auto device = GetContext()->GetDevice();
|
||||
auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()];
|
||||
auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0);
|
||||
|
||||
int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width);
|
||||
int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height);
|
||||
|
||||
extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h);
|
||||
os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
os_VideoRoutingTermVk();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::CheckFogTexture()
|
||||
{
|
||||
if (!fogTexture)
|
||||
{
|
||||
fogTexture = std::make_unique<Texture>();
|
||||
fogTexture->tex_type = TextureType::_8;
|
||||
fog_needs_update = true;
|
||||
}
|
||||
if (!fog_needs_update || !config::Fog)
|
||||
return;
|
||||
fog_needs_update = false;
|
||||
u8 texData[256];
|
||||
MakeFogTexture(texData);
|
||||
|
||||
fogTexture->SetCommandBuffer(texCommandBuffer);
|
||||
fogTexture->UploadToGPU(128, 2, texData, false);
|
||||
fogTexture->SetCommandBuffer(nullptr);
|
||||
}
|
||||
|
||||
void BaseVulkanRenderer::CheckPaletteTexture()
|
||||
{
|
||||
if (!paletteTexture)
|
||||
{
|
||||
paletteTexture = std::make_unique<Texture>();
|
||||
paletteTexture->tex_type = TextureType::_8888;
|
||||
palette_updated = true;
|
||||
}
|
||||
if (!palette_updated)
|
||||
return;
|
||||
palette_updated = false;
|
||||
|
||||
paletteTexture->SetCommandBuffer(texCommandBuffer);
|
||||
paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false);
|
||||
paletteTexture->SetCommandBuffer(nullptr);
|
||||
}
|
||||
|
||||
bool BaseVulkanRenderer::presentFramebuffer()
|
||||
{
|
||||
if (framebufferTexIndex >= (int)framebufferTextures.size())
|
||||
return false;
|
||||
Texture *fbTexture = framebufferTextures[framebufferTexIndex].get();
|
||||
if (fbTexture == nullptr)
|
||||
return false;
|
||||
GetContext()->PresentFrame(fbTexture->GetImage(), fbTexture->GetImageView(), fbTexture->getSize(),
|
||||
getDCFramebufferAspectRatio());
|
||||
framebufferRendered = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
class VulkanRenderer final : public BaseVulkanRenderer
|
||||
{
|
||||
|
|
|
@ -19,14 +19,9 @@
|
|||
#pragma once
|
||||
#include "vulkan.h"
|
||||
#include "hw/pvr/Renderer_if.h"
|
||||
#include "hw/pvr/ta.h"
|
||||
#include "commandpool.h"
|
||||
#include "pipeline.h"
|
||||
#include "rend/osd.h"
|
||||
#include "rend/transform_matrix.h"
|
||||
#ifndef LIBRETRO
|
||||
#include "ui/gui.h"
|
||||
#endif
|
||||
#include "shaders.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
@ -36,232 +31,21 @@ void os_VideoRoutingTermVk();
|
|||
class BaseVulkanRenderer : public Renderer
|
||||
{
|
||||
protected:
|
||||
bool BaseInit(vk::RenderPass renderPass, int subpass = 0)
|
||||
{
|
||||
texCommandPool.Init();
|
||||
fbCommandPool.Init();
|
||||
|
||||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
if (!vjoyTexture)
|
||||
{
|
||||
int w, h;
|
||||
u8 *image_data = loadOSDButtons(w, h);
|
||||
texCommandPool.BeginFrame();
|
||||
vjoyTexture = std::make_unique<Texture>();
|
||||
vjoyTexture->tex_type = TextureType::_8888;
|
||||
vk::CommandBuffer cmdBuffer = texCommandPool.Allocate();
|
||||
cmdBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
vjoyTexture->SetCommandBuffer(cmdBuffer);
|
||||
vjoyTexture->UploadToGPU(w, h, image_data, false);
|
||||
vjoyTexture->SetCommandBuffer(nullptr);
|
||||
cmdBuffer.end();
|
||||
texCommandPool.EndFrame();
|
||||
delete [] image_data;
|
||||
osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass());
|
||||
}
|
||||
if (!osdBuffer)
|
||||
{
|
||||
osdBuffer = std::make_unique<BufferData>(sizeof(OSDVertex) * VJOY_VISIBLE * 4,
|
||||
vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
}
|
||||
#endif
|
||||
quadPipeline = std::make_unique<QuadPipeline>(false, false);
|
||||
quadPipeline->Init(&shaderManager, renderPass, subpass);
|
||||
framebufferDrawer = std::make_unique<QuadDrawer>();
|
||||
framebufferDrawer->Init(quadPipeline.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
bool BaseInit(vk::RenderPass renderPass, int subpass = 0);
|
||||
|
||||
public:
|
||||
void Term() override
|
||||
{
|
||||
GetContext()->WaitIdle();
|
||||
GetContext()->PresentFrame(nullptr, nullptr, vk::Extent2D(), 0);
|
||||
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
|
||||
os_VideoRoutingTermVk();
|
||||
#endif
|
||||
framebufferDrawer.reset();
|
||||
quadPipeline.reset();
|
||||
osdBuffer.reset();
|
||||
osdPipeline.Term();
|
||||
vjoyTexture.reset();
|
||||
textureCache.Clear();
|
||||
fogTexture = nullptr;
|
||||
paletteTexture = nullptr;
|
||||
texCommandPool.Term();
|
||||
fbCommandPool.Term();
|
||||
framebufferTextures.clear();
|
||||
framebufferTexIndex = 0;
|
||||
shaderManager.term();
|
||||
void Term() override;
|
||||
BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override;
|
||||
void Process(TA_context* ctx) override;
|
||||
void ReInitOSD();
|
||||
void DrawOSD(bool clear_screen) override;
|
||||
void RenderFramebuffer(const FramebufferInfo& info) override;
|
||||
void RenderVideoRouting();
|
||||
|
||||
bool GetLastFrame(std::vector<u8>& data, int& width, int& height) override {
|
||||
return GetContext()->GetLastFrame(data, width, height);
|
||||
}
|
||||
|
||||
BaseTextureCacheData *GetTexture(TSP tsp, TCW tcw) override
|
||||
{
|
||||
Texture* tf = textureCache.getTextureCacheData(tsp, tcw);
|
||||
|
||||
//update if needed
|
||||
if (tf->NeedsUpdate())
|
||||
{
|
||||
// This kills performance when a frame is skipped and lots of texture updated each frame
|
||||
//if (textureCache.IsInFlight(tf, true))
|
||||
// textureCache.DestroyLater(tf);
|
||||
tf->SetCommandBuffer(texCommandBuffer);
|
||||
if (!tf->Update())
|
||||
{
|
||||
tf->SetCommandBuffer(nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
else if (tf->IsCustomTextureAvailable())
|
||||
{
|
||||
tf->deferDeleteResource(&texCommandPool);
|
||||
tf->SetCommandBuffer(texCommandBuffer);
|
||||
tf->CheckCustomTexture();
|
||||
}
|
||||
tf->SetCommandBuffer(nullptr);
|
||||
textureCache.SetInFlight(tf);
|
||||
|
||||
return tf;
|
||||
}
|
||||
|
||||
void Process(TA_context* ctx) override
|
||||
{
|
||||
if (KillTex)
|
||||
textureCache.Clear();
|
||||
|
||||
texCommandPool.BeginFrame();
|
||||
textureCache.SetCurrentIndex(texCommandPool.GetIndex());
|
||||
textureCache.Cleanup();
|
||||
|
||||
texCommandBuffer = texCommandPool.Allocate();
|
||||
texCommandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
|
||||
ta_parse(ctx, true);
|
||||
|
||||
CheckFogTexture();
|
||||
CheckPaletteTexture();
|
||||
texCommandBuffer.end();
|
||||
}
|
||||
|
||||
void ReInitOSD()
|
||||
{
|
||||
texCommandPool.Init();
|
||||
fbCommandPool.Init();
|
||||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
osdPipeline.Init(&shaderManager, vjoyTexture->GetImageView(), GetContext()->GetRenderPass());
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawOSD(bool clear_screen) override
|
||||
{
|
||||
#ifndef LIBRETRO
|
||||
gui_display_osd();
|
||||
if (!vjoyTexture)
|
||||
return;
|
||||
try {
|
||||
if (clear_screen)
|
||||
{
|
||||
GetContext()->NewFrame();
|
||||
GetContext()->BeginRenderPass();
|
||||
GetContext()->PresentLastFrame();
|
||||
}
|
||||
const float dc2s_scale_h = settings.display.height / 480.0f;
|
||||
const float sidebarWidth = (settings.display.width - dc2s_scale_h * 640.0f) / 2;
|
||||
|
||||
std::vector<OSDVertex> osdVertices = GetOSDVertices();
|
||||
const float x1 = 2.0f / (settings.display.width / dc2s_scale_h);
|
||||
const float y1 = 2.0f / 480;
|
||||
const float x2 = 1 - 2 * sidebarWidth / settings.display.width;
|
||||
const float y2 = 1;
|
||||
for (OSDVertex& vtx : osdVertices)
|
||||
{
|
||||
vtx.x = vtx.x * x1 - x2;
|
||||
vtx.y = vtx.y * y1 - y2;
|
||||
}
|
||||
|
||||
const vk::CommandBuffer cmdBuffer = GetContext()->GetCurrentCommandBuffer();
|
||||
cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, osdPipeline.GetPipeline());
|
||||
|
||||
osdPipeline.BindDescriptorSets(cmdBuffer);
|
||||
const vk::Viewport viewport(0, 0, (float)settings.display.width, (float)settings.display.height, 0, 1.f);
|
||||
cmdBuffer.setViewport(0, viewport);
|
||||
const vk::Rect2D scissor({ 0, 0 }, { (u32)settings.display.width, (u32)settings.display.height });
|
||||
cmdBuffer.setScissor(0, scissor);
|
||||
osdBuffer->upload((u32)(osdVertices.size() * sizeof(OSDVertex)), osdVertices.data());
|
||||
cmdBuffer.bindVertexBuffers(0, osdBuffer->buffer.get(), {0});
|
||||
for (u32 i = 0; i < (u32)osdVertices.size(); i += 4)
|
||||
cmdBuffer.draw(4, 1, i, 0);
|
||||
if (clear_screen)
|
||||
GetContext()->EndFrame();
|
||||
} catch (const InvalidVulkanContext&) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void RenderFramebuffer(const FramebufferInfo& info) override
|
||||
{
|
||||
framebufferTexIndex = (framebufferTexIndex + 1) % GetContext()->GetSwapChainSize();
|
||||
|
||||
if (framebufferTextures.size() != GetContext()->GetSwapChainSize())
|
||||
framebufferTextures.resize(GetContext()->GetSwapChainSize());
|
||||
std::unique_ptr<Texture>& curTexture = framebufferTextures[framebufferTexIndex];
|
||||
if (!curTexture)
|
||||
{
|
||||
curTexture = std::make_unique<Texture>();
|
||||
curTexture->tex_type = TextureType::_8888;
|
||||
}
|
||||
|
||||
fbCommandPool.BeginFrame();
|
||||
vk::CommandBuffer commandBuffer = fbCommandPool.Allocate();
|
||||
commandBuffer.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
||||
curTexture->SetCommandBuffer(commandBuffer);
|
||||
|
||||
if (info.fb_r_ctrl.fb_enable == 0 || info.vo_control.blank_video == 1)
|
||||
{
|
||||
// Video output disabled
|
||||
u8 rgba[] { (u8)info.vo_border_col._red, (u8)info.vo_border_col._green, (u8)info.vo_border_col._blue, 255 };
|
||||
curTexture->UploadToGPU(1, 1, rgba, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
PixelBuffer<u32> pb;
|
||||
int width;
|
||||
int height;
|
||||
ReadFramebuffer(info, pb, width, height);
|
||||
|
||||
curTexture->UploadToGPU(width, height, (u8*)pb.data(), false);
|
||||
}
|
||||
|
||||
curTexture->SetCommandBuffer(nullptr);
|
||||
commandBuffer.end();
|
||||
fbCommandPool.EndFrame();
|
||||
framebufferRendered = true;
|
||||
}
|
||||
|
||||
void RenderVideoRouting()
|
||||
{
|
||||
#if defined(VIDEO_ROUTING) && defined(TARGET_MAC)
|
||||
if (config::VideoRouting)
|
||||
{
|
||||
auto device = GetContext()->GetDevice();
|
||||
auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()];
|
||||
auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0);
|
||||
|
||||
int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width);
|
||||
int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height);
|
||||
|
||||
extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h);
|
||||
os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
os_VideoRoutingTermVk();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
protected:
|
||||
BaseVulkanRenderer() : viewport(640, 480) {}
|
||||
|
||||
|
@ -273,57 +57,9 @@ protected:
|
|||
viewport.height = h;
|
||||
}
|
||||
|
||||
void CheckFogTexture()
|
||||
{
|
||||
if (!fogTexture)
|
||||
{
|
||||
fogTexture = std::make_unique<Texture>();
|
||||
fogTexture->tex_type = TextureType::_8;
|
||||
fog_needs_update = true;
|
||||
}
|
||||
if (!fog_needs_update || !config::Fog)
|
||||
return;
|
||||
fog_needs_update = false;
|
||||
u8 texData[256];
|
||||
MakeFogTexture(texData);
|
||||
fogTexture->SetCommandBuffer(texCommandBuffer);
|
||||
|
||||
fogTexture->UploadToGPU(128, 2, texData, false);
|
||||
|
||||
fogTexture->SetCommandBuffer(nullptr);
|
||||
}
|
||||
|
||||
void CheckPaletteTexture()
|
||||
{
|
||||
if (!paletteTexture)
|
||||
{
|
||||
paletteTexture = std::make_unique<Texture>();
|
||||
paletteTexture->tex_type = TextureType::_8888;
|
||||
forcePaletteUpdate();
|
||||
}
|
||||
if (!palette_updated)
|
||||
return;
|
||||
palette_updated = false;
|
||||
|
||||
paletteTexture->SetCommandBuffer(texCommandBuffer);
|
||||
|
||||
paletteTexture->UploadToGPU(1024, 1, (u8 *)palette32_ram, false);
|
||||
|
||||
paletteTexture->SetCommandBuffer(nullptr);
|
||||
}
|
||||
|
||||
bool presentFramebuffer()
|
||||
{
|
||||
if (framebufferTexIndex >= (int)framebufferTextures.size())
|
||||
return false;
|
||||
Texture *fbTexture = framebufferTextures[framebufferTexIndex].get();
|
||||
if (fbTexture == nullptr)
|
||||
return false;
|
||||
GetContext()->PresentFrame(fbTexture->GetImage(), fbTexture->GetImageView(), fbTexture->getSize(),
|
||||
getDCFramebufferAspectRatio());
|
||||
framebufferRendered = false;
|
||||
return true;
|
||||
}
|
||||
void CheckFogTexture();
|
||||
void CheckPaletteTexture();
|
||||
bool presentFramebuffer();
|
||||
|
||||
ShaderManager shaderManager;
|
||||
std::unique_ptr<Texture> fogTexture;
|
||||
|
|
|
@ -99,3 +99,59 @@ void dc_deserialize(Deserializer& deser)
|
|||
|
||||
DEBUG_LOG(SAVESTATE, "Loaded %d bytes", (u32)deser.size());
|
||||
}
|
||||
|
||||
Deserializer::Deserializer(const void *data, size_t limit, bool rollback)
|
||||
: SerializeBase(limit, rollback), data((const u8 *)data)
|
||||
{
|
||||
if (!memcmp(data, "RASTATE\001", 8))
|
||||
{
|
||||
// RetroArch savestates now have several sections: MEM, ACHV, RPLY, etc.
|
||||
const u8 *p = this->data + 8;
|
||||
limit -= 8;
|
||||
while (limit > 8)
|
||||
{
|
||||
const u8 *section = p;
|
||||
u32 sectionSize = *(const u32 *)&p[4];
|
||||
p += 8;
|
||||
limit -= 8;
|
||||
if (!memcmp(section, "MEM ", 4))
|
||||
{
|
||||
// That's the part we're interested in
|
||||
this->data = p;
|
||||
this->limit = sectionSize;
|
||||
break;
|
||||
}
|
||||
sectionSize = (sectionSize + 7) & ~7; // align to 8 bytes
|
||||
if (limit < sectionSize) {
|
||||
limit = 0;
|
||||
break;
|
||||
}
|
||||
p += sectionSize;
|
||||
limit -= sectionSize;
|
||||
}
|
||||
if (limit <= 8)
|
||||
throw Exception("Can't find MEM section in RetroArch savestate");
|
||||
}
|
||||
deserialize(_version);
|
||||
if (_version < V16)
|
||||
throw Exception("Unsupported version");
|
||||
if (_version > Current)
|
||||
throw Exception("Version too recent");
|
||||
|
||||
if(_version >= V42 && settings.platform.isConsole())
|
||||
{
|
||||
u32 ramSize;
|
||||
deserialize(ramSize);
|
||||
if (ramSize != settings.platform.ram_size)
|
||||
throw Exception("Selected RAM Size doesn't match Save State");
|
||||
}
|
||||
}
|
||||
|
||||
Serializer::Serializer(void *data, size_t limit, bool rollback)
|
||||
: SerializeBase(limit, rollback), data((u8 *)data)
|
||||
{
|
||||
Version v = Current;
|
||||
serialize(v);
|
||||
if (settings.platform.isConsole())
|
||||
serialize(settings.platform.ram_size);
|
||||
}
|
||||
|
|
|
@ -87,29 +87,7 @@ public:
|
|||
Exception(const char *msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
Deserializer(const void *data, size_t limit, bool rollback = false)
|
||||
: SerializeBase(limit, rollback), data((const u8 *)data)
|
||||
{
|
||||
if (!memcmp(data, "RASTATE\001", 8))
|
||||
{
|
||||
// RetroArch savestate: a 16-byte header is now added here because why not?
|
||||
this->data += 16;
|
||||
this->limit -= 16;
|
||||
}
|
||||
deserialize(_version);
|
||||
if (_version < V16)
|
||||
throw Exception("Unsupported version");
|
||||
if (_version > Current)
|
||||
throw Exception("Version too recent");
|
||||
|
||||
if(_version >= V42 && settings.platform.isConsole())
|
||||
{
|
||||
u32 ramSize;
|
||||
deserialize(ramSize);
|
||||
if (ramSize != settings.platform.ram_size)
|
||||
throw Exception("Selected RAM Size doesn't match Save State");
|
||||
}
|
||||
}
|
||||
Deserializer(const void *data, size_t limit, bool rollback = false);
|
||||
|
||||
template<typename T>
|
||||
void deserialize(T& obj)
|
||||
|
@ -165,14 +143,7 @@ public:
|
|||
Serializer()
|
||||
: Serializer(nullptr, std::numeric_limits<size_t>::max(), false) {}
|
||||
|
||||
Serializer(void *data, size_t limit, bool rollback = false)
|
||||
: SerializeBase(limit, rollback), data((u8 *)data)
|
||||
{
|
||||
Version v = Current;
|
||||
serialize(v);
|
||||
if (settings.platform.isConsole())
|
||||
serialize(settings.platform.ram_size);
|
||||
}
|
||||
Serializer(void *data, size_t limit, bool rollback = false);
|
||||
|
||||
template<typename T>
|
||||
void serialize(const T& obj)
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <cassert>
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <sys/mman.h>
|
||||
|
@ -199,3 +201,39 @@ public:
|
|||
};
|
||||
|
||||
u64 getTimeMs();
|
||||
|
||||
class ThreadRunner
|
||||
{
|
||||
public:
|
||||
void init() {
|
||||
threadId = std::this_thread::get_id();
|
||||
}
|
||||
void runOnThread(std::function<void()> func)
|
||||
{
|
||||
if (threadId == std::this_thread::get_id()) {
|
||||
func();
|
||||
}
|
||||
else {
|
||||
LockGuard _(mutex);
|
||||
tasks.push_back(func);
|
||||
}
|
||||
}
|
||||
void execTasks()
|
||||
{
|
||||
assert(threadId == std::this_thread::get_id());
|
||||
std::vector<std::function<void()>> localTasks;
|
||||
{
|
||||
LockGuard _(mutex);
|
||||
std::swap(localTasks, tasks);
|
||||
}
|
||||
for (auto& func : localTasks)
|
||||
func();
|
||||
}
|
||||
|
||||
private:
|
||||
using LockGuard = std::lock_guard<std::mutex>;
|
||||
|
||||
std::thread::id threadId;
|
||||
std::vector<std::function<void()>> tasks;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
|
187
core/ui/gui.cpp
187
core/ui/gui.cpp
|
@ -50,6 +50,8 @@
|
|||
#include "gui_achievements.h"
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "oslib/storage.h"
|
||||
#include <stb_image_write.h>
|
||||
#include "hw/pvr/Renderer_if.h"
|
||||
#if defined(USE_SDL)
|
||||
#include "sdl/sdl.h"
|
||||
#endif
|
||||
|
@ -101,6 +103,7 @@ using LockGuard = std::lock_guard<std::recursive_mutex>;
|
|||
|
||||
ImFont *largeFont;
|
||||
static Toast toast;
|
||||
static ThreadRunner uiThreadRunner;
|
||||
|
||||
static void emuEventCallback(Event event, void *)
|
||||
{
|
||||
|
@ -190,12 +193,13 @@ void gui_initFonts()
|
|||
static float uiScale;
|
||||
|
||||
verify(inited);
|
||||
uiThreadRunner.init();
|
||||
|
||||
#if !defined(TARGET_UWP) && !defined(__SWITCH__)
|
||||
settings.display.uiScale = std::max(1.f, settings.display.dpi / 100.f * 0.75f);
|
||||
// Limit scaling on small low-res screens
|
||||
if (settings.display.width <= 640 || settings.display.height <= 480)
|
||||
settings.display.uiScale = std::min(1.4f, settings.display.uiScale);
|
||||
settings.display.uiScale = std::min(1.2f, settings.display.uiScale);
|
||||
#endif
|
||||
settings.display.uiScale *= config::UIScaling / 100.f;
|
||||
if (settings.display.uiScale == uiScale && ImGui::GetIO().Fonts->IsBuilt())
|
||||
|
@ -470,7 +474,6 @@ static void delayedKeysUp()
|
|||
|
||||
static void gui_endFrame(bool gui_open)
|
||||
{
|
||||
ImGui::Render();
|
||||
imguiDriver->renderDrawData(ImGui::GetDrawData(), gui_open);
|
||||
delayedKeysUp();
|
||||
}
|
||||
|
@ -581,6 +584,50 @@ static bool savestateAllowed()
|
|||
return !settings.content.path.empty() && !settings.network.online && !settings.naomi.multiboard;
|
||||
}
|
||||
|
||||
static void appendVectorData(void *context, void *data, int size)
|
||||
{
|
||||
std::vector<u8>& v = *(std::vector<u8> *)context;
|
||||
const u8 *bytes = (const u8 *)data;
|
||||
v.insert(v.end(), bytes, bytes + size);
|
||||
}
|
||||
|
||||
static void getScreenshot(std::vector<u8>& data)
|
||||
{
|
||||
data.clear();
|
||||
std::vector<u8> rawData;
|
||||
int width, height;
|
||||
if (renderer == nullptr || !renderer->GetLastFrame(rawData, width, height))
|
||||
return;
|
||||
stbi_flip_vertically_on_write(0);
|
||||
stbi_write_png_to_func(appendVectorData, &data, width, height, 3, &rawData[0], 0);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static struct tm *localtime_r(const time_t *_clock, struct tm *_result)
|
||||
{
|
||||
return localtime_s(_result, _clock) ? nullptr : _result;
|
||||
}
|
||||
#endif
|
||||
|
||||
static std::string timeToString(time_t time)
|
||||
{
|
||||
tm t;
|
||||
if (localtime_r(&time, &t) == nullptr)
|
||||
return {};
|
||||
std::string s(32, '\0');
|
||||
s.resize(snprintf(s.data(), 32, "%04d/%02d/%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec));
|
||||
return s;
|
||||
}
|
||||
|
||||
static void savestate()
|
||||
{
|
||||
std::vector<u8> pngData;
|
||||
getScreenshot(pngData);
|
||||
dc_savestate(config::SavestateSlot, pngData.empty() ? nullptr : &pngData[0], pngData.size());
|
||||
ImguiStateTexture savestatePic;
|
||||
savestatePic.invalidate();
|
||||
}
|
||||
|
||||
static void gui_display_commands()
|
||||
{
|
||||
fullScreenWindow(false);
|
||||
|
@ -595,33 +642,38 @@ static void gui_display_commands()
|
|||
(ImGui::GetContentRegionAvail().x - uiScaled(100 + 150) - ImGui::GetStyle().FramePadding.x * 2)
|
||||
/ 2 / uiScaled(1));
|
||||
float buttonWidth = 150.f; // not scaled
|
||||
bool lowW = ImGui::GetContentRegionAvail().x < (uiScaled(100 + buttonWidth * 3)
|
||||
+ ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetStyle().ItemSpacing.x * 2);
|
||||
if (lowW)
|
||||
bool lowWidth = ImGui::GetContentRegionAvail().x < uiScaled(100 + buttonWidth * 3)
|
||||
+ ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetStyle().ItemSpacing.x * 2;
|
||||
if (lowWidth)
|
||||
buttonWidth = std::min(150.f,
|
||||
(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x * 2)
|
||||
/ 3 / uiScaled(1));
|
||||
bool lowHeight = ImGui::GetContentRegionAvail().y < uiScaled(100 + 50 * 2 + buttonWidth * 3 / 4) + ImGui::GetTextLineHeightWithSpacing() * 2
|
||||
+ ImGui::GetStyle().ItemSpacing.y * 2 + ImGui::GetStyle().WindowPadding.y;
|
||||
|
||||
GameMedia game;
|
||||
game.path = settings.content.path;
|
||||
game.fileName = settings.content.fileName;
|
||||
GameBoxart art = boxart.getBoxart(game);
|
||||
ImguiTexture tex(art.boxartPath);
|
||||
ImguiFileTexture tex(art.boxartPath);
|
||||
// TODO use placeholder image if not available
|
||||
tex.draw(ScaledVec2(100, 100));
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None);
|
||||
ImGui::PushFont(largeFont);
|
||||
ImGui::Text("%s", art.name.c_str());
|
||||
ImGui::PopFont();
|
||||
if (!lowHeight)
|
||||
{
|
||||
ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
|
||||
ImGui::TextWrapped("%s", art.fileName.c_str());
|
||||
ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None);
|
||||
ImGui::PushFont(largeFont);
|
||||
ImGui::Text("%s", art.name.c_str());
|
||||
ImGui::PopFont();
|
||||
{
|
||||
ImguiStyleColor _(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
|
||||
ImGui::TextWrapped("%s", art.fileName.c_str());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (lowW) {
|
||||
if (lowWidth) {
|
||||
ImGui::Columns(3, "buttons", false);
|
||||
}
|
||||
else
|
||||
|
@ -632,7 +684,7 @@ static void gui_display_commands()
|
|||
ImGui::SetColumnWidth(2, uiScaled(columnWidth));
|
||||
const ImVec2 vmuPos = ImGui::GetStyle().WindowPadding + ScaledVec2(0.f, 100.f)
|
||||
+ ImVec2(insetLeft, ImGui::GetStyle().ItemSpacing.y);
|
||||
imguiDriver->displayVmus(vmuPos);
|
||||
ImguiVmuTexture::displayVmus(vmuPos);
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImguiStyleVar _1{ImGuiStyleVar_FramePadding, ScaledVec2(12.f, 3.f)};
|
||||
|
@ -657,6 +709,17 @@ static void gui_display_commands()
|
|||
if (ImGui::Button(ICON_FA_TROPHY " Achievements", ScaledVec2(buttonWidth, 50)) && achievements::isActive())
|
||||
gui_setState(GuiState::Achievements);
|
||||
}
|
||||
// Barcode
|
||||
if (card_reader::barcodeAvailable())
|
||||
{
|
||||
ImGui::Text("Barcode Card");
|
||||
char cardBuf[64] {};
|
||||
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
||||
ImGui::SetNextItemWidth(uiScaled(buttonWidth));
|
||||
if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
||||
card_reader::barcodeSetCard(cardBuf);
|
||||
}
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Insert/Eject Disk
|
||||
|
@ -674,6 +737,7 @@ static void gui_display_commands()
|
|||
// Settings
|
||||
if (ImGui::Button(ICON_FA_GEAR " Settings", ScaledVec2(buttonWidth, 50)))
|
||||
gui_setState(GuiState::Settings);
|
||||
|
||||
// Exit
|
||||
if (ImGui::Button(commandLineStart ? ICON_FA_POWER_OFF " Exit" : ICON_FA_POWER_OFF " Close Game", ScaledVec2(buttonWidth, 50)))
|
||||
gui_stop_game();
|
||||
|
@ -681,10 +745,12 @@ static void gui_display_commands()
|
|||
ImGui::NextColumn();
|
||||
{
|
||||
DisabledScope _{!savestateAllowed()};
|
||||
ImguiStateTexture savestatePic;
|
||||
time_t savestateDate = dc_getStateCreationDate(config::SavestateSlot);
|
||||
|
||||
// Load State
|
||||
{
|
||||
DisabledScope _{settings.raHardcoreMode};
|
||||
// Load State
|
||||
DisabledScope _{settings.raHardcoreMode || savestateDate == 0};
|
||||
if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT " Load State", ScaledVec2(buttonWidth, 50)) && savestateAllowed())
|
||||
{
|
||||
gui_setState(GuiState::Closed);
|
||||
|
@ -696,7 +762,7 @@ static void gui_display_commands()
|
|||
if (ImGui::Button(ICON_FA_DOWNLOAD " Save State", ScaledVec2(buttonWidth, 50)) && savestateAllowed())
|
||||
{
|
||||
gui_setState(GuiState::Closed);
|
||||
dc_savestate(config::SavestateSlot);
|
||||
savestate();
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -720,30 +786,20 @@ static void gui_display_commands()
|
|||
ImGui::SameLine(0, spacingW);
|
||||
if (ImGui::ArrowButton("##next-slot", ImGuiDir_Right))
|
||||
{
|
||||
if (config::SavestateSlot == 9)
|
||||
config::SavestateSlot = 0;
|
||||
else
|
||||
if (config::SavestateSlot == 9)
|
||||
config::SavestateSlot = 0;
|
||||
else
|
||||
config::SavestateSlot++;
|
||||
SaveSettings();
|
||||
SaveSettings();
|
||||
}
|
||||
std::string date = dc_getStateUpdateDate(config::SavestateSlot);
|
||||
{
|
||||
ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f);
|
||||
if (date.empty())
|
||||
if (savestateDate == 0)
|
||||
ImGui::TextColored(gray, "Empty");
|
||||
else
|
||||
ImGui::TextColored(gray, "%s", date.c_str());
|
||||
ImGui::TextColored(gray, "%s", timeToString(savestateDate).c_str());
|
||||
}
|
||||
}
|
||||
// Barcode
|
||||
if (card_reader::barcodeAvailable())
|
||||
{
|
||||
ImGui::NewLine();
|
||||
ImGui::Text("Barcode Card");
|
||||
char cardBuf[64] {};
|
||||
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
||||
if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
||||
card_reader::barcodeSetCard(cardBuf);
|
||||
savestatePic.draw(ScaledVec2(buttonWidth, 0.f));
|
||||
}
|
||||
|
||||
ImGui::Columns(1, nullptr, false);
|
||||
|
@ -929,6 +985,7 @@ const Mapping dcButtons[] = {
|
|||
{ EMU_BTN_LOADSTATE, "Load State" },
|
||||
{ EMU_BTN_SAVESTATE, "Save State" },
|
||||
{ EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" },
|
||||
{ EMU_BTN_SCREENSHOT, "Save Screenshot" },
|
||||
|
||||
{ EMU_BTN_NONE, nullptr }
|
||||
};
|
||||
|
@ -982,6 +1039,7 @@ const Mapping arcadeButtons[] = {
|
|||
{ EMU_BTN_LOADSTATE, "Load State" },
|
||||
{ EMU_BTN_SAVESTATE, "Save State" },
|
||||
{ EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" },
|
||||
{ EMU_BTN_SCREENSHOT, "Save Screenshot" },
|
||||
|
||||
{ EMU_BTN_NONE, nullptr }
|
||||
};
|
||||
|
@ -3088,7 +3146,7 @@ static void gui_display_content()
|
|||
{
|
||||
GameMedia game;
|
||||
GameBoxart art = boxart.getBoxartAndLoad(game);
|
||||
ImguiTexture tex(art.boxartPath);
|
||||
ImguiFileTexture tex(art.boxartPath);
|
||||
pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS");
|
||||
}
|
||||
else
|
||||
|
@ -3130,7 +3188,7 @@ static void gui_display_content()
|
|||
// Put the image inside a child window so we can detect when it's fully clipped and doesn't need to be loaded
|
||||
if (ImGui::BeginChild("img", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_NavFlattened))
|
||||
{
|
||||
ImguiTexture tex(art.boxartPath);
|
||||
ImguiFileTexture tex(art.boxartPath);
|
||||
pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
@ -3417,7 +3475,10 @@ void gui_display_ui()
|
|||
break;
|
||||
}
|
||||
error_popup();
|
||||
ImGui::Render();
|
||||
gui_endFrame(gui_open);
|
||||
uiThreadRunner.execTasks();
|
||||
ImguiFileTexture::resetLoadCount();
|
||||
|
||||
if (gui_state == GuiState::Closed)
|
||||
emu.start();
|
||||
|
@ -3447,7 +3508,7 @@ static std::string getFPSNotification()
|
|||
return std::string(settings.input.fastForwardMode ? ">>" : "");
|
||||
}
|
||||
|
||||
void gui_display_osd()
|
||||
void gui_draw_osd()
|
||||
{
|
||||
if (gui_state == GuiState::VJoyEdit)
|
||||
return;
|
||||
|
@ -3488,7 +3549,13 @@ void gui_display_osd()
|
|||
}
|
||||
if (!settings.raHardcoreMode)
|
||||
lua::overlay();
|
||||
ImGui::Render();
|
||||
uiThreadRunner.execTasks();
|
||||
}
|
||||
|
||||
void gui_display_osd()
|
||||
{
|
||||
gui_draw_osd();
|
||||
gui_endFrame(gui_is_open());
|
||||
}
|
||||
|
||||
|
@ -3523,7 +3590,7 @@ void gui_display_profiler()
|
|||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
ImGui::Render();
|
||||
gui_endFrame(true);
|
||||
#endif
|
||||
}
|
||||
|
@ -3604,17 +3671,22 @@ void gui_loadState()
|
|||
}
|
||||
}
|
||||
|
||||
void gui_saveState()
|
||||
void gui_saveState(bool stopRestart)
|
||||
{
|
||||
const LockGuard lock(guiMutex);
|
||||
if (gui_state == GuiState::Closed && savestateAllowed())
|
||||
{
|
||||
try {
|
||||
emu.stop();
|
||||
dc_savestate(config::SavestateSlot);
|
||||
emu.start();
|
||||
if (stopRestart)
|
||||
emu.stop();
|
||||
savestate();
|
||||
if (stopRestart)
|
||||
emu.start();
|
||||
} catch (const FlycastException& e) {
|
||||
gui_stop_game(e.what());
|
||||
if (stopRestart)
|
||||
gui_stop_game(e.what());
|
||||
else
|
||||
WARN_LOG(COMMON, "gui_saveState: %s", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3641,6 +3713,33 @@ std::string gui_getCurGameBoxartUrl()
|
|||
return art.boxartUrl;
|
||||
}
|
||||
|
||||
void gui_takeScreenshot()
|
||||
{
|
||||
if (!game_started)
|
||||
return;
|
||||
uiThreadRunner.runOnThread([]() {
|
||||
std::string date = timeToString(time(nullptr));
|
||||
std::replace(date.begin(), date.end(), '/', '-');
|
||||
std::replace(date.begin(), date.end(), ':', '-');
|
||||
std::string name = "Flycast-" + date + ".png";
|
||||
|
||||
std::vector<u8> data;
|
||||
getScreenshot(data);
|
||||
if (data.empty()) {
|
||||
gui_display_notification("No screenshot available", 2000);
|
||||
}
|
||||
else
|
||||
{
|
||||
try {
|
||||
hostfs::saveScreenshot(name, data);
|
||||
gui_display_notification("Screenshot saved", 2000, name.c_str());
|
||||
} catch (const FlycastException& e) {
|
||||
gui_display_notification("Error saving screenshot", 5000, e.what());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef TARGET_UWP
|
||||
// Ugly but a good workaround for MS stupidity
|
||||
// UWP doesn't allow the UI thread to wait on a thread/task. When an std::future is ready, it is possible
|
||||
|
|
|
@ -26,6 +26,7 @@ void gui_initFonts();
|
|||
void gui_open_settings();
|
||||
void gui_display_ui();
|
||||
void gui_display_notification(const char *msg, int duration, const char *details = nullptr);
|
||||
void gui_draw_osd();
|
||||
void gui_display_osd();
|
||||
void gui_display_profiler();
|
||||
void gui_open_onboarding();
|
||||
|
@ -49,8 +50,9 @@ void gui_error(const std::string& what);
|
|||
void gui_setOnScreenKeyboardCallback(void (*callback)(bool show));
|
||||
void gui_save();
|
||||
void gui_loadState();
|
||||
void gui_saveState();
|
||||
void gui_saveState(bool stopRestart = true);
|
||||
std::string gui_getCurGameBoxartUrl();
|
||||
void gui_takeScreenshot();
|
||||
|
||||
enum class GuiState {
|
||||
Closed,
|
||||
|
|
|
@ -77,7 +77,7 @@ void Notification::notify(Type type, const std::string& image, const std::string
|
|||
void Notification::showChallenge(const std::string& image)
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
ImguiTexture texture{ image };
|
||||
ImguiFileTexture texture{ image };
|
||||
if (std::find(challenges.begin(), challenges.end(), texture) != challenges.end())
|
||||
return;
|
||||
challenges.push_back(texture);
|
||||
|
@ -183,7 +183,7 @@ bool Notification::draw()
|
|||
dl->AddRect(pos, pos + totalSize, borderCol, 0.f);
|
||||
|
||||
pos += padding;
|
||||
for (const auto& img : challenges) {
|
||||
for (auto& img : challenges) {
|
||||
img.draw(dl, pos, size, alpha);
|
||||
pos.x += hspacing + size.x;
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ void achievementList()
|
|||
float w = ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().ItemSpacing.x * 2 - ImGui::GetStyle().WindowPadding.x
|
||||
- uiScaled(80.f + 20.f * 2); // image width and button frame padding
|
||||
Game game = getCurrentGame();
|
||||
ImguiTexture tex(game.image);
|
||||
ImguiFileTexture tex(game.image);
|
||||
tex.draw(ScaledVec2(80.f, 80.f));
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("game_info", ImVec2(w, uiScaled(80.f)), ImGuiChildFlags_None, ImGuiWindowFlags_None);
|
||||
|
@ -330,7 +330,7 @@ void achievementList()
|
|||
ImGui::Unindent(uiScaled(10));
|
||||
}
|
||||
ImguiID _("achiev" + std::to_string(id++));
|
||||
ImguiTexture tex(ach.image);
|
||||
ImguiFileTexture tex(ach.image);
|
||||
tex.draw(ScaledVec2(80.f, 80.f));
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild(ImGui::GetID("ach_item"), ImVec2(0, 0), ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None);
|
||||
|
|
|
@ -51,10 +51,10 @@ private:
|
|||
u64 startTime = 0;
|
||||
u64 endTime = 0;
|
||||
Type type = Type::None;
|
||||
ImguiTexture image;
|
||||
ImguiFileTexture image;
|
||||
std::string text[3];
|
||||
std::mutex mutex;
|
||||
std::vector<ImguiTexture> challenges;
|
||||
std::vector<ImguiFileTexture> challenges;
|
||||
std::map<u32, std::string> leaderboards;
|
||||
};
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "stdclass.h"
|
||||
#include "rend/osd.h"
|
||||
#include <stb_image.h>
|
||||
|
||||
static std::string select_current_directory = "**home**";
|
||||
static std::vector<hostfs::FileInfo> subfolders;
|
||||
|
@ -691,13 +693,6 @@ void windowDragScroll()
|
|||
}
|
||||
}
|
||||
|
||||
ImTextureID ImguiTexture::getId() const
|
||||
{
|
||||
if (path.empty())
|
||||
return {};
|
||||
return imguiDriver->getOrLoadTexture(path);
|
||||
}
|
||||
|
||||
static void setUV(float ar, ImVec2& uv0, ImVec2& uv1)
|
||||
{
|
||||
uv0 = { 0.f, 0.f };
|
||||
|
@ -715,49 +710,182 @@ static void setUV(float ar, ImVec2& uv0, ImVec2& uv1)
|
|||
}
|
||||
}
|
||||
|
||||
void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) const
|
||||
void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col)
|
||||
{
|
||||
ImTextureID id = getId();
|
||||
if (id == ImTextureID{})
|
||||
ImGui::Dummy(size);
|
||||
else
|
||||
{
|
||||
float ar = imguiDriver->getAspectRatio(id);
|
||||
const float ar = imguiDriver->getAspectRatio(id);
|
||||
ImVec2 drawSize(size);
|
||||
if (size.x == 0.f)
|
||||
drawSize.x = size.y * ar;
|
||||
else if (size.y == 0.f)
|
||||
drawSize.y = size.x / ar;
|
||||
ImVec2 uv0, uv1;
|
||||
setUV(ar, uv0, uv1);
|
||||
ImGui::Image(id, size, uv0, uv1, tint_col, border_col);
|
||||
setUV(ar / drawSize.x * drawSize.y, uv0, uv1);
|
||||
ImGui::Image(id, drawSize, uv0, uv1, tint_col, border_col);
|
||||
}
|
||||
}
|
||||
|
||||
void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha) const
|
||||
void ImguiTexture::draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha)
|
||||
{
|
||||
ImTextureID id = getId();
|
||||
if (id == ImTextureID{})
|
||||
return;
|
||||
float ar = imguiDriver->getAspectRatio(id);
|
||||
const float ar = imguiDriver->getAspectRatio(id);
|
||||
ImVec2 uv0, uv1;
|
||||
setUV(ar, uv0, uv1);
|
||||
ImVec2 pos_b = pos + size;
|
||||
setUV(ar / size.x * size.y, uv0, uv1);
|
||||
u32 col = alphaOverride(0xffffff, alpha);
|
||||
drawList->AddImage(id, pos, pos_b, uv0, uv1, col);
|
||||
drawList->AddImage(id, pos, pos + size, uv0, uv1, col);
|
||||
}
|
||||
|
||||
bool ImguiTexture::button(const char* str_id, const ImVec2& image_size, const std::string& title,
|
||||
const ImVec4& bg_col, const ImVec4& tint_col) const
|
||||
const ImVec4& bg_col, const ImVec4& tint_col)
|
||||
{
|
||||
ImTextureID id = getId();
|
||||
if (id == ImTextureID{})
|
||||
return ImGui::Button(title.c_str(), image_size);
|
||||
else
|
||||
{
|
||||
float ar = imguiDriver->getAspectRatio(id);
|
||||
const float ar = imguiDriver->getAspectRatio(id);
|
||||
const ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2;
|
||||
ImVec2 uv0, uv1;
|
||||
setUV(ar, uv0, uv1);
|
||||
ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2;
|
||||
setUV(ar / size.x * size.y, uv0, uv1);
|
||||
return ImGui::ImageButton(str_id, id, size, uv0, uv1, bg_col, tint_col);
|
||||
}
|
||||
}
|
||||
|
||||
static u8 *loadImage(const std::string& path, int& width, int& height)
|
||||
{
|
||||
FILE *file = nowide::fopen(path.c_str(), "rb");
|
||||
if (file == nullptr)
|
||||
return nullptr;
|
||||
|
||||
int channels;
|
||||
stbi_set_flip_vertically_on_load(0);
|
||||
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
|
||||
std::fclose(file);
|
||||
return imgData;
|
||||
}
|
||||
|
||||
int ImguiFileTexture::textureLoadCount;
|
||||
|
||||
ImTextureID ImguiFileTexture::getId()
|
||||
{
|
||||
if (path.empty())
|
||||
return {};
|
||||
ImTextureID id = imguiDriver->getTexture(path);
|
||||
if (id == ImTextureID() && textureLoadCount < 10)
|
||||
{
|
||||
textureLoadCount++;
|
||||
int width, height;
|
||||
u8 *imgData = loadImage(path, width, height);
|
||||
if (imgData != nullptr)
|
||||
{
|
||||
try {
|
||||
id = imguiDriver->updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling);
|
||||
} catch (...) {
|
||||
// vulkan can throw during resizing
|
||||
}
|
||||
free(imgData);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
bool ImguiStateTexture::exists()
|
||||
{
|
||||
std::string path = hostfs::getSavestatePath(config::SavestateSlot, false);
|
||||
try {
|
||||
hostfs::storage().getFileInfo(path);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ImTextureID ImguiStateTexture::getId()
|
||||
{
|
||||
std::string path = hostfs::getSavestatePath(config::SavestateSlot, false);
|
||||
ImTextureID texid = imguiDriver->getTexture(path);
|
||||
if (texid == ImTextureID())
|
||||
{
|
||||
// load savestate info
|
||||
std::vector<u8> pngData;
|
||||
dc_getStateScreenshot(config::SavestateSlot, pngData);
|
||||
if (pngData.empty())
|
||||
return {};
|
||||
|
||||
int width, height, channels;
|
||||
stbi_set_flip_vertically_on_load(0);
|
||||
u8 *imgData = stbi_load_from_memory(&pngData[0], pngData.size(), &width, &height, &channels, STBI_rgb_alpha);
|
||||
if (imgData != nullptr)
|
||||
{
|
||||
try {
|
||||
texid = imguiDriver->updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling);
|
||||
} catch (...) {
|
||||
// vulkan can throw during resizing
|
||||
}
|
||||
free(imgData);
|
||||
}
|
||||
}
|
||||
return texid;
|
||||
}
|
||||
|
||||
void ImguiStateTexture::invalidate()
|
||||
{
|
||||
if (imguiDriver)
|
||||
{
|
||||
std::string path = hostfs::getSavestatePath(config::SavestateSlot, false);
|
||||
imguiDriver->deleteTexture(path);
|
||||
}
|
||||
}
|
||||
|
||||
std::array<ImguiVmuTexture, 8> ImguiVmuTexture::Vmus { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||
constexpr float VMU_WIDTH = 96.f;
|
||||
constexpr float VMU_HEIGHT = 64.f;
|
||||
constexpr float VMU_PADDING = 8.f;
|
||||
|
||||
ImTextureID ImguiVmuTexture::getId()
|
||||
{
|
||||
if (!vmu_lcd_status[index])
|
||||
return {};
|
||||
if (idPath.empty())
|
||||
idPath = ":vmu:" + std::to_string(index);
|
||||
ImTextureID texid = imguiDriver->getTexture(idPath);
|
||||
if (texid == ImTextureID() || vmuLastChanged != ::vmuLastChanged[index])
|
||||
{
|
||||
try {
|
||||
texid = imguiDriver->updateTexture(idPath, (const u8 *)vmu_lcd_data[index], 48, 32, true);
|
||||
vmuLastChanged = ::vmuLastChanged[index];
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
return texid;
|
||||
}
|
||||
|
||||
void ImguiVmuTexture::displayVmus(const ImVec2& pos)
|
||||
{
|
||||
const ScaledVec2 size(VMU_WIDTH, VMU_HEIGHT);
|
||||
const float padding = uiScaled(VMU_PADDING);
|
||||
ImDrawList *dl = ImGui::GetForegroundDrawList();
|
||||
ImVec2 cpos(pos + ScaledVec2(2.f, 0)); // 96 pixels wide + 2 * 2 -> 100
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (!vmu_lcd_status[i])
|
||||
continue;
|
||||
|
||||
ImTextureID texid = Vmus[i].getId();
|
||||
if (texid == ImTextureID())
|
||||
continue;
|
||||
ImVec2 pos_b = cpos + size;
|
||||
dl->AddImage(texid, cpos, pos_b, ImVec2(0, 1), ImVec2(1, 0), 0x80ffffff);
|
||||
cpos.y += size.y + padding;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom version of ImGui::BeginListBox that allows passing window flags
|
||||
bool BeginListBox(const char* label, const ImVec2& size_arg, ImGuiWindowFlags windowFlags)
|
||||
{
|
||||
|
@ -827,8 +955,9 @@ bool Toast::draw()
|
|||
// Fade out
|
||||
alpha = (std::cos((now - endTime) / (float)END_ANIM_TIME * (float)M_PI) + 1.f) / 2.f;
|
||||
|
||||
const ImVec2 displaySize(ImGui::GetIO().DisplaySize);
|
||||
const float maxW = std::min(uiScaled(640.f), displaySize.x);
|
||||
ImFont *regularFont = ImGui::GetFont();
|
||||
const float maxW = uiScaled(640.f);
|
||||
const ImVec2 titleSize = title.empty() ? ImVec2()
|
||||
: largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, maxW, &title.front(), &title.back() + 1);
|
||||
const ImVec2 msgSize = message.empty() ? ImVec2()
|
||||
|
@ -838,7 +967,6 @@ bool Toast::draw()
|
|||
ImVec2 totalSize(std::max(titleSize.x, msgSize.x), titleSize.y + msgSize.y);
|
||||
totalSize += padding * 2.f + spacing * (float)(!title.empty() && !message.empty());
|
||||
|
||||
const ImVec2 displaySize(ImGui::GetIO().DisplaySize);
|
||||
ImVec2 pos(insetLeft, displaySize.y - totalSize.y);
|
||||
if (now - startTime < START_ANIM_TIME)
|
||||
// Slide up
|
||||
|
|
|
@ -199,27 +199,70 @@ public:
|
|||
class ImguiTexture
|
||||
{
|
||||
public:
|
||||
ImguiTexture() = default;
|
||||
ImguiTexture(const std::string& path) : path(path) {}
|
||||
|
||||
void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1),
|
||||
const ImVec4& border_col = ImVec4(0, 0, 0, 0)) const;
|
||||
void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f) const;
|
||||
const ImVec4& border_col = ImVec4(0, 0, 0, 0));
|
||||
void draw(ImDrawList *drawList, const ImVec2& pos, const ImVec2& size, float alpha = 1.f);
|
||||
bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0),
|
||||
const ImVec4& tint_col = ImVec4(1, 1, 1, 1)) const;
|
||||
|
||||
ImTextureID getId() const;
|
||||
const ImVec4& tint_col = ImVec4(1, 1, 1, 1));
|
||||
|
||||
operator ImTextureID() {
|
||||
return getId();
|
||||
}
|
||||
void setNearestSampling(bool nearestSampling) {
|
||||
this->nearestSampling = nearestSampling;
|
||||
}
|
||||
|
||||
bool operator==(const ImguiTexture& other) const {
|
||||
virtual ImTextureID getId() = 0;
|
||||
virtual ~ImguiTexture() = default;
|
||||
|
||||
protected:
|
||||
bool nearestSampling = false;
|
||||
};
|
||||
|
||||
class ImguiFileTexture : public ImguiTexture
|
||||
{
|
||||
public:
|
||||
ImguiFileTexture() = default;
|
||||
ImguiFileTexture(const std::string& path) : ImguiTexture(), path(path) {}
|
||||
|
||||
bool operator==(const ImguiFileTexture& other) const {
|
||||
return other.path == path;
|
||||
}
|
||||
ImTextureID getId() override;
|
||||
|
||||
static void resetLoadCount() {
|
||||
textureLoadCount = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path;
|
||||
static int textureLoadCount;
|
||||
};
|
||||
|
||||
class ImguiStateTexture : public ImguiTexture
|
||||
{
|
||||
public:
|
||||
ImTextureID getId() override;
|
||||
|
||||
bool exists();
|
||||
void invalidate();
|
||||
};
|
||||
|
||||
class ImguiVmuTexture : public ImguiTexture
|
||||
{
|
||||
public:
|
||||
ImguiVmuTexture(int index = 0) : index(index) {}
|
||||
|
||||
// draw all active vmus in a single column at the given position
|
||||
static void displayVmus(const ImVec2& pos);
|
||||
ImTextureID getId() override;
|
||||
|
||||
private:
|
||||
int index = 0;
|
||||
std::string idPath;
|
||||
u64 vmuLastChanged = 0;
|
||||
|
||||
static std::array<ImguiVmuTexture, 8> Vmus;
|
||||
};
|
||||
|
||||
static inline bool iconButton(const char *icon, const std::string& label, const ImVec2& size = {})
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 flyinghead
|
||||
|
||||
This file is part of Flycast.
|
||||
|
||||
Flycast is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Flycast is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "imgui_driver.h"
|
||||
#include "gui_util.h"
|
||||
#include "rend/osd.h"
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_ONLY_PNG
|
||||
#include <stb_image.h>
|
||||
|
||||
constexpr float VMU_WIDTH = 96.f;
|
||||
constexpr float VMU_HEIGHT = 64.f;
|
||||
constexpr float VMU_PADDING = 8.f;
|
||||
|
||||
void ImGuiDriver::reset()
|
||||
{
|
||||
aspectRatios.clear();
|
||||
for (auto& tex : vmu_lcd_tex_ids)
|
||||
tex = ImTextureID{};
|
||||
textureLoadCount = 0;
|
||||
vmuLastChanged.fill({});
|
||||
}
|
||||
|
||||
static u8 *loadImage(const std::string& path, int& width, int& height)
|
||||
{
|
||||
FILE *file = nowide::fopen(path.c_str(), "rb");
|
||||
if (file == nullptr)
|
||||
return nullptr;
|
||||
|
||||
int channels;
|
||||
stbi_set_flip_vertically_on_load(0);
|
||||
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
|
||||
std::fclose(file);
|
||||
return imgData;
|
||||
}
|
||||
|
||||
ImTextureID ImGuiDriver::getOrLoadTexture(const std::string& path, bool nearestSampling)
|
||||
{
|
||||
ImTextureID id = getTexture(path);
|
||||
if (id == ImTextureID() && textureLoadCount < 10)
|
||||
{
|
||||
textureLoadCount++;
|
||||
int width, height;
|
||||
u8 *imgData = loadImage(path, width, height);
|
||||
if (imgData != nullptr)
|
||||
{
|
||||
try {
|
||||
id = updateTextureAndAspectRatio(path, imgData, width, height, nearestSampling);
|
||||
} catch (...) {
|
||||
// vulkan can throw during resizing
|
||||
}
|
||||
free(imgData);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
void ImGuiDriver::updateVmuTextures()
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (!vmu_lcd_status[i])
|
||||
continue;
|
||||
|
||||
if (this->vmuLastChanged[i] != ::vmuLastChanged[i] || vmu_lcd_tex_ids[i] == ImTextureID())
|
||||
{
|
||||
try {
|
||||
vmu_lcd_tex_ids[i] = updateTexture("__vmu" + std::to_string(i), (const u8 *)vmu_lcd_data[i], 48, 32, true);
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
if (vmu_lcd_tex_ids[i] != ImTextureID())
|
||||
this->vmuLastChanged[i] = ::vmuLastChanged[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiDriver::displayVmus(const ImVec2& pos)
|
||||
{
|
||||
updateVmuTextures();
|
||||
const ScaledVec2 size(VMU_WIDTH, VMU_HEIGHT);
|
||||
const float padding = uiScaled(VMU_PADDING);
|
||||
ImDrawList *dl = ImGui::GetForegroundDrawList();
|
||||
ImVec2 cpos(pos + ScaledVec2(2.f, 0)); // 96 pixels wide + 2 * 2 -> 100
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (!vmu_lcd_status[i])
|
||||
continue;
|
||||
|
||||
ImVec2 pos_b = cpos + size;
|
||||
dl->AddImage(vmu_lcd_tex_ids[i], cpos, pos_b, ImVec2(0, 1), ImVec2(1, 0), 0x80ffffff);
|
||||
cpos.y += size.y + padding;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,54 +30,47 @@ public:
|
|||
gui_initFonts();
|
||||
}
|
||||
virtual ~ImGuiDriver() = default;
|
||||
virtual void reset();
|
||||
|
||||
virtual void reset() {
|
||||
aspectRatios.clear();
|
||||
}
|
||||
|
||||
virtual void newFrame() = 0;
|
||||
virtual void renderDrawData(ImDrawData* drawData, bool gui_open) = 0;
|
||||
|
||||
virtual void displayVmus() {} // TODO OpenGL only. Get rid of it
|
||||
virtual void displayCrosshairs() {} // same
|
||||
// draw all active vmus in a single column at the given position
|
||||
void displayVmus(const ImVec2& pos);
|
||||
|
||||
void doPresent() {
|
||||
textureLoadCount = 0;
|
||||
present();
|
||||
}
|
||||
|
||||
virtual void present() = 0;
|
||||
virtual void setFrameRendered() {}
|
||||
|
||||
float getAspectRatio(ImTextureID textureId) {
|
||||
auto it = aspectRatios.find(textureId);
|
||||
if (it != aspectRatios.end())
|
||||
return it->second;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
ImTextureID getOrLoadTexture(const std::string& path, bool nearestSampling = false);
|
||||
|
||||
protected:
|
||||
virtual ImTextureID getTexture(const std::string& name) = 0;
|
||||
virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height, bool nearestSampling) = 0;
|
||||
virtual void present() = 0;
|
||||
void updateVmuTextures();
|
||||
virtual void deleteTexture(const std::string& name) = 0;
|
||||
|
||||
ImTextureID vmu_lcd_tex_ids[8] {};
|
||||
std::array<u64, 8> vmuLastChanged {};
|
||||
|
||||
private:
|
||||
ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height, bool nearestSampling)
|
||||
{
|
||||
textureLoadCount++;
|
||||
ImTextureID textureId = updateTexture(name, data, width, height, nearestSampling);
|
||||
if (textureId != ImTextureID())
|
||||
aspectRatios[textureId] = (float)width / height;
|
||||
return textureId;
|
||||
}
|
||||
|
||||
std::unordered_map<ImTextureID, float> aspectRatios;
|
||||
int textureLoadCount = 0;
|
||||
float getAspectRatio(ImTextureID textureId)
|
||||
{
|
||||
auto it = aspectRatios.find(textureId);
|
||||
if (it != aspectRatios.end())
|
||||
return it->second;
|
||||
else
|
||||
return 1.f;
|
||||
}
|
||||
void updateAspectRatio(ImTextureID textureId, float aspectRatio) {
|
||||
if (textureId != ImTextureID())
|
||||
aspectRatios[textureId] = aspectRatio;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<ImTextureID, float> aspectRatios; // TODO move this out
|
||||
};
|
||||
|
||||
extern std::unique_ptr<ImGuiDriver> imguiDriver;
|
||||
|
|
|
@ -96,7 +96,7 @@ void mainui_loop()
|
|||
if (imguiDriver == nullptr)
|
||||
forceReinit = true;
|
||||
else
|
||||
imguiDriver->doPresent();
|
||||
imguiDriver->present();
|
||||
|
||||
if (config::RendererType != currentRenderer || forceReinit)
|
||||
{
|
||||
|
|
|
@ -24,17 +24,22 @@ import android.content.ContentUris;
|
|||
import android.content.CursorLoader;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class AndroidStorage {
|
||||
|
@ -386,4 +391,35 @@ public class AndroidStorage {
|
|||
cursor.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public void saveScreenshot(String name, byte data[])
|
||||
{
|
||||
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
File file = new File(path, name);
|
||||
|
||||
try {
|
||||
// Make sure the Pictures directory exists.
|
||||
path.mkdirs();
|
||||
|
||||
OutputStream os = new FileOutputStream(file);
|
||||
try {
|
||||
os.write(data);
|
||||
} catch (IOException e) {
|
||||
try { os.close(); } catch (IOException e1) {}
|
||||
file.delete();
|
||||
throw e;
|
||||
}
|
||||
os.close();
|
||||
|
||||
// Tell the media scanner about the new file so that it is
|
||||
// immediately available to the user.
|
||||
MediaScannerConnection.scanFile(activity,
|
||||
new String[] { file.toString() }, null, null);
|
||||
} catch (IOException e) {
|
||||
// Unable to create file, likely because external storage is
|
||||
// not currently mounted.
|
||||
Log.w("flycast", "saveScreenshot: Error writing " + file, e);
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
jgetSubPath = env->GetMethodID(clazz, "getSubPath", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
jgetFileInfo = env->GetMethodID(clazz, "getFileInfo", "(Ljava/lang/String;)Lcom/flycast/emulator/FileInfo;");
|
||||
jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZ)V");
|
||||
jsaveScreenshot = env->GetMethodID(clazz, "saveScreenshot", "(Ljava/lang/String;[B)V");
|
||||
}
|
||||
|
||||
bool isKnownPath(const std::string& path) override {
|
||||
|
@ -137,6 +138,15 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data)
|
||||
{
|
||||
jni::String jname(name);
|
||||
jni::ByteArray jdata(data.size());
|
||||
jdata.setData(&data[0]);
|
||||
jni::env()->CallVoidMethod(jstorage, jsaveScreenshot, (jstring)jname, (jbyteArray)jdata);
|
||||
checkException();
|
||||
}
|
||||
|
||||
private:
|
||||
void checkException()
|
||||
{
|
||||
|
@ -184,6 +194,7 @@ private:
|
|||
jmethodID jaddStorage;
|
||||
jmethodID jgetSubPath;
|
||||
jmethodID jgetFileInfo;
|
||||
jmethodID jsaveScreenshot;
|
||||
// FileInfo accessors lazily initialized to avoid having to load the class
|
||||
jmethodID jgetName = nullptr;
|
||||
jmethodID jgetPath = nullptr;
|
||||
|
@ -201,6 +212,11 @@ Storage& customStorage()
|
|||
return *androidStorage;
|
||||
}
|
||||
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data)
|
||||
{
|
||||
return static_cast<AndroidStorage&>(customStorage()).saveScreenshot(name, data);
|
||||
}
|
||||
|
||||
} // namespace hostfs
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_AndroidStorage_addStorageCallback(JNIEnv *env, jobject obj, jstring path)
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
int darw_printf(const char* text,...)
|
||||
|
@ -45,3 +46,15 @@ std::string os_Locale(){
|
|||
std::string os_PrecomposedString(std::string string){
|
||||
return [[[NSString stringWithUTF8String:string.c_str()] precomposedStringWithCanonicalMapping] UTF8String];
|
||||
}
|
||||
|
||||
namespace hostfs
|
||||
{
|
||||
|
||||
void saveScreenshot(const std::string& name, const std::vector<u8>& data)
|
||||
{
|
||||
NSData* imageData = [NSData dataWithBytes:&data[0] length:data.size()];
|
||||
UIImage* pngImage = [UIImage imageWithData:imageData];
|
||||
UIImageWriteToSavedPhotosAlbum(pngImage, nil, nil, nil);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@
|
|||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Flycast requires microphone access to emulate the Dreamcast microphone</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Flycast can save screenshots to your Photo library.</string>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
|
|
|
@ -255,3 +255,14 @@ void os_VideoRoutingTermVk()
|
|||
[syphonMtlServer release];
|
||||
syphonMtlServer = NULL;
|
||||
}
|
||||
|
||||
namespace hostfs
|
||||
{
|
||||
|
||||
std::string getScreenshotsPath()
|
||||
{
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES);
|
||||
return [[paths objectAtIndex:0] UTF8String];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -130,16 +130,6 @@ std::string getTextureDumpPath()
|
|||
|
||||
}
|
||||
|
||||
void dc_savestate(int index = 0)
|
||||
{
|
||||
die("unsupported");
|
||||
}
|
||||
|
||||
void dc_loadstate(int index = 0)
|
||||
{
|
||||
die("unsupported");
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void os_SetThreadName(const char *name) {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue