@@ -10,16 +10,37 @@ struct Plugin; | |||||
} // namespace plugin | } // namespace plugin | ||||
namespace engine { | |||||
struct Module; | |||||
} // namespace engine | |||||
namespace asset { | namespace asset { | ||||
void init(); | void init(); | ||||
/** Returns the path of a system resource. Should only read files from this location. */ | |||||
std::string system(std::string filename); | |||||
/** Returns the path of a user resource. Can read and write files to this location. */ | |||||
std::string user(std::string filename); | |||||
/** Returns the path of a resource in the plugin's folder. Should only read files from this location. */ | |||||
std::string plugin(plugin::Plugin* plugin, std::string filename); | |||||
/** Returns the path of a system asset. Should only read files from this location. */ | |||||
std::string system(std::string filename = ""); | |||||
/** Returns the path of a user asset. Can read and write files to this location. */ | |||||
std::string user(std::string filename = ""); | |||||
/** Returns the path of a asset in the plugin's folder. | |||||
Plugin assets should be read-only by plugins. | |||||
Examples: | |||||
asset::plugin(pluginInstance, "samples/00.wav") // "/path/to/Rack/user/folder/plugins/MyPlugin/samples/00.wav" | |||||
*/ | |||||
std::string plugin(plugin::Plugin* plugin, std::string filename = ""); | |||||
/** Returns the path to an asset in the module patch folder. | |||||
The module patch folder is *not* created automatically. Before creating files at these paths, call | |||||
system::createDirectories(asset::module(module)) | |||||
Examples: | |||||
asset::module(module, "recordings/00.wav") // "/path/to/Rack/user/folder/autosave/modules/1234/recordings/00.wav" | |||||
*/ | |||||
std::string module(engine::Module* module, const std::string& filename = ""); | |||||
// Set these before calling init() to override the default paths | // Set these before calling init() to override the default paths | ||||
@@ -73,7 +73,7 @@ struct Plugin { | |||||
~Plugin(); | ~Plugin(); | ||||
void addModel(Model* model); | void addModel(Model* model); | ||||
Model* getModel(std::string slug); | |||||
Model* getModel(const std::string& slug); | |||||
void fromJson(json_t* rootJ); | void fromJson(json_t* rootJ); | ||||
std::string getBrand(); | std::string getBrand(); | ||||
}; | }; | ||||
@@ -14,6 +14,15 @@ namespace system { | |||||
// Filesystem | // Filesystem | ||||
/** Joins two paths with a directory separator. | |||||
If `path2` is an empty string, returns `path1`. | |||||
*/ | |||||
std::string join(const std::string& path1, const std::string& path2 = ""); | |||||
/** Join an arbitrary number of paths, from left to right. */ | |||||
template <typename... Paths> | |||||
std::string join(const std::string& path1, const std::string& path2, Paths... paths) { | |||||
return join(join(path1, path2), paths...); | |||||
} | |||||
/** Returns a list of all entries (directories, files, symbolic links, etc) in a directory. | /** Returns a list of all entries (directories, files, symbolic links, etc) in a directory. | ||||
`depth` is the number of directories to recurse. 0 depth does not recurse. -1 depth recurses infinitely. | `depth` is the number of directories to recurse. 0 depth does not recurse. -1 depth recurses infinitely. | ||||
*/ | */ | ||||
@@ -674,7 +674,7 @@ void ModuleWidget::loadAction(std::string filename) { | |||||
} | } | ||||
void ModuleWidget::loadTemplate() { | void ModuleWidget::loadTemplate() { | ||||
std::string templatePath = model->getUserPresetDir() + "/" + "template.vcvm"; | |||||
std::string templatePath = system::join(model->getUserPresetDir(), "template.vcvm"); | |||||
try { | try { | ||||
load(templatePath); | load(templatePath); | ||||
} | } | ||||
@@ -735,7 +735,7 @@ void ModuleWidget::saveTemplate() { | |||||
std::string presetDir = model->getUserPresetDir(); | std::string presetDir = model->getUserPresetDir(); | ||||
system::createDirectories(presetDir); | system::createDirectories(presetDir); | ||||
std::string templatePath = presetDir + "/" + "template.vcvm"; | |||||
std::string templatePath = system::join(presetDir, "template.vcvm"); | |||||
save(templatePath); | save(templatePath); | ||||
} | } | ||||
@@ -21,6 +21,7 @@ | |||||
#include <settings.hpp> | #include <settings.hpp> | ||||
#include <string.hpp> | #include <string.hpp> | ||||
#include <plugin/Plugin.hpp> | #include <plugin/Plugin.hpp> | ||||
#include <engine/Module.hpp> | |||||
#include <app/common.hpp> | #include <app/common.hpp> | ||||
@@ -29,7 +30,7 @@ namespace asset { | |||||
static void initSystemDir() { | static void initSystemDir() { | ||||
if (systemDir != "") | |||||
if (!systemDir.empty()) | |||||
return; | return; | ||||
if (settings::devMode) { | if (settings::devMode) { | ||||
@@ -76,7 +77,7 @@ static void initSystemDir() { | |||||
static void initUserDir() { | static void initUserDir() { | ||||
if (userDir != "") | |||||
if (!userDir.empty()) | |||||
return; | return; | ||||
if (settings::devMode) { | if (settings::devMode) { | ||||
@@ -89,15 +90,13 @@ static void initUserDir() { | |||||
wchar_t documentsBufW[MAX_PATH] = L"."; | wchar_t documentsBufW[MAX_PATH] = L"."; | ||||
HRESULT result = SHGetFolderPathW(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBufW); | HRESULT result = SHGetFolderPathW(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBufW); | ||||
assert(result == S_OK); | assert(result == S_OK); | ||||
userDir = string::U16toU8(documentsBufW); | |||||
userDir += "/Rack"; | |||||
userDir = system::join(string::U16toU8(documentsBufW), "Rack"); | |||||
#endif | #endif | ||||
#if defined ARCH_MAC | #if defined ARCH_MAC | ||||
// Get home directory | // Get home directory | ||||
struct passwd* pw = getpwuid(getuid()); | struct passwd* pw = getpwuid(getuid()); | ||||
assert(pw); | assert(pw); | ||||
userDir = pw->pw_dir; | |||||
userDir += "/Documents/Rack"; | |||||
userDir = system::join(pw->pw_dir, "Documents/Rack"); | |||||
#endif | #endif | ||||
#if defined ARCH_LIN | #if defined ARCH_LIN | ||||
// Get home directory | // Get home directory | ||||
@@ -107,8 +106,7 @@ static void initUserDir() { | |||||
assert(pw); | assert(pw); | ||||
homeBuf = pw->pw_dir; | homeBuf = pw->pw_dir; | ||||
} | } | ||||
userDir = homeBuf; | |||||
userDir += "/.Rack"; | |||||
userDir = system::join(homeBuf, ".Rack"); | |||||
#endif | #endif | ||||
} | } | ||||
@@ -120,34 +118,40 @@ void init() { | |||||
// Set paths | // Set paths | ||||
if (settings::devMode) { | if (settings::devMode) { | ||||
pluginsPath = userDir + "/plugins"; | |||||
settingsPath = userDir + "/settings.json"; | |||||
autosavePath = userDir + "/autosave"; | |||||
templatePath = userDir + "/template.vcv"; | |||||
pluginsPath = system::join(userDir, "plugins"); | |||||
settingsPath = system::join(userDir, "settings.json"); | |||||
autosavePath = system::join(userDir, "autosave"); | |||||
templatePath = system::join(userDir, "template.vcv"); | |||||
} | } | ||||
else { | else { | ||||
logPath = userDir + "/log.txt"; | |||||
pluginsPath = userDir + "/plugins-v" + ABI_VERSION; | |||||
settingsPath = userDir + "/settings-v" + ABI_VERSION + ".json"; | |||||
autosavePath = userDir + "/autosave-v" + ABI_VERSION; | |||||
templatePath = userDir + "/template-v" + ABI_VERSION + ".vcv"; | |||||
logPath = system::join(userDir, "log.txt"); | |||||
pluginsPath = system::join(userDir, "plugins-v" + ABI_VERSION); | |||||
settingsPath = system::join(userDir, "settings-v" + ABI_VERSION + ".json"); | |||||
autosavePath = system::join(userDir, "autosave-v" + ABI_VERSION); | |||||
templatePath = system::join(userDir, "template-v" + ABI_VERSION + ".vcv"); | |||||
} | } | ||||
} | } | ||||
std::string system(std::string filename) { | std::string system(std::string filename) { | ||||
return systemDir + "/" + filename; | |||||
return system::join(systemDir, filename); | |||||
} | } | ||||
std::string user(std::string filename) { | std::string user(std::string filename) { | ||||
return userDir + "/" + filename; | |||||
return system::join(userDir, filename); | |||||
} | } | ||||
std::string plugin(plugin::Plugin* plugin, std::string filename) { | std::string plugin(plugin::Plugin* plugin, std::string filename) { | ||||
assert(plugin); | assert(plugin); | ||||
return plugin->path + "/" + filename; | |||||
return system::join(plugin->path, filename); | |||||
} | |||||
std::string module(engine::Module* module, const std::string& filename) { | |||||
assert(module); | |||||
return system::join(autosavePath, "modules", std::to_string(module->id), filename); | |||||
} | } | ||||
@@ -1,5 +1,6 @@ | |||||
#include <engine/Module.hpp> | #include <engine/Module.hpp> | ||||
#include <plugin.hpp> | #include <plugin.hpp> | ||||
#include <asset.hpp> | |||||
namespace rack { | namespace rack { | ||||
@@ -153,7 +153,7 @@ void PatchManager::saveAutosave() { | |||||
// Write to temporary path and then rename it to the correct path | // Write to temporary path and then rename it to the correct path | ||||
system::createDirectories(asset::autosavePath); | system::createDirectories(asset::autosavePath); | ||||
std::string patchPath = asset::autosavePath + "/patch.json"; | |||||
std::string patchPath = system::join(asset::autosavePath, "patch.json"); | |||||
std::string tmpPath = patchPath + ".tmp"; | std::string tmpPath = patchPath + ".tmp"; | ||||
FILE* file = std::fopen(tmpPath.c_str(), "w"); | FILE* file = std::fopen(tmpPath.c_str(), "w"); | ||||
if (!file) { | if (!file) { | ||||
@@ -189,7 +189,7 @@ void PatchManager::load(std::string path) { | |||||
if (isPatchLegacyPre2(path)) { | if (isPatchLegacyPre2(path)) { | ||||
// Copy the .vcv file directly to "patch.json". | // Copy the .vcv file directly to "patch.json". | ||||
system::copy(path, asset::autosavePath + "/patch.json"); | |||||
system::copy(path, system::join(asset::autosavePath, "patch.json")); | |||||
} | } | ||||
else { | else { | ||||
// Extract the .vcv file as a .tar.zst archive. | // Extract the .vcv file as a .tar.zst archive. | ||||
@@ -237,7 +237,7 @@ void PatchManager::loadTemplateDialog() { | |||||
void PatchManager::loadAutosave() { | void PatchManager::loadAutosave() { | ||||
INFO("Loading autosave"); | INFO("Loading autosave"); | ||||
std::string patchPath = asset::autosavePath + "/patch.json"; | |||||
std::string patchPath = system::join(asset::autosavePath, "patch.json"); | |||||
FILE* file = std::fopen(patchPath.c_str(), "r"); | FILE* file = std::fopen(patchPath.c_str(), "r"); | ||||
if (!file) { | if (!file) { | ||||
// Exit silently | // Exit silently | ||||
@@ -79,7 +79,7 @@ static InitCallback loadPluginCallback(Plugin* plugin) { | |||||
#elif ARCH_MAC | #elif ARCH_MAC | ||||
libraryExt = "dylib"; | libraryExt = "dylib"; | ||||
#endif | #endif | ||||
std::string libraryPath = plugin->path + "/plugin." + libraryExt; | |||||
std::string libraryPath = system::join(plugin->path, "plugin." + libraryExt); | |||||
// Check file existence | // Check file existence | ||||
if (!system::isFile(libraryPath)) | if (!system::isFile(libraryPath)) | ||||
@@ -124,7 +124,7 @@ static Plugin* loadPlugin(std::string path) { | |||||
} | } | ||||
// Load plugin.json | // Load plugin.json | ||||
std::string manifestFilename = (path == "") ? asset::system("Core.json") : (path + "/plugin.json"); | |||||
std::string manifestFilename = (path == "") ? asset::system("Core.json") : system::join(path, "plugin.json"); | |||||
FILE* file = std::fopen(manifestFilename.c_str(), "r"); | FILE* file = std::fopen(manifestFilename.c_str(), "r"); | ||||
if (!file) | if (!file) | ||||
throw Exception(string::f("Manifest file %s does not exist", manifestFilename.c_str())); | throw Exception(string::f("Manifest file %s does not exist", manifestFilename.c_str())); | ||||
@@ -226,7 +226,7 @@ void init() { | |||||
#else | #else | ||||
std::string fundamentalSrc = asset::system("Fundamental.zip"); | std::string fundamentalSrc = asset::system("Fundamental.zip"); | ||||
#endif | #endif | ||||
std::string fundamentalDir = asset::pluginsPath + "/Fundamental"; | |||||
std::string fundamentalDir = system::join(asset::pluginsPath, "Fundamental"); | |||||
if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | ||||
INFO("Extracting bundled Fundamental package"); | INFO("Extracting bundled Fundamental package"); | ||||
system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); | system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); | ||||
@@ -471,7 +471,7 @@ void syncUpdate(Update* update) { | |||||
INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), APP_ARCH.c_str()); | INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), APP_ARCH.c_str()); | ||||
// Download zip | // Download zip | ||||
std::string pluginDest = asset::pluginsPath + "/" + update->pluginSlug + ".zip"; | |||||
std::string pluginDest = system::join(asset::pluginsPath, update->pluginSlug + ".zip"); | |||||
if (!network::requestDownload(downloadUrl, pluginDest, &update->progress, cookies)) { | if (!network::requestDownload(downloadUrl, pluginDest, &update->progress, cookies)) { | ||||
WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); | WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); | ||||
return; | return; | ||||
@@ -59,12 +59,12 @@ std::string Model::getFullName() { | |||||
std::string Model::getFactoryPresetDir() { | std::string Model::getFactoryPresetDir() { | ||||
return asset::plugin(plugin, "presets/" + slug); | |||||
return asset::plugin(plugin, system::join("presets", slug)); | |||||
} | } | ||||
std::string Model::getUserPresetDir() { | std::string Model::getUserPresetDir() { | ||||
return asset::user("presets/" + plugin->slug + "/" + slug); | |||||
return asset::user(system::join("presets", plugin->slug, slug)); | |||||
} | } | ||||
@@ -22,7 +22,7 @@ void Plugin::addModel(Model* model) { | |||||
models.push_back(model); | models.push_back(model); | ||||
} | } | ||||
Model* Plugin::getModel(std::string slug) { | |||||
Model* Plugin::getModel(const std::string& slug) { | |||||
for (Model* model : models) { | for (Model* model : models) { | ||||
if (model->slug == slug) { | if (model->slug == slug) { | ||||
return model; | return model; | ||||
@@ -50,11 +50,16 @@ namespace rack { | |||||
namespace system { | namespace system { | ||||
std::string join(const std::string& path1, const std::string& path2) { | |||||
return (fs::u8path(path1) / fs::u8path(path2)).generic_u8string(); | |||||
} | |||||
std::list<std::string> getEntries(const std::string& dirPath, int depth) { | std::list<std::string> getEntries(const std::string& dirPath, int depth) { | ||||
try { | try { | ||||
std::list<std::string> entries; | std::list<std::string> entries; | ||||
for (auto& entry : fs::directory_iterator(fs::u8path(dirPath))) { | for (auto& entry : fs::directory_iterator(fs::u8path(dirPath))) { | ||||
std::string subEntry = entry.path().u8string(); | |||||
std::string subEntry = entry.path().generic_u8string(); | |||||
entries.push_back(subEntry); | entries.push_back(subEntry); | ||||
// Recurse if depth > 0 (limited recursion) or depth < 0 (infinite recursion). | // Recurse if depth > 0 (limited recursion) or depth < 0 (infinite recursion). | ||||
if (depth != 0) { | if (depth != 0) { | ||||
@@ -174,7 +179,7 @@ int removeRecursively(const std::string& path) { | |||||
std::string getWorkingDirectory() { | std::string getWorkingDirectory() { | ||||
try { | try { | ||||
return fs::current_path().u8string(); | |||||
return fs::current_path().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -194,7 +199,7 @@ void setWorkingDirectory(const std::string& path) { | |||||
std::string getTempDir() { | std::string getTempDir() { | ||||
try { | try { | ||||
return fs::temp_directory_path().u8string(); | |||||
return fs::temp_directory_path().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -204,7 +209,7 @@ std::string getTempDir() { | |||||
std::string getAbsolute(const std::string& path) { | std::string getAbsolute(const std::string& path) { | ||||
try { | try { | ||||
return fs::absolute(fs::u8path(path)).u8string(); | |||||
return fs::absolute(fs::u8path(path)).generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -214,7 +219,7 @@ std::string getAbsolute(const std::string& path) { | |||||
std::string getCanonical(const std::string& path) { | std::string getCanonical(const std::string& path) { | ||||
try { | try { | ||||
return fs::canonical(fs::u8path(path)).u8string(); | |||||
return fs::canonical(fs::u8path(path)).generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -224,7 +229,7 @@ std::string getCanonical(const std::string& path) { | |||||
std::string getDirectory(const std::string& path) { | std::string getDirectory(const std::string& path) { | ||||
try { | try { | ||||
return fs::u8path(path).parent_path().u8string(); | |||||
return fs::u8path(path).parent_path().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -234,7 +239,7 @@ std::string getDirectory(const std::string& path) { | |||||
std::string getFilename(const std::string& path) { | std::string getFilename(const std::string& path) { | ||||
try { | try { | ||||
return fs::u8path(path).filename().u8string(); | |||||
return fs::u8path(path).filename().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -244,7 +249,7 @@ std::string getFilename(const std::string& path) { | |||||
std::string getStem(const std::string& path) { | std::string getStem(const std::string& path) { | ||||
try { | try { | ||||
return fs::u8path(path).stem().u8string(); | |||||
return fs::u8path(path).stem().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -254,7 +259,7 @@ std::string getStem(const std::string& path) { | |||||
std::string getExtension(const std::string& path) { | std::string getExtension(const std::string& path) { | ||||
try { | try { | ||||
return fs::u8path(path).extension().u8string(); | |||||
return fs::u8path(path).extension().generic_u8string(); | |||||
} | } | ||||
catch (fs::filesystem_error& e) { | catch (fs::filesystem_error& e) { | ||||
throw Exception(e.what()); | throw Exception(e.what()); | ||||
@@ -410,7 +415,7 @@ void unarchiveToFolder(const std::string& archivePath, const std::string& folder | |||||
std::string entryPath = archive_entry_pathname(entry); | std::string entryPath = archive_entry_pathname(entry); | ||||
if (!fs::u8path(entryPath).is_relative()) | if (!fs::u8path(entryPath).is_relative()) | ||||
throw Exception(string::f("unzipToFolder() does not support absolute paths: %s", entryPath.c_str())); | throw Exception(string::f("unzipToFolder() does not support absolute paths: %s", entryPath.c_str())); | ||||
entryPath = fs::absolute(fs::u8path(entryPath), fs::u8path(folderPath)).u8string(); | |||||
entryPath = fs::absolute(fs::u8path(entryPath), fs::u8path(folderPath)).generic_u8string(); | |||||
#if defined ARCH_WIN | #if defined ARCH_WIN | ||||
archive_entry_copy_pathname_w(entry, string::U8toU16(entryPath).c_str()); | archive_entry_copy_pathname_w(entry, string::U8toU16(entryPath).c_str()); | ||||
#else | #else | ||||
@@ -76,7 +76,7 @@ void update() { | |||||
system::runProcessDetached(path); | system::runProcessDetached(path); | ||||
#elif defined ARCH_MAC | #elif defined ARCH_MAC | ||||
std::string cmd; | std::string cmd; | ||||
// std::string appPath = asset::userDir + "/Rack.app"; | |||||
// std::string appPath = system::join(asset::userDir, "Rack.app"); | |||||
// cmd = "rm -rf '" + appPath + "'"; | // cmd = "rm -rf '" + appPath + "'"; | ||||
// std::system(cmd.c_str()); | // std::system(cmd.c_str()); | ||||
// // Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | // // Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | ||||
@@ -434,10 +434,10 @@ void Window::screenshot(float zoom) { | |||||
std::string screenshotsDir = asset::user("screenshots"); | std::string screenshotsDir = asset::user("screenshots"); | ||||
system::createDirectory(screenshotsDir); | system::createDirectory(screenshotsDir); | ||||
for (plugin::Plugin* p : plugin::plugins) { | for (plugin::Plugin* p : plugin::plugins) { | ||||
std::string dir = screenshotsDir + "/" + p->slug; | |||||
std::string dir = system::join(screenshotsDir, p->slug); | |||||
system::createDirectory(dir); | system::createDirectory(dir); | ||||
for (plugin::Model* model : p->models) { | for (plugin::Model* model : p->models) { | ||||
std::string filename = dir + "/" + model->slug + ".png"; | |||||
std::string filename = system::join(dir, model->slug + ".png"); | |||||
// Skip model if screenshot already exists | // Skip model if screenshot already exists | ||||
if (system::isFile(filename)) | if (system::isFile(filename)) | ||||
continue; | continue; | ||||
@@ -468,9 +468,9 @@ void Window::screenshot(float zoom) { | |||||
for (int y = 0; y < height / 2; y++) { | for (int y = 0; y < height / 2; y++) { | ||||
int flipY = height - y - 1; | int flipY = height - y - 1; | ||||
uint8_t tmp[width * 4]; | uint8_t tmp[width * 4]; | ||||
memcpy(tmp, &data[y * width * 4], width * 4); | |||||
memcpy(&data[y * width * 4], &data[flipY * width * 4], width * 4); | |||||
memcpy(&data[flipY * width * 4], tmp, width * 4); | |||||
std::memcpy(tmp, &data[y * width * 4], width * 4); | |||||
std::memcpy(&data[y * width * 4], &data[flipY * width * 4], width * 4); | |||||
std::memcpy(&data[flipY * width * 4], tmp, width * 4); | |||||
} | } | ||||
// Write pixels to PNG | // Write pixels to PNG | ||||