/** * Looking Glass * Copyright © 2017-2024 The Looking Glass Authors * https://looking-glass.io * * This program 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. * * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #define _GNU_SOURCE #include "postprocess.h" #include "filters.h" #include "app.h" #include "cimgui.h" #include #include #include #include #include #include #include "common/debug.h" #include "common/array.h" #include "common/option.h" #include "common/paths.h" #include "common/stringlist.h" #include "common/stringutils.h" #include "common/vector.h" static const EGL_FilterOps * EGL_Filters[] = { &egl_filterDownscaleOps, &egl_filterFFXFSR1Ops, &egl_filterFFXCASOps }; struct EGL_PostProcess { Vector filters, internalFilters; EGL_Texture * output; unsigned int outputX, outputY; _Atomic(bool) modified; EGL_DesktopRects * rects; StringList presets; char * presetDir; int activePreset; char presetEdit[128]; char * presetError; }; void egl_postProcessEarlyInit(void) { static struct Option options[] = { { .module = "eglFilter", .name = "order", .description = "The order of filters to use", .preset = true, .type = OPTION_TYPE_STRING, .value.x_string = "" }, { .module = "egl", .name = "preset", .description = "The initial filter preset to load", .type = OPTION_TYPE_STRING }, { 0 } }; option_register(options); for (int i = 0; i < ARRAY_LENGTH(EGL_Filters); ++i) egl_filterEarlyInit(EGL_Filters[i]); } static void loadPreset(struct EGL_PostProcess * this, const char * name); static void loadPresetList(struct EGL_PostProcess * this) { DIR * dir = NULL; alloc_sprintf(&this->presetDir, "%s/presets", lgConfigDir()); if (!this->presetDir) { DEBUG_ERROR("Failed to allocate memory for presets"); return; } if (mkdir(this->presetDir, S_IRWXU) < 0 && errno != EEXIST) { DEBUG_ERROR("Failed to create presets directory: %s", this->presetDir); goto fail; } dir = opendir(this->presetDir); if (!dir) { DEBUG_ERROR("Failed to open presets directory: %s", this->presetDir); goto fail; } this->presets = stringlist_new(true); if (!this->presets) { DEBUG_ERROR("Failed to allocate memory for preset list"); goto fail; } struct dirent * entry; const char * preset = option_get_string("egl", "preset"); this->activePreset = -1; while ((entry = readdir(dir)) != NULL) { if (entry->d_type != DT_REG) continue; DEBUG_INFO("Found preset: %s", entry->d_name); char * name = strdup(entry->d_name); if (!name) { DEBUG_ERROR("Failed to allocate memory"); goto fail; } stringlist_push(this->presets, name); if (preset && strcmp(preset, name) == 0) this->activePreset = stringlist_count(this->presets) - 1; } closedir(dir); if (preset) { if (this->activePreset > -1) loadPreset(this, preset); else DEBUG_WARN("egl:preset '%s' does not exist", preset); } return; fail: free(this->presetDir); this->presetDir = NULL; if (dir) closedir(dir); if (this->presets) stringlist_free(&this->presets); } static void presetError(struct EGL_PostProcess * this, char * message) { free(this->presetError); this->presetError = message; } static bool savePreset(struct EGL_PostProcess * this, const char * name) { EGL_Filter * filter; vector_forEach(filter, &this->filters) egl_filterSaveState(filter); size_t orderLen = 0; vector_forEach(filter, &this->filters) orderLen += strlen(filter->ops.id) + 1; char order[orderLen]; char * p = order; vector_forEach(filter, &this->filters) { strcpy(p, filter->ops.id); p += strlen(filter->ops.id); *p++ = ';'; } if (p > order) p[-1] = '\0'; option_set_string("eglFilter", "order", order); char * path; alloc_sprintf(&path, "%s/%s", this->presetDir, name); if (!path) { DEBUG_ERROR("Failed to allocate memory"); return false; } FILE * file = fopen(path, "w"); if (!file) { const char * strError = strerror(errno); DEBUG_ERROR("Failed to open preset \"%s\" for writing: %s", name, strError); free(path); char * error; alloc_sprintf(&error, "Failed to save preset: %s\nError: %s", name, strError); if (error) presetError(this, error); return false; } free(path); DEBUG_INFO("Saving preset: %s", name); option_dump_preset(file); fclose(file); return true; } static int stringListIndex(StringList list, const char * str) { unsigned int count = stringlist_count(list); for (unsigned int i = 0; i < count; ++i) if (strcmp(stringlist_at(list, i), str) == 0) return i; return INT_MAX; } static int compareFilterOrder(const void * a_, const void * b_, void * opaque) { const EGL_Filter * a = *(const EGL_Filter **)a_; const EGL_Filter * b = *(const EGL_Filter **)b_; StringList order = opaque; return stringListIndex(order, a->ops.id) - stringListIndex(order, b->ops.id); } static void reorderFilters(struct EGL_PostProcess * this) { StringList order = stringlist_new(false); if (!order) { DEBUG_ERROR("Failed to allocate memory"); return; } char * orderStr = strdup(option_get_string("eglFilter", "order")); if (!orderStr) { DEBUG_ERROR("Failed to allocate memory"); stringlist_free(&order); return; } char * p = orderStr; while (*p) { stringlist_push(order, p); char * end = strchr(p, ';'); if (!end) break; *end = '\0'; p = end + 1; } qsort_r(vector_data(&this->filters), vector_size(&this->filters), sizeof(EGL_Filter *), compareFilterOrder, order); stringlist_free(&order); free(orderStr); } static void loadPreset(struct EGL_PostProcess * this, const char * name) { char * path; alloc_sprintf(&path, "%s/%s", this->presetDir, name); if (!path) { DEBUG_ERROR("Failed to allocate memory"); return; } if (!option_load(path)) { DEBUG_ERROR("Failed to load preset: %s", name); free(path); char * error; alloc_sprintf(&error, "Failed to load preset: %s", name); if (error) presetError(this, error); return; } free(path); DEBUG_INFO("Loading preset: %s", name); EGL_Filter * filter; vector_forEach(filter, &this->filters) egl_filterLoadState(filter); reorderFilters(this); } static void savePresetAs(struct EGL_PostProcess * this) { if (!savePreset(this, this->presetEdit)) return; for (unsigned i = 0; i < stringlist_count(this->presets); ++i) { DEBUG_INFO("Saw preset: %s", stringlist_at(this->presets, i)); if (strcmp(stringlist_at(this->presets, i), this->presetEdit) == 0) { this->activePreset = i; return; } } this->activePreset = stringlist_push(this->presets, strdup(this->presetEdit)); } static void deletePreset(struct EGL_PostProcess * this) { char * path; alloc_sprintf(&path, "%s/%s", this->presetDir, stringlist_at(this->presets, this->activePreset)); if (!path) { DEBUG_ERROR("Failed to allocate memory"); return; } unlink(path); free(path); stringlist_remove(this->presets, this->activePreset); if (this->activePreset >= stringlist_count(this->presets)) this->activePreset = stringlist_count(this->presets) - 1; } static bool presetsUI(struct EGL_PostProcess * this) { if (!this->presets) return false; bool redraw = false; const char * active = ""; if (this->activePreset >= 0) active = stringlist_at(this->presets, this->activePreset); if (igBeginCombo("Preset name", active, 0)) { for (unsigned i = 0; i < stringlist_count(this->presets); ++i) { bool selected = i == this->activePreset; if (igSelectable_Bool(stringlist_at(this->presets, i), selected, 0, (ImVec2) { 0.0f, 0.0f })) { this->activePreset = i; redraw = true; loadPreset(this, stringlist_at(this->presets, this->activePreset)); } if (selected) igSetItemDefaultFocus(); } igEndCombo(); } if (igIsItemHovered(ImGuiHoveredFlags_None)) igSetTooltip("Selecting a preset will load it"); if (igButton("Save preset", (ImVec2) { 0.0f, 0.0f })) { if (this->activePreset >= 0) savePreset(this, stringlist_at(this->presets, this->activePreset)); else presetError(this, strdup("You must select a preset to save.")); } if (igIsItemHovered(ImGuiHoveredFlags_None) && this->activePreset >= 0) igSetTooltip("This will overwrite the preset named: %s", stringlist_at(this->presets, this->activePreset)); igSameLine(0.0f, -1.0f); if (igButton("Save preset as...", (ImVec2) { 0.0f, 0.0f })) { this->presetEdit[0] = '\0'; igOpenPopup_Str("Save preset as...", ImGuiPopupFlags_None); } igSameLine(0.0f, -1.0f); if (igButton("Delete preset", (ImVec2) { 0.0f, 0.0f }) && this->activePreset >= 0) deletePreset(this); if (igBeginPopupModal("Save preset as...", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { igText("Enter a name for the new preset:"); if (!igIsAnyItemActive()) igSetKeyboardFocusHere(0); if (igInputText("##name", this->presetEdit, sizeof(this->presetEdit), ImGuiInputTextFlags_EnterReturnsTrue, NULL, NULL)) { savePresetAs(this); igCloseCurrentPopup(); } if (igButton("Save", (ImVec2) { 0.0f, 0.0f })) { savePresetAs(this); igCloseCurrentPopup(); } igSameLine(0.0f, -1.0f); if (igButton("Cancel", (ImVec2) { 0.0f, 0.0f })) igCloseCurrentPopup(); igEndPopup(); } if (this->presetError) igOpenPopup_Str("Preset error", ImGuiPopupFlags_None); if (igBeginPopupModal("Preset error", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { igText("%s", this->presetError); if (!igIsAnyItemActive()) igSetKeyboardFocusHere(0); if (igButton("OK", (ImVec2) { 0.0f, 0.0f })) { free(this->presetError); this->presetError = NULL; igCloseCurrentPopup(); } igEndPopup(); } return redraw; } static void drawDropTarget(void) { igPushStyleColor_Vec4(ImGuiCol_Separator, (ImVec4) { 1.0f, 1.0f, 0.0f, 1.0f }); igSeparator(); igPopStyleColor(1); } static void configUI(void * opaque, int * id) { struct EGL_PostProcess * this = opaque; bool redraw = false; redraw |= presetsUI(this); igSeparator(); static size_t mouseIdx = -1; static bool moving = false; static size_t moveIdx = 0; bool doMove = false; ImVec2 window, pos; igGetWindowPos(&window); igGetMousePos(&pos); EGL_Filter ** filters = vector_data(&this->filters); size_t count = vector_size(&this->filters); for (size_t i = 0; i < count; ++i) { EGL_Filter * filter = filters[i]; if (moving && mouseIdx < moveIdx && i == mouseIdx) drawDropTarget(); igPushID_Ptr(filter); bool draw = igCollapsingHeader_BoolPtr(filter->ops.name, NULL, 0); if (igIsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) mouseIdx = i; bool active = igIsItemActive(); if (draw) redraw |= egl_filterImguiConfig(filter); igPopID(); if (moving) { if (!igIsMouseDragging(ImGuiMouseButton_Left, -1.0f)) { moving = false; doMove = true; } } else if (active && igIsMouseDragging(ImGuiMouseButton_Left, -1.0f)) { moveIdx = mouseIdx; moving = true; } if (moving && mouseIdx > moveIdx && i == mouseIdx) drawDropTarget(); } if (moving) { igSetMouseCursor(ImGuiMouseCursor_Hand); igSetTooltip(filters[moveIdx]->ops.name); } if (doMove) { EGL_Filter * tmp = filters[moveIdx]; if (mouseIdx > moveIdx) // moving down memmove( filters + moveIdx, filters + moveIdx + 1, (mouseIdx - moveIdx) * sizeof(EGL_Filter *)); else // moving up memmove( filters + mouseIdx + 1, filters + mouseIdx, (moveIdx - mouseIdx) * sizeof(EGL_Filter *)); filters[mouseIdx] = tmp; } if (redraw) { atomic_store(&this->modified, true); app_invalidateWindow(true); } } bool egl_postProcessInit(EGL_PostProcess ** pp) { EGL_PostProcess * this = calloc(1, sizeof(*this)); if (!this) { DEBUG_ERROR("Failed to allocate memory"); return false; } if (!vector_create(&this->filters, sizeof(EGL_Filter *), ARRAY_LENGTH(EGL_Filters))) { DEBUG_ERROR("Failed to allocate the filter list"); goto error_this; } if (!vector_create(&this->internalFilters, sizeof(EGL_Filter *), ARRAY_LENGTH(EGL_Filters))) { DEBUG_ERROR("Failed to allocate the filter list"); goto error_filters; } if (!egl_desktopRectsInit(&this->rects, 1)) { DEBUG_ERROR("Failed to initialize the desktop rects"); goto error_internal; } loadPresetList(this); reorderFilters(this); app_overlayConfigRegisterTab("EGL Filters", configUI, this); *pp = this; return true; error_internal: vector_destroy(&this->internalFilters); error_filters: vector_destroy(&this->filters); error_this: free(this); return false; } void egl_postProcessFree(EGL_PostProcess ** pp) { if (!*pp) return; EGL_PostProcess * this = *pp; EGL_Filter ** filter; vector_forEachRef(filter, &this->filters) egl_filterFree(filter); vector_destroy(&this->filters); vector_forEachRef(filter, &this->internalFilters) egl_filterFree(filter); vector_destroy(&this->internalFilters); free(this->presetDir); if (this->presets) stringlist_free(&this->presets); egl_desktopRectsFree(&this->rects); free(this->presetError); free(this); *pp = NULL; } bool egl_postProcessAdd(EGL_PostProcess * this, const EGL_FilterOps * ops) { EGL_Filter * filter; if (!egl_filterInit(ops, &filter)) return false; if (ops->type == EGL_FILTER_TYPE_INTERNAL) vector_push(&this->internalFilters, &filter); else vector_push(&this->filters, &filter); return true; } bool egl_postProcessConfigModified(EGL_PostProcess * this) { return atomic_load(&this->modified); } bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex, EGL_DesktopRects * rects, int desktopWidth, int desktopHeight, unsigned int targetX, unsigned int targetY, bool useDMA) { if (targetX == 0 && targetY == 0) DEBUG_FATAL("targetX || targetY == 0"); EGL_Filter * lastFilter = NULL; unsigned int sizeX, sizeY; //TODO: clean this up GLuint _unused; EGL_PixelFormat pixFmt; if (egl_textureGet(tex, &_unused, &sizeX, &sizeY, &pixFmt) != EGL_TEX_STATUS_OK) return false; if (atomic_exchange(&this->modified, false)) { rects = this->rects; egl_desktopRectsUpdate(rects, NULL, desktopWidth, desktopHeight); } GLfloat matrix[6]; egl_desktopRectsMatrix(matrix, desktopWidth, desktopHeight, 0.0f, 0.0f, 1.0f, 1.0f, LG_ROTATE_0); EGL_FilterRects filterRects = { .rects = rects, .matrix = matrix, .width = desktopWidth, .height = desktopHeight, }; EGL_Filter * filter; EGL_Texture * texture = tex; const Vector * lists[] = { &this->internalFilters, &this->filters, NULL }; for(const Vector ** filters = lists; *filters; ++filters) vector_forEach(filter, *filters) { egl_filterSetOutputResHint(filter, targetX, targetY); if (!egl_filterSetup(filter, pixFmt, sizeX, sizeY, desktopWidth, desktopHeight, useDMA) || !egl_filterPrepare(filter)) continue; texture = egl_filterRun(filter, &filterRects, texture); egl_filterGetOutputRes(filter, &sizeX, &sizeY, &pixFmt); if (lastFilter) egl_filterRelease(lastFilter); lastFilter = filter; // the first filter to run will convert to a normal texture useDMA = false; } this->output = texture; this->outputX = sizeX; this->outputY = sizeY; return true; } EGL_Texture * egl_postProcessGetOutput(EGL_PostProcess * this, unsigned int * outputX, unsigned int * outputY) { *outputX = this->outputX; *outputY = this->outputY; return this->output; }